릴리스: v1.3.10 게임 허브 카드 폭과 SVG 아이콘 렌더링 정리
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
# 할 일 및 이슈
|
||||
|
||||
## 즉시 확인 필요
|
||||
- SVG 파일을 원본 소스로 보유하고 있는데 전부 img 형식으로 사용되고 있는것 같음. 수정 보완 필요.
|
||||
- 티어표 형식 추가 필요. 최근 게임들은 S, A, B,C 같은 랭크 뿐만 아니라 가로 열도 나누어진형태의 티어표를 원함 (공격, 방어, 지원 등 각 파트별 랭크를 보고싶어함)
|
||||
- 레거시 파일 정리 스크립트는 준비됐으므로, 운영 단계에서는 cron 등으로 주기 실행할지와 삭제 전 보관 기간을 함께 정한다.
|
||||
- 관리자 기본 아이템 다중 업로드는 현재 파일명 기반 자동 라벨만 지원하므로, 필요하면 업로드 후 일괄 라벨 수정/정렬 UX를 추가 검토한다.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## 2026-04-01 v1.3.10
|
||||
- 게임 허브 공개 티어표 카드 그리드는 최소/최대 폭을 고정해, 목록이 1~2장뿐일 때도 카드가 화면 전체를 먹으며 과하게 커지지 않도록 보정함.
|
||||
- 티어표 행 삭제는 상단 아이콘 대신 우측 하단의 작은 텍스트 액션으로 바꿔, 랭크 카드 안에서 더 조용하고 정돈된 편집 흐름으로 정리함.
|
||||
- 공통 `SvgIcon` 컴포넌트를 추가하고 앱 셸, 홈 즐겨찾기, 관리자 회원 액션 같은 UI 아이콘은 `img` 대신 SVG 아이콘 컴포넌트로 렌더링하도록 전환함.
|
||||
|
||||
## 2026-04-01 v1.3.9
|
||||
- 관리자 오른쪽 사이드의 Image Optimization 패널은 이제 기본 탭인 목록 관리에서만 노출되도록 줄여, 게임/아이템/티어표/회원 관리 화면에서는 실제 작업 패널에 더 집중할 수 있게 정리함.
|
||||
- 커스텀 아이템 상세의 '이미 사용 중인 게임' 목록에서는 개인 보드용 freeform 템플릿을 제외하고, 실제 템플릿에 연결된 게임만 보이도록 다듬음.
|
||||
|
||||
@@ -12,6 +12,7 @@ import iconLists from './assets/icons/lists.svg'
|
||||
import iconSearch from './assets/icons/search.svg'
|
||||
import iconSettings from './assets/icons/settings.svg'
|
||||
import RightRailAd from './components/RightRailAd.vue'
|
||||
import SvgIcon from './components/SvgIcon.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -302,7 +303,7 @@ function submitGlobalSearch() {
|
||||
<aside class="leftRail">
|
||||
<div class="leftRail__top railHeader">
|
||||
<button v-if="!isMobileLayout" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="왼쪽 패널 토글" @click="toggleLeftRail">
|
||||
<img :src="iconDockToRight" alt="" />
|
||||
<SvgIcon :src="iconDockToRight" :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -322,7 +323,7 @@ function submitGlobalSearch() {
|
||||
<form class="searchStub" @submit.prevent="submitGlobalSearch">
|
||||
<button class="searchStub__iconButton" type="button" :aria-label="leftRailCollapsed ? '검색 열기' : '검색'" @click="handleLeftRailSearch">
|
||||
<span class="searchStub__icon">
|
||||
<img :src="iconSearch" alt="" />
|
||||
<SvgIcon :src="iconSearch" :size="24" />
|
||||
</span>
|
||||
</button>
|
||||
<input v-model="searchQuery" class="searchStub__input" type="search" :placeholder="leftRailCollapsed ? '' : searchPlaceholder" />
|
||||
@@ -339,7 +340,7 @@ function submitGlobalSearch() {
|
||||
:aria-label="leftRailCollapsed ? item.label : undefined"
|
||||
>
|
||||
<span class="leftNav__glyph">
|
||||
<img v-if="item.iconSrc" :src="item.iconSrc" alt="" />
|
||||
<SvgIcon v-if="item.iconSrc" :src="item.iconSrc" :size="24" />
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true"><path :d="item.icon" /></svg>
|
||||
</span>
|
||||
<span class="leftNav__label">{{ item.label }}</span>
|
||||
@@ -365,14 +366,14 @@ function submitGlobalSearch() {
|
||||
<div class="workspaceHead__actions">
|
||||
<div v-if="showGameHubViewToggle" class="viewToggle" role="group" aria-label="티어표 보기 방식">
|
||||
<button class="ghostIcon ghostIcon--iconOnly" :class="{ 'ghostIcon--active': gameHubViewMode === 'grid' }" type="button" aria-label="그리드 보기" @click="setGameHubViewMode('grid')">
|
||||
<img :src="iconGridView" alt="" />
|
||||
<SvgIcon :src="iconGridView" :size="24" />
|
||||
</button>
|
||||
<button class="ghostIcon ghostIcon--iconOnly" :class="{ 'ghostIcon--active': gameHubViewMode === 'list' }" type="button" aria-label="리스트 보기" @click="setGameHubViewMode('list')">
|
||||
<img :src="iconLists" alt="" />
|
||||
<SvgIcon :src="iconLists" :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
<button v-if="!rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 열기" @click="toggleRightRail">
|
||||
<img :src="iconDockToLeft" alt="" />
|
||||
<SvgIcon :src="iconDockToLeft" :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -385,7 +386,7 @@ function submitGlobalSearch() {
|
||||
<div v-if="isCollapsedSearchOpen" class="collapsedSearchModal" role="dialog" aria-modal="true" :aria-label="searchPlaceholder" @click.self="closeCollapsedSearch">
|
||||
<form class="collapsedSearchBar" @submit.prevent="submitGlobalSearch">
|
||||
<span class="collapsedSearchBar__icon">
|
||||
<img :src="iconSearch" alt="" />
|
||||
<SvgIcon :src="iconSearch" :size="24" />
|
||||
</span>
|
||||
<input v-model="searchQuery" class="collapsedSearchBar__input" type="search" :placeholder="searchPlaceholder" autofocus />
|
||||
</form>
|
||||
@@ -396,7 +397,7 @@ function submitGlobalSearch() {
|
||||
<aside class="rightRail" :class="{ 'rightRail--closed': !rightRailOpen, 'rightRail--overlay': isRightRailOverlay }" :aria-hidden="!rightRailOpen">
|
||||
<div class="rightRail__top railHeader">
|
||||
<button v-if="rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 닫기" @click="toggleRightRail">
|
||||
<img :src="iconDockToLeft" alt="" />
|
||||
<SvgIcon :src="iconDockToLeft" :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="rightRail__body">
|
||||
|
||||
38
frontend/src/components/SvgIcon.vue
Normal file
38
frontend/src/components/SvgIcon.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
src: { type: String, required: true },
|
||||
size: { type: [Number, String], default: 20 },
|
||||
color: { type: String, default: 'currentColor' },
|
||||
})
|
||||
|
||||
const normalizedSize = computed(() => (typeof props.size === "number" ? `${props.size}px` : props.size))
|
||||
const iconStyle = computed(() => ({
|
||||
"--svg-icon-src": `url("${props.src}")`,
|
||||
"--svg-icon-size": normalizedSize.value,
|
||||
"--svg-icon-color": props.color,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="svgIcon" :style="iconStyle" aria-hidden="true"></span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.svgIcon {
|
||||
display: inline-block;
|
||||
width: var(--svg-icon-size);
|
||||
height: var(--svg-icon-size);
|
||||
background-color: var(--svg-icon-color);
|
||||
-webkit-mask-image: var(--svg-icon-src);
|
||||
mask-image: var(--svg-icon-src);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import { api } from '../lib/api'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
import lockResetIcon from '../assets/icons/lock_reset.svg'
|
||||
import deleteIcon from '../assets/icons/delete.svg'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useToast } from '../composables/useToast'
|
||||
|
||||
@@ -1573,7 +1574,7 @@ async function saveFeaturedOrder() {
|
||||
:disabled="user.isAvatarBusy"
|
||||
@click.stop="removeUserAvatar(user)"
|
||||
>
|
||||
<img :src="deleteIcon" alt="" />
|
||||
<SvgIcon class="userAvatarRemoveIcon" :src="deleteIcon" :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="userCard__identityMeta">
|
||||
@@ -1604,10 +1605,10 @@ async function saveFeaturedOrder() {
|
||||
|
||||
<div class="userCard__actions userCard__actions--compact">
|
||||
<button class="iconActionButton" type="button" title="비밀번호 초기화" @click="openUserPasswordModal(user)">
|
||||
<img :src="lockResetIcon" alt="" />
|
||||
<SvgIcon class="iconActionButton__icon" :src="lockResetIcon" :size="18" />
|
||||
</button>
|
||||
<button class="iconActionButton iconActionButton--danger" type="button" title="회원 삭제" @click="openUserDeleteModal(user)">
|
||||
<img :src="deleteIcon" alt="" />
|
||||
<SvgIcon class="iconActionButton__icon" :src="deleteIcon" :size="18" />
|
||||
</button>
|
||||
<button class="btn btn--ghost userSaveButton" :disabled="!isUserDirty(user)" @click="saveUser(user)">회원정보 저장</button>
|
||||
</div>
|
||||
@@ -2928,10 +2929,8 @@ async function saveFeaturedOrder() {
|
||||
transform: translateY(2px) scale(0.96);
|
||||
transition: opacity 160ms ease, transform 160ms ease, background 160ms ease, visibility 160ms ease;
|
||||
}
|
||||
.userAvatarRemoveButton img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
filter: brightness(0) invert(1);
|
||||
.userAvatarRemoveIcon {
|
||||
color: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
.userAvatarRemoveButton:disabled {
|
||||
opacity: 0.45;
|
||||
@@ -3033,9 +3032,8 @@ async function saveFeaturedOrder() {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
.iconActionButton img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
.iconActionButton__icon {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
.iconActionButton:disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -222,7 +222,8 @@ function submitSearch() {
|
||||
}
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 320px));
|
||||
justify-content: start;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '../lib/api'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import kidStarIcon from '../assets/icons/kid_star.svg'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
@@ -92,7 +93,7 @@ function thumbUrl(g) {
|
||||
:disabled="loadingFavoriteId === g.id"
|
||||
@click.stop="toggleFavorite(g, $event)"
|
||||
>
|
||||
<img class="libraryCard__favoriteIcon" :src="kidStarIcon" alt="" aria-hidden="true" />
|
||||
<SvgIcon class="libraryCard__favoriteIcon" :src="kidStarIcon" :size="18" />
|
||||
</button>
|
||||
<button class="libraryCard__main" type="button" @click="goGame(g.id)">
|
||||
<div class="libraryCard__thumbWrap">
|
||||
@@ -179,15 +180,12 @@ function thumbUrl(g) {
|
||||
border-color: rgba(255, 216, 107, 0.28);
|
||||
}
|
||||
.libraryCard__favoriteIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
opacity: 0.76;
|
||||
filter: brightness(0) saturate(100%) invert(100%);
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
}
|
||||
.libraryCard__favorite--active .libraryCard__favoriteIcon {
|
||||
opacity: 1;
|
||||
filter: brightness(0) saturate(100%) invert(86%) sepia(45%) saturate(529%) hue-rotate(351deg) brightness(103%) contrast(101%);
|
||||
color: #ffd86b;
|
||||
}
|
||||
.libraryCard__thumbWrap {
|
||||
width: 100%;
|
||||
|
||||
@@ -919,13 +919,13 @@ onUnmounted(() => {
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="rowRemoveIcon"
|
||||
class="rowRemoveText"
|
||||
type="button"
|
||||
title="티어 라인 삭제"
|
||||
:disabled="groups.length <= 1"
|
||||
@click="openGroupDeleteModal(g.id)"
|
||||
>
|
||||
×
|
||||
열 삭제
|
||||
</button>
|
||||
<span class="grab" title="드래그로 순서 변경" data-group-handle>↕</span>
|
||||
<input v-model="g.name" class="groupName" :readonly="!canEdit" />
|
||||
@@ -1562,7 +1562,7 @@ onUnmounted(() => {
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 12px;
|
||||
padding: 10px 12px 30px;
|
||||
font-weight: 900;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1590,29 +1590,24 @@ onUnmounted(() => {
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.rowRemoveIcon {
|
||||
.rowRemoveText {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
background: rgba(15, 15, 15, 0.7);
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
right: 12px;
|
||||
bottom: 10px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
}
|
||||
.rowRemoveIcon:hover {
|
||||
background: rgba(70, 20, 20, 0.82);
|
||||
border-color: rgba(239, 68, 68, 0.34);
|
||||
.rowRemoveText:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.rowRemoveIcon:disabled {
|
||||
opacity: 0.4;
|
||||
.rowRemoveText:disabled {
|
||||
opacity: 0.32;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.row__exportName {
|
||||
|
||||
Reference in New Issue
Block a user