Compare commits

...

3 Commits

13 changed files with 926 additions and 564 deletions

View File

@@ -1,5 +1,20 @@
# 의사결정 이력
## 2026-03-30 v1.2.2
- 우측 패널은 본문 내부 보조 박스가 아니라 별도 컬럼으로 보이는 것이 핵심이므로, 폭을 `320px`로 고정하고 접힘/펼침도 레이아웃 레벨에서 처리하는 편이 맞다고 판단했다.
- 좌측 패널도 시안 기준 인지 폭이 중요하므로 `248px`로 고정하고, 중앙 콘텐츠는 나머지 공간을 유동적으로 쓰게 하는 구조로 정리했다.
- 우측 패널 토글은 라우트별 개별 구현보다 공통 셸의 상단 컨트롤로 두는 편이 모든 화면에서 일관된 사용성을 제공한다고 판단했다.
## 2026-03-30 v1.2.1
- 공통 셸을 먼저 올린 직후에는 에디터와 관리자처럼 자체 패널이 많은 화면이 가장 크게 깨지므로, 이 화면들은 우선 공통 우측 패널을 숨기고 중앙 폭을 회복시키는 편이 안정적이라고 판단했다.
- 목록형 카드 화면은 셸 안쪽 폭이 줄어든 상태에서 이전보다 더 많은 컬럼을 유지하면 즉시 사용성이 무너지므로, 기본 컬럼 수를 줄여 먼저 읽히는 상태를 만드는 쪽을 우선하기로 했다.
- 리디자인 초기 단계에서는 “완벽한 시안 재현”보다 먼저 실제 조작 가능한 상태를 되찾는 것이 중요하므로, 이번 단계는 안정화 릴리스로 짧게 끊어 가기로 정리했다.
## 2026-03-30 v1.2.0
- 피그마 시안은 단순 컴포넌트 교체보다 앱 전체의 정보 구조를 바꾸는 성격이 강하므로, 우선 공통 앱 셸부터 `좌측 내비 / 중앙 워크스페이스 / 우측 컨텍스트 패널`로 올리는 단계적 리디자인이 더 안전하다고 판단했다.
- 홈, 게임 허브, 내 티어표, 즐겨찾기처럼 카드 목록 중심 화면은 시안 톤을 먼저 맞추고, 에디터와 관리자처럼 상호작용이 무거운 화면은 같은 셸 안에서 후속 이관하는 방식이 리스크가 적다고 정리했다.
- 이번 리디자인은 사용자 체감 변화가 큰 편이므로, 버전도 기존 `0.1.x`가 아니라 `v1.2.0`으로 점프해 기록하는 편이 더 자연스럽다고 판단했다.
## 2026-03-27 v0.1.52
- 관리자 확인용 완성본은 사이트 전체가 아니라 보드만 보여주는 preview 전용 모드가 더 적합하다고 판단했다.
- 티어표 썸네일은 비어 있는 것보다 자동 기본값이 있는 편이 낫다고 보고, 사용자가 직접 지정하지 않으면 티어표 아이템 중 대표 이미지를 자동 썸네일로 채우기로 결정했다.

View File

@@ -42,7 +42,8 @@
## 공통 레이아웃
- 앱 셸 파일: `frontend/src/App.vue`
- 역할: 상단 내비게이션, 로그인 상태 반영, 아바타 메뉴, 관리자 메뉴 노출 제어, 전역 우측 상단 토스트 렌더링
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, 로그인 상태 반영, 사용자 메뉴, 관리자 메뉴 노출 제어, 전역 우측 상단 토스트 렌더링
- 세부: 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 상단 토글 버튼으로 우측 패널을 접고 펼칠 수 있다.
## 백엔드 진입점
- 서버 엔트리: `backend/index.js`

View File

@@ -9,6 +9,8 @@
- 운영 배포: `frontend(Nginx 정적 서빙 + /api,/uploads 프록시) + backend + mariadb` Docker Compose 구조
- NAS HTTPS 리버스 프록시 운영 시 프런트 Nginx는 백엔드로 `X-Forwarded-Proto: https`를 전달하고, Express 세션은 프록시 환경에서 `secure` 쿠키를 허용하도록 설정한다.
- 프런트 파비콘은 운영 정적 파일 차단 영향을 줄이기 위해 `index.html`의 인라인 데이터 URL로 제공한다.
- 프런트 앱 셸은 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 구조로 재정의되었고, preview 모드에서는 이 셸을 숨기고 콘텐츠만 렌더링한다.
- 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다.
## 데이터 저장 구조
- 메인 데이터베이스: MariaDB `tier_cursor` (기본값)
@@ -19,6 +21,16 @@
- 커스텀 아이템: `backend/uploads/custom/`
- 시드 이미지: `backend/uploads/seeds/`
## 화면 구조
- 좌측 패널
- 사용자 요약, 빠른 검색 버튼, 주요 라우트 내비게이션, 즐겨찾기 성격의 빠른 링크, 관리자 진입 버튼을 배치한다.
- 중앙 워크스페이스
- 현재 라우트의 핵심 콘텐츠를 렌더링하는 영역이며, 홈/목록 계열 화면은 카드형 대시보드 레이아웃을 우선 적용한다.
- 우측 패널
- 현재 화면 문맥에 맞는 설명, 빠른 액션, 계정 상태 같은 보조 정보를 배치한다.
- 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다.
- 공통 토글 버튼으로 패널을 접으면 중앙 워크스페이스가 남는 공간을 확장 사용한다.
## DB 스키마
- `users`
- `id`: string

View File

