Compare commits

...

4 Commits

15 changed files with 160 additions and 128 deletions

View File

@@ -1,5 +1,18 @@
# 의사결정 이력 # 의사결정 이력
## 2026-04-02 v1.4.2
- 용어 정리를 시작한 뒤에는 일부 화면만 바꾸는 것보다, 관리자 모달과 확인 메시지처럼 실제 운영 중 많이 보는 문구도 함께 맞춰 주는 편이 체감 일관성이 더 높다고 판단했다.
## 2026-04-02 v1.4.1
- 좌측 메뉴와 화면 타이틀의 명칭이 서로 다르면 사용자가 현재 위치를 직관적으로 매칭하기 어렵기 때문에, 메뉴 이름과 진입 타이틀을 같은 문구로 맞추는 편이 맞다고 판단했다.
## 2026-04-02 v1.4.0
- 서비스가 게임 외 주제 전반을 다룰 수 있는 단계에 온 만큼, 내부 모델명은 유지하더라도 사용자에게 보이는 주요 용어는 `주제 / 템플릿` 기준으로 먼저 정리하는 편이 맞다고 판단했다.
- 대규모 내부 리네이밍은 API와 DB까지 손대야 하므로, 이번 단계에서는 사용자 화면 문구만 우선 바꾸고 내부 `game` 모델은 그대로 두는 점진적 전환이 더 안전하다고 정리했다.
## 2026-04-02 v1.3.93
- 목록 카드 썸네일은 드래그 대상이 아니라 클릭 대상에 가깝기 때문에, 브라우저 기본 이미지 드래그 프리뷰는 전부 막아 두는 편이 UX 측면에서 맞다고 판단했다.
## 2026-04-02 v1.3.92 ## 2026-04-02 v1.3.92
- 왼쪽 레일 활성 메뉴도 로그인 토글과 같은 이동형 배경 문법을 쓰는 편이 앱 전체 인터랙션 언어를 더 일관되게 만든다고 판단했다. - 왼쪽 레일 활성 메뉴도 로그인 토글과 같은 이동형 배경 문법을 쓰는 편이 앱 전체 인터랙션 언어를 더 일관되게 만든다고 판단했다.

View File

@@ -1,6 +1,11 @@
# 할 일 및 이슈 # 할 일 및 이슈
## 단기 확인 ## 단기 확인
- 관리자 화면까지 포함한 사용자 노출 `게임` 문구를 2차로 정리했으므로, 실제 운영 흐름에서 뜨는 확인창/토스트/선택 모달까지 표현이 자연스러운지 한 번 더 QA한다.
- 왼쪽 사이드 메뉴와 각 화면 타이틀을 한글 기준으로 맞췄으므로, 홈/나의 티어표/즐겨찾기/설정 진입 시 실제 체감이 자연스럽고 중복 표현이 어색하지 않은지 한 번 더 QA한다.
- 용어 정리 1차는 사용자 노출 문구만 `주제 / 템플릿`으로 바꿨으므로, 홈/주제 화면/관리자 템플릿 관리에서 어색하게 남은 `게임` 문구가 없는지 한 번 더 QA한다.
- 내부 모델명은 아직 `game`을 유지하므로, 다음 단계에서는 문서와 보조 화면 문구를 더 정리할지, 아니면 내부 리네이밍 계획을 따로 잡을지 결정한다.
- 게임 목록과 티어표 카드 썸네일은 기본 이미지 드래그를 막았으므로, 데스크톱 브라우저에서 클릭/드래그 시 원본 이미지 프리뷰가 더 이상 뜨지 않는지 한 번 더 QA한다.
- 왼쪽 레일 활성 배경은 공용 인디케이터가 이동하는 방식으로 바뀌었으므로, 홈/내 티어표/즐겨찾기/설정 전환과 레일 접힘 상태 양쪽에서 위치 보정이 자연스러운지 한 번 더 QA한다. - 왼쪽 레일 활성 배경은 공용 인디케이터가 이동하는 방식으로 바뀌었으므로, 홈/내 티어표/즐겨찾기/설정 전환과 레일 접힘 상태 양쪽에서 위치 보정이 자연스러운지 한 번 더 QA한다.
- 로그인 화면 상단 토글은 이동형 인디케이터로 바뀌었으므로, 데스크톱과 모바일에서 `로그인 / 회원가입` 전환 애니메이션이 어색하지 않고 포커스/클릭 상태도 자연스러운지 한 번 더 QA한다. - 로그인 화면 상단 토글은 이동형 인디케이터로 바뀌었으므로, 데스크톱과 모바일에서 `로그인 / 회원가입` 전환 애니메이션이 어색하지 않고 포커스/클릭 상태도 자연스러운지 한 번 더 QA한다.
- 관리자 카드 설명 줄임은 `line-clamp` 표준 속성까지 함께 선언했으므로, 실제 브라우저별 표시 차이가 없는지 한 번 더 QA한다. - 관리자 카드 설명 줄임은 `line-clamp` 표준 속성까지 함께 선언했으므로, 실제 브라우저별 표시 차이가 없는지 한 번 더 QA한다.

View File

@@ -1,5 +1,18 @@
# 업데이트 로그 # 업데이트 로그
## 2026-04-02 v1.4.2
- 관리자 화면과 보조 모달에 남아 있던 사용자 노출 `게임` 문구를 추가로 걷어내고, `템플릿 / 주제` 기준 표현으로 더 통일했다.
## 2026-04-02 v1.4.1
- 왼쪽 사이드 메뉴를 `주제 선택 / 나의 티어표 / 즐겨찾기 / 설정` 한글 문구로 통일하고, 해당 화면 진입 시 헤더 타이틀도 같은 이름 기준으로 맞췄다.
## 2026-04-02 v1.4.0
- 사용자 노출 용어 1차 정리를 시작해 홈/좌측 레일/가이드/주제 화면에서는 `게임` 대신 `주제`, 관리자 핵심 화면에서는 `게임 관리` 대신 `템플릿 관리` 중심 표현으로 바꿨다.
- 내부 데이터 모델과 API의 `gameId`, `/games` 구조는 아직 유지하고, 이번 단계는 화면 문구와 안내 텍스트를 먼저 정리하는 안전한 1차 리네이밍 범위로 제한했다.
## 2026-04-02 v1.3.93
- 게임 목록, 티어표 리스트, 사용자 아바타 버튼 등 목록성 썸네일 이미지에 `draggable=\"false\"`를 적용해 브라우저 기본 이미지 드래그 프리뷰가 뜨지 않도록 정리함.
## 2026-04-02 v1.3.92 ## 2026-04-02 v1.3.92
- 왼쪽 네비게이션의 활성 메뉴 배경은 개별 항목에 즉시 붙는 방식에서, 공용 인디케이터가 현재 메뉴 위치로 미끄러져 이동하는 토글형 인터랙션으로 정리함. - 왼쪽 네비게이션의 활성 메뉴 배경은 개별 항목에 즉시 붙는 방식에서, 공용 인디케이터가 현재 메뉴 위치로 미끄러져 이동하는 토글형 인터랙션으로 정리함.

View File

