Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1afa658c2 |
@@ -91,3 +91,10 @@
|
|||||||
- **커스텀 아이템 조회 강화**: 사용자 커스텀 아이템 목록에 파일명 검색, `50/200` 단위 페이지네이션, 다운로드 흐름 추가
|
- **커스텀 아이템 조회 강화**: 사용자 커스텀 아이템 목록에 파일명 검색, `50/200` 단위 페이지네이션, 다운로드 흐름 추가
|
||||||
- **회원 비밀번호 초기화 추가**: 관리자 페이지와 API에서 회원 비밀번호를 직접 재설정할 수 있도록 기능 추가
|
- **회원 비밀번호 초기화 추가**: 관리자 페이지와 API에서 회원 비밀번호를 직접 재설정할 수 있도록 기능 추가
|
||||||
- **가변 티어 행 지원**: 티어표 에디터에서 `S~D` 고정 5단이 아니라 티어 행을 직접 추가/삭제할 수 있도록 보강
|
- **가변 티어 행 지원**: 티어표 에디터에서 `S~D` 고정 5단이 아니라 티어 행을 직접 추가/삭제할 수 있도록 보강
|
||||||
|
|
||||||
|
## 2026-03-19 v0.1.14
|
||||||
|
- **커스텀 아이템 카드 반응형 수정**: 관리자 아이템 관리 탭의 커스텀 아이템 카드에서 이미지 폭을 유동값으로 조정하고, 텍스트 영역에 `min-width: 0`과 강제 줄바꿈 기준을 추가해 카드 바깥 overflow를 방지
|
||||||
|
|
||||||
|
## 2026-03-19 v0.1.15
|
||||||
|
- **셀렉트 화살표 여백 정리**: 전역 `select` 스타일에 커스텀 화살표 위치와 오른쪽 여백을 추가해 텍스트와 화살표가 지나치게 붙지 않도록 조정
|
||||||
|
- **티어표 다운로드 결과 개선**: `TierEditorView`의 이미지 저장을 Blob 다운로드 방식으로 바꾸고, 캡처 대상을 보드 영역만 포함하는 전용 export 뷰로 분리해 우측 아이템 영역과 편집용 버튼/입력 UI가 저장 이미지에 섞이지 않도록 수정
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, rgba(255, 255, 255, 0.78) 50%),
|
||||||
|
linear-gradient(135deg, rgba(255, 255, 255, 0.78) 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 18px) calc(50% - 2px),
|
||||||
|
calc(100% - 12px) calc(50% - 2px);
|
||||||
|
background-size: 6px 6px, 6px 6px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
font-family: var(--heading);
|
font-family: var(--heading);
|
||||||
|
|||||||
@@ -497,12 +497,10 @@ function fmt(ts) {
|
|||||||
<button class="btn btn--ghost toolbar__button" @click="submitCustomItemSearch">검색</button>
|
<button class="btn btn--ghost toolbar__button" @click="submitCustomItemSearch">검색</button>
|
||||||
<select :value="customItemLimit" class="select toolbar__select" @change="changeCustomItemLimit(Number($event.target.value))">
|
<select :value="customItemLimit" class="select toolbar__select" @change="changeCustomItemLimit(Number($event.target.value))">
|
||||||
<option :value="50">50개씩 보기</option>
|
<option :value="50">50개씩 보기</option>
|
||||||
<option :value="200">200개씩 보기</option>
|
<option :value="100">100개씩 보기</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hint">현재 목록은 사용자 커스텀 이미지 전용입니다. 여기서 보는 항목은 게임 기본 아이템 삭제와 연결되지 않아요.</div>
|
|
||||||
|
|
||||||
<div v-if="!customItems.length" class="hint">조건에 맞는 커스텀 아이템이 없어요.</div>
|
<div v-if="!customItems.length" class="hint">조건에 맞는 커스텀 아이템이 없어요.</div>
|
||||||
<div v-else class="customItemGrid">
|
<div v-else class="customItemGrid">
|
||||||
<article v-for="item in customItems" :key="item.id" class="customItemCard">
|
<article v-for="item in customItems" :key="item.id" class="customItemCard">
|
||||||
@@ -726,6 +724,7 @@ function fmt(ts) {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
|
font-size: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -751,9 +750,6 @@ function fmt(ts) {
|
|||||||
.btn--ghost {
|
.btn--ghost {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
}
|
}
|
||||||
.btn--small {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.detailHead {
|
.detailHead {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -866,7 +862,7 @@ function fmt(ts) {
|
|||||||
.customItemGrid {
|
.customItemGrid {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.customItemCard {
|
.customItemCard {
|
||||||
@@ -874,27 +870,39 @@ function fmt(ts) {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.customItemCard__image {
|
.customItemCard__image {
|
||||||
width: min(100%, 150px);
|
width: clamp(88px, 22vw, 150px);
|
||||||
|
flex: 0 1 150px;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 12px auto 0;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(0, 0, 0, 0.18);
|
background: rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
.customItemCard__body {
|
.customItemCard__body {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 12px;
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.customItemCard__title {
|
.customItemCard__title {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
.customItemCard__meta {
|
.customItemCard__meta {
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
.pager {
|
.pager {
|
||||||
@@ -979,5 +987,12 @@ function fmt(ts) {
|
|||||||
.userList {
|
.userList {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.customItemCard {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.customItemCard__image {
|
||||||
|
width: clamp(72px, 28vw, 120px);
|
||||||
|
flex-basis: 120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ const description = ref('')
|
|||||||
const isPublic = ref(false)
|
const isPublic = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
const isExporting = ref(false)
|
||||||
const ownerId = ref('')
|
const ownerId = ref('')
|
||||||
const isDragActive = ref(false)
|
const isDragActive = ref(false)
|
||||||
|
|
||||||
const boardEl = ref(null)
|
const boardEl = ref(null)
|
||||||
|
const exportBoardEl = ref(null)
|
||||||
const groupListEl = ref(null)
|
const groupListEl = ref(null)
|
||||||
const poolEl = ref(null)
|
const poolEl = ref(null)
|
||||||
const groupDropEls = ref({})
|
const groupDropEls = ref({})
|
||||||
@@ -211,11 +213,25 @@ function onDropFiles(event) {
|
|||||||
|
|
||||||
async function downloadImage() {
|
async function downloadImage() {
|
||||||
if (!boardEl.value) return
|
if (!boardEl.value) return
|
||||||
const dataUrl = await htmlToImage.toPng(boardEl.value, { pixelRatio: 2, backgroundColor: '#0b1220' })
|
isExporting.value = true
|
||||||
const a = document.createElement('a')
|
await nextTick()
|
||||||
a.href = dataUrl
|
|
||||||
a.download = `${(title.value || gameName.value || 'tierlist').trim()}.png`
|
try {
|
||||||
a.click()
|
const targetEl = exportBoardEl.value || boardEl.value
|
||||||
|
const blob = await htmlToImage.toBlob(targetEl, { pixelRatio: 2, backgroundColor: '#0b1220' })
|
||||||
|
if (!blob) throw new Error('image_export_failed')
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${(title.value || gameName.value || 'tierlist').trim()}.png`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} finally {
|
||||||
|
isExporting.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadPendingCustomItems() {
|
async function uploadPendingCustomItems() {
|
||||||
@@ -375,15 +391,22 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<section class="layout">
|
<section class="layout">
|
||||||
<div ref="boardEl" class="board">
|
<div ref="boardEl" class="board">
|
||||||
<div v-if="canEdit" class="boardTools">
|
<div v-if="canEdit && !isExporting" class="boardTools">
|
||||||
<button class="btn btn--ghost" @click="addGroup">티어 추가</button>
|
<button class="btn btn--ghost" @click="addGroup">티어 추가</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="groupListEl" class="rows">
|
<div ref="exportBoardEl" class="exportBoard" :class="{ 'exportBoard--active': isExporting }">
|
||||||
|
<div v-if="isExporting" class="exportBoard__title">{{ title || gameName || gameId }}</div>
|
||||||
|
<div ref="groupListEl" class="rows">
|
||||||
<div v-for="g in groups" :key="g.id" class="row">
|
<div v-for="g in groups" :key="g.id" class="row">
|
||||||
<div class="row__label">
|
<div class="row__label">
|
||||||
<span class="grab" title="드래그로 순서 변경" data-group-handle>↕</span>
|
<template v-if="isExporting">
|
||||||
<input v-model="g.name" class="groupName" :readonly="!canEdit" />
|
<div class="row__exportName">{{ g.name }}</div>
|
||||||
<button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="grab" title="드래그로 순서 변경" data-group-handle>↕</span>
|
||||||
|
<input v-model="g.name" class="groupName" :readonly="!canEdit" />
|
||||||
|
<button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="row__drop"
|
class="row__drop"
|
||||||
@@ -398,6 +421,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
@@ -522,6 +546,7 @@ onUnmounted(() => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 320px;
|
grid-template-columns: 1fr 320px;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
margin: 10px 0 14px;
|
margin: 10px 0 14px;
|
||||||
@@ -535,12 +560,23 @@ onUnmounted(() => {
|
|||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
align-self: start;
|
||||||
}
|
}
|
||||||
.boardTools {
|
.boardTools {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
.exportBoard--active {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.exportBoard__title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.rows {
|
.rows {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -587,6 +623,12 @@ onUnmounted(() => {
|
|||||||
outline: none;
|
outline: none;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.row__exportName {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 900;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
.rowRemoveBtn {
|
.rowRemoveBtn {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user