Compare commits

..

3 Commits

4 changed files with 190 additions and 28 deletions

View File

@@ -1,5 +1,20 @@
# 업데이트 로그
## 2026-04-01 v1.3.17
- 티어 에디터 열 헤더 입력창과 행 라벨은 좌우 패딩을 대칭으로 다시 잡아, 드래그 핸들과 삭제 아이콘이 있어도 제목이 한쪽으로 쏠려 보이지 않도록 보정함.
- 열 삭제도 이제 행 삭제와 같은 확인 모달을 거쳐 진행되도록 바꿔, 실수로 즉시 제거되던 문제를 막음.
- 내보내기 보드는 여전히 960px 고정 폭이라 열 수가 늘수록 각 칸 폭이 줄어드는 구조라는 점을 기준으로 정리했고, 현재 보정은 헤더 정렬 문제를 우선 해결하는 쪽에 맞춤.
## 2026-04-01 v1.3.16
- 티어 에디터의 행 삭제와 열 삭제는 다시 작은 X 아이콘 액션으로 정리해, 행/열 이름 주변의 반복 텍스트 때문에 보드가 답답해 보이던 문제를 줄임.
- 열 헤더 편집 영역은 입력창 오른쪽에 아이콘 삭제만 남기고, 행 라벨도 상단 우측의 작은 제거 버튼으로 맞춰 더 압축된 편집 밀도를 유지하도록 조정함.
- 저장 이미지에서 열 제목이 살짝 위로 떠 보이던 문제는 내보내기 헤더의 비대칭 패딩을 제거하고 flex 중앙 정렬로 바꿔, 시각적으로 정확한 중앙에 오도록 보정함.
## 2026-04-01 v1.3.15
- 티어 에디터의 열 이름은 각 행 안에서 반복 렌더링되지 않도록 공통 상단 헤더로 분리해, 행 제목과 같은 구조로 더 또렷하게 구분되도록 수정함.
- 행 추가/열 추가 액션은 새 SVG 아이콘 버튼으로 압축해, 텍스트 때문에 보드 상단 툴바 높이가 과하게 커지던 문제를 정리함.
- 미리보기와 삭제 모달 문구도 행/열 기준으로 함께 정리해, 전체 티어 에디터 흐름을 더 일관된 용어와 레이아웃으로 다듬음.
## 2026-04-01 v1.3.14
- 티어 에디터를 단일 세로 랭크형에서 행/열 혼합 보드로 확장해, 공격·방어·지원 같은 가로 열을 추가하고 각 열 이름도 직접 입력할 수 있게 함.
- 에디터 액션 문구를 `행 추가 / 열 추가` 기준으로 정리하고, 행 라벨 폭과 드래그 아이콘 위치를 다듬어 실제 사용 빈도에 맞는 더 압축된 보드 레이아웃으로 보정함.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M160-760v560h240v-560H160ZM80-120v-720h720v160h-80v-80H480v560h240v-80h80v160H80Zm400-360Zm-80 0h80-80Zm0 0Zm320 120v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M200-560h560v-240H200v240Zm-80 400v-720h720v720H680v-80h80v-240H200v240h80v80H120Zm360-320Zm0-80v80-80Zm0 0ZM440-80v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Z"/></svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -3,6 +3,9 @@ import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watc
import { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
import SvgIcon from '../components/SvgIcon.vue'
import addColumnRightIcon from '../assets/icons/add_column_right.svg'
import addRowBelowIcon from '../assets/icons/add_row_below.svg'
import { api } from '../lib/api'
import { toApiUrl } from '../lib/runtime'
import { useAuthStore } from '../stores/auth'
@@ -48,7 +51,9 @@ const templateRequestDraftDescription = ref('')
const templateRequestSaveToMyTierList = ref(true)
const isDeleteModalOpen = ref(false)
const isGroupDeleteModalOpen = ref(false)
const isColumnDeleteModalOpen = ref(false)
const pendingRemoveGroupId = ref('')
const pendingRemoveColumnIndex = ref(-1)
const ownerId = ref('')
const authorName = ref('')
const authorAccountName = ref('')
@@ -381,6 +386,24 @@ async function removeColumn(columnIndex) {
await syncSortables()
}
function openColumnDeleteModal(columnIndex) {
if (!canEdit.value || columns.value.length <= 1) return
pendingRemoveColumnIndex.value = columnIndex
isColumnDeleteModalOpen.value = true
}
function closeColumnDeleteModal() {
isColumnDeleteModalOpen.value = false
pendingRemoveColumnIndex.value = -1
}
async function confirmRemoveColumn() {
const columnIndex = pendingRemoveColumnIndex.value
closeColumnDeleteModal()
if (columnIndex < 0) return
await removeColumn(columnIndex)
}
async function performRemoveGroup(groupId) {
if (groups.value.length <= 1) return
const target = groups.value.find((group) => group.id === groupId)
@@ -865,12 +888,17 @@ onUnmounted(() => {
<div class="previewOnly__sheet">
<div class="previewOnly__title">{{ effectiveTitle }}</div>
<div v-if="description" class="previewOnly__description">{{ description }}</div>
<div v-if="columns.length > 1" class="previewOnly__columns">
<div class="previewOnly__columnsSpacer" aria-hidden="true"></div>
<div class="previewOnly__columnsGrid" :style="{ '--column-count': columns.length }">
<div v-for="(column, columnIndex) in columns" :key="column.id" class="previewOnly__columnHeader">{{ column.name || ' ' + (columnIndex + 1) }}</div>
</div>
</div>
<div class="previewOnly__rows">
<div v-for="g in groups" :key="g.id" class="previewOnly__row">
<div class="previewOnly__label">{{ g.name }}</div>
<div class="previewOnly__dropGrid" :style="{ '--column-count': columns.length }">
<div v-for="(column, columnIndex) in columns" :key="column.id" class="previewOnly__dropColumn">
<div v-if="columns.length > 1" class="previewOnly__columnLabel">{{ column.name || '' }}</div>
<div class="previewOnly__drop">
<div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="previewOnly__cell">
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
@@ -999,13 +1027,26 @@ onUnmounted(() => {
<div v-if="isGroupDeleteModalOpen" class="modalOverlay" @click.self="closeGroupDeleteModal">
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="deleteGroupTitle">
<div id="deleteGroupTitle" class="modalCard__title">티어 라인 삭제</div>
<div id="deleteGroupTitle" class="modalCard__title">티어 삭제</div>
<div class="modalCard__desc">
라인 삭제하면 현재 들어 있는 아이템은 모두 아래 아이템 영역으로 이동합니다. 삭제 후에도 아이템 자체는 유지돼요.
삭제하면 현재 들어 있는 아이템은 모두 아래 아이템 영역으로 이동합니다. 삭제 후에도 아이템 자체는 유지돼요.
</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeGroupDeleteModal">취소</button>
<button class="btn btn--danger" @click="confirmRemoveGroup">라인 삭제</button>
<button class="btn btn--danger" @click="confirmRemoveGroup"> 삭제</button>
</div>
</div>
</div>
<div v-if="isColumnDeleteModalOpen" class="modalOverlay" @click.self="closeColumnDeleteModal">
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="deleteColumnTitle">
<div id="deleteColumnTitle" class="modalCard__title">티어 삭제</div>
<div class="modalCard__desc">
열을 삭제하면 현재 들어 있는 아이템은 모두 번째 열로 이동합니다. 삭제 후에도 아이템 자체는 유지돼요.
</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeColumnDeleteModal">취소</button>
<button class="btn btn--danger" @click="confirmRemoveColumn"> 삭제</button>
</div>
</div>
</div>
@@ -1017,7 +1058,7 @@ onUnmounted(() => {
<div class="editorMain__title">{{ gameName || gameId }}</div>
<div class="editorMain__subtitle">
<template v-if="canEdit">
그룹 이름/순서 변경과 아이템 드래그&드롭이 가능합니다.
/ 이름 순서 바꾸고 아이템 드래그해서 배치할 있어요.
</template>
<template v-else>
공개된 티어표를 보는 중입니다. 로그인한 작성자만 수정할 있어요.
@@ -1034,8 +1075,12 @@ onUnmounted(() => {
<div ref="boardEl" class="board">
<div v-if="canEdit && !isExporting" class="boardTools">
<div class="boardTools__left">
<button class="btn btn--ghost" @click="addGroup"> 추가</button>
<button class="btn btn--ghost" @click="addColumn"> 추가</button>
<button class="boardActionIcon" type="button" title="행 추가" aria-label=" 추가" @click="addGroup">
<SvgIcon :src="addRowBelowIcon" :size="18" />
</button>
<button class="boardActionIcon" type="button" title="열 추가" aria-label=" 추가" @click="addColumn">
<SvgIcon :src="addColumnRightIcon" :size="18" />
</button>
</div>
<div class="boardTools__right">
<span class="boardTools__label">아이콘 크기</span>
@@ -1055,6 +1100,22 @@ onUnmounted(() => {
<div ref="exportBoardEl" class="exportBoard" :class="{ 'exportBoard--active': isExporting }">
<div v-if="isExporting" class="exportBoard__title">{{ effectiveTitle }}</div>
<div v-if="isExporting && description" class="exportBoard__description">{{ description }}</div>
<div v-if="columns.length > 1" class="boardColumnsHeader" :class="{ 'boardColumnsHeader--export': isExporting }">
<div class="boardColumnsHeader__spacer" aria-hidden="true"></div>
<div class="boardColumnsHeader__grid" :style="{ '--column-count': columns.length }">
<div v-for="(column, columnIndex) in columns" :key="column.id" class="boardColumnsHeader__cell">
<template v-if="isExporting">
<div class="boardColumnsHeader__name">{{ column.name || '열 ' + (columnIndex + 1) }}</div>
</template>
<template v-else>
<div class="columnHeader">
<input v-model="column.name" class="columnName" maxlength="16" placeholder="열 이름" />
<button class="columnRemoveText" type="button" title="열 삭제" aria-label=" 삭제" :disabled="columns.length <= 1" @click="openColumnDeleteModal(columnIndex)">×</button>
</div>
</template>
</div>
</div>
</div>
<div ref="groupListEl" class="rows">
<div v-for="g in groups" :key="g.id" class="row">
<div class="row__label">
@@ -1069,19 +1130,16 @@ onUnmounted(() => {
class="rowRemoveText"
type="button"
title="행 삭제"
aria-label=" 삭제"
:disabled="groups.length <= 1"
@click="openGroupDeleteModal(g.id)"
>
삭제
×
</button>
</template>
</div>
<div class="row__content" :style="{ '--column-count': columns.length }">
<div v-for="(column, columnIndex) in columns" :key="column.id" class="row__column">
<div v-if="!isExporting && columns.length > 1" class="columnHeader">
<input v-model="column.name" class="columnName" maxlength="16" placeholder="열 이름" />
<button class="columnRemoveText" type="button" :disabled="columns.length <= 1" @click="removeColumn(columnIndex)"> 삭제</button>
</div>
<div
class="row__drop"
:data-list-type="'group'"
@@ -1333,6 +1391,24 @@ onUnmounted(() => {
line-height: 1.6;
opacity: 0.76;
}
.previewOnly__columns {
display: grid;
grid-template-columns: 132px 1fr;
gap: 10px;
margin-bottom: 10px;
}
.previewOnly__columnsGrid {
display: grid;
grid-template-columns: repeat(var(--column-count, 1), minmax(0, 1fr));
gap: 10px;
}
.previewOnly__columnHeader {
min-height: 20px;
font-size: 12px;
font-weight: 800;
text-align: center;
opacity: 0.72;
}
.previewOnly__rows {
display: grid;
gap: 10px;
@@ -1351,13 +1427,6 @@ onUnmounted(() => {
display: grid;
gap: 8px;
}
.previewOnly__columnLabel {
min-height: 20px;
font-size: 12px;
font-weight: 800;
text-align: center;
opacity: 0.72;
}
.previewOnly__label {
display: grid;
place-items: center;
@@ -1665,6 +1734,51 @@ onUnmounted(() => {
.boardTools__left {
margin-right: auto;
}
.boardActionIcon {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.9);
cursor: pointer;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
}
.boardActionIcon:hover {
background: rgba(96, 165, 250, 0.12);
border-color: rgba(96, 165, 250, 0.28);
color: rgba(255, 255, 255, 0.98);
}
.boardColumnsHeader {
display: grid;
grid-template-columns: 132px 1fr;
gap: 10px;
margin-bottom: 10px;
}
.boardColumnsHeader__grid {
display: grid;
grid-template-columns: repeat(var(--column-count, 1), minmax(0, 1fr));
gap: 10px;
}
.boardColumnsHeader__cell {
min-width: 0;
position: relative;
}
.boardColumnsHeader__name {
min-height: 38px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
text-align: center;
font-size: 12px;
line-height: 1.2;
font-weight: 800;
opacity: 0.74;
}
.boardTools__label {
font-size: 13px;
opacity: 0.76;
@@ -1743,7 +1857,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
padding: 14px 12px 30px;
padding: 14px 28px;
font-weight: 900;
overflow: hidden;
}
@@ -1758,9 +1872,11 @@ onUnmounted(() => {
min-width: 0;
}
.columnHeader {
position: relative;
display: flex;
align-items: center;
gap: 8px;
min-height: 38px;
padding: 0 28px;
}
.columnName {
width: 100%;
@@ -1771,6 +1887,7 @@ onUnmounted(() => {
padding: 4px 0;
text-align: center;
font-size: 12px;
line-height: 1.2;
font-weight: 800;
outline: none;
}
@@ -1778,14 +1895,28 @@ onUnmounted(() => {
color: rgba(255, 255, 255, 0.34);
}
.columnRemoveText {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
width: 20px;
height: 20px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 999px;
background: transparent;
color: rgba(255, 255, 255, 0.56);
font-size: 11px;
font-size: 16px;
line-height: 1;
font-weight: 800;
cursor: pointer;
}
.columnRemoveText:hover {
color: rgba(255, 255, 255, 0.92);
background: rgba(255, 255, 255, 0.06);
}
.columnRemoveText:disabled {
opacity: 0.32;
cursor: not-allowed;
@@ -1819,19 +1950,25 @@ onUnmounted(() => {
}
.rowRemoveText {
position: absolute;
right: 12px;
bottom: 10px;
top: 10px;
right: 10px;
width: 20px;
height: 20px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 999px;
background: transparent;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
font-size: 11px;
font-size: 16px;
line-height: 1;
font-weight: 800;
}
.rowRemoveText:hover {
color: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.92);
background: rgba(255, 255, 255, 0.06);
}
.rowRemoveText:disabled {
opacity: 0.32;
@@ -2265,10 +2402,18 @@ onUnmounted(() => {
.previewOnly {
padding: 14px;
}
.previewOnly__row {
.previewOnly__columns,
.previewOnly__row,
.boardColumnsHeader,
.row {
grid-template-columns: 1fr;
}
.previewOnly__dropGrid {
.previewOnly__columnsSpacer,
.boardColumnsHeader__spacer {
display: none;
}
.previewOnly__dropGrid,
.boardColumnsHeader__grid {
grid-template-columns: 1fr;
}
.pool {