@@ -26,7 +26,7 @@ const RIGHT_RAIL_COPYRIGHT_URL = 'https://zenn.town/@murabito'
const leftRailCollapsed = ref(false) const leftRailCollapsed = ref(false)
const rightRailOpen = ref(true) const rightRailOpen = ref(true)
const searchQuery = ref('') const searchQuery = ref('')
const leftRailSearchPlaceholder = '게임 템플릿 검색' const leftRailSearchPlaceholder = '주제 템플릿 검색'
const isCollapsedSearchOpen = ref(false) const isCollapsedSearchOpen = ref(false)
const isGuideModalOpen = ref(false) const isGuideModalOpen = ref(false)
const themeMode = ref('dark') const themeMode = ref('dark')
@@ -60,10 +60,10 @@ const shellStyle = computed(() => ({
})) }))
const leftNavItems = computed(() => { const leftNavItems = computed(() => {
const items = [ const items = [
{ key: 'home', label: 'Games', path: '/', iconSrc: iconGridView }, { key: 'home', label: '주제 선택', path: '/', iconSrc: iconGridView },
{ key: 'me', label: 'My Lists', path: '/me', iconSrc: iconLists, requiresAuth: true }, { key: 'me', label: '나의 티어표', path: '/me', iconSrc: iconLists, requiresAuth: true },
{ key: 'favorites', label: 'Favorites', path: '/favorites', iconSrc: iconFavorite, requiresAuth: true }, { key: 'favorites', label: '즐겨찾기', path: '/favorites', iconSrc: iconFavorite, requiresAuth: true },
{ key: 'profile', label: 'Settings', path: '/profile', iconSrc: iconSettings, requiresAuth: true }, { key: 'profile', label: '설정', path: '/profile', iconSrc: iconSettings, requiresAuth: true },
] ]
return items.filter((item) => !item.requiresAuth || (authReady.value && auth.user)) return items.filter((item) => !item.requiresAuth || (authReady.value && auth.user))
}) })
@@ -73,10 +73,10 @@ const showSettingsGuideButton = computed(() => route.name === 'profile')
const guideSteps = [ const guideSteps = [
{ {
id: 'select-game', id: 'select-game',
title: '게임 또는 양식 선택', title: '주제 또는 양식 선택',
summary: '게임 템플릿을 고르거나 커스텀 티어표 만들기로 바로 시작합니다.', summary: '주제 템플릿을 고르거나 커스텀 티어표 만들기로 바로 시작합니다.',
description: description:
'홈 화면에서는 게임 템플릿을 선택하거나 커스텀 티어표 만들기로 바로 새 보드를 열 수 있어요. 게임을 먼저 고르면 해당 게임의 공개 티어표도 같이 살펴볼 수 있어서, 완전히 처음 만드는지 기존 흐름을 참고할지 결정하기 쉽습니다.', '홈 화면에서는 주제 템플릿을 선택하거나 커스텀 티어표 만들기로 바로 새 보드를 열 수 있어요. 원하는 주제를 먼저 고르면 해당 주제의 공개 티어표도 같이 살펴볼 수 있어서, 완전히 처음 만드는지 기존 흐름을 참고할지 결정하기 쉽습니다.',
}, },
{ {
id: 'arrange-board', id: 'arrange-board',
@@ -90,7 +90,7 @@ const guideSteps = [
title: '아이템 배치와 커스텀 추가', title: '아이템 배치와 커스텀 추가',
summary: '프리셋 아이템과 직접 올린 이미지를 드래그로 배치합니다.', summary: '프리셋 아이템과 직접 올린 이미지를 드래그로 배치합니다.',
description: description:
'오른쪽 아이템 영역의 이미지를 원하는 칸으로 끌어다 놓으면 바로 배치됩니다. 게임 템플릿에 없는 이미지는 커스텀 이미지로 추가해 같이 쓸 수 있고, 이름 표시 옵션을 켜면 결과 이미지를 더 설명적으로 정리할 수 있어요.', '오른쪽 아이템 영역의 이미지를 원하는 칸으로 끌어다 놓으면 바로 배치됩니다. 주제 템플릿에 없는 이미지는 커스텀 이미지로 추가해 같이 쓸 수 있고, 이름 표시 옵션을 켜면 결과 이미지를 더 설명적으로 정리할 수 있어요.',
}, },
{ {
id: 'save-share', id: 'save-share',
@@ -109,23 +109,23 @@ const guideSteps = [
{ {
id: 'request-template-update', id: 'request-template-update',
title: '템플릿 업그레이드 요청', title: '템플릿 업그레이드 요청',
summary: '현재 게임 템플릿에 공통 아이템을 추가해 달라고 관리자에게 요청합니다.', summary: '현재 템플릿에 공통 아이템을 추가해 달라고 관리자에게 요청합니다.',
description: description:
'직접 추가한 아이템 중 여러 사람이 함께 써도 좋을 것 같은 항목이 있다면 템플릿 업데이트 요청을 보낼 수 있어요. 요청 모달에서는 현재 티어표 제목과 설명을 기본값으로 가져오고, 필요하면 요청 제목과 설명을 더 다듬어 공통 템플릿에 왜 필요한지 설명할 수 있습니다.', '직접 추가한 아이템 중 여러 사람이 함께 써도 좋을 것 같은 항목이 있다면 템플릿 업데이트 요청을 보낼 수 있어요. 요청 모달에서는 현재 티어표 제목과 설명을 기본값으로 가져오고, 필요하면 요청 제목과 설명을 더 다듬어 공통 템플릿에 왜 필요한지 설명할 수 있습니다.',
}, },
{ {
id: 'request-new-template', id: 'request-new-template',
title: '새 템플릿 추가 요청', title: '새 템플릿 추가 요청',
summary: '아직 없는 게임이나 새로운 양식을 관리자에게 제안합니다.', summary: '아직 없는 주제나 새로운 양식을 관리자에게 제안합니다.',
description: description:
'원하는 게임 템플릿이 아직 없다면 새 템플릿 추가 요청으로 관리자에게 직접 제안할 수 있어요. 이때는 제목과 설명에 어떤 게임인지, 어떤 캐릭터나 항목이 기본으로 필요할지 적어두면 검토 속도가 훨씬 빨라집니다.', '원하는 주제 템플릿이 아직 없다면 새 템플릿 추가 요청으로 관리자에게 직접 제안할 수 있어요. 이때는 제목과 설명에 어떤 주제인지, 어떤 캐릭터나 항목이 기본으로 필요할지 적어두면 검토 속도가 훨씬 빨라집니다.',
}, },
{ {
id: 'manage-library', id: 'manage-library',
title: '즐겨찾기와 내 티어표 관리', title: '즐겨찾기와 내 티어표 관리',
summary: '마음에 드는 템플릿과 저장한 결과물을 나중에 다시 쉽게 찾습니다.', summary: '마음에 드는 템플릿과 저장한 결과물을 나중에 다시 쉽게 찾습니다.',
description: description:
'게임 템플릿은 즐겨찾기로 상단에 고정해둘 수 있고, 저장한 보드는 내 티어표에서 다시 열어 이어서 수정할 수 있어요. 자주 보는 템플릿, 공개 티어표, 내가 만든 결과물을 각각 다른 화면에서 정리해두면 이후 작업이 훨씬 빨라집니다.', '주제 템플릿은 즐겨찾기로 상단에 고정해둘 수 있고, 저장한 보드는 내 티어표에서 다시 열어 이어서 수정할 수 있어요. 자주 보는 템플릿, 공개 티어표, 내가 만든 결과물을 각각 다른 화면에서 정리해두면 이후 작업이 훨씬 빨라집니다.',
}, },
] ]
const currentGuideStep = computed(() => guideSteps[guideStepIndex.value] || guideSteps[0]) const currentGuideStep = computed(() => guideSteps[guideStepIndex.value] || guideSteps[0])
@@ -151,10 +151,10 @@ const leftBottomPrimaryAction = computed(() => {
const routeMeta = computed(() => { const routeMeta = computed(() => {
if (route.name === 'home') { if (route.name === 'home') {
return { return {
title: 'Tier Maker', title: '주제 선택',
subtitle: '게임 템플릿 선택과 커스텀 보드 시작', subtitle: '주제 템플릿 선택과 커스텀 보드 시작',
contextTitle: '빠른 시작', contextTitle: '빠른 시작',
contextText: auth.user ? '커스텀 티어표를 만들거나 원하는 게임을 바로 선택할 수 있어요.' : '로그인하면 커스텀 티어표 생성과 개인 목록 관리가 열립니다.', contextText: auth.user ? '커스텀 티어표를 만들거나 원하는 주제를 바로 선택할 수 있어요.' : '로그인하면 커스텀 티어표 생성과 개인 목록 관리가 열립니다.',
actionLabel: auth.user ? '커스텀 티어표 만들기' : '로그인하러 가기', actionLabel: auth.user ? '커스텀 티어표 만들기' : '로그인하러 가기',
action: () => { action: () => {
router.push(auth.user ? '/editor/freeform/new' : '/login') router.push(auth.user ? '/editor/freeform/new' : '/login')
@@ -163,10 +163,10 @@ const routeMeta = computed(() => {
} }
if (route.name === 'gameHub') { if (route.name === 'gameHub') {
return { return {
title: 'Game Boards', title: '주제 티어표',
subtitle: '게임별 공개 티어표 탐색', subtitle: '주제별 공개 티어표 탐색',
contextTitle: '작성 작업', contextTitle: '작성 작업',
contextText: auth.user ? '이 게임의 새 티어표를 만들거나 기존 공개 티어표를 확인할 수 있어요.' : '로그인 후 새 티어표를 만들 수 있어요.', contextText: auth.user ? '이 주제의 새 티어표를 만들거나 기존 공개 티어표를 확인할 수 있어요.' : '로그인 후 새 티어표를 만들 수 있어요.',
actionLabel: auth.user ? '새 티어표 만들기' : '로그인하러 가기', actionLabel: auth.user ? '새 티어표 만들기' : '로그인하러 가기',
action: () => { action: () => {
const target = `/editor/${route.params.gameId}/new` const target = `/editor/${route.params.gameId}/new`
@@ -180,24 +180,24 @@ const routeMeta = computed(() => {
subtitle: '티어표 편집 및 공유', subtitle: '티어표 편집 및 공유',
contextTitle: '편집 패널', contextTitle: '편집 패널',
contextText: '현재 편집 옵션은 중앙 화면 안에 유지되어 있습니다. 다음 단계에서 우측 패널로 정리해갈게요.', contextText: '현재 편집 옵션은 중앙 화면 안에 유지되어 있습니다. 다음 단계에서 우측 패널로 정리해갈게요.',
actionLabel: '게임 목록으로', actionLabel: '주제 목록으로',
action: () => router.push('/'), action: () => router.push('/'),
} }
} }
if (isAdminRoute.value) { if (isAdminRoute.value) {
return { return {
title: 'Admin Workspace', title: 'Admin Workspace',
subtitle: '게임·아이템·회원 관리', subtitle: '템플릿·아이템·회원 관리',
contextTitle: '운영 노트', contextTitle: '운영 노트',
contextText: '관리자 화면은 기능이 많아 우선 공통 셸 톤을 맞췄고, 세부 패널은 다음 단계에서 시안 방식으로 더 세밀하게 나눌 예정입니다.', contextText: '관리자 화면은 기능이 많아 우선 공통 셸 톤을 맞췄고, 세부 패널은 다음 단계에서 시안 방식으로 더 세밀하게 나눌 예정입니다.',
actionLabel: '게임 목록으로', actionLabel: '주제 목록으로',
action: () => router.push('/'), action: () => router.push('/'),
} }
} }
if (route.name === 'me') { if (route.name === 'me') {
return { return {
title: 'My Lists', title: '나의 티어표',
subtitle: '내가 저장한 티어표', subtitle: '저장한 티어표 모아보기',
contextTitle: '작성 이력', contextTitle: '작성 이력',
contextText: '최근 저장한 티어표를 열람하고 정리할 수 있어요.', contextText: '최근 저장한 티어표를 열람하고 정리할 수 있어요.',
actionLabel: '즐겨찾기 보기', actionLabel: '즐겨찾기 보기',
@@ -206,27 +206,27 @@ const routeMeta = computed(() => {
} }
if (route.name === 'favorites') { if (route.name === 'favorites') {
return { return {
title: 'Favorites', title: '즐겨찾기',
subtitle: '마음에 드는 티어표 모음', subtitle: '마음에 드는 티어표 모음',
contextTitle: '정리 도구', contextTitle: '정리 도구',
contextText: '정렬과 검색으로 원하는 티어표를 빠르게 다시 찾을 수 있어요.', contextText: '정렬과 검색으로 원하는 티어표를 빠르게 다시 찾을 수 있어요.',
actionLabel: ' 티어표 보기', actionLabel: '나의 티어표 보기',
action: () => router.push('/me'), action: () => router.push('/me'),
} }
} }
if (route.name === 'profile') { if (route.name === 'profile') {
return { return {
title: 'Profile', title: '설정',
subtitle: '프로필 및 계정 설정', subtitle: '프로필 및 계정 설정',
contextTitle: '계정 관리', contextTitle: '계정 관리',
contextText: '아바타와 닉네임을 관리하고 표시 정보를 확인할 수 있어요.', contextText: '아바타와 닉네임을 관리하고 표시 정보를 확인할 수 있어요.',
actionLabel: ' 티어표 보기', actionLabel: '나의 티어표 보기',
action: () => router.push('/me'), action: () => router.push('/me'),
} }
} }
if (route.name === 'search') { if (route.name === 'search') {
return { return {
title: 'Search', title: '검색',
subtitle: '전체 공개 티어표 검색 결과', subtitle: '전체 공개 티어표 검색 결과',
contextTitle: '검색', contextTitle: '검색',
contextText: '제목, 작성자 기준으로 공개 티어표를 전체 검색할 수 있어요.', contextText: '제목, 작성자 기준으로 공개 티어표를 전체 검색할 수 있어요.',
@@ -236,7 +236,7 @@ const routeMeta = computed(() => {
} }
return { return {
title: 'Tier Maker', title: 'Tier Maker',
subtitle: '게임 템플릿으로 만드는 티어표', subtitle: '주제 템플릿으로 만드는 티어표',
contextTitle: 'Workspace', contextTitle: 'Workspace',
contextText: '현재 화면에 맞는 도구와 안내를 여기에 배치할 수 있습니다.', contextText: '현재 화면에 맞는 도구와 안내를 여기에 배치할 수 있습니다.',
actionLabel: '홈으로', actionLabel: '홈으로',
@@ -429,7 +429,7 @@ function submitGlobalSearch() {
<div class="leftRail__content"> <div class="leftRail__content">
<div v-if="authReady && auth.user" class="appUserCard"> <div v-if="authReady && auth.user" class="appUserCard">
<div class="appUserCard__button"> <div class="appUserCard__button">
<img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" /> <img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" draggable="false" />
<div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div> <div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div>
<div class="appUserCard__meta"> <div class="appUserCard__meta">
<div class="appUserCard__name">{{ accountName }}</div> <div class="appUserCard__name">{{ accountName }}</div>

View File

@@ -16,7 +16,7 @@ const props = defineProps({
<div class="sectionHeader"> <div class="sectionHeader">
<div> <div>
<div class="panel__title"> 화면 상단 고정 순서</div> <div class="panel__title"> 화면 상단 고정 순서</div>
<div class="hint hint--tight">여기에 넣은 게임 지정한 순서대로 먼저 노출되고, 나머지 게임 최근 생성순으로 뒤에 이어집니다. 최대 50개까지 설정할 있어요.</div> <div class="hint hint--tight">여기에 넣은 템플릿 지정한 순서대로 먼저 노출되고, 나머지 템플릿 최근 생성순으로 뒤에 이어집니다. 최대 50개까지 설정할 있어요.</div>
</div> </div>
<button class="btn btn--primary" @click="props.saveFeaturedOrder">순서 저장</button> <button class="btn btn--primary" @click="props.saveFeaturedOrder">순서 저장</button>
</div> </div>
@@ -24,7 +24,7 @@ const props = defineProps({
<div class="featuredOrderPanel"> <div class="featuredOrderPanel">
<div class="featuredOrderPanel__list"> <div class="featuredOrderPanel__list">
<div class="section__title">상단 고정 목록</div> <div class="section__title">상단 고정 목록</div>
<div v-if="!props.featuredGames.length" class="hint">아직 상단 고정 게임 없어요.</div> <div v-if="!props.featuredGames.length" class="hint">아직 상단 고정 템플릿 없어요.</div>
<div v-else :ref="props.featuredListRef" class="featuredList"> <div v-else :ref="props.featuredListRef" class="featuredList">
<article v-for="(game, index) in props.featuredGames" :key="game.id" class="featuredCard" :data-featured-id="game.id"> <article v-for="(game, index) in props.featuredGames" :key="game.id" class="featuredCard" :data-featured-id="game.id">
<div class="featuredCard__meta"> <div class="featuredCard__meta">
@@ -45,7 +45,7 @@ const props = defineProps({
</div> </div>
<div class="featuredOrderPanel__picker"> <div class="featuredOrderPanel__picker">
<div class="section__title">게임 추가</div> <div class="section__title">템플릿 추가</div>
<div class="featuredPickerList"> <div class="featuredPickerList">
<button <button
v-for="game in props.availableGamesForFeatured" v-for="game in props.availableGamesForFeatured"

View File

@@ -67,17 +67,17 @@ function setThumbFileElement(el) {
props.activeTemplateRequest.type === 'create' props.activeTemplateRequest.type === 'create'
? (props.activeTemplateRequest.targetGameId ? (props.activeTemplateRequest.targetGameId
? '이미 연결된 신규 템플릿이 있어요. 필요한 아이템만 골라 저장하세요.' ? '이미 연결된 신규 템플릿이 있어요. 필요한 아이템만 골라 저장하세요.'
: '새 게임을 한 번 만든 뒤 필요한 아이템만 골라 저장하세요.') : '새 템플릿을 한 번 만든 뒤 필요한 아이템만 골라 저장하세요.')
: '필요한 아이템만 남긴 뒤 기본 아이템 추가 버튼으로 반영하세요.' : '필요한 아이템만 남긴 뒤 기본 아이템 추가 버튼으로 반영하세요.'
}} }}
</div> </div>
</div> </div>
<div class="requestWorkspace__stats"> <div class="requestWorkspace__stats">
<span class="pill pill--accent">{{ props.activeTemplateRequest.type === 'create' ? '신규 게임 요청' : '기존 게임 업데이트' }}</span> <span class="pill pill--accent">{{ props.activeTemplateRequest.type === 'create' ? '신규 템플릿 요청' : '기존 템플릿 업데이트' }}</span>
<span class="pill">요청 아이템 {{ props.stagedRequestDraftCount }}</span> <span class="pill">요청 아이템 {{ props.stagedRequestDraftCount }}</span>
<span v-if="props.appliedRequestItemCount" class="pill pill--soft">이미 반영 {{ props.appliedRequestItemCount }}</span> <span v-if="props.appliedRequestItemCount" class="pill pill--soft">이미 반영 {{ props.appliedRequestItemCount }}</span>
<span v-if="props.activeTemplateRequest.type === 'create' && (props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId)" class="pill pill--soft"> <span v-if="props.activeTemplateRequest.type === 'create' && (props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId)" class="pill pill--soft">
연결된 게임 · {{ props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId }} 연결된 템플릿 · {{ props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId }}
</span> </span>
</div> </div>
</div> </div>
@@ -97,15 +97,15 @@ function setThumbFileElement(el) {
type="button" type="button"
@click="props.openGameCreateModal" @click="props.openGameCreateModal"
> >
게임 만들기 템플릿 만들기
</button> </button>
</div> </div>
</div> </div>
<div v-if="props.isGameLoading" class="panel panel--empty"> <div v-if="props.isGameLoading" class="panel panel--empty">
<div class="emptyState"> <div class="emptyState">
<div class="emptyState__title">게임 정보를 불러오는 중이에요.</div> <div class="emptyState__title">템플릿 정보를 불러오는 중이에요.</div>
<div class="emptyState__desc">선택한 게임 썸네일과 기본 아이템을 표시합니다.</div> <div class="emptyState__desc">선택한 템플릿 썸네일과 기본 아이템을 표시합니다.</div>
</div> </div>
</div> </div>
<div v-else-if="props.hasSelectedGame" class="panel"> <div v-else-if="props.hasSelectedGame" class="panel">
@@ -133,7 +133,7 @@ function setThumbFileElement(el) {
</button> </button>
</div> </div>
<div class="gameSettingsCard__body"> <div class="gameSettingsCard__body">
<div class="panel__title">게임 설정</div> <div class="panel__title">템플릿 설정</div>
<div class="gameSettingsCard__meta">{{ props.selectedGame.game.name }} · {{ props.selectedGame.game.id }}</div> <div class="gameSettingsCard__meta">{{ props.selectedGame.game.name }} · {{ props.selectedGame.game.id }}</div>
<label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': props.gameVisibilitySaving }"> <label class="toggleSwitch" :class="{ 'toggleSwitch--disabled': props.gameVisibilitySaving }">
<input :checked="!!props.selectedGame.game.isPublic" type="checkbox" @change="props.toggleSelectedGameVisibility($event.target.checked)" /> <input :checked="!!props.selectedGame.game.isPublic" type="checkbox" @change="props.toggleSelectedGameVisibility($event.target.checked)" />
@@ -142,7 +142,7 @@ function setThumbFileElement(el) {
</label> </label>
<div class="gameSettingsCard__actions"> <div class="gameSettingsCard__actions">
<button class="btn" :disabled="!props.canApplyThumbnail" @click="props.uploadThumbnail">썸네일 적용</button> <button class="btn" :disabled="!props.canApplyThumbnail" @click="props.uploadThumbnail">썸네일 적용</button>
<button class="btn btn--danger" @click="props.removeGame">게임 삭제</button> <button class="btn btn--danger" @click="props.removeGame">템플릿 삭제</button>
</div> </div>
</div> </div>
</section> </section>
@@ -236,9 +236,9 @@ function setThumbFileElement(el) {
</div> </div>
<div v-else class="panel panel--empty"> <div v-else class="panel panel--empty">
<div class="emptyState"> <div class="emptyState">
<div class="emptyState__title">게임 선택해 주세요.</div> <div class="emptyState__title">템플릿 선택해 주세요.</div>
<div v-if="props.activeTemplateRequest?.type === 'create'" class="hint hint--tight">진행 중인 신규 게임 요청이 있어요. 위의 ` 게임 만들기` 게임 만든 아이템을 추가할 있습니다.</div> <div v-if="props.activeTemplateRequest?.type === 'create'" class="hint hint--tight">진행 중인 신규 템플릿 요청이 있어요. 위의 ` 템플릿 만들기` 템플릿 만든 아이템을 추가할 있습니다.</div>
<div v-if="props.selectedGameId" class="hint hint--tight">선택한 게임 찾지 못했거나 로딩 오류가 발생했어요. 다시 선택해보세요.</div> <div v-if="props.selectedGameId" class="hint hint--tight">선택한 템플릿 찾지 못했거나 로딩 오류가 발생했어요. 다시 선택해보세요.</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -53,18 +53,18 @@ const props = defineProps({
<div class="templateRequestCard__thumbMeta"> <div class="templateRequestCard__thumbMeta">
<template v-if="request.type === 'create'"> <template v-if="request.type === 'create'">
<label class="templateRequestField"> <label class="templateRequestField">
<span class="templateRequestField__label">게임 이름</span> <span class="templateRequestField__label">템플릿 이름</span>
<input v-model="request.draftGameName" class="input" placeholder="새 게임 이름" /> <input v-model="request.draftGameName" class="input" placeholder="새 템플릿 이름" />
</label> </label>
<label class="templateRequestField"> <label class="templateRequestField">
<span class="templateRequestField__label">게임 ID</span> <span class="templateRequestField__label">템플릿 ID</span>
<input v-model="request.draftGameId" class="input" placeholder="임시 게임 ID" /> <input v-model="request.draftGameId" class="input" placeholder="임시 템플릿 ID" />
</label> </label>
</template> </template>
<template v-else> <template v-else>
<div class="templateRequestCard__thumbLabel">게임 이름</div> <div class="templateRequestCard__thumbLabel">템플릿 이름</div>
<div class="templateRequestCard__thumbValue">{{ request.draftGameName || request.sourceGameName || '-' }}</div> <div class="templateRequestCard__thumbValue">{{ request.draftGameName || request.sourceGameName || '-' }}</div>
<div class="templateRequestCard__thumbLabel">게임 ID</div> <div class="templateRequestCard__thumbLabel">템플릿 ID</div>
<div class="templateRequestCard__thumbValue">{{ request.draftGameId || request.sourceGameId || '-' }}</div> <div class="templateRequestCard__thumbValue">{{ request.draftGameId || request.sourceGameId || '-' }}</div>
</template> </template>
</div> </div>
@@ -111,7 +111,7 @@ const props = defineProps({
request.isHandling request.isHandling
? '이동중...' ? '이동중...'
: request.type === 'create' && (request.targetGameName || request.targetGameId) : request.type === 'create' && (request.targetGameName || request.targetGameId)
? '연결된 게임 열기' ? '연결된 템플릿 열기'
: '확인하기' : '확인하기'
}} }}
</button> </button>

View File

@@ -214,7 +214,7 @@ const customItemTargetGame = computed(() => games.value.find((game) => game.id =
const importModalItemCount = computed(() => importModalItems.value.length) const importModalItemCount = computed(() => importModalItems.value.length)
const activeTabTitle = computed(() => { const activeTabTitle = computed(() => {
if (activeTab.value === 'featured') return '목록 관리' if (activeTab.value === 'featured') return '목록 관리'
if (activeTab.value === 'game-admin') return '게임 관리' if (activeTab.value === 'game-admin') return '템플릿 관리'
if (activeTab.value === 'items') return '아이템 관리' if (activeTab.value === 'items') return '아이템 관리'
if (activeTab.value === 'tierlists') { if (activeTab.value === 'tierlists') {
return tierlistsMode.value === 'requests' ? '템플릿 요청 관리' : '전체 티어표 관리' return tierlistsMode.value === 'requests' ? '템플릿 요청 관리' : '전체 티어표 관리'
@@ -223,17 +223,17 @@ const activeTabTitle = computed(() => {
}) })
const activeTabDescription = computed(() => { const activeTabDescription = computed(() => {
if (activeTab.value === 'featured') { if (activeTab.value === 'featured') {
return '홈 화면 상단에 고정 노출되는 게임 순서를 따로 관리합니다.' return '홈 화면 상단에 고정 노출되는 템플릿 순서를 따로 관리합니다.'
} }
if (activeTab.value === 'game-admin') { if (activeTab.value === 'game-admin') {
return '게임 생성, 선택, 썸네일, 기본 아이템 관리를 전용 작업 화면으로 분리했습니다.' return '템플릿 생성, 선택, 썸네일, 기본 아이템 관리를 전용 작업 화면으로 분리했습니다.'
} }
if (activeTab.value === 'items') { if (activeTab.value === 'items') {
return '사용자 업로드와 관리자 템플릿 이미지를 함께 검수하고, 필요한 게임에 직접 연결할 수 있어요.' return '사용자 업로드와 관리자 템플릿 이미지를 함께 검수하고, 필요한 템플릿에 직접 연결할 수 있어요.'
} }
if (activeTab.value === 'tierlists') { if (activeTab.value === 'tierlists') {
return tierlistsMode.value === 'requests' return tierlistsMode.value === 'requests'
? '사용자 요청을 확인하고, 게임 관리 화면에서 필요한 아이템만 선별 반영한 뒤 직접 완료 처리합니다.' ? '사용자 요청을 확인하고, 템플릿 관리 화면에서 필요한 아이템만 선별 반영한 뒤 직접 완료 처리합니다.'
: '공개/비공개 포함 전체 티어표를 확인하고, 추가 아이템을 템플릿으로 가져올 수 있어요.' : '공개/비공개 포함 전체 티어표를 확인하고, 추가 아이템을 템플릿으로 가져올 수 있어요.'
} }
return '계정 정보, 권한, 비밀번호와 최근 활동을 더 가볍게 확인하고 수정합니다.' return '계정 정보, 권한, 비밀번호와 최근 활동을 더 가볍게 확인하고 수정합니다.'
@@ -245,14 +245,14 @@ const adminOverviewStats = computed(() => {
if (activeTab.value === 'featured') { if (activeTab.value === 'featured') {
return [ return [
{ label: '전체 게임', value: `${games.value.length}` }, { label: '전체 템플릿', value: `${games.value.length}` },
{ label: '상단 고정', value: `${featuredGameIds.value.length}/50` }, { label: '상단 고정', value: `${featuredGameIds.value.length}/50` },
{ label: '추가 가능', value: `${Math.max(0, 50 - featuredGameIds.value.length)}` }, { label: '추가 가능', value: `${Math.max(0, 50 - featuredGameIds.value.length)}` },
] ]
} }
if (activeTab.value === 'game-admin') { if (activeTab.value === 'game-admin') {
return [ return [
{ label: '전체 게임', value: `${games.value.length}` }, { label: '전체 템플릿', value: `${games.value.length}` },
{ label: '티어표 전체', value: `${selectedGameTierListStats.value.total || 0}` }, { label: '티어표 전체', value: `${selectedGameTierListStats.value.total || 0}` },
{ label: '공개', value: `${selectedGameTierListStats.value.publicCount || 0}` }, { label: '공개', value: `${selectedGameTierListStats.value.publicCount || 0}` },
{ label: '비공개', value: `${selectedGameTierListStats.value.privateCount || 0}` }, { label: '비공개', value: `${selectedGameTierListStats.value.privateCount || 0}` },
@@ -571,7 +571,7 @@ function formatImageJobSourceCategory(category) {
case 'tierlists': case 'tierlists':
return '티어표 썸네일' return '티어표 썸네일'
case 'games': case 'games':
return '게임/템플릿 이미지' return '주제/템플릿 이미지'
case 'avatars': case 'avatars':
return '프로필 아바타' return '프로필 아바타'
default: default:
@@ -599,7 +599,7 @@ function customItemDeleteImpactText(item) {
if (item.sourceType === 'template') { if (item.sourceType === 'template') {
return item.isAssetLibraryItem return item.isAssetLibraryItem
? `"${item.label}" 보관 자산 항목을 정리할까요? 라이브러리 항목만 제거되고, 같은 이미지를 쓰는 다른 참조는 그대로 유지됩니다.` ? `"${item.label}" 보관 자산 항목을 정리할까요? 라이브러리 항목만 제거되고, 같은 이미지를 쓰는 다른 참조는 그대로 유지됩니다.`
: `"${item.label}" 템플릿 항목을 정리할까요? 연결된 템플릿과 같은 게임의 저장된 티어표에서 이 항목이 함께 제거될 수 있어요.` : `"${item.label}" 템플릿 항목을 정리할까요? 연결된 템플릿과 같은 주제의 저장된 티어표에서 이 항목이 함께 제거될 수 있어요.`
} }
return `"${item.label}" 사용자 업로드 이미지를 삭제할까요? 현재 항목만 정리됩니다.` return `"${item.label}" 사용자 업로드 이미지를 삭제할까요? 현재 항목만 정리됩니다.`
@@ -703,7 +703,7 @@ async function confirmImageReset() {
} }
async function cleanupMissingImageReferences() { async function cleanupMissingImageReferences() {
const ok = window.confirm('파일이 실제로 없는 이미지 참조만 정리할까요? 누락된 썸네일은 비워지고, 누락된 게임/커스텀 아이템은 관련 참조와 함께 정리됩니다.') const ok = window.confirm('파일이 실제로 없는 이미지 참조만 정리할까요? 누락된 썸네일은 비워지고, 누락된 템플릿/커스텀 아이템은 관련 참조와 함께 정리됩니다.')
if (!ok) return if (!ok) return
try { try {
@@ -714,10 +714,10 @@ async function cleanupMissingImageReferences() {
success.value = success.value =
`누락 참조를 정리했어요. ` + `누락 참조를 정리했어요. ` +
`아바타 ${result.clearedAvatars || 0}건, ` + `아바타 ${result.clearedAvatars || 0}건, ` +
`게임 썸네일 ${result.clearedGameThumbnails || 0}건, ` + `템플릿 썸네일 ${result.clearedGameThumbnails || 0}건, ` +
`티어표 썸네일 ${result.clearedTierListThumbnails || 0}건, ` + `티어표 썸네일 ${result.clearedTierListThumbnails || 0}건, ` +
`요청 썸네일 ${result.clearedTemplateRequestThumbnails || 0}건, ` + `요청 썸네일 ${result.clearedTemplateRequestThumbnails || 0}건, ` +
`게임 아이템 ${result.deletedGameItems || 0}건, ` + `템플릿 아이템 ${result.deletedGameItems || 0}건, ` +
`커스텀 아이템 ${result.deletedCustomItems || 0}` `커스텀 아이템 ${result.deletedCustomItems || 0}`
} catch (e) { } catch (e) {
error.value = '누락 이미지 참조 정리에 실패했어요.' error.value = '누락 이미지 참조 정리에 실패했어요.'
@@ -794,7 +794,7 @@ async function refreshGames() {
.map((game) => game.id) .map((game) => game.id)
await syncFeaturedSortable() await syncFeaturedSortable()
} catch (e) { } catch (e) {
error.value = '게임 목록을 불러오지 못했어요.' error.value = '템플릿 목록을 불러오지 못했어요.'
} }
} }
@@ -1180,10 +1180,10 @@ async function saveGameVisibility() {
}, },
} }
await refreshGames() await refreshGames()
success.value = data.game?.isPublic ? '게임을 공개 상태로 전환했어요.' : '게임을 비공개 상태로 전환했어요.' success.value = data.game?.isPublic ? '템플릿을 공개 상태로 전환했어요.' : '템플릿을 비공개 상태로 전환했어요.'
return true return true
} catch (e) { } catch (e) {
error.value = '게임 공개 상태를 저장하지 못했어요.' error.value = '템플릿 공개 상태를 저장하지 못했어요.'
return false return false
} finally { } finally {
gameVisibilitySaving.value = false gameVisibilitySaving.value = false
@@ -1225,9 +1225,9 @@ async function removeGameItem(itemId) {
if (!res.ok) throw new Error('failed') if (!res.ok) throw new Error('failed')
await loadGame() await loadGame()
success.value = '게임 기본 아이템을 삭제했어요.' success.value = '템플릿 기본 아이템을 삭제했어요.'
} catch (e) { } catch (e) {
error.value = '게임 기본 아이템 삭제에 실패했어요.' error.value = '템플릿 기본 아이템 삭제에 실패했어요.'
} }
} }
@@ -1258,7 +1258,7 @@ async function removeGame() {
resetMessages() resetMessages()
if (!selectedGameId.value || !selectedGame.value?.game) return if (!selectedGameId.value || !selectedGame.value?.game) return
const ok = window.confirm(`"${selectedGame.value.game.name}" 게임을 삭제할까요? 관련 기본 아이템과 티어표도 함께 삭제됩니다.`) const ok = window.confirm(`"${selectedGame.value.game.name}" 템플릿을 삭제할까요? 관련 기본 아이템과 티어표도 함께 삭제됩니다.`)
if (!ok) return if (!ok) return
try { try {
@@ -1273,9 +1273,9 @@ async function removeGame() {
selectedGame.value = null selectedGame.value = null
resetUploadState() resetUploadState()
await refreshGames() await refreshGames()
success.value = `${deletedName} 게임을 삭제했어요.` success.value = `${deletedName} 템플릿을 삭제했어요.`
} catch (e) { } catch (e) {
error.value = '게임 삭제에 실패했어요.' error.value = '템플릿 삭제에 실패했어요.'
} }
} }
@@ -1570,7 +1570,7 @@ async function confirmTierListImport() {
try { try {
if (importModalMode.value === 'existing') { if (importModalMode.value === 'existing') {
if (!importModalTargetGameId.value) { if (!importModalTargetGameId.value) {
error.value = '아이템을 추가할 기존 게임을 선택해주세요.' error.value = '아이템을 추가할 기존 템플릿을 선택해주세요.'
return return
} }
@@ -1584,7 +1584,7 @@ async function confirmTierListImport() {
const nextGameId = (importModalNewGameId.value || '').trim() const nextGameId = (importModalNewGameId.value || '').trim()
const nextGameName = (importModalNewGameName.value || '').trim() const nextGameName = (importModalNewGameName.value || '').trim()
if (!nextGameId || !nextGameName) { if (!nextGameId || !nextGameName) {
error.value = '새 게임 ID와 이름을 모두 입력해주세요.' error.value = '새 템플릿 ID와 이름을 모두 입력해주세요.'
return return
} }
@@ -1610,9 +1610,9 @@ function templateRequestTypeLabel(request) {
function templateRequestTargetLabel(request) { function templateRequestTargetLabel(request) {
if (request.type === 'create') { if (request.type === 'create') {
if (request.targetGameName || request.targetGameId) { if (request.targetGameName || request.targetGameId) {
return `연결된 게임 · ${request.targetGameName || request.targetGameId}` return `연결된 템플릿 · ${request.targetGameName || request.targetGameId}`
} }
return '연결된 게임 없음' return '연결된 템플릿 없음'
} }
return request.targetGameName || request.targetGameId || request.sourceGameName return request.targetGameName || request.targetGameId || request.sourceGameName
} }
@@ -1787,16 +1787,16 @@ function userAvatarFallback(user) {
<div v-if="gameCreateModalOpen" class="modalOverlay" @click.self="closeGameCreateModal"> <div v-if="gameCreateModalOpen" class="modalOverlay" @click.self="closeGameCreateModal">
<div class="modalCard" role="dialog" aria-modal="true"> <div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title"> 게임 만들기</div> <div class="modalCard__title"> 템플릿 만들기</div>
<div class="modalCard__desc">게임 이름과 고유 ID를 입력한 생성하면 바로 아래 상세 관리 화면으로 이어집니다.</div> <div class="modalCard__desc">템플릿 이름과 고유 ID를 입력한 생성하면 바로 아래 상세 관리 화면으로 이어집니다.</div>
<div class="modalCard__form"> <div class="modalCard__form">
<label class="field"> <label class="field">
<span class="field__label">게임 이름</span> <span class="field__label">템플릿 이름</span>
<input v-model="newGameName" class="field__input" maxlength="60" placeholder="게임 이름" /> <input v-model="newGameName" class="field__input" maxlength="60" placeholder="템플릿 이름" />
<span class="field__hint">{{ newGameName.length }}/60</span> <span class="field__hint">{{ newGameName.length }}/60</span>
</label> </label>
<label class="field"> <label class="field">
<span class="field__label">게임 ID</span> <span class="field__label">템플릿 ID</span>
<input <input
v-model="newGameId" v-model="newGameId"
class="field__input" class="field__input"
@@ -1814,7 +1814,7 @@ function userAvatarFallback(user) {
</div> </div>
<div class="modalCard__actions"> <div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeGameCreateModal">취소</button> <button class="btn btn--ghost" @click="closeGameCreateModal">취소</button>
<button class="btn btn--primary" :disabled="!newGameId.trim() || !newGameName.trim()" @click="createGame">게임 생성</button> <button class="btn btn--primary" :disabled="!newGameId.trim() || !newGameName.trim()" @click="createGame">템플릿 생성</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1925,14 +1925,14 @@ function userAvatarFallback(user) {
<div v-if="importModalMode === 'existing'" class="modalCard__form"> <div v-if="importModalMode === 'existing'" class="modalCard__form">
<select v-model="importModalTargetGameId" class="select"> <select v-model="importModalTargetGameId" class="select">
<option value="">기존 게임 선택</option> <option value="">기존 템플릿 선택</option>
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option> <option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
</select> </select>
</div> </div>
<div v-else class="modalCard__form"> <div v-else class="modalCard__form">
<input v-model="importModalNewGameId" class="input" placeholder="새 게임 ID" /> <input v-model="importModalNewGameId" class="input" placeholder="새 템플릿 ID" />
<input v-model="importModalNewGameName" class="input" placeholder="새 게임 이름" /> <input v-model="importModalNewGameName" class="input" placeholder="새 템플릿 이름" />
</div> </div>
<div class="modalCard__actions"> <div class="modalCard__actions">
@@ -1950,15 +1950,15 @@ function userAvatarFallback(user) {
<aside class="customItemModal__pickerPanel"> <aside class="customItemModal__pickerPanel">
<div class="customItemModal__pickerHead"> <div class="customItemModal__pickerHead">
<div class="customItemModal__pickerEyebrow">GAME PICKER</div> <div class="customItemModal__pickerEyebrow">GAME PICKER</div>
<div class="customItemModal__pickerTitle">템플릿으로 추가할 게임</div> <div class="customItemModal__pickerTitle">아이템을 추가할 템플릿</div>
</div> </div>
<div class="adminSelectionCard"> <div class="adminSelectionCard">
<div class="adminSelectionCard__label">선택한 게임</div> <div class="adminSelectionCard__label">선택한 템플릿</div>
<div class="adminSelectionCard__title">{{ customItemTargetGame?.name || '아직 선택하지 않음' }}</div> <div class="adminSelectionCard__title">{{ customItemTargetGame?.name || '아직 선택하지 않음' }}</div>
<div class="adminSelectionCard__meta">{{ customItemTargetGame?.id || '게임을 골라 주세요.' }}</div> <div class="adminSelectionCard__meta">{{ customItemTargetGame?.id || '템플릿을 골라 주세요.' }}</div>
</div> </div>
<div class="customItemModal__pickerActions"> <div class="customItemModal__pickerActions">
<button class="btn btn--ghost" type="button" @click="openGamePickerModal('custom-item-target')">게임 선택</button> <button class="btn btn--ghost" type="button" @click="openGamePickerModal('custom-item-target')">템플릿 선택</button>
<button class="btn btn--ghost btn--small customItemModal__createGameButton" type="button" @click="openGameCreateModal"> 템플릿 만들기</button> <button class="btn btn--ghost btn--small customItemModal__createGameButton" type="button" @click="openGameCreateModal"> 템플릿 만들기</button>
</div> </div>
</aside> </aside>
@@ -1983,15 +1983,15 @@ function userAvatarFallback(user) {
<div class="customItemModal__metaList"> <div class="customItemModal__metaList">
<div class="customItemModal__metaRow"><span>파일</span><strong :title="modalTargetCustomItem.src.split('/').pop()">{{ modalTargetCustomItem.src.split('/').pop() }}</strong></div> <div class="customItemModal__metaRow"><span>파일</span><strong :title="modalTargetCustomItem.src.split('/').pop()">{{ modalTargetCustomItem.src.split('/').pop() }}</strong></div>
<div class="customItemModal__metaRow"><span>업로더/출처</span><strong :title="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</strong></div> <div class="customItemModal__metaRow"><span>업로더/출처</span><strong :title="modalTargetCustomItem.ownerName">{{ modalTargetCustomItem.ownerName }}</strong></div>
<div class="customItemModal__metaRow"><span>템플릿 연결</span><strong>{{ visibleLinkedGames.length }} 게임</strong></div> <div class="customItemModal__metaRow"><span>템플릿 연결</span><strong>{{ visibleLinkedGames.length }} 템플릿</strong></div>
<div class="customItemModal__metaRow"><span>등록일</span><strong>{{ fmt(modalTargetCustomItem.createdAt) }}</strong></div> <div class="customItemModal__metaRow"><span>등록일</span><strong>{{ fmt(modalTargetCustomItem.createdAt) }}</strong></div>
</div> </div>
<div class="customItemModal__linked"> <div class="customItemModal__linked">
<span class="customItemModal__label">템플릿에 사용 중인 게임</span> <span class="customItemModal__label"> 이미지를 사용하는 템플릿</span>
<div v-if="visibleLinkedGames.length" class="customItemModal__chips"> <div v-if="visibleLinkedGames.length" class="customItemModal__chips">
<button v-for="game in visibleLinkedGames" :key="game.id" type="button" class="pill pill--link" @click="jumpToGameAdmin(game.id)">{{ game.name }}</button> <button v-for="game in visibleLinkedGames" :key="game.id" type="button" class="pill pill--link" @click="jumpToGameAdmin(game.id)">{{ game.name }}</button>
</div> </div>
<div v-else class="hint hint--tight">아직 템플릿에 연결된 게임 없어요.</div> <div v-else class="hint hint--tight">아직 템플릿에 연결된 항목 없어요.</div>
</div> </div>
<div class="customItemModal__actions"> <div class="customItemModal__actions">
<a class="btn btn--ghost customItemModal__action" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a> <a class="btn btn--ghost customItemModal__action" :href="toApiUrl(modalTargetCustomItem.src)" :download="modalTargetCustomItem.label">이미지 다운로드</a>
@@ -2010,15 +2010,15 @@ function userAvatarFallback(user) {
<div class="modalCard" role="dialog" aria-modal="true"> <div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__titleRow"> <div class="modalCard__titleRow">
<div> <div>
<div class="modalCard__title">게임 선택</div> <div class="modalCard__title">템플릿 선택</div>
<div class="modalCard__desc"> <div class="modalCard__desc">
{{ gamePickerMode === 'tierlists-filter' ? '특정 게임의 티어표만 보려면 게임을 선택하세요.' : '관리할 게임을 검색해서 바로 열 수 있어요.' }} {{ gamePickerMode === 'tierlists-filter' ? '특정 주제의 티어표만 보려면 템플릿을 선택하세요.' : '관리할 템플릿을 검색해서 바로 열 수 있어요.' }}
</div> </div>
</div> </div>
<button class="btn btn--ghost btn--small" @click="closeGamePickerModal">닫기</button> <button class="btn btn--ghost btn--small" @click="closeGamePickerModal">닫기</button>
</div> </div>
<div class="modalCard__form"> <div class="modalCard__form">
<input v-model="gamePickerQuery" class="input" placeholder="게임 이름 또는 ID 검색" /> <input v-model="gamePickerQuery" class="input" placeholder="템플릿 이름 또는 ID 검색" />
<select v-model="gamePickerSort" class="select"> <select v-model="gamePickerSort" class="select">
<option value="recent">최신순</option> <option value="recent">최신순</option>
<option value="oldest">오래된순</option> <option value="oldest">오래된순</option>
@@ -2029,7 +2029,7 @@ function userAvatarFallback(user) {
type="button" type="button"
@click="setAdminTierListGameId(''); closeGamePickerModal()" @click="setAdminTierListGameId(''); closeGamePickerModal()"
> >
모든 게임 보기 모든 주제 보기
</button> </button>
</div> </div>
<div class="gamePickerModalList"> <div class="gamePickerModalList">
@@ -2201,7 +2201,7 @@ function userAvatarFallback(user) {
<div class="adminSidebar__label">Mode</div> <div class="adminSidebar__label">Mode</div>
<div class="adminSidebar__tabs"> <div class="adminSidebar__tabs">
<button class="tab" :class="{ 'tab--active': activeTab === 'featured' }" @click="setTab('featured')">목록 관리</button> <button class="tab" :class="{ 'tab--active': activeTab === 'featured' }" @click="setTab('featured')">목록 관리</button>
<button class="tab" :class="{ 'tab--active': activeTab === 'game-admin' }" @click="setTab('game-admin')">게임 관리</button> <button class="tab" :class="{ 'tab--active': activeTab === 'game-admin' }" @click="setTab('game-admin')">템플릿 관리</button>
<button class="tab" :class="{ 'tab--active': activeTab === 'items' }" @click="setTab('items')">아이템 관리</button> <button class="tab" :class="{ 'tab--active': activeTab === 'items' }" @click="setTab('items')">아이템 관리</button>
<button class="tab" :class="{ 'tab--active': activeTab === 'tierlists' }" @click="setTab('tierlists')">티어표 관리</button> <button class="tab" :class="{ 'tab--active': activeTab === 'tierlists' }" @click="setTab('tierlists')">티어표 관리</button>
<button class="tab" :class="{ 'tab--active': activeTab === 'users' }" @click="setTab('users')">회원 관리</button> <button class="tab" :class="{ 'tab--active': activeTab === 'users' }" @click="setTab('users')">회원 관리</button>
@@ -2209,16 +2209,16 @@ function userAvatarFallback(user) {
</section> </section>
<section v-if="activeTab === 'game-admin'" class="adminSidebar__panel"> <section v-if="activeTab === 'game-admin'" class="adminSidebar__panel">
<div class="adminSidebar__label">Game</div> <div class="adminSidebar__label">Template</div>
<div class="adminSidebar__group"> <div class="adminSidebar__group">
<button class="btn btn--primary" @click="openGameCreateModal"> 게임 생성</button> <button class="btn btn--primary" @click="openGameCreateModal"> 템플릿 생성</button>
<button class="btn btn--ghost" @click="openGamePickerModal('game-admin')">게임 선택</button> <button class="btn btn--ghost" @click="openGamePickerModal('game-admin')">템플릿 선택</button>
<div v-if="selectedGame?.game" class="adminSelectionCard"> <div v-if="selectedGame?.game" class="adminSelectionCard">
<div class="adminSelectionCard__label">선택한 게임</div> <div class="adminSelectionCard__label">선택한 템플릿</div>
<div class="adminSelectionCard__title">{{ selectedGame.game.name }}</div> <div class="adminSelectionCard__title">{{ selectedGame.game.name }}</div>
<div class="adminSelectionCard__meta">{{ selectedGame.game.id }}</div> <div class="adminSelectionCard__meta">{{ selectedGame.game.id }}</div>
</div> </div>
<div v-if="selectedGameId && !hasSelectedGame && !isGameLoading" class="hint hint--tight">선택된 게임 ID: {{ selectedGameId }}</div> <div v-if="selectedGameId && !hasSelectedGame && !isGameLoading" class="hint hint--tight">선택된 템플릿 ID: {{ selectedGameId }}</div>
</div> </div>
</section> </section>
@@ -2277,14 +2277,14 @@ function userAvatarFallback(user) {
<input <input
v-model="adminTierListQuery" v-model="adminTierListQuery"
class="input" class="input"
placeholder="제목, 작성자, 게임 이름 검색" placeholder="제목, 작성자, 주제 이름 검색"
@keydown.enter.prevent="submitAdminTierListSearch" @keydown.enter.prevent="submitAdminTierListSearch"
/> />
<button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button> <button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button>
</div> </div>
<button class="btn btn--ghost" @click="openGamePickerModal('tierlists-filter')">게임 선택</button> <button class="btn btn--ghost" @click="openGamePickerModal('tierlists-filter')">주제 선택</button>
<div v-if="adminTierListGameId" class="adminSelectionCard"> <div v-if="adminTierListGameId" class="adminSelectionCard">
<div class="adminSelectionCard__label">필터된 게임</div> <div class="adminSelectionCard__label">필터된 주제</div>
<div class="adminSelectionCard__title">{{ games.find((game) => game.id === adminTierListGameId)?.name || adminTierListGameId }}</div> <div class="adminSelectionCard__title">{{ games.find((game) => game.id === adminTierListGameId)?.name || adminTierListGameId }}</div>
<div class="adminSelectionCard__meta">{{ adminTierListGameId }}</div> <div class="adminSelectionCard__meta">{{ adminTierListGameId }}</div>
<button class="btn btn--ghost btn--small" @click="setAdminTierListGameId('')">필터 해제</button> <button class="btn btn--ghost btn--small" @click="setAdminTierListGameId('')">필터 해제</button>

View File

@@ -58,11 +58,11 @@ onMounted(loadFavorites)
<div class="pageHead"> <div class="pageHead">
<div class="pageHead__main"> <div class="pageHead__main">
<div class="pageHead__eyebrow">Collection</div> <div class="pageHead__eyebrow">Collection</div>
<h2 class="pageHead__title"> 즐겨찾기</h2> <h2 class="pageHead__title">즐겨찾기</h2>
<div class="pageHead__desc">마음에 드는 티어표를 모아보고, 원하는 기준으로 정렬할 있어요.</div> <div class="pageHead__desc">마음에 드는 티어표를 모아보고, 원하는 기준으로 정렬할 있어요.</div>
</div> </div>
<div class="pageHead__aside toolbar"> <div class="pageHead__aside toolbar">
<input v-model="query" class="input" placeholder="제목, 게임, 작성자 검색" @keydown.enter.prevent="loadFavorites" /> <input v-model="query" class="input" placeholder="제목, 주제, 작성자 검색" @keydown.enter.prevent="loadFavorites" />
<select v-model="sort" class="select" @change="loadFavorites"> <select v-model="sort" class="select" @change="loadFavorites">
<option value="favorited">즐겨찾기한 </option> <option value="favorited">즐겨찾기한 </option>
<option value="updated">최신 업데이트순</option> <option value="updated">최신 업데이트순</option>
@@ -77,7 +77,7 @@ onMounted(loadFavorites)
<article v-for="tierList in favorites" :key="tierList.id" class="boardCard"> <article v-for="tierList in favorites" :key="tierList.id" class="boardCard">
<button class="boardCard__body" @click="openTierList(tierList)"> <button class="boardCard__body" @click="openTierList(tierList)">
<div class="boardCard__thumbWrap"> <div class="boardCard__thumbWrap">
<img v-if="tierListThumbnailUrl(tierList)" class="boardCard__thumb" :src="tierListThumbnailUrl(tierList)" :alt="tierList.title" /> <img v-if="tierListThumbnailUrl(tierList)" class="boardCard__thumb" :src="tierListThumbnailUrl(tierList)" :alt="tierList.title" draggable="false" />
<div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div> <div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div>
</div> </div>
<div class="boardCard__head"> <div class="boardCard__head">
@@ -87,7 +87,7 @@ onMounted(loadFavorites)
</div> </div>
<div class="boardCard__metaRow"> <div class="boardCard__metaRow">
<div class="boardCard__author"> <div class="boardCard__author">
<img v-if="avatarSrcOf(tierList)" class="boardCard__avatar" :src="avatarSrcOf(tierList)" :alt="displayNameOf(tierList)" /> <img v-if="avatarSrcOf(tierList)" class="boardCard__avatar" :src="avatarSrcOf(tierList)" :alt="displayNameOf(tierList)" draggable="false" />
<div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(tierList) }}</div> <div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(tierList) }}</div>
<span class="boardCard__authorName">{{ displayNameOf(tierList) }}</span> <span class="boardCard__authorName">{{ displayNameOf(tierList) }}</span>
</div> </div>

View File

@@ -62,7 +62,7 @@ async function loadTierLists() {
brokenThumbnailIds.value = {} brokenThumbnailIds.value = {}
tierLists.value = listRes.tierLists || [] tierLists.value = listRes.tierLists || []
} catch (e) { } catch (e) {
error.value = '게임 정보를 불러오지 못했어요.' error.value = '주제 정보를 불러오지 못했어요.'
} }
} }
@@ -88,7 +88,7 @@ function submitSearch() {
<div class="dashboardHero__left"> <div class="dashboardHero__left">
<div class="dashboardHero__eyebrow">Collection</div> <div class="dashboardHero__eyebrow">Collection</div>
<h2 class="dashboardHero__title">{{ gameName || gameId }}</h2> <h2 class="dashboardHero__title">{{ gameName || gameId }}</h2>
<p class="dashboardHero__desc"> 게임 공개 티어표를 탐색하고, 바로 보드를 만들어 같은 흐름으로 이어갈 있어요.</p> <p class="dashboardHero__desc"> 주제 공개 티어표를 탐색하고, 바로 보드를 만들어 같은 흐름으로 이어갈 있어요.</p>
</div> </div>
</section> </section>
@@ -109,7 +109,7 @@ function submitSearch() {
<article v-for="t in tierLists" :key="t.id" class="boardCard" :class="{ 'boardCard--list': isListView }"> <article v-for="t in tierLists" :key="t.id" class="boardCard" :class="{ 'boardCard--list': isListView }">
<button class="boardCard__body" :class="{ 'boardCard__body--list': isListView }" @click="openTierList(t.id)"> <button class="boardCard__body" :class="{ 'boardCard__body--list': isListView }" @click="openTierList(t.id)">
<div class="boardCard__thumbWrap"> <div class="boardCard__thumbWrap">
<img v-if="tierListThumbnailUrl(t)" class="boardCard__thumb" :src="tierListThumbnailUrl(t)" alt="" @error="handleThumbnailError(t.id)" /> <img v-if="tierListThumbnailUrl(t)" class="boardCard__thumb" :src="tierListThumbnailUrl(t)" alt="" draggable="false" @error="handleThumbnailError(t.id)" />
<div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div> <div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div>
</div> </div>
<div class="boardCard__head"> <div class="boardCard__head">
@@ -121,7 +121,7 @@ function submitSearch() {
</div> </div>
<div class="boardCard__metaRow"> <div class="boardCard__metaRow">
<div class="boardCard__author"> <div class="boardCard__author">
<img v-if="avatarSrcOf(t)" class="boardCard__avatar" :src="avatarSrcOf(t)" :alt="displayNameOf(t)" /> <img v-if="avatarSrcOf(t)" class="boardCard__avatar" :src="avatarSrcOf(t)" :alt="displayNameOf(t)" draggable="false" />
<div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(t) }}</div> <div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(t) }}</div>
<span class="boardCard__authorName">{{ displayNameOf(t) }}</span> <span class="boardCard__authorName">{{ displayNameOf(t) }}</span>
</div> </div>

View File

@@ -77,9 +77,9 @@ function thumbUrl(g) {
<section class="pageHead"> <section class="pageHead">
<div class="pageHead__main"> <div class="pageHead__main">
<div class="pageHead__eyebrow">Workspace</div> <div class="pageHead__eyebrow">Workspace</div>
<h1 class="pageHead__title">Game Library</h1> <h1 class="pageHead__title">Topic Library</h1>
<p class="pageHead__desc">자주 쓰는 게임 템플릿을 빠르게 고르고, 필요하면 바로 커스텀 티어표를 시작할 있어요.</p> <p class="pageHead__desc">자주 쓰는 주제 템플릿을 빠르게 고르고, 필요하면 바로 커스텀 티어표를 시작할 있어요.</p>
<p v-if="query" class="pageHead__searchState">"{{ query }}" 맞는 게임 템플릿만 보고 있어요.</p> <p v-if="query" class="pageHead__searchState">"{{ query }}" 맞는 주제 템플릿만 보고 있어요.</p>
</div> </div>
</section> </section>
@@ -97,7 +97,7 @@ function thumbUrl(g) {
</button> </button>
<button class="libraryCard__main" type="button" @click="goGame(g.id)"> <button class="libraryCard__main" type="button" @click="goGame(g.id)">
<div class="libraryCard__thumbWrap"> <div class="libraryCard__thumbWrap">
<img v-if="thumbUrl(g)" class="libraryCard__thumb" :src="thumbUrl(g)" :alt="g.name" /> <img v-if="thumbUrl(g)" class="libraryCard__thumb" :src="thumbUrl(g)" :alt="g.name" draggable="false" />
<div v-else class="libraryCard__thumbFallback">대표 썸네일</div> <div v-else class="libraryCard__thumbFallback">대표 썸네일</div>
</div> </div>
<div class="libraryCard__body"> <div class="libraryCard__body">
@@ -107,7 +107,7 @@ function thumbUrl(g) {
</button> </button>
</article> </article>
</TransitionGroup> </TransitionGroup>
<div v-else class="libraryEmpty">{{ query ? '검색어에 맞는 게임 템플릿이 없어요.' : '표시할 게임 템플릿이 없어요.' }}</div> <div v-else class="libraryEmpty">{{ query ? '검색어에 맞는 주제 템플릿이 없어요.' : '표시할 주제 템플릿이 없어요.' }}</div>
</template> </template>
<style scoped> <style scoped>

View File

@@ -69,7 +69,7 @@ function openList(t) {
<section class="pageHead"> <section class="pageHead">
<div class="pageHead__main"> <div class="pageHead__main">
<div class="pageHead__eyebrow">Library</div> <div class="pageHead__eyebrow">Library</div>
<h2 class="pageHead__title"> 티어표</h2> <h2 class="pageHead__title">나의 티어표</h2>
<div class="pageHead__desc">직접 저장한 티어표를 같은 카드 레이아웃으로 다시 열고 정리할 있어요.</div> <div class="pageHead__desc">직접 저장한 티어표를 같은 카드 레이아웃으로 다시 열고 정리할 있어요.</div>
</div> </div>
</section> </section>
@@ -85,6 +85,7 @@ function openList(t) {
class="boardCard__thumb" class="boardCard__thumb"
:src="tierListThumbnailUrl(t)" :src="tierListThumbnailUrl(t)"
alt="" alt=""
draggable="false"
@error="handleThumbnailError(t.id)" @error="handleThumbnailError(t.id)"
/> />
<div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div> <div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div>
@@ -96,7 +97,7 @@ function openList(t) {
</div> </div>
<div class="boardCard__metaRow"> <div class="boardCard__metaRow">
<div class="boardCard__author"> <div class="boardCard__author">
<img v-if="avatarSrcOf(t)" class="boardCard__avatar" :src="avatarSrcOf(t)" :alt="displayNameOf(t)" /> <img v-if="avatarSrcOf(t)" class="boardCard__avatar" :src="avatarSrcOf(t)" :alt="displayNameOf(t)" draggable="false" />
<div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(t) }}</div> <div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(t) }}</div>
<span class="boardCard__authorName">{{ displayNameOf(t) }}</span> <span class="boardCard__authorName">{{ displayNameOf(t) }}</span>
</div> </div>

View File

@@ -121,7 +121,7 @@ async function logout() {
<header class="pageHead"> <header class="pageHead">
<div class="pageHead__main"> <div class="pageHead__main">
<div class="pageHead__eyebrow">Account</div> <div class="pageHead__eyebrow">Account</div>
<h2 class="pageHead__title">Settings</h2> <h2 class="pageHead__title">설정</h2>
<div class="pageHead__desc">계정 정보를 간결하게 정리하고, 프로필 이미지를 클릭해서 바로 변경할 있어요.</div> <div class="pageHead__desc">계정 정보를 간결하게 정리하고, 프로필 이미지를 클릭해서 바로 변경할 있어요.</div>
</div> </div>
</header> </header>
@@ -134,7 +134,7 @@ async function logout() {
<div class="settingsIdentity"> <div class="settingsIdentity">
<div class="avatarButtonWrap"> <div class="avatarButtonWrap">
<button class="avatarButton" type="button" @click="openAvatarPicker"> <button class="avatarButton" type="button" @click="openAvatarPicker">
<img v-if="avatarUrl" :src="avatarUrl" class="avatarButton__image" alt="avatar" /> <img v-if="avatarUrl" :src="avatarUrl" class="avatarButton__image" alt="avatar" draggable="false" />
<div v-else class="avatarButton__fallback">{{ displayInitial }}</div> <div v-else class="avatarButton__fallback">{{ displayInitial }}</div>
<div class="avatarButton__overlay"> <div class="avatarButton__overlay">
<span>{{ avatarUrl ? '이미지 변경' : '이미지 추가' }}</span> <span>{{ avatarUrl ? '이미지 변경' : '이미지 추가' }}</span>

View File

@@ -67,7 +67,7 @@ watch(
<section class="wrap"> <section class="wrap">
<div class="head"> <div class="head">
<div> <div>
<div class="head__eyebrow">Search</div> <div class="head__eyebrow">검색</div>
<h2 class="title">전체 티어표 검색</h2> <h2 class="title">전체 티어표 검색</h2>
<div class="desc">공개된 모든 티어표를 제목과 작성자 기준으로 찾아볼 있어요.</div> <div class="desc">공개된 모든 티어표를 제목과 작성자 기준으로 찾아볼 있어요.</div>
</div> </div>
@@ -80,7 +80,7 @@ watch(
<article v-for="tierList in tierLists" :key="tierList.id" class="boardCard"> <article v-for="tierList in tierLists" :key="tierList.id" class="boardCard">
<button class="boardCard__body" @click="openTierList(tierList)"> <button class="boardCard__body" @click="openTierList(tierList)">
<div class="boardCard__thumbWrap"> <div class="boardCard__thumbWrap">
<img v-if="tierListThumbnailUrl(tierList)" class="boardCard__thumb" :src="tierListThumbnailUrl(tierList)" :alt="tierList.title" /> <img v-if="tierListThumbnailUrl(tierList)" class="boardCard__thumb" :src="tierListThumbnailUrl(tierList)" :alt="tierList.title" draggable="false" />
<div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div> <div v-else class="boardCard__thumbPlaceholder">대표 썸네일</div>
</div> </div>
<div class="boardCard__head"> <div class="boardCard__head">
@@ -92,7 +92,7 @@ watch(
</div> </div>
<div class="boardCard__metaRow"> <div class="boardCard__metaRow">
<div class="boardCard__author"> <div class="boardCard__author">
<img v-if="avatarSrcOf(tierList)" class="boardCard__avatar" :src="avatarSrcOf(tierList)" :alt="displayNameOf(tierList)" /> <img v-if="avatarSrcOf(tierList)" class="boardCard__avatar" :src="avatarSrcOf(tierList)" :alt="displayNameOf(tierList)" draggable="false" />
<div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(tierList) }}</div> <div v-else class="boardCard__avatar boardCard__avatar--fallback">{{ avatarFallbackOf(tierList) }}</div>
<span class="boardCard__authorName">{{ displayNameOf(tierList) }}</span> <span class="boardCard__authorName">{{ displayNameOf(tierList) }}</span>
</div> </div>

View File

@@ -130,7 +130,7 @@ const canRequestTemplateUpdate = computed(
) )
const canSubmitTemplateCreateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim()) const canSubmitTemplateCreateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const canSubmitTemplateUpdateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim()) const canSubmitTemplateUpdateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const templateRequestTargetLabel = computed(() => (gameId.value === 'freeform' ? '새로운 템플릿' : (gameName.value || gameId.value || '선택한 게임'))) const templateRequestTargetLabel = computed(() => (gameId.value === 'freeform' ? '새로운 템플릿' : (gameName.value || gameId.value || '선택한 주제')))
const shareTierListUrl = computed(() => { const shareTierListUrl = computed(() => {
const savedTierListId = persistedTierListId.value || (tierListId.value && tierListId.value !== 'new' ? tierListId.value : '') const savedTierListId = persistedTierListId.value || (tierListId.value && tierListId.value !== 'new' ? tierListId.value : '')
if (!savedTierListId) return '' if (!savedTierListId) return ''
@@ -910,7 +910,7 @@ onMounted(() => {
itemsById.value = map itemsById.value = map
pool.value = base.map((it) => it.id) pool.value = base.map((it) => it.id)
} catch (e) { } catch (e) {
error.value = '게임 기본 이미지를 불러오지 못했어요.' error.value = '기본 템플릿 이미지를 불러오지 못했어요.'
} }
if (tierListId.value && tierListId.value !== 'new') { if (tierListId.value && tierListId.value !== 'new') {
@@ -1021,7 +1021,7 @@ onUnmounted(() => {
</div> </div>
<div class="requestChecklist__hint"> <div class="requestChecklist__hint">
제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 있어요. 제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 있어요.
예시: 제목 `템플릿 등록 요청`, 설명 `여름 이벤트 한정 캐릭터 중심으로 새 게임 템플릿이 필요합니다.` 예시: 제목 `템플릿 등록 요청`, 설명 `여름 이벤트 한정 캐릭터 중심으로 새 주제 템플릿이 필요합니다.`
</div> </div>
<div class="templateRequestDraft"> <div class="templateRequestDraft">
<label class="templateRequestDraft__field"> <label class="templateRequestDraft__field">