Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 074d028f04 | |||
| 208e9709f8 | |||
| 4ed7f275ba | |||
| 88ce413c31 | |||
| 7f7475fb20 | |||
| 8a44b51cce | |||
| 9d63ed2e76 | |||
| 99eb79f2c3 |
@@ -138,6 +138,7 @@ function mapTierListRow(row) {
|
|||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
isPublic: !!row.is_public,
|
isPublic: !!row.is_public,
|
||||||
showCharacterNames: !!row.show_character_names,
|
showCharacterNames: !!row.show_character_names,
|
||||||
|
iconSize: Number(row.icon_size || 80),
|
||||||
sourceTierListId: row.source_tierlist_id || '',
|
sourceTierListId: row.source_tierlist_id || '',
|
||||||
sourceSnapshotTitle: row.source_snapshot_title || '',
|
sourceSnapshotTitle: row.source_snapshot_title || '',
|
||||||
sourceSnapshotAuthor: row.source_snapshot_author || '',
|
sourceSnapshotAuthor: row.source_snapshot_author || '',
|
||||||
@@ -314,6 +315,7 @@ async function ensureSchema() {
|
|||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
show_character_names TINYINT(1) NOT NULL DEFAULT 0,
|
show_character_names TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
icon_size INT NOT NULL DEFAULT 80,
|
||||||
source_tierlist_id VARCHAR(64) NULL DEFAULT NULL,
|
source_tierlist_id VARCHAR(64) NULL DEFAULT NULL,
|
||||||
source_snapshot_title VARCHAR(120) NOT NULL DEFAULT '',
|
source_snapshot_title VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
source_snapshot_author VARCHAR(120) NOT NULL DEFAULT '',
|
source_snapshot_author VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
@@ -455,9 +457,13 @@ async function ensureSchema() {
|
|||||||
if (!tierListShowNamesColumns.length) {
|
if (!tierListShowNamesColumns.length) {
|
||||||
await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public")
|
await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public")
|
||||||
}
|
}
|
||||||
|
const tierListIconSizeColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'icon_size'")
|
||||||
|
if (!tierListIconSizeColumns.length) {
|
||||||
|
await query("ALTER TABLE tierlists ADD COLUMN icon_size INT NOT NULL DEFAULT 80 AFTER show_character_names")
|
||||||
|
}
|
||||||
const tierListSourceIdColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'source_tierlist_id'")
|
const tierListSourceIdColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'source_tierlist_id'")
|
||||||
if (!tierListSourceIdColumns.length) {
|
if (!tierListSourceIdColumns.length) {
|
||||||
await query("ALTER TABLE tierlists ADD COLUMN source_tierlist_id VARCHAR(64) NULL DEFAULT NULL AFTER show_character_names")
|
await query("ALTER TABLE tierlists ADD COLUMN source_tierlist_id VARCHAR(64) NULL DEFAULT NULL AFTER icon_size")
|
||||||
} else if (tierListSourceIdColumns[0]?.Null !== 'YES') {
|
} else if (tierListSourceIdColumns[0]?.Null !== 'YES') {
|
||||||
await query('ALTER TABLE tierlists MODIFY source_tierlist_id VARCHAR(64) NULL DEFAULT NULL')
|
await query('ALTER TABLE tierlists MODIFY source_tierlist_id VARCHAR(64) NULL DEFAULT NULL')
|
||||||
}
|
}
|
||||||
@@ -1847,6 +1853,7 @@ async function listFavoriteTierLists(userId, { queryText = '', sort = 'favorited
|
|||||||
t.description,
|
t.description,
|
||||||
t.is_public,
|
t.is_public,
|
||||||
t.show_character_names,
|
t.show_character_names,
|
||||||
|
t.icon_size,
|
||||||
t.source_tierlist_id,
|
t.source_tierlist_id,
|
||||||
t.source_snapshot_title,
|
t.source_snapshot_title,
|
||||||
t.source_snapshot_author,
|
t.source_snapshot_author,
|
||||||
@@ -1990,6 +1997,7 @@ async function listAdminTierLists({ queryText = '', gameId = '', page = 1, limit
|
|||||||
t.description,
|
t.description,
|
||||||
t.is_public,
|
t.is_public,
|
||||||
t.show_character_names,
|
t.show_character_names,
|
||||||
|
t.icon_size,
|
||||||
t.source_tierlist_id,
|
t.source_tierlist_id,
|
||||||
t.source_snapshot_title,
|
t.source_snapshot_title,
|
||||||
t.source_snapshot_author,
|
t.source_snapshot_author,
|
||||||
@@ -2093,6 +2101,7 @@ async function findTierListById(id, currentUserId = '') {
|
|||||||
t.description,
|
t.description,
|
||||||
t.is_public,
|
t.is_public,
|
||||||
t.show_character_names,
|
t.show_character_names,
|
||||||
|
t.icon_size,
|
||||||
t.source_tierlist_id,
|
t.source_tierlist_id,
|
||||||
t.source_snapshot_title,
|
t.source_snapshot_title,
|
||||||
t.source_snapshot_author,
|
t.source_snapshot_author,
|
||||||
@@ -2346,6 +2355,7 @@ async function saveTierList({
|
|||||||
description,
|
description,
|
||||||
isPublic,
|
isPublic,
|
||||||
showCharacterNames = false,
|
showCharacterNames = false,
|
||||||
|
iconSize = 80,
|
||||||
sourceTierListId = '',
|
sourceTierListId = '',
|
||||||
sourceSnapshotTitle = '',
|
sourceSnapshotTitle = '',
|
||||||
sourceSnapshotAuthor = '',
|
sourceSnapshotAuthor = '',
|
||||||
@@ -2360,10 +2370,10 @@ async function saveTierList({
|
|||||||
await query(
|
await query(
|
||||||
`
|
`
|
||||||
UPDATE tierlists
|
UPDATE tierlists
|
||||||
SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, show_character_names = ?, source_tierlist_id = ?, source_snapshot_title = ?, source_snapshot_author = ?, groups_json = ?, pool_json = ?, updated_at = ?
|
SET title = ?, thumbnail_src = ?, description = ?, is_public = ?, show_character_names = ?, icon_size = ?, source_tierlist_id = ?, source_snapshot_title = ?, source_snapshot_author = ?, groups_json = ?, pool_json = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`,
|
`,
|
||||||
[title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), now(), existing.id]
|
[title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, Number(iconSize) || 80, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), now(), existing.id]
|
||||||
)
|
)
|
||||||
return findTierListById(existing.id, authorId)
|
return findTierListById(existing.id, authorId)
|
||||||
}
|
}
|
||||||
@@ -2373,11 +2383,11 @@ async function saveTierList({
|
|||||||
await query(
|
await query(
|
||||||
`
|
`
|
||||||
INSERT INTO tierlists (
|
INSERT INTO tierlists (
|
||||||
id, author_id, game_id, title, thumbnail_src, description, is_public, show_character_names, source_tierlist_id, source_snapshot_title, source_snapshot_author, groups_json, pool_json, created_at, updated_at
|
id, author_id, game_id, title, thumbnail_src, description, is_public, show_character_names, icon_size, source_tierlist_id, source_snapshot_title, source_snapshot_author, groups_json, pool_json, created_at, updated_at
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[nextId, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), createdAt, createdAt]
|
[nextId, authorId, gameId, title, nextThumbnailSrc, description || '', isPublic ? 1 : 0, showCharacterNames ? 1 : 0, Number(iconSize) || 80, sourceTierListId || null, sourceSnapshotTitle || '', sourceSnapshotAuthor || '', serializeJson(groups), serializeJson(pool), createdAt, createdAt]
|
||||||
)
|
)
|
||||||
return findTierListById(nextId, authorId)
|
return findTierListById(nextId, authorId)
|
||||||
}
|
}
|
||||||
@@ -2396,6 +2406,7 @@ async function duplicateTierListForUser({ tierList, targetUserId }) {
|
|||||||
description: tierList.description || '',
|
description: tierList.description || '',
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
showCharacterNames: !!tierList.showCharacterNames,
|
showCharacterNames: !!tierList.showCharacterNames,
|
||||||
|
iconSize: Number(tierList.iconSize || 80),
|
||||||
sourceTierListId: tierList.id,
|
sourceTierListId: tierList.id,
|
||||||
sourceSnapshotTitle: tierList.title || '',
|
sourceSnapshotTitle: tierList.title || '',
|
||||||
sourceSnapshotAuthor: tierList.authorName || tierList.authorAccountName || '',
|
sourceSnapshotAuthor: tierList.authorName || tierList.authorAccountName || '',
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const tierListUpsertSchema = z.object({
|
|||||||
description: z.string().max(1000).optional().default(''),
|
description: z.string().max(1000).optional().default(''),
|
||||||
isPublic: z.boolean().default(false),
|
isPublic: z.boolean().default(false),
|
||||||
showCharacterNames: z.boolean().optional().default(false),
|
showCharacterNames: z.boolean().optional().default(false),
|
||||||
|
iconSize: z.number().int().min(48).max(112).optional().default(80),
|
||||||
sourceTierListId: z.string().max(64).optional().default(''),
|
sourceTierListId: z.string().max(64).optional().default(''),
|
||||||
sourceSnapshotTitle: z.string().max(120).optional().default(''),
|
sourceSnapshotTitle: z.string().max(120).optional().default(''),
|
||||||
sourceSnapshotAuthor: z.string().max(120).optional().default(''),
|
sourceSnapshotAuthor: z.string().max(120).optional().default(''),
|
||||||
@@ -289,6 +290,7 @@ router.post('/', requireAuth, async (req, res) => {
|
|||||||
description: payload.description || '',
|
description: payload.description || '',
|
||||||
isPublic: !!payload.isPublic,
|
isPublic: !!payload.isPublic,
|
||||||
showCharacterNames: !!payload.showCharacterNames,
|
showCharacterNames: !!payload.showCharacterNames,
|
||||||
|
iconSize: Number(payload.iconSize || 80),
|
||||||
sourceTierListId: payload.sourceTierListId || existing.sourceTierListId || '',
|
sourceTierListId: payload.sourceTierListId || existing.sourceTierListId || '',
|
||||||
sourceSnapshotTitle: payload.sourceSnapshotTitle || existing.sourceSnapshotTitle || '',
|
sourceSnapshotTitle: payload.sourceSnapshotTitle || existing.sourceSnapshotTitle || '',
|
||||||
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || existing.sourceSnapshotAuthor || '',
|
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || existing.sourceSnapshotAuthor || '',
|
||||||
@@ -307,6 +309,7 @@ router.post('/', requireAuth, async (req, res) => {
|
|||||||
description: payload.description || '',
|
description: payload.description || '',
|
||||||
isPublic: !!payload.isPublic,
|
isPublic: !!payload.isPublic,
|
||||||
showCharacterNames: !!payload.showCharacterNames,
|
showCharacterNames: !!payload.showCharacterNames,
|
||||||
|
iconSize: Number(payload.iconSize || 80),
|
||||||
sourceTierListId: payload.sourceTierListId || '',
|
sourceTierListId: payload.sourceTierListId || '',
|
||||||
sourceSnapshotTitle: payload.sourceSnapshotTitle || '',
|
sourceSnapshotTitle: payload.sourceSnapshotTitle || '',
|
||||||
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || '',
|
sourceSnapshotAuthor: payload.sourceSnapshotAuthor || '',
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.86
|
||||||
|
- 아이콘 크기는 이미지 다운로드 결과에만 반영되고 저장본에는 남지 않으면 사용자가 체감상 “저장되지 않는 설정”으로 느끼게 되므로, 티어표 본문 설정으로 저장하는 편이 맞다고 정리했다.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.83
|
||||||
|
- 모바일에서 열 헤더가 칸과 시각적으로 분리되는 문제는 전체 레이아웃을 다시 갈아엎기보다, 각 칸 안에 열 이름 배지를 같이 보여주는 편이 가장 적은 변경으로 효과를 낸다고 정리했다.
|
||||||
|
- 배지를 쓰는 반응형 구간에서는 기존 상단 열 헤더까지 남겨두면 중복 정보가 되므로, 같은 브레이크포인트에서 헤더는 숨기고 칸 배지 하나만 남기는 편이 맞다고 정리했다.
|
||||||
|
- 반응형 보정은 한 미디어 구간 안에서 서로 다른 규칙이 다시 덮어쓰지 않게 정리해야 하므로, 모바일용 `1fr` 레이아웃을 선언한 뒤 예전 `140px/150px` 규칙은 제거하는 편이 맞다고 판단했다.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.82
|
||||||
|
- 프리뷰 완성본도 결국 공유/열람용 결과물이므로, 이미지 다운로드 결과와 같은 작성자/저장 시각 메타를 같이 보여주는 편이 자연스럽다고 정리했다.
|
||||||
|
- 관리자 템플릿 요청 카드는 “요청 티어표 보기”가 실제로 새창 이동용이라면 하단 버튼과 썸네일 클릭을 둘 다 유지하기보다, 썸네일 클릭 하나로 통합하는 편이 더 단순하고 직관적이라고 판단했다.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.81
|
||||||
|
- 저장된 티어표 공유는 별도 새 페이지를 만들기보다, 이미 완성본 열람에 쓰고 있는 `preview=1` 주소를 그대로 공유 링크로 재사용하는 편이 가장 단순하고 일관적이라고 정리했다.
|
||||||
|
- 공유 액션은 저장/삭제처럼 저장본 전제의 보조 기능이므로, 메인 저장 버튼 영역보다 하단 유틸리티 링크 영역에 두는 편이 더 자연스럽다고 판단했다.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.79
|
||||||
|
- 카피라이트처럼 앱 전체 브랜딩 성격의 footer는 관리자 텔레포트 안에 두기보다, `App.vue`의 공통 오른쪽 레일 footer로 두는 편이 위치도 안정적이고 화면 간 일관성도 높다고 정리했다.
|
||||||
|
|
||||||
## 2026-04-02 v1.3.78
|
## 2026-04-02 v1.3.78
|
||||||
- 축소 상태에서는 텍스트가 사라지므로 같은 `티어표 만들기` 계열 액션이라도 커스텀 제작과 템플릿 기반 제작을 아이콘으로 구분해 주는 편이 맞다고 정리했다.
|
- 축소 상태에서는 텍스트가 사라지므로 같은 `티어표 만들기` 계열 액션이라도 커스텀 제작과 템플릿 기반 제작을 아이콘으로 구분해 주는 편이 맞다고 정리했다.
|
||||||
- 관리자 우측 카피라이트처럼 “사이드바 하단”에 붙어야 하는 정보는 텔레포트 루트의 형제 노드로 두기보다, 실제 사이드바 컨테이너 내부의 마지막 행으로 두는 편이 레이아웃상 안전하다고 판단했다.
|
- 관리자 우측 카피라이트처럼 “사이드바 하단”에 붙어야 하는 정보는 텔레포트 루트의 형제 노드로 두기보다, 실제 사이드바 컨테이너 내부의 마지막 행으로 두는 편이 레이아웃상 안전하다고 판단했다.
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# 할 일 및 이슈
|
# 할 일 및 이슈
|
||||||
|
|
||||||
## 단기 확인
|
## 단기 확인
|
||||||
|
- 티어표 `아이콘 크기`는 이제 저장 데이터로 승격됐으므로, 저장 후 재진입/프리뷰/복사본 생성에서 같은 크기가 유지되는지 한 번 더 QA한다.
|
||||||
|
- 티어표 편집/프리뷰 모바일 열 배지는 새로 붙였으므로, 실제 좁은 화면에서 칸 상단 배지와 아이템 썸네일이 겹치지 않고 열 구분이 자연스러운지 한 번 더 QA한다.
|
||||||
|
- 모바일 열 배지는 같은 구간에서 상단 열 제목을 숨기도록 다시 맞췄으므로, 720px 안팎뿐 아니라 980px 이하 전 구간에서 중복 표기 없이 자연스러운지 한 번 더 QA한다.
|
||||||
|
- 모바일 티어표 편집 레이아웃은 행 라벨 폭을 다시 덮어쓰던 규칙을 걷어냈으므로, 실제 980px 이하 구간에서 행 라벨이 과하게 넓지 않고 칸 폭을 충분히 남기는지 한 번 더 QA한다.
|
||||||
|
- 프리뷰 완성본 하단 메타는 새로 붙였으므로, 작성자/저장 시각이 공개 열람 화면과 이미지 다운로드 결과 기준에서 모두 자연스럽게 읽히는지 한 번 더 QA한다.
|
||||||
|
- 관리자 템플릿 요청 카드는 썸네일 클릭이 새창 열기 역할로 바뀌었으므로, 썸네일 클릭과 `확인하기` 액션이 서로 헷갈리지 않는지 한 번 더 QA한다.
|
||||||
|
- 티어표 만들기 화면의 `공유하기`는 저장된 티어표에서만 노출되므로, 저장 직후/수정 중/복사본/읽기 전용 상태 각각에서 노출 조건과 클립보드 복사가 자연스러운지 한 번 더 QA한다.
|
||||||
|
- 우측 카피라이트는 이제 공통 오른쪽 레일 footer이므로, 관리자 화면뿐 아니라 홈/프로필 등 오른쪽 사이드가 보이는 화면에서도 같은 최하단 위치에 유지되는지 한 번 더 QA한다.
|
||||||
- 왼쪽 레일 축소 상태의 하단 액션 아이콘은 홈과 게임 허브에서 서로 다른 아이콘을 쓰도록 나눴으므로, 실제로 두 문맥이 한눈에 구분되는지 한 번 더 QA한다.
|
- 왼쪽 레일 축소 상태의 하단 액션 아이콘은 홈과 게임 허브에서 서로 다른 아이콘을 쓰도록 나눴으므로, 실제로 두 문맥이 한눈에 구분되는지 한 번 더 QA한다.
|
||||||
- 왼쪽 레일 축소 상태 최하단의 `티어표 만들기` 아이콘 버튼은 새로 추가했으므로, 홈/게임 허브에서 실제로 같은 위치 감각으로 동작하는지 한 번 더 QA한다.
|
- 왼쪽 레일 축소 상태 최하단의 `티어표 만들기` 아이콘 버튼은 새로 추가했으므로, 홈/게임 허브에서 실제로 같은 위치 감각으로 동작하는지 한 번 더 QA한다.
|
||||||
- 관리자 우측 카피라이트 문구는 사이드바 내부 최하단으로 다시 옮겼으므로, 실제 관리자 화면에서 스크롤/창 크기 변화에도 계속 보이는지 한 번 더 QA한다.
|
- 관리자 우측 카피라이트 문구는 사이드바 내부 최하단으로 다시 옮겼으므로, 실제 관리자 화면에서 스크롤/창 크기 변화에도 계속 보이는지 한 번 더 QA한다.
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.86
|
||||||
|
- 티어표 편집의 `아이콘 크기`는 이제 임시 화면 상태가 아니라 저장 데이터에 함께 포함되며, 저장 후 다시 열기와 프리뷰 화면에서도 같은 크기로 복원되도록 정리함.
|
||||||
|
- 이를 위해 티어표 저장 payload, 서버 검증, DB 저장/조회에 `iconSize`를 추가하고 기존 데이터는 기본값 `80`으로 안전하게 보정되게 맞춤.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.83
|
||||||
|
- 티어표 편집/프리뷰 화면에서 열을 여러 개 쓰는 경우, 모바일처럼 좁은 화면에서는 기존 상단 열 헤더만으로 각 칸의 의미를 읽기 어려웠으므로 각 칸 상단에 작은 열 이름 배지를 추가함.
|
||||||
|
- 이 배지는 모바일 구간에서만 보이고 데스크톱 레이아웃은 그대로 유지되므로, 작은 화면에서는 `메인 / 밸런스 / 서포트` 같은 열 맥락을 스크롤 중에도 잃지 않게 정리함.
|
||||||
|
- 이후 배지가 칸 기준이 아니라 화면 한쪽에 겹치던 문제를 바로잡기 위해 각 칸을 기준점으로 다시 잡았고, 배지가 보이는 구간에서는 기존 상단 열 제목을 함께 숨겨 중복 표기를 제거함.
|
||||||
|
- 추가로 같은 미디어 구간 안에서 행/열 모바일 레이아웃을 다시 `140px/150px`로 덮어쓰던 중복 규칙을 제거해, 모바일에서는 행 라벨이 화면 절반을 차지하지 않고 실제로 한 줄 전체 폭 기준 레이아웃으로 정리되게 맞춤.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.82
|
||||||
|
- 프리뷰 전용 완성본 화면에도 이미지 다운로드 결과와 같은 하단 메타를 붙여, 작성자 이름과 마지막 저장 시각을 바로 확인할 수 있게 정리함.
|
||||||
|
- 관리자 `티어표 관리 > 템플릿 요청 관리`에서는 더 이상 썸네일 클릭으로 요청 미리보기 모달을 열지 않고, 썸네일 자체가 `요청 티어표 보기` 새창 링크 역할을 하도록 바꿨으며, 하단의 중복 `요청 티어표 보기` 버튼은 제거함.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.81
|
||||||
|
- 티어표 만들기 화면에는 저장된 티어표에서만 보이는 `공유하기` 액션을 추가하고, 누르면 현재 티어표의 완성본 링크(`preview=1`)를 클립보드에 복사한 뒤 토스트로 안내하도록 정리함.
|
||||||
|
- 공유 링크는 관리자가 새 창에서 보던 완성본 주소와 같은 문법을 사용하므로, 저장된 티어표를 그대로 외부에 전달하거나 다시 열람하는 흐름으로 바로 이어짐.
|
||||||
|
|
||||||
|
## 2026-04-02 v1.3.79
|
||||||
|
- 우측 카피라이트는 관리자 전용 레이아웃에서 분리해 앱 공통 `rightRail` footer로 올렸고, 이제 관리자 페이지뿐 아니라 오른쪽 사이드가 보이는 모든 화면에서 같은 최하단 위치에 표시됨.
|
||||||
|
- 따라서 관리자 패널 길이나 페이지별 로컬 사이드바 내용과 무관하게, 카피라이트는 항상 오른쪽 레일 전체 기준 바닥에 고정되는 공통 footer 역할로 정리됨.
|
||||||
|
|
||||||
## 2026-04-02 v1.3.78
|
## 2026-04-02 v1.3.78
|
||||||
- 왼쪽 레일 축소 상태의 하단 액션 아이콘은 문맥에 따라 구분되도록 바꿔, 홈의 `커스텀 티어표 만들기`는 `dashboard_customize` 아이콘을 쓰고 게임 허브의 일반 `티어표 만들기`만 `add_notes` 아이콘을 유지하도록 정리함.
|
- 왼쪽 레일 축소 상태의 하단 액션 아이콘은 문맥에 따라 구분되도록 바꿔, 홈의 `커스텀 티어표 만들기`는 `dashboard_customize` 아이콘을 쓰고 게임 허브의 일반 `티어표 만들기`만 `add_notes` 아이콘을 유지하도록 정리함.
|
||||||
- 관리자 우측 카피라이트 문구는 사이드바 바깥 형제로 밀려 보이지 않을 수 있었으므로, 다시 관리자 사이드바 `aside` 내부 최하단으로 옮겨 레이아웃 안에서 안정적으로 보이게 정리함.
|
- 관리자 우측 카피라이트 문구는 사이드바 바깥 형제로 밀려 보이지 않을 수 있었으므로, 다시 관리자 사이드바 `aside` 내부 최하단으로 옮겨 레이아웃 안에서 안정적으로 보이게 정리함.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const { toasts, dismissToast } = useToast()
|
const { toasts, dismissToast } = useToast()
|
||||||
|
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)
|
||||||
@@ -628,6 +629,11 @@ function submitGlobalSearch() {
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="rightRail__footer">
|
||||||
|
<span>Copyright © 2026 </span>
|
||||||
|
<a :href="RIGHT_RAIL_COPYRIGHT_URL" target="_blank" rel="noreferrer">zenn</a>
|
||||||
|
<span>. All rights reserved.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -748,8 +754,11 @@ function submitGlobalSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rightRail__content {
|
.rightRail__content {
|
||||||
flex: 0 0 auto;
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghostIcon {
|
.ghostIcon {
|
||||||
@@ -1197,13 +1206,31 @@ function submitGlobalSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rightRail__bottom {
|
.rightRail__bottom {
|
||||||
display: flex;
|
margin-top: auto;
|
||||||
align-items: flex-end;
|
display: grid;
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rightRail__footer {
|
||||||
|
padding: 0 4px 2px;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--theme-text-faint);
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightRail__footer a {
|
||||||
|
color: #00ffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightRail__footer a:hover {
|
||||||
|
color: #00ffff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.settingsThemePanel {
|
.settingsThemePanel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1627,9 +1654,10 @@ function submitGlobalSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.localRightRailRoot {
|
.localRightRailRoot {
|
||||||
min-height: auto;
|
flex: 1 1 auto;
|
||||||
display: grid;
|
min-height: 100%;
|
||||||
align-content: start;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
frontend/src/assets/icons/share.svg
Normal file
1
frontend/src/assets/icons/share.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M680-80q-50 0-85-35t-35-85q0-6 3-28L282-392q-16 15-37 23.5t-45 8.5q-50 0-85-35t-35-85q0-50 35-85t85-35q24 0 45 8.5t37 23.5l281-164q-2-7-2.5-13.5T560-760q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35q-24 0-45-8.5T598-672L317-508q2 7 2.5 13.5t.5 14.5q0 8-.5 14.5T317-452l281 164q16-15 37-23.5t45-8.5q50 0 85 35t35 85q0 50-35 85t-85 35Zm0-80q17 0 28.5-11.5T720-200q0-17-11.5-28.5T680-240q-17 0-28.5 11.5T640-200q0 17 11.5 28.5T680-160ZM200-440q17 0 28.5-11.5T240-480q0-17-11.5-28.5T200-520q-17 0-28.5 11.5T160-480q0 17 11.5 28.5T200-440Zm508.5-291.5Q720-743 720-760t-11.5-28.5Q697-800 680-800t-28.5 11.5Q640-777 640-760t11.5 28.5Q663-720 680-720t28.5-11.5ZM680-200ZM200-480Zm480-280Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 810 B |
@@ -39,10 +39,17 @@ const props = defineProps({
|
|||||||
<div v-else class="templateRequestList">
|
<div v-else class="templateRequestList">
|
||||||
<article v-for="request in props.templateRequests" :key="request.id" class="tierAdminCard templateRequestCard templateRequestCard--aligned">
|
<article v-for="request in props.templateRequests" :key="request.id" class="tierAdminCard templateRequestCard templateRequestCard--aligned">
|
||||||
<div class="templateRequestCard__side">
|
<div class="templateRequestCard__side">
|
||||||
<button class="tierAdminCard__preview templateRequestCard__preview" type="button" @click="props.openTemplateRequestPreview(request)">
|
<a
|
||||||
|
class="tierAdminCard__preview templateRequestCard__preview"
|
||||||
|
:href="props.templateRequestSourceUrl(request) || undefined"
|
||||||
|
:target="props.templateRequestSourceUrl(request) ? '_blank' : undefined"
|
||||||
|
:rel="props.templateRequestSourceUrl(request) ? 'noreferrer' : undefined"
|
||||||
|
:aria-disabled="!props.templateRequestSourceUrl(request)"
|
||||||
|
@click.prevent="props.templateRequestSourceUrl(request) && window.open(props.templateRequestSourceUrl(request), '_blank', 'noopener,noreferrer')"
|
||||||
|
>
|
||||||
<img v-if="request.thumbnailSrc" class="tierAdminCard__thumb" :src="toApiUrl(request.thumbnailSrc)" :alt="request.sourceTierListTitle" draggable="false" />
|
<img v-if="request.thumbnailSrc" class="tierAdminCard__thumb" :src="toApiUrl(request.thumbnailSrc)" :alt="request.sourceTierListTitle" draggable="false" />
|
||||||
<div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
|
<div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
|
||||||
</button>
|
</a>
|
||||||
<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">
|
||||||
@@ -97,17 +104,7 @@ const props = defineProps({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="templateRequestCard__footer">
|
<div class="templateRequestCard__footer">
|
||||||
<div class="templateRequestCard__footerLeft">
|
<div class="templateRequestCard__footerLeft"></div>
|
||||||
<a
|
|
||||||
v-if="props.templateRequestSourceUrl(request)"
|
|
||||||
class="btn btn--ghost btn--small"
|
|
||||||
:href="props.templateRequestSourceUrl(request)"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
요청 티어표 보기
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="templateRequestCard__actions">
|
<div class="templateRequestCard__actions">
|
||||||
<button class="btn btn--primary" :disabled="request.isHandling" @click="props.startTemplateRequestReview(request)">
|
<button class="btn btn--primary" :disabled="request.isHandling" @click="props.startTemplateRequestReview(request)">
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const router = useRouter()
|
|||||||
const globalRightRailOpen = inject('rightRailOpen', ref(true))
|
const globalRightRailOpen = inject('rightRailOpen', ref(true))
|
||||||
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
|
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
|
||||||
const isAdmin = computed(() => !!auth.user?.isAdmin)
|
const isAdmin = computed(() => !!auth.user?.isAdmin)
|
||||||
const ADMIN_COPYRIGHT_URL = 'https://zenn.town/@murabito'
|
|
||||||
|
|
||||||
const activeTab = ref('featured')
|
const activeTab = ref('featured')
|
||||||
const tierlistsMode = ref('requests')
|
const tierlistsMode = ref('requests')
|
||||||
@@ -2389,11 +2388,6 @@ function userAvatarFallback(user) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="adminSidebarFooter">
|
|
||||||
<span>Copyright © 2026 </span>
|
|
||||||
<a :href="ADMIN_COPYRIGHT_URL" target="_blank" rel="noreferrer">zenn</a>
|
|
||||||
<span>. All rights reserved.</span>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
@@ -2473,27 +2467,6 @@ function userAvatarFallback(user) {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.adminUiScope .adminSidebarFooter {
|
|
||||||
margin-top: auto;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
.adminUiScope .adminSidebarFooter {
|
|
||||||
margin-top: 6px;
|
|
||||||
padding: 0 4px 2px;
|
|
||||||
font-size: 9px;
|
|
||||||
line-height: 1.4;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--theme-text-faint);
|
|
||||||
opacity: 0.72;
|
|
||||||
}
|
|
||||||
.adminUiScope .adminSidebarFooter a {
|
|
||||||
color: #00ffff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.adminUiScope .adminSidebarFooter a:hover {
|
|
||||||
color: #00ffff;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.adminUiScope .adminSidebar__panel {
|
.adminUiScope .adminSidebar__panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import SvgIcon from '../components/SvgIcon.vue'
|
|||||||
import addColumnRightIcon from '../assets/icons/add_column_right.svg'
|
import addColumnRightIcon from '../assets/icons/add_column_right.svg'
|
||||||
import addRowBelowIcon from '../assets/icons/add_row_below.svg'
|
import addRowBelowIcon from '../assets/icons/add_row_below.svg'
|
||||||
import addPhotoAlternateIcon from '../assets/icons/add_photo_alternate.svg'
|
import addPhotoAlternateIcon from '../assets/icons/add_photo_alternate.svg'
|
||||||
|
import shareIcon from '../assets/icons/share.svg'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import { toApiUrl } from '../lib/runtime'
|
import { toApiUrl } from '../lib/runtime'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
@@ -130,6 +131,12 @@ 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 savedTierListId = persistedTierListId.value || (tierListId.value && tierListId.value !== 'new' ? tierListId.value : '')
|
||||||
|
if (!savedTierListId) return ''
|
||||||
|
if (typeof window === 'undefined') return `/editor/${gameId.value}/${savedTierListId}?preview=1`
|
||||||
|
return new URL(`/editor/${gameId.value}/${savedTierListId}?preview=1`, window.location.origin).toString()
|
||||||
|
})
|
||||||
|
|
||||||
watch(error, (message) => {
|
watch(error, (message) => {
|
||||||
if (!message) return
|
if (!message) return
|
||||||
@@ -671,6 +678,7 @@ function buildPayload(existingId) {
|
|||||||
description: (description.value || '').trim(),
|
description: (description.value || '').trim(),
|
||||||
isPublic: !!isPublic.value,
|
isPublic: !!isPublic.value,
|
||||||
showCharacterNames: !!showCharacterNames.value,
|
showCharacterNames: !!showCharacterNames.value,
|
||||||
|
iconSize: Number(iconSize.value || 80),
|
||||||
sourceTierListId: sourceTierListId.value || '',
|
sourceTierListId: sourceTierListId.value || '',
|
||||||
sourceSnapshotTitle: sourceSnapshotTitle.value || '',
|
sourceSnapshotTitle: sourceSnapshotTitle.value || '',
|
||||||
sourceSnapshotAuthor: sourceSnapshotAuthor.value || '',
|
sourceSnapshotAuthor: sourceSnapshotAuthor.value || '',
|
||||||
@@ -712,6 +720,32 @@ async function save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyShareUrl() {
|
||||||
|
if (!shareTierListUrl.value) {
|
||||||
|
toast.error('먼저 티어표를 저장한 뒤 공유할 수 있어요.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator?.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(shareTierListUrl.value)
|
||||||
|
} else {
|
||||||
|
const helper = document.createElement('textarea')
|
||||||
|
helper.value = shareTierListUrl.value
|
||||||
|
helper.setAttribute('readonly', '')
|
||||||
|
helper.style.position = 'absolute'
|
||||||
|
helper.style.left = '-9999px'
|
||||||
|
document.body.appendChild(helper)
|
||||||
|
helper.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
helper.remove()
|
||||||
|
}
|
||||||
|
toast.success('공유 링크를 클립보드에 복사했어요.')
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('공유 링크를 복사하지 못했어요.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeSaveModal() {
|
function closeSaveModal() {
|
||||||
isSaveModalOpen.value = false
|
isSaveModalOpen.value = false
|
||||||
}
|
}
|
||||||
@@ -890,6 +924,7 @@ onMounted(() => {
|
|||||||
description.value = t.description || ''
|
description.value = t.description || ''
|
||||||
isPublic.value = !!t.isPublic
|
isPublic.value = !!t.isPublic
|
||||||
showCharacterNames.value = !!t.showCharacterNames
|
showCharacterNames.value = !!t.showCharacterNames
|
||||||
|
iconSize.value = Number(t.iconSize || 80)
|
||||||
authorName.value = t.authorName || ''
|
authorName.value = t.authorName || ''
|
||||||
authorAccountName.value = t.authorAccountName || ''
|
authorAccountName.value = t.authorAccountName || ''
|
||||||
updatedAt.value = Number(t.updatedAt || 0)
|
updatedAt.value = Number(t.updatedAt || 0)
|
||||||
@@ -941,6 +976,7 @@ onUnmounted(() => {
|
|||||||
<div class="previewOnly__dropGrid" :style="{ '--column-count': columns.length }">
|
<div class="previewOnly__dropGrid" :style="{ '--column-count': columns.length }">
|
||||||
<div v-for="(column, columnIndex) in columns" :key="column.id" class="previewOnly__dropColumn">
|
<div v-for="(column, columnIndex) in columns" :key="column.id" class="previewOnly__dropColumn">
|
||||||
<div class="previewOnly__drop">
|
<div class="previewOnly__drop">
|
||||||
|
<div v-if="columns.length > 1" class="previewOnly__columnBadge">{{ column.name || '열 ' + (columnIndex + 1) }}</div>
|
||||||
<div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="previewOnly__cell">
|
<div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="previewOnly__cell">
|
||||||
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
||||||
<div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
|
<div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
|
||||||
@@ -958,6 +994,10 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="previewOnly__footer">
|
||||||
|
<span>{{ effectiveAuthorName }}</span>
|
||||||
|
<span>{{ formatExportDate(fallbackTimestamp) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -966,7 +1006,7 @@ onUnmounted(() => {
|
|||||||
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
|
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
|
||||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
|
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="saveModalTitle">
|
||||||
<div id="saveModalTitle" class="modalCard__title">저장 완료</div>
|
<div id="saveModalTitle" class="modalCard__title">저장 완료</div>
|
||||||
<div class="modalCard__desc">티어표가 저장되었어요. 이어서 더 수정한 뒤 다시 저장할 수도 있어요.</div>
|
<div class="modalCard__desc">티어표가 저장되었어요.<br />이어서 더 수정한 뒤 다시 저장할 수도 있어요.</div>
|
||||||
<div class="modalCard__actions">
|
<div class="modalCard__actions">
|
||||||
<button class="btn btn--save" @click="closeSaveModal">확인</button>
|
<button class="btn btn--save" @click="closeSaveModal">확인</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1165,13 +1205,14 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="row__content" :style="{ '--column-count': columns.length }">
|
<div class="row__content" :style="{ '--column-count': columns.length }">
|
||||||
<div v-for="(column, columnIndex) in columns" :key="column.id" class="row__column">
|
<div v-for="(column, columnIndex) in columns" :key="column.id" class="row__column">
|
||||||
<div
|
<div
|
||||||
class="row__drop"
|
class="row__drop"
|
||||||
:data-list-type="'group'"
|
:data-list-type="'group'"
|
||||||
:data-group-id="g.id"
|
:data-group-id="g.id"
|
||||||
:data-column-index="columnIndex"
|
:data-column-index="columnIndex"
|
||||||
:ref="(el) => setGroupDropEl(g.id, columnIndex, el)"
|
:ref="(el) => setGroupDropEl(g.id, columnIndex, el)"
|
||||||
>
|
>
|
||||||
|
<div v-if="columns.length > 1" class="row__columnBadge">{{ column.name || '열 ' + (columnIndex + 1) }}</div>
|
||||||
<div v-if="!isExporting" class="row__empty" v-show="getGroupCellIds(g, columnIndex).length === 0">여기로 드래그해서 배치</div>
|
<div v-if="!isExporting" class="row__empty" v-show="getGroupCellIds(g, columnIndex).length === 0">여기로 드래그해서 배치</div>
|
||||||
<div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="cell" :data-item-id="id">
|
<div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="cell" :data-item-id="id">
|
||||||
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
||||||
@@ -1324,6 +1365,10 @@ onUnmounted(() => {
|
|||||||
<button v-if="canEdit" class="btn btn--save editorSidebar__button" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
|
<button v-if="canEdit" class="btn btn--save editorSidebar__button" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="editorSidebar__utilityLinks">
|
<div class="editorSidebar__utilityLinks">
|
||||||
|
<button v-if="hasSavedTierList" class="editorSidebar__utilityLink editorSidebar__utilityLink--share" @click="copyShareUrl">
|
||||||
|
<SvgIcon :src="shareIcon" :size="16" />
|
||||||
|
<span>공유하기</span>
|
||||||
|
</button>
|
||||||
<button v-if="canEdit && hasSavedTierList" class="editorSidebar__utilityLink editorSidebar__utilityLink--danger" @click="openDeleteModal">삭제하기</button>
|
<button v-if="canEdit && hasSavedTierList" class="editorSidebar__utilityLink editorSidebar__utilityLink--danger" @click="openDeleteModal">삭제하기</button>
|
||||||
<button v-if="canDuplicate" class="editorSidebar__utilityLink" @click="duplicateCurrentTierList">복사해서 내 티어표로 가져오기</button>
|
<button v-if="canDuplicate" class="editorSidebar__utilityLink" @click="duplicateCurrentTierList">복사해서 내 티어표로 가져오기</button>
|
||||||
<button
|
<button
|
||||||
@@ -1466,6 +1511,7 @@ onUnmounted(() => {
|
|||||||
border: 1px solid var(--theme-border-strong);
|
border: 1px solid var(--theme-border-strong);
|
||||||
}
|
}
|
||||||
.previewOnly__drop {
|
.previewOnly__drop {
|
||||||
|
position: relative;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--theme-pill-bg);
|
background: var(--theme-pill-bg);
|
||||||
border: 1px solid var(--theme-border);
|
border: 1px solid var(--theme-border);
|
||||||
@@ -1476,6 +1522,10 @@ onUnmounted(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
}
|
}
|
||||||
|
.previewOnly__columnBadge,
|
||||||
|
.row__columnBadge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.previewOnly__cell {
|
.previewOnly__cell {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1502,6 +1552,15 @@ onUnmounted(() => {
|
|||||||
opacity: 0.52;
|
opacity: 0.52;
|
||||||
filter: grayscale(0.22) brightness(0.78);
|
filter: grayscale(0.22) brightness(0.78);
|
||||||
}
|
}
|
||||||
|
.previewOnly__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 8px;
|
||||||
|
color: var(--theme-text-soft);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.toggleSwitch {
|
.toggleSwitch {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2282,6 +2341,9 @@ onUnmounted(() => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--theme-text-muted);
|
color: var(--theme-text-muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2293,6 +2355,10 @@ onUnmounted(() => {
|
|||||||
.editorSidebar__utilityLink--danger {
|
.editorSidebar__utilityLink--danger {
|
||||||
color: rgba(248, 113, 113, 0.96);
|
color: rgba(248, 113, 113, 0.96);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editorSidebar__utilityLink--share {
|
||||||
|
color: var(--theme-text-soft);
|
||||||
|
}
|
||||||
.sidebar__title {
|
.sidebar__title {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -2434,8 +2500,45 @@ onUnmounted(() => {
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
}
|
}
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.previewOnly__row {
|
.previewOnly__row,
|
||||||
grid-template-columns: 140px 1fr;
|
.row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.previewOnly__columns,
|
||||||
|
.boardColumnsHeader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.previewOnly__columnsSpacer,
|
||||||
|
.boardColumnsHeader__spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.previewOnly__dropGrid,
|
||||||
|
.boardColumnsHeader__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.previewOnly__drop,
|
||||||
|
.row__drop {
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
.previewOnly__columnBadge,
|
||||||
|
.row__columnBadge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: calc(100% - 20px);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: var(--theme-text-soft);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.heroCard {
|
.heroCard {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -2446,9 +2549,6 @@ onUnmounted(() => {
|
|||||||
.row__content {
|
.row__content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.row {
|
|
||||||
grid-template-columns: 150px 1fr;
|
|
||||||
}
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
@@ -2477,20 +2577,6 @@ onUnmounted(() => {
|
|||||||
.previewOnly {
|
.previewOnly {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
.previewOnly__columns,
|
|
||||||
.previewOnly__row,
|
|
||||||
.boardColumnsHeader,
|
|
||||||
.row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.previewOnly__columnsSpacer,
|
|
||||||
.boardColumnsHeader__spacer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.previewOnly__dropGrid,
|
|
||||||
.boardColumnsHeader__grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.pool {
|
.pool {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user