Compare commits

...

6 Commits

Author SHA1 Message Date
360ec5ac3d ui: prevent title space scroll 2026-04-06 13:33:56 +09:00
71a13488d9 ui: let editor title focus workspace 2026-04-06 13:32:56 +09:00
ba9ba8013b ui: constrain tier editor item scroll 2026-04-06 13:28:31 +09:00
da35351747 ui: add editor tips panel 2026-04-06 12:41:46 +09:00
305160663d ui: restore rail menu spacing 2026-04-06 12:32:28 +09:00
58b8df51ab ui: improve modal escape and rail wrapping 2026-04-06 12:30:49 +09:00
5 changed files with 203 additions and 4 deletions

View File

@@ -1,5 +1,24 @@
# 의사결정 이력
## 2026-04-06 v1.4.96
- 템플릿 제목을 버튼화하면 접근성은 좋아지지만, 포커스가 남은 상태의 `Space` 입력이 브라우저 스크롤과 섞이면 작업 화면을 갑자기 밀어낼 수 있다. 따라서 제목 버튼에서는 `Space` 기본 스크롤을 막고 의도한 본문 이동만 실행하는 편이 맞다고 정리했다.
## 2026-04-06 v1.4.95
- 티어표 편집 중에는 공통 헤더보다 보드와 아이템 풀이 더 중요한 작업 기준점이므로, 템플릿 제목을 본문 위치로 빠르게 이동하는 가벼운 컨트롤로 활용하는 편이 좋다고 정리했다. 별도 버튼을 추가하기보다 기존 제목 클릭 동작으로 두어 화면 복잡도를 늘리지 않는 쪽을 택했다.
## 2026-04-06 v1.4.94
- 아이템 수가 많을 때 오른쪽 풀 때문에 페이지 전체가 길어지면 왼쪽 티어표까지 함께 움직여 방송/녹화 환경에서 기준 화면이 흔들릴 수 있다. 그래서 데스크톱에서는 오른쪽 사이드의 실제 화면 시작 위치를 감안해 높이를 제한하되, 제목과 검색창은 유지하고 아이템 그리드만 스크롤되게 하는 편이 더 적절하다고 정리했다. 모바일에서는 기존처럼 문서 흐름을 유지한다.
## 2026-04-06 v1.4.93
- 티어표 편집기의 커스텀 이미지 추가 영역 아래는 아이템 수가 적을 때 비어 보이기 쉬우므로, 이 공간에는 큰 기능보다 방해되지 않는 작은 작업 팁을 두는 편이 자연스럽다고 정리했다. 특히 우클릭 복제, 미사용 아이템 처리, 브라우저 확대/축소처럼 초반 시행착오를 줄여 주는 내용이 효과적이라고 판단했다.
## 2026-04-06 v1.4.92
- 모바일 왼쪽 레일은 사용자 카드, 검색, 메뉴가 세로로 붙는 구조라 기본 `gap`이 빠지면 브라우저별 렌더링 차이에 따라 훨씬 답답하게 보일 수 있으므로, 이 영역 간격은 명시적으로 주는 편이 안전하다고 정리했다.
## 2026-04-06 v1.4.91
- 관리자 화면 모달이 많아질수록 `Esc` 동작이 일부 모달에서만 먹으면 예측 가능성이 떨어지므로, 열려 있는 공통 모달은 모두 `Esc = 취소` 규칙으로 맞추는 편이 더 자연스럽다고 정리했다.
- 왼쪽 레일 사용자 카드의 두 번째 줄은 로그인된 상태에선 이메일이라 말줄임이 맞지만, 로그인 전/확인 중 메시지는 설명 성격이 강하므로 같은 `nowrap` 규칙을 쓰면 가로 스크롤을 유발할 수 있다. 그래서 이메일과 설명 문구의 줄 처리 정책을 분리하는 쪽이 맞다고 판단했다.
## 2026-04-06 v1.4.90
- `templateSettingsCard__actions`는 카드 안에서 버튼이 줄바꿈될 수 있어야 하지만, 공통 버튼 스타일의 높이 100% 규칙까지 그대로 받으면 줄바꿈된 행이 비정상적으로 늘어날 수 있으므로 이 영역의 버튼만 높이를 자동으로 되돌리는 편이 맞다고 정리했다.
- 또 템플릿 기본 아이템 삭제는 “기존 저장 티어표에는 영향 없음”이라는 정책 설명이 중요하므로, 브라우저 기본 확인창보다 관리자 공통 모달로 통일해 같은 톤과 문구 체계 안에서 보여주는 쪽이 더 낫다고 판단했다.

