Compare commits

...

6 Commits

5 changed files with 219 additions and 79 deletions

View File

@@ -1,5 +1,29 @@
# 업데이트 로그
## 2026-03-31 v1.2.67
- 홈 화면 게임 템플릿 즐겨찾기 버튼 위치 변경은 유지하면서, 즐겨찾기 on/off 시 카드가 즉시 튀지 않고 부드럽게 재정렬되도록 이동/페이드 전환을 추가함.
- 별 아이콘을 눌렀을 때 카드가 즐겨찾기 우선순위 위치로 자연스럽게 이동해 전체 라이브러리 전환감이 덜 거칠게 보이도록 보정함.
## 2026-03-31 v1.2.66
- 내 티어표 카드 하단의 큰 삭제 버튼은 제거하고, 삭제는 상세 편집 화면에서만 하도록 흐름을 단순화함.
- 내 티어표 카드 그리드를 고정 4/3/2열에서 `auto-fit` 기반 최소 폭 카드로 바꾸고, 제목/메타가 좁은 화면에서도 말줄임과 유연한 폭 계산을 유지하도록 보정함.
## 2026-03-31 v1.2.65
- 에디터 옵션 토글의 라벨과 스위치 순서를 바꾼 뒤 체크 상태 셀렉터가 끊긴 문제를 보정해, 왼쪽 라벨·오른쪽 스위치 배치에서도 정상 동작하도록 수정함.
- 왼쪽 사이드 축소 상태 검색은 전용 모달의 기본 스타일이 빠져 있던 문제를 복구해, 다시 중앙 상단 검색 오버레이로 열리도록 정리함.
## 2026-03-31 v1.2.64
- 메인 콘텐츠가 길어질 때 스크롤 끝이 화면 바닥에 붙지 않도록 중앙 워크스페이스 하단 여백을 추가하고, 긴 작업 화면에서도 마감선이 답답하지 않게 보정함.
- 템플릿 요청 모달 입력창을 Settings 화면과 같은 어두운 언더라인 입력 문법으로 통일하고, 에디터의 공개/이름 표시 옵션은 체크박스 대신 스위치형 토글로 재구성함.
## 2026-03-31 v1.2.63
- 앱 셸과 워크스페이스에 걸려 있던 고정 `100dvh` 높이를 풀어, 본문이 길어질 때 중앙 `main` 영역이 잘리거나 접히는 현상을 보정함.
- 좌우 레일은 그대로 화면 기준 높이를 유지하되, 중앙 작업 영역은 내용만큼 자연스럽게 늘어나도록 높이 계산을 다시 정리함.
## 2026-03-31 v1.2.62
- 템플릿 요청 모달의 제목/설명 입력을 Settings 화면과 같은 어두운 입력 문법으로 맞춰 흰 배경/흰 글자처럼 보이던 문제를 정리함.
- 앱 셸은 사이드 기본 바탕색을 중심으로 재정리하고, 중앙 바디에 배경과 좌우 보더를 줘 긴 스크롤에서도 사이드가 잘리는 듯한 인상을 줄이도록 조정함.
## 2026-03-31 v1.2.61
- Game Library 왼쪽 검색을 전체 티어표 검색이 아니라 게임 템플릿 검색으로 바꾸고, 홈 화면에서 검색어에 맞는 게임만 필터링하도록 조정함.
- 게임 템플릿에 사용자별 즐겨찾기 별 아이콘을 추가하고, 즐겨찾기한 게임이 관리자 고정 순서보다 우선 노출되도록 백엔드와 홈 화면을 함께 확장함.

View File

