feat(view): drop the 3D title band; pier name in 교각 목록; view menu under 부재 표시, origin under 색 기준

On wide screens the pier name moves to the 교각 목록 (name · type), the band is gone and the 3D view takes the full height.
A left stack holds 부재 표시 and then 시점; a right stack holds 색 기준 and then the ⓘ origin note. The active pier is scrolled
into view when it changes. Below 600 px the title is kept above the model and the stacks sit below it.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 11:31:46 +09:00
1 parent e66c259d1e
commit 86a4b2fd80
3 files changed
+50 -39

No files matched your search

+21 -15
View File
@@ -74,27 +74,33 @@
<section class="panel viewer" id="viewer-block" aria-labelledby="viewer-title">
<div class="viewer-head">
<h2 id="viewer-title">교각 3D 뷰</h2>
</div>
<div id="canvas" class="canvas" aria-label="교각 3D 뷰"></div>
<div class="viewer-stack viewer-stack-left">
<details class="info-panel parts-info" id="parts-info">
<summary>부재 표시 <span id="parts-count" class="panel-state"></span></summary>
<div id="parts"></div>
</details>
<div class="views-menu" id="views-menu">
<button id="views-toggle" class="btn views-toggle" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="views">시점</button>
<div id="views" class="views views-list" role="group" aria-label="시점" hidden></div>
</div>
<button id="origin-toggle" class="info-btn" type="button" aria-label="원점 표기" aria-expanded="false" aria-controls="origin">ⓘ</button>
<span id="origin" class="origin-pop muted" hidden></span>
</div>
<div id="canvas" class="canvas" aria-label="교각 3D 뷰"></div>
<details class="info-panel parts-info" id="parts-info">
<summary>부재 표시 <span id="parts-count" class="panel-state"></span></summary>
<div id="parts"></div>
</details>
<details class="info-panel color-info" id="color-info" hidden>
<summary>색 기준 <span id="color-state" class="panel-state"></span></summary>
<div class="rebar-color" id="rebar-color" role="group" aria-label="철근 색 기준">
<button type="button" data-color="role" aria-pressed="true">역할</button>
<button type="button" data-color="diameter" aria-pressed="false">직경</button>
<button type="button" data-color="member" aria-pressed="false">부재</button>
<ul id="rebar-legend" class="rebar-legend"></ul>
<div class="viewer-stack viewer-stack-right">
<details class="info-panel color-info" id="color-info" hidden>
<summary>색 기준 <span id="color-state" class="panel-state"></span></summary>
<div class="rebar-color" id="rebar-color" role="group" aria-label="철근 색 기준">
<button type="button" data-color="role" aria-pressed="true">역할</button>
<button type="button" data-color="diameter" aria-pressed="false">직경</button>
<button type="button" data-color="member" aria-pressed="false">부재</button>
<ul id="rebar-legend" class="rebar-legend"></ul>
</div>
</details>
<div class="origin-wrap">
<button id="origin-toggle" class="info-btn" type="button" aria-label="원점 표기" aria-expanded="false" aria-controls="origin">ⓘ</button>
<span id="origin" class="origin-pop muted" hidden></span>
</div>
</details>
</div>
</section>
<aside class="panel results" aria-labelledby="result-title">
+10 -1
View File
@@ -314,7 +314,8 @@ function renderPierRows(list: HTMLUListElement, mode: Stage): void {
pier.checked = check.checked;
renderAll();
});
const name = el('button', pier.input.name, 'pier-name');
// The pier name and type are shown here, since the 3D title band is gone on wide screens (승인 2026-10-23).
const name = el('button', `${pier.input.name} · ${pier.input.type}`, 'pier-name');
name.type = 'button';
name.setAttribute('aria-pressed', String(pier.input.name === state.active));
name.addEventListener('click', () => {
@@ -372,6 +373,7 @@ const pt = (p: { e: number; n: number }) => `E ${p.e.toFixed(3)}, N ${p.n.toFixe
function renderPanels(): void {
const pier = activePier();
const m = pier?.model;
// The pier name is in 교각 목록 on wide screens; the title keeps it on narrow ones (승인 2026-10-23).
$('viewer-title').textContent = pier ? `${pier.input.name} · ${pier.input.type} 교각 형상` : '교각 3D 뷰';
$('origin').textContent = m ? `원점 = 교각 중심 (${pt(m.pivot)})` : '';
@@ -447,8 +449,15 @@ function renderDefaults(): void {
for (const text of items) box.appendChild(el('li', text));
}
/** Pier last brought into view in 교각 목록, so that a manual scroll is not undone by the next render. */
let lastScrolledPier: string | null = null;
function renderAll(): void {
updatePartsCount();
if (state.active !== lastScrolledPier) {
lastScrolledPier = state.active;
document.querySelector<HTMLButtonElement>('.pier-items [aria-pressed="true"]')?.scrollIntoView({ block: 'nearest' });
}
$('status').textContent = state.piers.length ? '' : '엑셀을 불러오면 교각 형상을 생성합니다.';
renderHeader();
renderPierList();
+19 -23
View File
@@ -761,36 +761,32 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
.rebar-table thead th { position: sticky; top: 0; background: #ffffff; z-index: 1; }
}
/* 3D 영역 (승인 2026-10-23): 상단 띠와 좌·우 정보창을 캔버스 위에 얇게 띄움. 조작 요소 44px. */
/* 3D 영역 (승인 2026-10-23): 제목은 교각 목록으로. 좌측 스택(부재 표시 · 시점), 우측 스택(색 기준 · 원점)을 캔버스 위에 둠. */
#viewer-block { position: relative; }
#viewer-block .viewer-head { position: absolute; top: 8px; left: 8px; right: 8px; z-index: 5; margin: 0; height: 44px; padding: 0 10px; flex-wrap: nowrap; background: rgba(255, 255, 255, 0.92); border-radius: 6px; }
#viewer-block .viewer-head h2 { font-size: 14px; white-space: nowrap; }
#viewer-block .viewer-head #origin { margin-left: auto; font-size: 12px; white-space: nowrap; }
#viewer-block .info-panel { position: absolute; z-index: 5; top: 60px; max-height: calc(100% - 120px); overflow: auto; background: rgba(255, 255, 255, 0.92); border: 1px solid #d5dde6; border-radius: 6px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); padding: 0 8px 8px; font-size: 13px; }
#viewer-block .viewer-head { display: none; }
.viewer-stack { display: flex; flex-direction: column; gap: 8px; width: 168px; z-index: 5; pointer-events: none; }
.viewer-stack > * { pointer-events: auto; }
#viewer-block .viewer-stack-left { position: absolute; top: 8px; left: 8px; }
#viewer-block .viewer-stack-right { position: absolute; top: 8px; right: 8px; width: 180px; align-items: stretch; }
#viewer-block .info-panel { background: rgba(255, 255, 255, 0.92); border: 1px solid #d5dde6; border-radius: 6px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); padding: 0 8px 8px; font-size: 13px; }
#viewer-block .info-panel > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 700; color: #18212b; list-style-position: outside; }
#viewer-block .parts-info { left: 8px; width: 168px; }
#viewer-block .color-info { right: 8px; width: 180px; }
#viewer-block .parts-info #parts label { display: flex; align-items: center; gap: 8px; min-height: 44px; }
#viewer-block .parts-info .parts-row { display: flex; flex-direction: column; align-items: stretch; gap: 0; }
#viewer-block .color-info .rebar-color { display: flex; flex-direction: column; align-items: stretch; gap: 0; margin: 0; }
#viewer-block .color-info .rebar-color button { width: 100%; }
#viewer-block .color-info .rebar-legend { flex-direction: column; gap: 4px; padding: 6px 0 0; }
@media (max-width: 600px) {
#viewer-block .viewer-head { position: static; height: auto; flex-wrap: wrap; }
#viewer-block .info-panel { position: static; width: auto; max-height: none; margin: 8px 0 0; }
}
#viewer-block .parts-info .parts-row { display: flex; flex-direction: column; align-items: stretch; gap: 0; }
/* 좁은 창 (601–1100px, 승인 2026-10-23): 상단 띠에서 시점 버튼과 원점 표기를 오른쪽으로 */
@media (min-width: 601px) and (max-width: 1100px) {
#viewer-block .viewer-head #views-menu { margin-left: auto; }
}
/* 3D band (승인 2026-10-23): view menu, origin popover, collapsed panel summaries */
#viewer-block .views-menu { position: relative; margin-left: auto; }
#viewer-block .views-toggle { min-height: 44px; font-weight: 700; background: #eef3f8; border: 1px solid #c3cbd4; }
#viewer-block #views.views-list { position: absolute; top: 100%; right: 0; z-index: 10; display: flex; flex-direction: column; gap: 0; min-width: 120px; padding: 4px; margin-top: 4px; background: #ffffff; border: 1px solid #c3cbd4; border-radius: 6px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15); }
#viewer-block .views-menu { position: relative; }
#viewer-block .views-toggle { width: 100%; min-height: 44px; font-weight: 700; background: #eef3f8; border: 1px solid #c3cbd4; border-radius: 6px; }
#viewer-block #views.views-list { position: absolute; top: 100%; left: 0; right: 0; z-index: 10; display: flex; flex-direction: column; gap: 0; padding: 4px; margin-top: 4px; background: #ffffff; border: 1px solid #c3cbd4; border-radius: 6px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15); }
#viewer-block #views.views-list .btn { min-height: 44px; justify-content: flex-start; text-align: left; border: 0; background: transparent; }
#viewer-block #views.views-list .btn[aria-pressed='true'] { background: #1f5fa8; color: #ffffff; }
#viewer-block .origin-wrap { position: relative; align-self: flex-end; }
#viewer-block .info-btn { min-width: 44px; min-height: 44px; border: 1px solid #c3cbd4; border-radius: 6px; background: #ffffff; font-size: 16px; cursor: pointer; }
#viewer-block .origin-pop { position: absolute; top: 100%; right: 8px; z-index: 10; margin-top: 4px; padding: 6px 10px; background: #ffffff; border: 1px solid #c3cbd4; border-radius: 6px; font-size: 12px; white-space: nowrap; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); }
#viewer-block .origin-pop { position: absolute; top: 100%; right: 0; z-index: 10; margin-top: 4px; padding: 6px 10px; background: #ffffff; border: 1px solid #c3cbd4; border-radius: 6px; font-size: 12px; white-space: nowrap; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); }
#viewer-block .panel-state { font-weight: 400; color: #55606e; margin-left: 6px; font-size: 12px; }
@media (max-width: 600px) {
#viewer-block .viewer-head { display: flex; height: auto; margin: 0 0 8px; }
#viewer-block .viewer-stack { position: static; width: auto; margin-top: 8px; pointer-events: auto; }
#viewer-block .viewer-stack-left, #viewer-block .viewer-stack-right { position: static; width: auto; }
#viewer-block .origin-wrap { align-self: flex-start; }
}