View File

@@ -1,5 +1,29 @@
# 업데이트 로그
## 2026-04-06 v1.4.96
- 티어표 편집 화면의 템플릿 제목에 포커스가 남은 상태에서 `Space`를 누르면 브라우저 기본 스크롤이 섞일 수 있어, 제목 버튼의 `Space` 기본 동작을 막고 본문 이동만 실행되도록 보정했다.
- 확인: `npm run build`
## 2026-04-06 v1.4.95
- 티어표 편집 화면의 템플릿 제목을 클릭하면 `workspaceBody`가 화면 최상단에 오도록 부드럽게 스크롤되게 했다. 작업 중 공통 헤더를 화면 밖으로 밀어내고 본문 중심으로 볼 수 있다.
- 확인: `npm run build`
## 2026-04-06 v1.4.94
- 티어표 편집 화면에서 아이템이 많을 때 오른쪽 아이템 사이드가 문서 높이를 밀어 왼쪽 티어표까지 함께 움직이던 흐름을 줄였다. 데스크톱에서는 오른쪽 사이드의 실제 화면 시작 위치를 감안해 높이를 제한하고, 아이템 그리드만 내부 스크롤되게 해 검색창은 위에 유지하면서 필요한 아이템을 찾아 가져올 수 있게 했다.
- 확인: `npm run build`
## 2026-04-06 v1.4.93
- 티어표 편집 화면의 커스텀 이미지 추가 영역 아래에는 작은 `작업 팁` 안내를 추가했다. 복수 사용, 미사용 아이템 미리보기/저장 제외, 브라우저 확대/축소 활용 같은 자주 묻는 흐름을 바로 확인할 수 있다.
- 확인: `npm run build`
## 2026-04-06 v1.4.92
- 모바일 왼쪽 사이드 메뉴(`leftRail__mobileMenu`)에 `gap`이 빠져 일부 브라우저에서 사용자 카드와 검색창/메뉴가 더 붙어 보일 수 있던 간격을 다시 정리했다.
## 2026-04-06 v1.4.91
- 관리자 화면의 각종 모달은 이제 `Esc` 키를 누르면 현재 열려 있는 모달이 바로 닫히도록 정리했다. 브라우저 기본 동작 대신 공통 `취소` 흐름으로 맞췄다.
- 왼쪽 사이드에서 일부 브라우저 환경에 가로 스크롤이 생기던 문제를 보정했다. 사용자 카드와 검색창에 `min-width: 0`을 더 명확히 주고, 이메일은 계속 말줄임 처리하되 로그인 전 안내 문구처럼 긴 설명은 자연스럽게 줄바꿈되도록 분리했다.
- 확인: `npm run build`
## 2026-04-06 v1.4.90
- `templateSettingsCard__actions` 내부 버튼은 좁은 화면에서 과하게 세로로 늘어나지 않도록 버튼 높이를 자동으로 풀고, 카드 너비 안에서 자연스럽게 줄바꿈되도록 다시 보정했다.
- 템플릿 기본 아이템 삭제 확인은 브라우저 기본 `confirm` 대신 관리자 공통 모달로 바꿨다. 저장된 다른 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외된다는 안내도 모달 안에서 함께 보여준다.

View File

