Compare commits

..

2 Commits

Author SHA1 Message Date
08ec6f42d1 ui: measure editor sidebar height 2026-04-06 13:44:16 +09:00
360ec5ac3d ui: prevent title space scroll 2026-04-06 13:33:56 +09:00
3 changed files with 49 additions and 3 deletions

View File

@@ -1,5 +1,11 @@
# 의사결정 이력
## 2026-04-06 v1.4.97
- 티어표 편집기의 오른쪽 아이템 패널은 페이지 내부 위치가 헤더, 제목, 스크롤 상태에 따라 달라지므로 `100dvh - 고정값` 방식으로는 왼쪽 레일처럼 하단이 자연스럽게 맞지 않을 수 있다. 실제 패널의 화면 내 시작 위치를 측정해 남은 높이를 계산하는 편이 더 안정적이라고 정리했다.
## 2026-04-06 v1.4.96
- 템플릿 제목을 버튼화하면 접근성은 좋아지지만, 포커스가 남은 상태의 `Space` 입력이 브라우저 스크롤과 섞이면 작업 화면을 갑자기 밀어낼 수 있다. 따라서 제목 버튼에서는 `Space` 기본 스크롤을 막고 의도한 본문 이동만 실행하는 편이 맞다고 정리했다.
## 2026-04-06 v1.4.95
- 티어표 편집 중에는 공통 헤더보다 보드와 아이템 풀이 더 중요한 작업 기준점이므로, 템플릿 제목을 본문 위치로 빠르게 이동하는 가벼운 컨트롤로 활용하는 편이 좋다고 정리했다. 별도 버튼을 추가하기보다 기존 제목 클릭 동작으로 두어 화면 복잡도를 늘리지 않는 쪽을 택했다.

View File

@@ -1,5 +1,13 @@
# 업데이트 로그
## 2026-04-06 v1.4.97
- 티어표 편집 화면의 오른쪽 아이템 패널 높이를 고정 숫자 대신 실제 화면 내 시작 위치 기준으로 계산하도록 바꿨다. 공통 헤더/제목 영역/스크롤 위치가 달라져도 아이템 풀의 하단이 viewport 안에 더 자연스럽게 맞도록 보정했다.
- 확인: `npm run build`
## 2026-04-06 v1.4.96
- 티어표 편집 화면의 템플릿 제목에 포커스가 남은 상태에서 `Space`를 누르면 브라우저 기본 스크롤이 섞일 수 있어, 제목 버튼의 `Space` 기본 동작을 막고 본문 이동만 실행되도록 보정했다.
- 확인: `npm run build`
## 2026-04-06 v1.4.95
- 티어표 편집 화면의 템플릿 제목을 클릭하면 `workspaceBody`가 화면 최상단에 오도록 부드럽게 스크롤되게 했다. 작업 중 공통 헤더를 화면 밖으로 밀어내고 본문 중심으로 볼 수 있다.
- 확인: `npm run build`

View File

@@ -90,6 +90,7 @@ let editorLoadToken = 0
const boardEl = ref(null)
const exportBoardEl = ref(null)
const groupListEl = ref(null)
const sidebarEl = ref(null)
const poolEl = ref(null)
const groupDropEls = ref({})
const fileEl = ref(null)
@@ -97,6 +98,8 @@ const thumbnailFileEl = ref(null)
const groupSortable = ref(null)
const poolSortable = ref(null)
const dropSortables = ref([])
const editorSidebarMaxHeight = ref('')
let editorSidebarMeasureFrame = 0
const isNewTierList = computed(() => tierListId.value === 'new')
const isOwnTierList = computed(() => !!auth.user && !!ownerId.value && ownerId.value === auth.user.id)
@@ -364,6 +367,25 @@ function scrollWorkspaceBodyToTop() {
workspaceBody?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
function updateEditorSidebarMaxHeight() {
if (typeof window === 'undefined' || !sidebarEl.value) return
const bottomGap = 14
const stickyTop = 14
const minHeight = 260
const sidebarTop = Math.max(sidebarEl.value.getBoundingClientRect().top, stickyTop)
const nextHeight = Math.max(minHeight, Math.floor(window.innerHeight - sidebarTop - bottomGap))
editorSidebarMaxHeight.value = `${nextHeight}px`
}
function scheduleEditorSidebarMeasure() {
if (typeof window === 'undefined') return
if (editorSidebarMeasureFrame) return
editorSidebarMeasureFrame = window.requestAnimationFrame(() => {
editorSidebarMeasureFrame = 0
updateEditorSidebarMaxHeight()
})
}
function openItemContextMenu(itemId, event) {
if (!canEdit.value || !itemId || !itemsById.value[itemId] || shouldIgnoreItemClick()) return
selectedItemId.value = itemId
@@ -1355,6 +1377,7 @@ async function loadEditorState() {
if (loadToken !== editorLoadToken) return
syncSavedEditorSnapshot()
scheduleEditorSidebarMeasure()
if (canEdit.value) {
await initSortables()
}
@@ -1374,6 +1397,9 @@ onMounted(() => {
window.addEventListener('contextmenu', handleGlobalContextMenu, true)
window.addEventListener('blur', closeItemContextMenu)
window.addEventListener('scroll', closeItemContextMenu, true)
window.addEventListener('resize', scheduleEditorSidebarMeasure)
window.addEventListener('scroll', scheduleEditorSidebarMeasure, true)
nextTick(() => scheduleEditorSidebarMeasure())
})
onUnmounted(() => {
@@ -1382,6 +1408,12 @@ onUnmounted(() => {
window.removeEventListener('contextmenu', handleGlobalContextMenu, true)
window.removeEventListener('blur', closeItemContextMenu)
window.removeEventListener('scroll', closeItemContextMenu, true)
window.removeEventListener('resize', scheduleEditorSidebarMeasure)
window.removeEventListener('scroll', scheduleEditorSidebarMeasure, true)
if (editorSidebarMeasureFrame) {
window.cancelAnimationFrame(editorSidebarMeasureFrame)
editorSidebarMeasureFrame = 0
}
}
if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
destroySortables()
@@ -1592,6 +1624,7 @@ onUnmounted(() => {
type="button"
title="본문을 화면 위로 이동"
@click="scrollWorkspaceBodyToTop"
@keydown.space.prevent="scrollWorkspaceBodyToTop"
>
{{ templateName || templateId }}
</button>
@@ -1768,7 +1801,7 @@ onUnmounted(() => {
</div>
</div>
<div class="sidebar">
<div ref="sidebarEl" class="sidebar" :style="{ '--editor-sidebar-max-height': editorSidebarMaxHeight || undefined }">
<div class="sidebar__titleRow">
<div class="sidebar__title">아이템</div>
<div class="sidebar__count">{{ visiblePoolCount }} / {{ pool.length }}</div>
@@ -2773,7 +2806,6 @@ onUnmounted(() => {
object-fit: cover;
}
.sidebar {
--editor-sidebar-visible-offset: 136px;
min-width: 0;
display: flex;
flex-direction: column;
@@ -2784,7 +2816,7 @@ onUnmounted(() => {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
position: sticky;
top: 14px;
max-height: calc(100dvh - var(--editor-sidebar-visible-offset));
max-height: var(--editor-sidebar-max-height, calc(100dvh - 136px));
overflow: hidden;
overscroll-behavior: contain;
}