@@ -416,12 +416,9 @@ function submitGlobalSearch() {
<style scoped>
.appShell {
min-height: 100dvh;
height: 100dvh;
display: grid;
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr) var(--right-rail-width, 320px);
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.04), transparent 28%),
linear-gradient(180deg, #1a1a1a 0%, #121212 100%);
background: rgba(14, 14, 14, 0.96);
color: rgba(255, 255, 255, 0.92);
transition: grid-template-columns 220ms ease;
}
@@ -433,7 +430,6 @@ function submitGlobalSearch() {
.leftRail,
.rightRail {
min-height: 100dvh;
height: 100dvh;
border-right: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(14, 14, 14, 0.92);
box-sizing: border-box;
@@ -499,7 +495,7 @@ function submitGlobalSearch() {
}
.leftRail__body {
max-height: calc(100vh - 56px);
max-height: calc(100dvh - 56px);
}
.rightRail__body {
@@ -792,7 +788,11 @@ function submitGlobalSearch() {
.appMain {
min-width: 0;
min-height: 0;
box-sizing: border-box;
background: rgba(18, 18, 18, 0.98);
border-left: 1px solid rgba(255, 255, 255, 0.08);
border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.appMain--preview {
@@ -804,7 +804,6 @@ function submitGlobalSearch() {
grid-template-rows: 56px minmax(0, 1fr);
gap: 0;
min-height: 100dvh;
height: 100dvh;
}
.workspace--localRail {
@@ -846,22 +845,22 @@ function submitGlobalSearch() {
.workspaceBody {
min-height: 0;
padding: 0;
padding: 18px 18px 32px;
border: 0;
border-radius: 0;
background: transparent;
background: rgba(24, 24, 24, 0.92);
box-shadow: none;
margin: 18px 18px 0;
margin: 0;
}
.workspaceBody--localRail {
min-height: 0;
padding: 0;
padding: 18px 18px 32px;
border: 0;
border-radius: 0;
background: transparent;
background: rgba(24, 24, 24, 0.92);
box-shadow: none;
margin: 18px 18px 0;
margin: 0;
}
.rightRail {
@@ -894,6 +893,60 @@ function submitGlobalSearch() {
display: none;
}
.collapsedSearchModal {
position: fixed;
inset: 0;
z-index: 35;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 88px 20px 20px;
background: rgba(0, 0, 0, 0.44);
backdrop-filter: blur(6px);
}
.collapsedSearchBar {
width: min(520px, calc(100vw - 32px));
display: flex;
align-items: center;
gap: 14px;
padding: 18px 22px;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(26, 26, 26, 0.96);
box-shadow: 0 28px 60px rgba(0, 0, 0, 0.34);
}
.collapsedSearchBar__icon {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.collapsedSearchBar__icon img {
width: 28px;
height: 28px;
display: block;
filter: brightness(0) saturate(100%) invert(94%) sepia(6%) saturate(207%) hue-rotate(186deg) brightness(96%) contrast(92%);
}
.collapsedSearchBar__input {
min-width: 0;
flex: 1;
border: 0;
background: transparent;
color: rgba(255, 255, 255, 0.92);
font-size: 18px;
font-weight: 700;
outline: none;
}
.collapsedSearchBar__input::placeholder {
color: rgba(255, 255, 255, 0.46);
}
.localRightRailRoot {
min-height: auto;
@@ -1005,10 +1058,12 @@ function submitGlobalSearch() {
@media (max-width: 860px) {
.appShell {
grid-template-columns: 1fr;
min-height: 100dvh;
}
.leftRail {
min-height: auto;
height: auto;
border-right: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
@@ -1022,6 +1077,19 @@ function submitGlobalSearch() {
padding: 12px 14px;
}
.appMain {
min-height: auto;
border-left: 0;
border-right: 0;
}
.workspace,
.workspaceBody,
.workspaceBody--localRail {
min-height: 0;
height: auto;
}
.leftRail__content {
overflow: visible;
}
@@ -1073,10 +1141,11 @@ function submitGlobalSearch() {
}
.collapsedSearchModal {
padding-top: 72px;
padding: 72px 16px 16px;
}
.collapsedSearchBar {
width: min(100%, calc(100vw - 24px));
padding: 16px 18px;
border-radius: 20px;
}

View File

@@ -82,7 +82,7 @@ function thumbUrl(g) {
</section>
<div v-if="error" class="error">{{ error }}</div>
<section v-if="games.length" class="libraryGrid">
<TransitionGroup v-if="games.length" name="libraryCard" tag="section" class="libraryGrid">
<article v-for="g in games" :key="g.id" class="libraryCard">
<button
class="libraryCard__favorite"
@@ -104,7 +104,7 @@ function thumbUrl(g) {
</div>
</button>
</article>
</section>
</TransitionGroup>
<div v-else class="libraryEmpty">{{ query ? '검색어에 맞는 게임 템플릿이 없어요.' : '표시할 게임 템플릿이 없어요.' }}</div>
</template>
@@ -139,6 +139,7 @@ function thumbUrl(g) {
gap: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
transition: transform 0.16s ease, background 0.16s ease;
will-change: transform, opacity;
}
.libraryCard:hover {
background: rgba(70, 70, 70, 0.96);
@@ -156,8 +157,8 @@ function thumbUrl(g) {
}
.libraryCard__favorite {
position: absolute;
top: 12px;
right: 12px;
bottom: 24px;
right: 14px;
width: 34px;
height: 34px;
border-radius: 999px;
@@ -206,6 +207,24 @@ function thumbUrl(g) {
overflow: hidden;
text-overflow: ellipsis;
}
.libraryCard-move,
.libraryCard-enter-active,
.libraryCard-leave-active {
transition: transform 280ms ease, opacity 220ms ease;
}
.libraryCard-enter-from,
.libraryCard-leave-to {
opacity: 0;
transform: translateY(10px) scale(0.985);
}
.libraryCard-leave-active {
position: absolute;
width: calc(100% - 0px);
pointer-events: none;
}
.libraryEmpty {
padding: 20px 0;
color: rgba(255, 255, 255, 0.62);

View File

@@ -54,18 +54,6 @@ function openList(t) {
router.push(`/editor/${t.gameId}/${t.id}`)
}
async function removeList(t) {
error.value = ''
try {
const ok = window.confirm(`"${t.title}" 티어표를 삭제할까요?`)
if (!ok) return
await api.deleteTierList(t.id)
myLists.value = myLists.value.filter((entry) => entry.id !== t.id)
toast.success('티어표를 삭제했어요.')
} catch (e) {
error.value = '티어표 삭제에 실패했어요.'
}
}
</script>
<template>
@@ -101,7 +89,6 @@ async function removeList(t) {
</div>
</div>
</button>
<button class="link link--danger" @click="removeList(t)">삭제</button>
</article>
</div>
</div>
@@ -115,25 +102,17 @@ async function removeList(t) {
border-radius: 0;
padding: 0;
}
.link {
padding: 10px 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
font-weight: 800;
}
.empty {
opacity: 0.75;
}
.list {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
}
.boardCard {
display: grid;
min-width: 0;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
@@ -184,30 +163,40 @@ async function removeList(t) {
border-radius: 18px;
}
.boardCard__title {
font-weight: 900;
flex: 1 1 auto;
min-width: 0;
font-weight: 900;
font-size: 18px;
white-space: nowrap;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.boardCard__head {
padding: 16px 18px 18px;
display: grid;
gap: 6px;
gap: 8px;
min-width: 0;
}
.boardCard__titleRow,
.boardCard__metaRow {
display: flex;
gap: 10px;
min-width: 0;
align-items: center;
justify-content: space-between;
}
.boardCard__titleRow {
align-items: flex-start;
}
.boardCard__metaRow {
align-items: flex-end;
}
.boardCard__author {
flex: 1 1 auto;
min-width: 0;
display: inline-flex;
gap: 7px;
@@ -246,21 +235,6 @@ async function removeList(t) {
.boardCard__date {
font-size: 10px;
}
.link--danger {
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.28);
margin: 0 18px 18px;
}
@media (max-width: 1400px) {
.list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1024px) {
.list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.list {
grid-template-columns: 1fr;

View File

@@ -744,11 +744,11 @@ onUnmounted(() => {
<div class="templateRequestDraft">
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 제목</span>
<input v-model="templateRequestDraftTitle" class="input" maxlength="80" placeholder="예: 템플릿 등록 요청" />
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 등록 요청" />
</label>
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 설명</span>
<textarea v-model="templateRequestDraftDescription" class="textarea templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
</label>
</div>
<div class="modalCard__actions">
@@ -773,11 +773,11 @@ onUnmounted(() => {
<div class="templateRequestDraft">
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 제목</span>
<input v-model="templateRequestDraftTitle" class="input" maxlength="80" placeholder="예: 템플릿 업데이트 요청" />
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 업데이트 요청" />
</label>
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 설명</span>
<textarea v-model="templateRequestDraftDescription" class="textarea templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
</label>
</div>
<div class="modalCard__actions">
@@ -990,13 +990,15 @@ onUnmounted(() => {
</div>
<div class="editorSidebar__section editorSidebar__section--footer">
<label class="toggle" :class="{ 'toggle--disabled': !canEdit }">
<label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': !canEdit }">
<input v-model="isPublic" type="checkbox" :disabled="!canEdit" />
<span>공개</span>
<span class="toggleSwitch__label">공개</span>
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
</label>
<label class="toggle" :class="{ 'toggle--disabled': !canEdit }">
<label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': !canEdit }">
<input v-model="showCharacterNames" type="checkbox" :disabled="!canEdit" />
<span>캐릭터 이름 표시</span>
<span class="toggleSwitch__label">캐릭터 이름 표시</span>
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
</label>
<div class="editorSidebar__actionGrid">
<button class="btn btn--ghost editorSidebar__button" @click="downloadImage">이미지 다운로드</button>
@@ -1132,23 +1134,56 @@ onUnmounted(() => {
display: inline-flex;
position: relative;
}
.toggle {
.toggleSwitch {
display: inline-flex;
gap: 8px;
align-items: center;
padding: 8px 10px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.12);
font-weight: 800;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
cursor: pointer;
user-select: none;
}
.toggle input {
width: 16px;
height: 16px;
.toggleSwitch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.toggle--disabled {
.toggleSwitch__track {
position: relative;
width: 42px;
height: 24px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.12);
transition: background 180ms ease, border-color 180ms ease;
flex: 0 0 auto;
}
.toggleSwitch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24);
transition: transform 180ms ease;
}
.toggleSwitch__label {
font-weight: 800;
color: rgba(255, 255, 255, 0.9);
}
.toggleSwitch input:checked ~ .toggleSwitch__track {
background: rgba(96, 165, 250, 0.34);
border-color: rgba(96, 165, 250, 0.42);
}
.toggleSwitch input:checked ~ .toggleSwitch__track .toggleSwitch__thumb {
transform: translateX(18px);
}
.toggleSwitch--disabled {
opacity: 0.55;
pointer-events: none;
}
@@ -1305,6 +1340,25 @@ onUnmounted(() => {
font-size: 12px;
color: rgba(255, 255, 255, 0.64);
}
.templateRequestDraft__input {
width: 100%;
padding: 14px 0;
border: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
background: transparent;
color: rgba(255, 255, 255, 0.94);
outline: none;
font-size: 18px;
line-height: 1.5;
letter-spacing: -0.02em;
resize: none;
}
.templateRequestDraft__input:focus {
border-bottom-color: rgba(96, 165, 250, 0.9);
}
.templateRequestDraft__input::placeholder {
color: rgba(255, 255, 255, 0.34);
}
.templateRequestDraft__textarea {
min-height: 92px;
resize: vertical;