@@ -62,6 +62,7 @@ const accountEmail = computed(() => {
if (!authReady.value) return '계정 상태를 확인하고 있어요.'
return (auth.user?.email || '').trim() || '로그인 후 개인 메뉴를 사용할 수 있어요.'
})
const isAccountEmailHint = computed(() => !auth.user)
const shellStyle = computed(() => ({
'--left-rail-width': leftRailCollapsed.value ? '76px' : '248px',
'--right-rail-width': !isRightRailOverlay.value && rightRailOpen.value ? '325px' : '0px',
@@ -529,7 +530,7 @@ function reloadApp() {
<div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div>
<div class="appUserCard__meta">
<div class="appUserCard__name">{{ accountName }}</div>
<div class="appUserCard__email">{{ accountEmail }}</div>
<div class="appUserCard__email" :class="{ 'appUserCard__email--hint': isAccountEmailHint }">{{ accountEmail }}</div>
</div>
<button
v-if="isMobileLayout"
@@ -982,6 +983,7 @@ function reloadApp() {
.appUserCard__button,
.appUserCard__guest {
width: 100%;
min-width: 0;
display: flex;
gap: 12px;
align-items: center;
@@ -1022,6 +1024,7 @@ function reloadApp() {
.leftRail__mobileMenu {
display: grid;
min-width: 0;
}
.appUserCard__navToggle {
@@ -1042,18 +1045,30 @@ function reloadApp() {
.appUserCard__name {
font-size: 14px;
font-weight: 800;
min-width: 0;
overflow-wrap: anywhere;
}
.appUserCard__email {
font-size: 12px;
color: var(--theme-text-muted);
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.appUserCard__email--hint {
white-space: normal;
overflow: visible;
text-overflow: unset;
overflow-wrap: anywhere;
word-break: keep-all;
}
.searchStub {
width: 100%;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;

View File

@@ -419,6 +419,26 @@ function handleAdminPopState() {
function handleAdminKeydown(event) {
if (event.key !== 'Escape') return
if (previewModalOpen.value) {
event.preventDefault()
closePreviewModal()
return
}
if (imageResetModalOpen.value) {
event.preventDefault()
closeImageResetModal()
return
}
if (adminTierListManageModalOpen.value) {
event.preventDefault()
closeAdminTierListManageModal()
return
}
if (templateItemDeleteModalOpen.value) {
event.preventDefault()
closeTemplateItemDeleteModal()
return
}
if (customItemDeleteModalOpen.value) {
event.preventDefault()
closeCustomItemDeleteModal()
@@ -429,9 +449,55 @@ function handleAdminKeydown(event) {
closeCustomItemModal()
return
}
if (previewModalOpen.value) {
if (templatePickerModalOpen.value) {
event.preventDefault()
closePreviewModal()
closeTemplatePickerModal()
return
}
if (templateBulkTagModalOpen.value) {
event.preventDefault()
closeTemplateBulkTagModal()
return
}
if (templateLibraryItemModalOpen.value) {
event.preventDefault()
closeTemplateLibraryItemModal()
return
}
if (templateSourceImportModalOpen.value) {
event.preventDefault()
closeTemplateSourceImportModal()
return
}
if (importModalOpen.value) {
event.preventDefault()
closeTierListImportModal()
return
}
if (userRoleModalOpen.value) {
event.preventDefault()
closeUserRoleModal()
return
}
if (userDeleteModalOpen.value) {
event.preventDefault()
closeUserDeleteModal()
return
}
if (userPasswordModalOpen.value) {
event.preventDefault()
closeUserPasswordModal()
return
}
if (userEditModalOpen.value) {
event.preventDefault()
closeUserEditModal()
return
}
if (templateCreateModalOpen.value) {
event.preventDefault()
closeTemplateCreateModal()
return
}
}

View File

@@ -359,6 +359,11 @@ function closeItemContextMenu() {
}
}
function scrollWorkspaceBodyToTop() {
const workspaceBody = document.querySelector('.workspaceBody')
workspaceBody?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
function openItemContextMenu(itemId, event) {
if (!canEdit.value || !itemId || !itemsById.value[itemId] || shouldIgnoreItemClick()) return
selectedItemId.value = itemId
@@ -1582,7 +1587,15 @@ onUnmounted(() => {
<div class="editorMain">
<section class="head">
<div class="editorMain__headCopy">
<div class="editorMain__title">{{ templateName || templateId }}</div>
<button
class="editorMain__title editorMain__titleButton"
type="button"
title="본문을 화면 위로 이동"
@click="scrollWorkspaceBodyToTop"
@keydown.space.prevent="scrollWorkspaceBodyToTop"
>
{{ templateName || templateId }}
</button>
<div class="editorMain__subtitle">
<template v-if="canEdit">
/ 이름과 순서를 바꾸고 아이템을 드래그해서 배치할 있어요.
@@ -1746,6 +1759,14 @@ onUnmounted(() => {
<button class="btn btn--ghost btn--small dropzone__button" @click="openFile">파일 선택</button>
</div>
</div>
<div class="editorTips">
<div class="editorTips__title">작업 </div>
<ul class="editorTips__list">
<li>마우스 오른쪽 클릭으로 아이템을 복수 사용하거나 커스텀 이미지를 빠르게 정리할 있어요.</li>
<li>미사용 아이템은 미리보기와 이미지 저장 결과에 표시되지 않으니, 필요한 것만 골라 배치해도 괜찮아요.</li>
<li>아이템이 많아 번에 보기 어렵다면 브라우저 확대/축소(`Ctrl +`, `Ctrl -`) 화면 밀도를 조절해보세요.</li>
</ul>
</div>
</div>
<div class="sidebar">
@@ -1966,6 +1987,24 @@ onUnmounted(() => {
font-weight: 900;
letter-spacing: -0.04em;
}
.editorMain__titleButton {
width: fit-content;
max-width: 100%;
border: 0;
padding: 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.editorMain__titleButton:hover {
color: var(--theme-text-strong);
}
.editorMain__titleButton:focus-visible {
outline: 2px solid color-mix(in srgb, var(--theme-accent) 70%, white);
outline-offset: 4px;
border-radius: 8px;
}
.editorMain__subtitle {
color: var(--theme-text-soft);
font-size: 13px;
@@ -2735,7 +2774,10 @@ onUnmounted(() => {
object-fit: cover;
}
.sidebar {
--editor-sidebar-visible-offset: 136px;
min-width: 0;
display: flex;
flex-direction: column;
border: 1px solid var(--theme-border);
background: linear-gradient(180deg, color-mix(in srgb, var(--theme-card-bg) 94%, transparent), color-mix(in srgb, var(--theme-card-bg-hover) 88%, transparent));
border-radius: 22px;
@@ -2743,6 +2785,9 @@ 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));
overflow: hidden;
overscroll-behavior: contain;
}
.dropzone--board {
@@ -3057,6 +3102,9 @@ onUnmounted(() => {
color: var(--theme-text-faint);
}
.pool {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 10px;
@@ -3065,6 +3113,30 @@ onUnmounted(() => {
.pool--clickTarget {
cursor: copy;
}
.editorTips {
margin-top: 14px;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: color-mix(in srgb, var(--theme-card-bg) 82%, transparent);
}
.editorTips__title {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--theme-text-soft);
}
.editorTips__list {
margin: 8px 0 0;
padding-left: 16px;
color: var(--theme-text-faint);
font-size: 12px;
line-height: 1.55;
}
.editorTips__list li + li {
margin-top: 6px;
}
.poolItem {
position: relative;
min-width: 0;
@@ -3227,8 +3299,11 @@ onUnmounted(() => {
}
.sidebar {
position: static;
max-height: none;
overflow: visible;
}
.pool {
overflow: visible;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
}