@@ -1,10 +1,12 @@
# 할 일 및 이슈
## 즉시 확인 필요
- 피그마 기반 리디자인은 현재 공통 셸과 카드 목록 화면, 포커스 화면 안정화까지만 반영된 상태이므로, 에디터/관리자 우측 옵션 패널을 시안 구조에 맞게 실제 기능 패널로 이관하는 작업이 남아 있다.
- 공통 우측 패널의 토글과 독립 컬럼 구조는 반영되었지만, 현재는 안내 카드 중심이므로 실제 화면별 기능 컨트롤을 이 패널로 옮기는 작업이 남아 있다.
- 머티리얼 아이콘 SVG는 아직 임시 문자/배지 스타일로 대체된 부분이 있으므로, 최종 아이콘 에셋을 받아 반영하는 작업이 필요하다.
- 미사용 커스텀 이미지 일괄 삭제는 현재 "참조가 없는 항목" 기준이며, 보관 기간 정책 같은 운영 규칙은 아직 없다.
- 업로드 이미지는 현재 원본 파일을 그대로 저장하므로, 운영 부담이 커지면 서버 저장 전 리사이즈/압축(예: 긴 변 제한, WebP 변환) 도입이 필요하다.
- 관리자 기본 아이템 다중 업로드는 현재 파일명 기반 자동 라벨만 지원하므로, 필요하면 업로드 후 일괄 라벨 수정/정렬 UX를 추가 검토한다.
- 티어표 썸네일은 이제 대표 아이템 기반 자동 생성까지 지원하므로, 필요하면 업로드 이미지 크롭 UX나 자동 후보 선택 개선을 추가 검토한다.
- 사용자 커스텀 아이템 승격은 현재 수동 복제 방식이므로, 필요하면 중복 감지나 “비슷한 항목 추천” 같은 보조 UX를 검토한다.
- 템플릿 등록/업데이트 요청은 현재 승인/반려만 지원하므로, 필요하면 관리자 코멘트나 사용자용 요청 상태 이력 화면을 추가 검토한다.
- 관리자 티어표 관리의 템플릿 생성은 현재 `freeform`만 직접 지원하므로, 필요하면 일반 게임 티어표의 전체 아이템을 복제한 파생 템플릿 생성 UX도 검토한다.

View File

@@ -1,5 +1,20 @@
# 업데이트 로그
## 2026-03-30 v1.2.2
- **사이드 패널 폭 고정**: 공통 앱 셸의 좌측 패널 폭을 `248px`, 우측 패널 폭을 `320px` 기준으로 재정의해 피그마 시안과 더 가깝게 맞춤
- **우측 패널 토글 추가**: 상단 우측 토글 버튼으로 우측 패널을 접고 펼칠 수 있게 하고, 접힐 때는 중앙 작업 영역이 자연스럽게 확장되도록 전환 애니메이션을 추가
- **우측 패널 독립성 강화**: 우측 패널은 본문과 별도 컬럼으로 유지하고, 닫힐 때도 본문 레이아웃과 분리된 독립 패널처럼 동작하도록 셸 구조를 조정
## 2026-03-30 v1.2.1
- **포커스 화면 폭 복구**: 에디터·관리자·프로필·로그인 화면은 공통 우측 패널을 잠시 숨기고 중앙 작업 폭을 넓혀, 기존 기능 UI가 3단 셸과 충돌하며 깨지던 문제를 완화
- **목록 카드 밀도 재조정**: 홈, 게임 허브, 내 티어표, 즐겨찾기 화면의 기본 컬럼 수를 줄여 현재 셸 폭 안에서도 카드가 과도하게 눌리지 않도록 정리
- **에디터/관리자 패널 안정화**: 내부 작업 패널 색상과 폭을 새 셸 톤에 맞춰 다시 정리해, 중첩 패널 때문에 사용성이 무너지던 부분을 우선 복구
## 2026-03-30 v1.2.0
- **피그마 기반 공통 앱 셸 1차 적용**: 상단 헤더 중심 구조를 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 앱 셸로 재구성하고, 데스크톱 기준의 어두운 대시보드형 톤으로 전환
- **홈/목록 화면 카드 UI 리디자인**: 홈, 게임 허브, 내 티어표, 즐겨찾기 화면의 카드 그리드와 툴바를 시안에 맞춰 더 조밀한 대시보드 형태로 재배치
- **전역 스타일 리셋 정리**: 기존 Vite 기본 스타일 흔적을 제거하고, 서비스 전용 다크 테마와 입력/셀렉트/버튼 기본값을 새 레이아웃 기준으로 통일
## 2026-03-27 v0.1.52
- **관리자 완성본 프리뷰 전용화**: 관리자 모달의 완성본 확인은 이제 전용 preview 모드로 열려 전역 헤더와 편집/탐색 UI 없이 보드만 깔끔하게 확인할 수 있도록 정리
- **티어표 기본 썸네일 자동 생성**: 사용자가 별도 썸네일을 지정하지 않아도 저장 시 티어표에 포함된 아이템 중 대표 이미지를 골라 기본 썸네일을 자동으로 채우도록 보강

View File

