diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts index 67215bab..3083a19c 100644 --- a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts @@ -52,6 +52,36 @@ const CAD_CHANGED_MESSAGE = "aislo:b07:drawing-changed"; const CAD_SAVE_REQUEST_MESSAGE = "aislo:b07:save-request"; const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b07:save-response"; +/** 측점 간격을 연속 chainage 차이의 최빈값으로 추정한다 (B06 그래프와 동일 방식). */ +function inferStationInterval(chainages: number[]): number { + const counts = new Map(); + const sorted = [...chainages].sort((a, b) => a - b); + for (let index = 1; index < sorted.length; index += 1) { + const difference = sorted[index] - sorted[index - 1]; + if (difference <= 0) continue; + const rounded = Math.round(difference * 10) / 10; + counts.set(rounded, (counts.get(rounded) ?? 0) + 1); + } + return ( + [...counts.entries()].sort( + ([intervalA, countA], [intervalB, countB]) => countB - countA || intervalB - intervalA, + )[0]?.[0] ?? 1 + ); +} + +/** 측점 번호+나머지 표기 (B06 그래프 영역 횡단도 라벨과 동일 형식, 예: "2+0.0"). */ +function stationLabel(chainage: number, interval: number): string { + const safeInterval = interval > 0 ? interval : 1; + let stationNumber = Math.floor((chainage + 1e-6) / safeInterval); + let remainder = chainage - stationNumber * safeInterval; + if (Math.abs(remainder) < 0.05) remainder = 0; + if (remainder >= safeInterval - 0.05) { + stationNumber += 1; + remainder = 0; + } + return `${stationNumber}+${remainder.toFixed(1)}`; +} + /** B06 확정 산출물 기반 도면 목록 패널. */ function buildDrawingSidePanel( drawings: DesignDrawingItem[], @@ -77,14 +107,20 @@ function buildDrawingSidePanel( return panel; } - const groups: [string, DesignDrawingItem[]][] = [ - ["종단도", drawings.filter((item) => item.kind === "longitudinal")], - ["횡단도", drawings.filter((item) => item.kind === "cross")], + const crossChainages = drawings + .filter((item) => item.kind === "cross" && typeof item.chainage_m === "number") + .map((item) => item.chainage_m as number); + const stationInterval = inferStationInterval(crossChainages); + + const groups: [string, DesignDrawingItem["kind"], DesignDrawingItem[]][] = [ + ["종단도", "longitudinal", drawings.filter((item) => item.kind === "longitudinal")], + ["횡단도", "cross", drawings.filter((item) => item.kind === "cross")], ]; - for (const [label, items] of groups) { + for (const [label, kind, items] of groups) { if (!items.length) continue; const section = document.createElement("section"); section.className = "b07-drawing-group"; + section.dataset.kind = kind; const sectionTitle = document.createElement("h3"); sectionTitle.textContent = `${label} ${items.length}`; section.append(sectionTitle); @@ -95,14 +131,12 @@ function buildDrawingSidePanel( button.dataset.drawingId = drawing.id; button.dataset.confirmed = String(drawing.confirmed); const name = document.createElement("span"); - name.textContent = drawing.label; - const kind = document.createElement("small"); - kind.textContent = drawing.confirmed - ? "확정" - : drawing.kind === "longitudinal" - ? "PROFILE" - : "SECTION"; - button.append(name, kind); + name.className = "b07-drawing-button__name"; + name.textContent = + drawing.kind === "cross" && typeof drawing.chainage_m === "number" + ? stationLabel(drawing.chainage_m, stationInterval) + : drawing.label; + button.append(name); button.addEventListener("click", () => void onSelect(drawing, button)); section.append(button); } @@ -216,11 +250,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { currentConfirmed = true; currentDrawing.confirmed = true; confirmButton.disabled = true; - if (currentButton) { - currentButton.dataset.confirmed = "true"; - const status = currentButton.querySelector("small"); - if (status) status.textContent = "확정"; - } + if (currentButton) currentButton.dataset.confirmed = "true"; showToast("현재 도면을 확정하고 저장했습니다.", "success"); if (result.all_confirmed) { allDrawingsConfirmed = true; @@ -243,12 +273,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { allDrawingsConfirmed = false; currentDrawing.confirmed = false; confirmButton.disabled = false; - if (currentButton) { - currentButton.dataset.confirmed = "false"; - const status = currentButton.querySelector("small"); - if (status) - status.textContent = currentDrawing.kind === "longitudinal" ? "PROFILE" : "SECTION"; - } + if (currentButton) currentButton.dataset.confirmed = "false"; if (wasConfirmed) { try { await invalidateDesignDrawing(projectId, currentDrawing.id); diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css index 4fc8c1bf..85ed4df4 100644 --- a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css @@ -46,6 +46,13 @@ gap: var(--spacing-4); } +/* 횡단도는 2열 배치 */ +.b07-drawing-group[data-kind="cross"] { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-4); +} + .b07-drawing-group h3 { margin: var(--spacing-8) 0 var(--spacing-4); color: var(--color-text-muted); @@ -53,19 +60,33 @@ font-weight: 600; } +/* 그리드 제목은 두 열을 가로지른다 */ +.b07-drawing-group[data-kind="cross"] h3 { + grid-column: 1 / -1; +} + .b07-drawing-button { display: flex; align-items: center; - justify-content: space-between; + justify-content: center; width: 100%; - min-height: 38px; - padding: var(--spacing-8) var(--spacing-12); + min-height: 34px; + padding: var(--spacing-4) var(--spacing-8); border: 1px solid transparent; + /* 확정 여부를 나타내는 좌측 색 띠 (미확정: 투명) */ + border-left: 3px solid transparent; border-radius: var(--radius-buttons); background: transparent; color: var(--color-text); cursor: pointer; - text-align: left; + text-align: center; +} + +.b07-drawing-button__name { + overflow: hidden; + font-size: var(--text-body-sm); + white-space: nowrap; + text-overflow: ellipsis; } .b07-drawing-button:hover, @@ -78,20 +99,13 @@ color: var(--color-primary); } -.b07-drawing-button small { - color: var(--color-text-muted); - font-size: 9px; -} - -.b07-drawing-button[data-loading="true"] small { - visibility: hidden; -} - +/* 확정: 좌측 띠 + 측점 글자색을 함께 성공색으로 반영 */ .b07-drawing-button[data-confirmed="true"] { border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border)); + border-left-color: var(--color-success); } -.b07-drawing-button[data-confirmed="true"] small { +.b07-drawing-button[data-confirmed="true"] .b07-drawing-button__name { color: var(--color-success); } diff --git a/B07_wf4_DesignDetail/openwebcad/src/App.css b/B07_wf4_DesignDetail/openwebcad/src/App.css index cfbc3955..783731d9 100644 --- a/B07_wf4_DesignDetail/openwebcad/src/App.css +++ b/B07_wf4_DesignDetail/openwebcad/src/App.css @@ -164,7 +164,7 @@ body > canvas[data-id="canvas"] { text-align: center; } .cad-prop select, -.cad-prop input[type='number'] { +.cad-prop input[type="number"] { height: 24px; min-width: 64px; padding: 0 4px; @@ -174,11 +174,11 @@ body > canvas[data-id="canvas"] { border-radius: 4px; font-size: 11px; } -.cad-prop input[type='number'] { +.cad-prop input[type="number"] { min-width: 48px; width: 48px; } -.cad-prop input[type='color'] { +.cad-prop input[type="color"] { height: 24px; width: 40px; padding: 1px; @@ -332,6 +332,10 @@ body > canvas[data-id="canvas"] { .cad-view-controls button:hover { background: #31516b; } +.cad-view-controls__fit { + color: #7fd0ff; + font-size: 15px; +} .cad-view-controls span { min-width: 48px; color: #9fb0c1; diff --git a/B07_wf4_DesignDetail/openwebcad/src/components/Toolbar.tsx b/B07_wf4_DesignDetail/openwebcad/src/components/Toolbar.tsx index 2524bee8..a6052d77 100644 --- a/B07_wf4_DesignDetail/openwebcad/src/components/Toolbar.tsx +++ b/B07_wf4_DesignDetail/openwebcad/src/components/Toolbar.tsx @@ -425,13 +425,14 @@ export const Toolbar: FC = () => {