릴리스: v0.1.43 토스트와 즐겨찾기 추가

This commit is contained in:
2026-03-27 10:23:29 +09:00
parent 3bd9751621
commit 61fe758b7c
17 changed files with 559 additions and 209 deletions

View File

@@ -1,15 +1,17 @@
<script setup>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
import { api } from '../lib/api'
import { toApiUrl } from '../lib/runtime'
import { useAuthStore } from '../stores/auth'
import { useToast } from '../composables/useToast'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const toast = useToast()
const gameId = computed(() => route.params.gameId)
const tierListId = computed(() => route.params.tierListId)
const gameName = ref('')
@@ -41,6 +43,9 @@ const authorAccountName = ref('')
const updatedAt = ref(0)
const isDragActive = ref(false)
const iconSize = ref(80)
const isFavoriteBusy = ref(false)
const favoriteCount = ref(0)
const isFavorited = ref(false)
const boardEl = ref(null)
const exportBoardEl = ref(null)
@@ -78,6 +83,13 @@ const untitledWarning = computed(
!hasCustomTitle.value &&
'제목 없이 저장된 티어표는 무분별한 도배 방지를 위해 관리자에 의해 임의 삭제될 수 있어요.'
)
const canFavorite = computed(() => !!auth.user && !isNewTierList.value && !canEdit.value)
watch(error, (message) => {
if (!message) return
toast.error(message)
error.value = ''
})
function formatTitleDate(ts) {
const date = new Date(ts)
@@ -390,6 +402,8 @@ async function save() {
updatedAt.value = Number(res.tierList?.updatedAt || Date.now())
authorName.value = res.tierList?.authorName || effectiveAuthorName.value
authorAccountName.value = res.tierList?.authorAccountName || authorAccountName.value
favoriteCount.value = Number(res.tierList?.favoriteCount || favoriteCount.value || 0)
isFavorited.value = !!res.tierList?.isFavorited
isSaveModalOpen.value = true
} catch (e) {
error.value = '저장 실패: 로그인 상태인지 확인해주세요.'
@@ -409,12 +423,28 @@ async function removeTierList() {
const ok = window.confirm(`"${title.value || gameName.value || '이 티어표'}"를 삭제할까요?`)
if (!ok) return
await api.deleteTierList(tierListId.value)
toast.success('티어표를 삭제했어요.')
router.push(gameId.value === 'freeform' ? '/me' : `/games/${gameId.value}`)
} catch (e) {
error.value = '티어표 삭제에 실패했어요.'
}
}
async function toggleFavorite() {
if (!canFavorite.value || isFavoriteBusy.value) return
try {
isFavoriteBusy.value = true
const data = isFavorited.value ? await api.unfavoriteTierList(tierListId.value) : await api.favoriteTierList(tierListId.value)
favoriteCount.value = Number(data.tierList?.favoriteCount || 0)
isFavorited.value = !!data.tierList?.isFavorited
toast.success(isFavorited.value ? '즐겨찾기에 추가했어요.' : '즐겨찾기를 해제했어요.')
} catch (e) {
error.value = '즐겨찾기 처리에 실패했어요.'
} finally {
isFavoriteBusy.value = false
}
}
onMounted(() => {
;(async () => {
await auth.refresh()
@@ -455,6 +485,8 @@ onMounted(() => {
authorName.value = t.authorName || ''
authorAccountName.value = t.authorAccountName || ''
updatedAt.value = Number(t.updatedAt || 0)
favoriteCount.value = Number(t.favoriteCount || 0)
isFavorited.value = !!t.isFavorited
groups.value = t.groups
const map = {}
;(t.pool || []).forEach((it) => (map[it.id] = it))
@@ -525,6 +557,9 @@ onUnmounted(() => {
<button v-if="canEdit && !isNewTierList" class="btn btn--danger" @click="removeTierList">삭제</button>
</div>
<div class="actions__right">
<button v-if="canFavorite" class="btn btn--ghost" :disabled="isFavoriteBusy" @click="toggleFavorite">
{{ isFavorited ? ' 즐겨찾기' : ' 즐겨찾기' }} {{ favoriteCount }}
</button>
<label class="toggle" :class="{ 'toggle--disabled': !canEdit }">
<input v-model="isPublic" type="checkbox" :disabled="!canEdit" />
<span>{{ isPublic ? '공개 ON' : '공개 OFF' }}</span>
@@ -534,8 +569,6 @@ onUnmounted(() => {
</div>
</section>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
<div id="saveModalTitle" class="modalCard__title">저장 완료</div>