@@ -9,17 +9,130 @@ const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const { toasts, dismissToast } = useToast()
const isAdmin = computed(() => !!auth.user?.isAdmin)
const isPreviewMode = computed(() => route.query.preview === '1')
const avatarUrl = computed(() => {
if (!auth.user?.avatarSrc) return ''
return toApiUrl(auth.user.avatarSrc)
})
const menuOpen = ref(false)
const rightRailOpen = ref(true)
const isAdmin = computed(() => !!auth.user?.isAdmin)
const isPreviewMode = computed(() => route.query.preview === '1')
const avatarUrl = computed(() => (auth.user?.avatarSrc ? toApiUrl(auth.user.avatarSrc) : ''))
const accountName = computed(() => {
const nickname = (auth.user?.nickname || '').trim()
if (nickname) return nickname
const email = (auth.user?.email || '').trim()
if (email) return email.split('@')[0] || email
return 'Guest'
})
const accountEmail = computed(() => (auth.user?.email || '').trim() || '로그인 후 개인 메뉴를 사용할 수 있어요.')
const leftNavItems = computed(() => {
const items = [
{ key: 'home', label: 'Games', path: '/', initials: 'GM' },
{ key: 'me', label: '내 리스트', path: '/me', initials: 'ME', requiresAuth: true },
{ key: 'favorites', label: '즐겨찾기', path: '/favorites', initials: 'FV', requiresAuth: true },
{ key: 'profile', label: 'Settings', path: '/profile', initials: 'ST', requiresAuth: true },
]
if (isAdmin.value) {
items.push({ key: 'admin', label: 'Admin', path: '/admin', initials: 'AD' })
}
return items.filter((item) => !item.requiresAuth || auth.user)
})
const routeMeta = computed(() => {
if (route.name === 'home') {
return {
title: 'Main Title',
subtitle: '게임 선택 및 커스텀 티어표 진입',
contextTitle: '빠른 시작',
contextText: auth.user ? '커스텀 티어표를 만들거나 원하는 게임을 바로 선택할 수 있어요.' : '로그인하면 커스텀 티어표 생성과 개인 목록 관리가 열립니다.',
actionLabel: auth.user ? '커스텀 티어표 만들기' : '로그인하러 가기',
action: () => {
router.push(auth.user ? '/editor/freeform/new' : '/login')
},
}
}
if (route.name === 'gameHub') {
return {
title: 'Tier Lists',
subtitle: '게임별 공개 티어표 목록',
contextTitle: '작성 작업',
contextText: auth.user ? '이 게임의 새 티어표를 만들거나 기존 공개 티어표를 확인할 수 있어요.' : '로그인 후 새 티어표를 만들 수 있어요.',
actionLabel: auth.user ? '새 티어표 만들기' : '로그인하러 가기',
action: () => {
const target = `/editor/${route.params.gameId}/new`
router.push(auth.user ? target : `/login?redirect=${target}`)
},
}
}
if (route.name === 'editEditor' || route.name === 'newEditor') {
return {
title: 'Deck Builder',
subtitle: '티어표 편집 및 공유',
contextTitle: '편집 패널',
contextText: '현재 편집 옵션은 중앙 화면 안에 유지되어 있습니다. 다음 단계에서 우측 패널로 정리해갈게요.',
actionLabel: '게임 목록으로',
action: () => router.push('/'),
}
}
if (route.name === 'admin') {
return {
title: 'Admin Workspace',
subtitle: '게임·아이템·회원 관리',
contextTitle: '운영 노트',
contextText: '관리자 화면은 기능이 많아 우선 공통 셸 톤을 맞췄고, 세부 패널은 다음 단계에서 시안 방식으로 더 세밀하게 나눌 예정입니다.',
actionLabel: '게임 목록으로',
action: () => router.push('/'),
}
}
if (route.name === 'me') {
return {
title: 'My Lists',
subtitle: '내가 저장한 티어표',
contextTitle: '작성 이력',
contextText: '최근 저장한 티어표를 열람하고 정리할 수 있어요.',
actionLabel: '즐겨찾기 보기',
action: () => router.push('/favorites'),
}
}
if (route.name === 'favorites') {
return {
title: 'Favorites',
subtitle: '마음에 드는 티어표 모음',
contextTitle: '정리 도구',
contextText: '정렬과 검색으로 원하는 티어표를 빠르게 다시 찾을 수 있어요.',
actionLabel: '내 티어표 보기',
action: () => router.push('/me'),
}
}
if (route.name === 'profile') {
return {
title: 'Profile',
subtitle: '프로필 및 계정 설정',
contextTitle: '계정 관리',
contextText: '아바타와 닉네임을 관리하고 표시 정보를 확인할 수 있어요.',
actionLabel: '내 티어표 보기',
action: () => router.push('/me'),
}
}
return {
title: 'Tier Maker',
subtitle: 'by zenn',
contextTitle: 'Workspace',
contextText: '현재 화면에 맞는 도구와 안내를 여기에 배치할 수 있습니다.',
actionLabel: '홈으로',
action: () => router.push('/'),
}
})
const favoriteLinks = computed(() => [
{ label: 'Games', path: '/' },
...(auth.user ? [{ label: 'Favorites', path: '/favorites' }] : []),
...(auth.user ? [{ label: 'My Lists', path: '/me' }] : []),
])
onMounted(async () => {
await auth.refresh()
if (typeof window !== 'undefined') {
const saved = window.localStorage.getItem('tier-maker:right-rail-open')
if (saved === '0') rightRailOpen.value = false
}
document.addEventListener('click', onDocumentClick)
})
@@ -34,13 +147,25 @@ watch(
}
)
function onDocumentClick(event) {
if (!event.target.closest('.appUserCard')) {
menuOpen.value = false
}
}
function isRouteActive(path) {
if (path === '/') return route.path === '/'
return route.path.startsWith(path)
}
function toggleMenu() {
menuOpen.value = !menuOpen.value
}
function onDocumentClick(event) {
if (!event.target.closest('.user')) {
menuOpen.value = false
function toggleRightRail() {
rightRailOpen.value = !rightRailOpen.value
if (typeof window !== 'undefined') {
window.localStorage.setItem('tier-maker:right-rail-open', rightRailOpen.value ? '1' : '0')
}
}
@@ -57,176 +182,541 @@ async function logout() {
</script>
<template>
<div class="app-shell">
<header v-if="!isPreviewMode" class="app-header">
<div class="brand" @click="$router.push('/')">
<span class="brand__title">Tier Maker</span>
<span class="brand__sub">by zenn</span>
</div>
<nav class="nav">
<RouterLink to="/" class="nav__link">게임</RouterLink>
<RouterLink to="/me" class="nav__link"> 티어표</RouterLink>
<RouterLink v-if="auth.user" to="/favorites" class="nav__link">즐겨찾기</RouterLink>
<RouterLink v-if="isAdmin" to="/admin" class="nav__link">관리자</RouterLink>
<RouterLink v-if="!auth.user" to="/login" class="nav__link">로그인</RouterLink>
<div v-else class="user">
<button class="avatarBtn" @click.stop="toggleMenu" :title="auth.user.email">
<img v-if="avatarUrl" :src="avatarUrl" class="avatarImg" alt="avatar" />
<div v-else class="avatarFallback">{{ (auth.user.email || 'U')[0].toUpperCase() }}</div>
</button>
<div v-if="menuOpen" class="menu">
<button class="menuItem" @click="goProfile">프로필</button>
<button class="menuItem" @click="logout">로그아웃</button>
<div class="appShell" :class="{ 'appShell--preview': isPreviewMode, 'appShell--rightClosed': !rightRailOpen }">
<template v-if="isPreviewMode">
<main class="appMain appMain--preview">
<RouterView />
</main>
</template>
<template v-else>
<aside class="leftRail">
<div class="leftRail__top">
<button class="ghostIcon" type="button" aria-label="메뉴"></button>
<div class="brandBlock" @click="$router.push('/')">
<div class="brandBlock__title">Tier Maker</div>
<div class="brandBlock__sub">by zenn</div>
</div>
</div>
</nav>
</header>
<main class="app-main" :class="{ 'app-main--preview': isPreviewMode }">
<RouterView />
</main>
<div class="appUserCard">
<button v-if="auth.user" class="appUserCard__button" type="button" @click.stop="toggleMenu">
<img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" />
<div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div>
<div class="appUserCard__meta">
<div class="appUserCard__name">{{ accountName }}</div>
<div class="appUserCard__email">{{ accountEmail }}</div>
</div>
</button>
<div v-else class="appUserCard__guest" @click="$router.push('/login')">
<div class="appUserCard__avatar appUserCard__avatar--fallback">G</div>
<div class="appUserCard__meta">
<div class="appUserCard__name">로그인 필요</div>
<div class="appUserCard__email">개인 메뉴를 사용하려면 로그인하세요.</div>
</div>
</div>
<div v-if="menuOpen" class="appUserMenu">
<button class="appUserMenu__item" type="button" @click="goProfile">프로필</button>
<button class="appUserMenu__item" type="button" @click="logout">로그아웃</button>
</div>
</div>
<button class="searchStub" type="button" @click="$router.push('/favorites')">
<span class="searchStub__icon"></span>
<span>Search</span>
</button>
<nav class="leftNav">
<RouterLink
v-for="item in leftNavItems"
:key="item.key"
:to="item.path"
class="leftNav__item"
:class="{ 'leftNav__item--active': isRouteActive(item.path) }"
>
<span class="leftNav__glyph">{{ item.initials }}</span>
<span>{{ item.label }}</span>
</RouterLink>
</nav>
<div class="leftRail__section">
<div class="leftRail__sectionTitle">Favorites</div>
<RouterLink v-for="item in favoriteLinks" :key="item.path" :to="item.path" class="favoriteLink">
<span class="favoriteLink__dot"></span>
<span>{{ item.label }}</span>
</RouterLink>
</div>
<div class="leftRail__bottom">
<RouterLink v-if="isAdmin" to="/admin" class="adminButton">관리자 메뉴</RouterLink>
<RouterLink v-else-if="!auth.user" to="/login" class="adminButton">로그인</RouterLink>
</div>
</aside>
<main class="appMain">
<section class="workspace">
<header class="workspaceHead">
<div>
<div class="workspaceHead__title">{{ routeMeta.title }}</div>
<div class="workspaceHead__subtitle">{{ routeMeta.subtitle }}</div>
</div>
<div class="workspaceHead__actions">
<button class="ghostIcon ghostIcon--workspace" type="button" :aria-pressed="rightRailOpen" @click="toggleRightRail">
{{ rightRailOpen ? '우측 패널 숨기기' : '우측 패널 보기' }}
</button>
</div>
</header>
<div class="workspaceBody">
<RouterView />
</div>
</section>
</main>
<aside class="rightRail" :class="{ 'rightRail--closed': !rightRailOpen }" :aria-hidden="!rightRailOpen">
<div class="rightRail__top">
<button class="ghostIcon" type="button" aria-label="상태"></button>
</div>
<section class="contextCard">
<div class="contextCard__label">Context</div>
<h2 class="contextCard__title">{{ routeMeta.contextTitle }}</h2>
<p class="contextCard__text">{{ routeMeta.contextText }}</p>
<button class="contextCard__action" type="button" @click="routeMeta.action">
{{ routeMeta.actionLabel }}
</button>
</section>
<section class="contextCard">
<div class="contextCard__label">Account</div>
<div class="contextStat">
<span class="contextStat__name">현재 사용자</span>
<span class="contextStat__value">{{ accountName }}</span>
</div>
<div class="contextStat">
<span class="contextStat__name">권한</span>
<span class="contextStat__value">{{ isAdmin ? 'Admin' : auth.user ? 'Member' : 'Guest' }}</span>
</div>
</section>
</aside>
</template>
<div class="toastStack" :class="{ 'toastStack--preview': isPreviewMode }" aria-live="polite" aria-atomic="true">
<div v-for="item in toasts" :key="item.id" class="toast" :class="[`toast--${item.type}`, { 'toast--closing': item.isClosing }]">
<div class="toast__body">
<div class="toast__message">{{ item.message }}</div>
<div v-if="item.count > 1" class="toast__count">x{{ item.count }}</div>
</div>
<button class="toast__close" @click="dismissToast(item.id)">닫기</button>
<button class="toast__close" type="button" @click="dismissToast(item.id)">닫기</button>
</div>
</div>
</div>
</template>
<style scoped>
.app-shell {
.appShell {
min-height: 100vh;
background: radial-gradient(1200px 800px at 20% 10%, rgba(110, 231, 183, 0.18), transparent 55%),
radial-gradient(1000px 700px at 80% 20%, rgba(96, 165, 250, 0.18), transparent 55%),
#0b1220;
display: grid;
grid-template-columns: 248px minmax(0, 1fr) 320px;
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.04), transparent 28%),
linear-gradient(180deg, #1a1a1a 0%, #121212 100%);
color: rgba(255, 255, 255, 0.92);
transition: grid-template-columns 220ms ease;
}
.app-header {
position: sticky;
top: 0;
z-index: 10;
.appShell--preview {
display: block;
}
.leftRail,
.rightRail {
min-height: 100vh;
padding: 14px 12px;
border-right: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(14, 14, 14, 0.92);
box-sizing: border-box;
min-width: 0;
}
.rightRail {
border-right: 0;
border-left: 1px solid rgba(255, 255, 255, 0.08);
transition:
opacity 220ms ease,
transform 220ms ease,
padding 220ms ease,
border-color 220ms ease;
}
.appShell--rightClosed {
grid-template-columns: 248px minmax(0, 1fr) 0px;
}
.appShell--rightClosed .rightRail {
opacity: 0;
transform: translateX(18px);
pointer-events: none;
overflow: hidden;
padding-left: 0;
padding-right: 0;
border-left-color: transparent;
}
.leftRail__top,
.rightRail__top {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(11, 18, 32, 0.72);
backdrop-filter: blur(10px);
gap: 12px;
margin-bottom: 18px;
}
.brand {
display: flex;
gap: 10px;
align-items: baseline;
.ghostIcon {
min-width: 28px;
height: 28px;
padding: 0 10px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
user-select: none;
}
.brand__title {
font-weight: 800;
letter-spacing: -0.02em;
}
.brand__sub {
.ghostIcon--workspace {
min-width: 118px;
height: 36px;
padding: 0 14px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.88);
font-size: 12px;
padding: 2px 8px;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
opacity: 0.9;
font-weight: 800;
}
.nav {
.brandBlock {
display: grid;
gap: 2px;
cursor: pointer;
}
.brandBlock__title {
font-size: 21px;
font-weight: 900;
letter-spacing: -0.04em;
}
.brandBlock__sub {
font-size: 12px;
color: rgba(255, 255, 255, 0.56);
}
.appUserCard {
position: relative;
margin-bottom: 14px;
}
.appUserCard__button,
.appUserCard__guest {
width: 100%;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
}
.nav__link {
text-decoration: none;
color: rgba(255, 255, 255, 0.86);
padding: 8px 10px;
border-radius: 10px;
border: 1px solid transparent;
}
.nav__link.router-link-active {
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.06);
}
.app-main {
padding: 20px 18px 60px;
width: 100%;
padding: 10px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: inherit;
text-align: left;
cursor: pointer;
box-sizing: border-box;
}
.app-main--preview {
padding: 0;
min-height: 100vh;
.appUserCard__avatar {
width: 38px;
height: 38px;
border-radius: 12px;
object-fit: cover;
flex: 0 0 auto;
}
.user {
position: relative;
}
.avatarBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
cursor: pointer;
padding: 0;
}
.avatarImg {
width: 100%;
height: 100%;
border-radius: 999px;
object-fit: cover;
}
.avatarFallback {
font-weight: 900;
opacity: 0.9;
}
.menu {
position: absolute;
right: 0;
top: calc(100% + 8px);
width: 160px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(11, 18, 32, 0.92);
backdrop-filter: blur(10px);
padding: 6px;
.appUserCard__avatar--fallback {
display: grid;
gap: 6px;
place-items: center;
background: rgba(255, 255, 255, 0.12);
font-weight: 900;
}
.menuItem {
text-align: left;
padding: 10px 10px;
border-radius: 12px;
border: 1px solid transparent;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
.appUserCard__meta {
min-width: 0;
display: grid;
gap: 4px;
}
.appUserCard__name {
font-size: 14px;
font-weight: 800;
}
.menuItem:hover {
background: rgba(255, 255, 255, 0.09);
.appUserCard__email {
font-size: 12px;
color: rgba(255, 255, 255, 0.56);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.appUserMenu {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
display: grid;
gap: 6px;
padding: 8px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(10, 10, 10, 0.98);
z-index: 20;
}
.appUserMenu__item {
padding: 10px 12px;
border-radius: 10px;
border: 0;
background: rgba(255, 255, 255, 0.04);
color: inherit;
cursor: pointer;
text-align: left;
}
.searchStub {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.02);
color: rgba(255, 255, 255, 0.62);
cursor: pointer;
margin-bottom: 14px;
}
.searchStub__icon {
font-size: 14px;
}
.leftNav {
display: grid;
gap: 8px;
}
.leftNav__item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 12px;
color: rgba(255, 255, 255, 0.76);
text-decoration: none;
}
.leftNav__item--active,
.leftNav__item.router-link-active {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.96);
}
.leftNav__glyph {
width: 24px;
height: 24px;
border-radius: 8px;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.06);
font-size: 10px;
font-weight: 900;
letter-spacing: 0.06em;
flex: 0 0 auto;
}
.leftRail__section {
margin-top: 24px;
display: grid;
gap: 10px;
}
.leftRail__sectionTitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.favoriteLink {
display: flex;
gap: 10px;
align-items: center;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
font-size: 14px;
}
.favoriteLink__dot {
width: 10px;
height: 10px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.86);
}
.leftRail__bottom {
margin-top: auto;
padding-top: 20px;
}
.adminButton {
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 12px 14px;
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.92);
text-decoration: none;
box-sizing: border-box;
}
.leftRail {
display: flex;
flex-direction: column;
}
.appMain {
min-width: 0;
padding: 14px 18px 22px;
box-sizing: border-box;
}
.appMain--preview {
padding: 0;
}
.workspace {
display: grid;
gap: 16px;
}
.workspaceHead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.workspaceHead__actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.workspaceHead__title {
font-size: 28px;
font-weight: 900;
letter-spacing: -0.04em;
}
.workspaceHead__subtitle {
margin-top: 6px;
color: rgba(255, 255, 255, 0.58);
font-size: 13px;
}
.workspaceBody {
min-height: calc(100vh - 110px);
padding: 18px;
border-radius: 26px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #2b2b2b;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.rightRail {
display: grid;
align-content: start;
gap: 18px;
}
.contextCard {
display: grid;
gap: 12px;
padding: 14px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.02);
}
.contextCard__label {
font-size: 12px;
color: rgba(255, 255, 255, 0.42);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.contextCard__title {
margin: 0;
font-size: 20px;
line-height: 1.2;
}
.contextCard__text {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.66);
}
.contextCard__action {
width: 100%;
padding: 12px 14px;
border-radius: 12px;
border: 0;
background: #4b7fe9;
color: #fff;
font-weight: 800;
cursor: pointer;
}
.contextStat {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.contextStat__name {
color: rgba(255, 255, 255, 0.56);
font-size: 13px;
}
.contextStat__value {
font-size: 14px;
font-weight: 700;
}
.toastStack {
position: fixed;
top: 78px;
right: 18px;
z-index: 30;
top: 18px;
right: 20px;
z-index: 40;
display: grid;
gap: 10px;
width: min(360px, calc(100vw - 24px));
}
.toastStack--preview {
top: 12px;
}
.toast {
display: flex;
gap: 12px;
@@ -242,50 +732,71 @@ async function logout() {
transform: translateY(0);
transition: opacity 220ms ease, transform 220ms ease;
}
.toast--closing {
opacity: 0;
transform: translateY(-6px);
}
.toast--success {
border-color: rgba(52, 211, 153, 0.38);
}
.toast--error {
border-color: rgba(239, 68, 68, 0.34);
}
.toast--info {
border-color: rgba(96, 165, 250, 0.34);
}
.toast__message {
line-height: 1.5;
font-size: 14px;
}
.toast__body {
min-width: 0;
}
.toast__count {
margin-top: 6px;
width: fit-content;
padding: 2px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
font-size: 12px;
font-weight: 800;
opacity: 0.84;
color: rgba(255, 255, 255, 0.56);
}
.toast__close {
flex: 0 0 auto;
border: 0;
background: transparent;
color: rgba(255, 255, 255, 0.76);
color: rgba(255, 255, 255, 0.68);
cursor: pointer;
font-weight: 800;
font-size: 12px;
}
@media (max-width: 640px) {
.toastStack {
top: 70px;
right: 12px;
left: 12px;
width: auto;
@media (max-width: 1280px) {
.appShell {
grid-template-columns: 220px minmax(0, 1fr);
}
.rightRail {
display: none;
}
}
@media (max-width: 860px) {
.appShell {
grid-template-columns: 1fr;
}
.leftRail {
min-height: auto;
border-right: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.workspaceBody {
padding: 14px;
border-radius: 20px;
}
.workspaceHead__title {
font-size: 26px;
}
}
</style>

View File

@@ -1,57 +1,49 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-family: 'Pretendard', 'Inter', 'Segoe UI', sans-serif;
line-height: 1.5;
font-weight: 400;
color: rgba(255, 255, 255, 0.92);
background: #121212;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
* {
box-sizing: border-box;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
html,
body,
#app {
min-height: 100vh;
}
body {
margin: 0;
background: #121212;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
appearance: none;
}
a {
color: inherit;
}
input,
select,
textarea {
color: rgba(255, 255, 255, 0.92);
}
select {
@@ -59,253 +51,24 @@ select {
-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%);
linear-gradient(45deg, transparent 50%, rgba(255, 255, 255, 0.72) 50%),
linear-gradient(135deg, rgba(255, 255, 255, 0.72) 50%, transparent 50%);
background-position:
calc(100% - 18px) calc(50% - 2px),
calc(100% - 12px) calc(50% - 2px);
calc(100% - 20px) calc(50% - 2px),
calc(100% - 14px) calc(50% - 2px);
background-size: 6px 6px, 6px 6px;
background-repeat: no-repeat;
padding-right: 36px;
padding-right: 40px;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
h2,
h3,
h4,
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
/* width: 1126px; */
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -1354,18 +1354,14 @@ async function saveFeaturedOrder() {
<style scoped>
.wrap {
padding: 10px 2px;
}
.title {
margin: 0 0 10px;
font-size: 26px;
letter-spacing: -0.02em;
display: grid;
gap: 16px;
}
.card {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 14px;
border: 0;
background: transparent;
border-radius: 0;
padding: 0;
}
.desc {
opacity: 0.82;
@@ -1392,7 +1388,7 @@ async function saveFeaturedOrder() {
}
.tabs,
.modeTabs {
margin-top: 14px;
margin-top: 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
@@ -1400,26 +1396,27 @@ async function saveFeaturedOrder() {
.tab,
.modeTab {
padding: 10px 14px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
font-weight: 800;
}
.tab--active,
.modeTab--active {
background: rgba(96, 165, 250, 0.2);
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.18);
}
.panel {
margin-top: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.12);
border-radius: 16px;
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(48, 48, 48, 0.78);
border-radius: 18px;
padding: 16px;
}
.panel--compact {
max-width: 480px;
max-width: 520px;
}
.featuredOrderPanel {
margin-top: 14px;

View File

@@ -108,7 +108,7 @@ onMounted(loadFavorites)
<style scoped>
.wrap {
display: grid;
gap: 14px;
gap: 18px;
}
.head {
display: flex;
@@ -119,11 +119,13 @@ onMounted(loadFavorites)
}
.title {
margin: 0;
font-size: 28px;
font-size: 30px;
color: rgba(255, 255, 255, 0.96);
letter-spacing: -0.04em;
}
.desc {
margin-top: 6px;
opacity: 0.78;
color: rgba(255, 255, 255, 0.58);
}
.toolbar {
display: flex;
@@ -133,15 +135,15 @@ onMounted(loadFavorites)
.input,
.select {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92);
}
.btn {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
font-weight: 800;
@@ -153,12 +155,12 @@ onMounted(loadFavorites)
.list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
gap: 18px;
}
.row {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
overflow: hidden;
display: grid;
gap: 10px;
@@ -176,7 +178,7 @@ onMounted(loadFavorites)
.row__thumbWrap {
width: 100%;
aspect-ratio: 16 / 9;
background: rgba(255, 255, 255, 0.03);
background: #555;
}
.row__thumb,
.row__thumbPlaceholder {
@@ -188,7 +190,7 @@ onMounted(loadFavorites)
object-fit: cover;
}
.row__thumbPlaceholder {
background: linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
background: #555;
}
.row__head {
padding: 14px 14px 0;
@@ -245,6 +247,11 @@ onMounted(loadFavorites)
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 960px) {
.list {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.list {
grid-template-columns: 1fr;

View File

@@ -79,9 +79,9 @@ function submitSearch() {
<template>
<section class="head">
<div class="head__left">
<div class="kicker">게임</div>
<div class="kicker">Collection</div>
<h2 class="title">{{ gameName || gameId }}</h2>
<p class="desc"> 티어표를 만들거나, 다른 사람들이 올린 티어표를 확인하세.</p>
<p class="desc"> 티어표를 만들거나, 다른 사람들이 올린 티어표를 카드형 목록으로 탐색할 있어.</p>
</div>
<div class="head__right">
<button class="primary" @click="createNew">{{ auth.user ? '새로운 티어표 만들기' : '로그인 티어표 만들기' }}</button>
@@ -128,42 +128,45 @@ function submitSearch() {
<style scoped>
.head {
display: flex;
gap: 14px;
gap: 18px;
align-items: flex-end;
justify-content: space-between;
flex-wrap: wrap;
padding: 6px 2px 14px;
padding: 4px 2px 18px;
}
.kicker {
font-size: 12px;
opacity: 0.7;
color: rgba(255, 255, 255, 0.42);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.title {
margin: 4px 0 6px;
font-size: 26px;
letter-spacing: -0.02em;
font-size: 30px;
letter-spacing: -0.04em;
color: rgba(255, 255, 255, 0.96);
}
.desc {
margin: 0;
opacity: 0.84;
color: rgba(255, 255, 255, 0.58);
}
.primary {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(96, 165, 250, 0.2);
padding: 10px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
font-weight: 700;
}
.primary:hover {
background: rgba(96, 165, 250, 0.26);
background: rgba(255, 255, 255, 0.12);
}
.panel {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: transparent;
border-radius: 0;
padding: 0;
}
.error {
margin: 10px 0 14px;
@@ -181,7 +184,7 @@ function submitSearch() {
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 10px;
margin-bottom: 18px;
}
.searchBar {
display: flex;
@@ -192,15 +195,15 @@ function submitSearch() {
.searchBar__input {
min-width: 240px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.92);
}
.searchBar__button {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
font-weight: 800;
@@ -212,12 +215,12 @@ function submitSearch() {
.list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
gap: 18px;
}
.row {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
color: rgba(255, 255, 255, 0.92);
display: grid;
gap: 10px;
@@ -242,7 +245,7 @@ function submitSearch() {
.row__thumbWrap {
width: 100%;
aspect-ratio: 16 / 9;
background: rgba(255, 255, 255, 0.03);
background: #555;
}
.row__thumb {
width: 100%;
@@ -253,8 +256,7 @@ function submitSearch() {
.row__thumbPlaceholder {
width: 100%;
height: 100%;
background:
linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
background: #555;
}
.row__title {
font-weight: 800;
@@ -310,11 +312,16 @@ function submitSearch() {
padding: 7px 10px;
font-weight: 800;
}
@media (max-width: 1100px) {
@media (max-width: 1280px) {
.list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.list {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.list {
grid-template-columns: 1fr;

View File

@@ -43,10 +43,14 @@ function thumbUrl(g) {
<template>
<section class="topBar">
<div class="topBar__copy">
<h1 class="topBar__title">게임 선택</h1>
<p class="topBar__desc">관리자 고정 순서가 있으면 먼저 보여주고, 게임은 최근 생성순으로 렬됩니다.</p>
<h1 class="topBar__title">Main Title</h1>
<p class="topBar__desc">게임 선택과 커스텀 티어표 진입을 하나의 대시보드처럼 리했습니다.</p>
</div>
<div class="toolbar">
<button class="toolbar__ghost" @click="goFreeform">Toggle Filter</button>
<button class="toolbar__select" @click="goFreeform">Select Filter</button>
<button class="customTierBtn" @click="goFreeform">{{ auth.user ? '+ 커스텀 티어표 만들기' : '+ 로그인 커스텀 티어표 만들기' }}</button>
</div>
<button class="customTierBtn" @click="goFreeform">{{ auth.user ? '커스텀 티어표 만들기' : '로그인 커스텀 티어표 만들기' }}</button>
</section>
<div v-if="error" class="error">{{ error }}</div>
@@ -64,70 +68,82 @@ function thumbUrl(g) {
<style scoped>
.topBar {
display: flex;
gap: 16px;
gap: 18px;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
margin-top: 4px;
margin-top: 2px;
margin-bottom: 18px;
}
.topBar__copy {
display: grid;
gap: 6px;
gap: 8px;
}
.topBar__title {
margin: 0;
font-size: 30px;
letter-spacing: -0.03em;
font-size: 32px;
letter-spacing: -0.04em;
color: rgba(255, 255, 255, 0.96);
}
.topBar__desc {
margin: 0;
opacity: 0.78;
color: rgba(255, 255, 255, 0.58);
line-height: 1.5;
}
.toolbar {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.toolbar__ghost,
.toolbar__select,
.customTierBtn {
padding: 12px 16px;
border-radius: 14px;
border: 1px solid rgba(96, 165, 250, 0.28);
background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(16, 185, 129, 0.16));
color: rgba(255, 255, 255, 0.96);
font-weight: 900;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.84);
font-weight: 700;
cursor: pointer;
}
.customTierBtn {
background: rgba(255, 255, 255, 0.08);
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
gap: 18px;
}
.error {
margin-top: 12px;
margin: 0 0 16px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.12);
border: 1px solid rgba(239, 68, 68, 0.18);
background: rgba(239, 68, 68, 0.1);
color: rgba(255, 255, 255, 0.92);
}
.card {
text-align: left;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
padding: 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
display: grid;
gap: 10px;
gap: 12px;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04);
}
.card:hover {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.18);
background: rgba(72, 72, 72, 0.92);
}
.thumbWrap {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: #555;
overflow: hidden;
display: grid;
place-items: center;
@@ -138,28 +154,39 @@ function thumbUrl(g) {
object-fit: cover;
}
.thumbFallback {
font-weight: 900;
font-size: 28px;
opacity: 0.85;
font-weight: 700;
font-size: 14px;
color: rgba(255, 255, 255, 0.4);
}
.card__title {
font-weight: 800;
letter-spacing: -0.02em;
font-size: 15px;
}
@media (max-width: 1200px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.topBar {
align-items: stretch;
}
.customTierBtn {
.toolbar {
width: 100%;
}
.toolbar__ghost,
.toolbar__select,
.customTierBtn {
flex: 1 1 100%;
}
.grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 721px) and (max-width: 1100px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>

View File

@@ -101,18 +101,19 @@ async function removeList(t) {
<style scoped>
.wrap {
padding: 10px 2px;
padding: 4px 2px;
}
.title {
margin: 0 0 10px;
font-size: 26px;
letter-spacing: -0.02em;
margin: 0 0 18px;
font-size: 30px;
letter-spacing: -0.04em;
color: rgba(255, 255, 255, 0.96);
}
.card {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 14px;
border: 0;
background: transparent;
border-radius: 0;
padding: 0;
}
.link {
padding: 8px 10px;
@@ -129,14 +130,14 @@ async function removeList(t) {
.list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
gap: 18px;
}
.row {
display: grid;
gap: 10px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.10);
background: rgba(0, 0, 0, 0.16);
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(62, 62, 62, 0.82);
color: rgba(255, 255, 255, 0.92);
overflow: hidden;
}
@@ -155,7 +156,7 @@ async function removeList(t) {
.row__thumbWrap {
width: 100%;
aspect-ratio: 16 / 9;
background: rgba(255, 255, 255, 0.03);
background: #555;
}
.row__thumb {
width: 100%;
@@ -166,8 +167,7 @@ async function removeList(t) {
.row__thumbPlaceholder {
width: 100%;
height: 100%;
background:
linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
background: #555;
}
.row__title {
font-weight: 900;
@@ -218,6 +218,11 @@ async function removeList(t) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 960px) {
.list {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.list {
grid-template-columns: 1fr;

View File

@@ -857,8 +857,8 @@ onUnmounted(() => {
<style scoped>
.head {
display: grid;
gap: 14px;
padding: 6px 2px 14px;
gap: 16px;
padding: 2px 2px 16px;
}
.previewOnly {
min-height: 100vh;
@@ -938,8 +938,8 @@ onUnmounted(() => {
}
.heroCard {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 360px);
gap: 18px;
grid-template-columns: minmax(0, 1.65fr) minmax(260px, 320px);
gap: 16px;
align-items: stretch;
}
.heroCard__main,
@@ -1123,8 +1123,8 @@ onUnmounted(() => {
}
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 14px;
grid-template-columns: minmax(0, 1fr) 284px;
gap: 16px;
align-items: start;
}
.error {
@@ -1135,10 +1135,10 @@ onUnmounted(() => {
background: rgba(239, 68, 68, 0.12);
}
.board {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(48, 48, 48, 0.78);
border-radius: 18px;
padding: 18px;
align-self: start;
}
.modalOverlay {
@@ -1419,9 +1419,9 @@ onUnmounted(() => {
object-fit: cover;
}
.sidebar {
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(48, 48, 48, 0.78);
border-radius: 18px;
padding: 12px;
}
.sidebar__title {