설정과 모바일 동선 정리
This commit is contained in:
@@ -10,4 +10,4 @@ SMTP_SECURE=true
|
|||||||
SMTP_USER=zenn.sori.studio@gmail.com
|
SMTP_USER=zenn.sori.studio@gmail.com
|
||||||
SMTP_PASS=kcasoehxcspqdoxz
|
SMTP_PASS=kcasoehxcspqdoxz
|
||||||
SMTP_FROM="Tier Maker <zenn.sori.studio@gmail.com>"
|
SMTP_FROM="Tier Maker <zenn.sori.studio@gmail.com>"
|
||||||
NICKNAME_CHANGE_INTERVAL_DAYS=20
|
NICKNAME_CHANGE_INTERVAL_DAYS=14
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000
|
|||||||
const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000
|
const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000
|
||||||
|
|
||||||
function resolveNicknameChangeIntervalMs() {
|
function resolveNicknameChangeIntervalMs() {
|
||||||
const rawMs = String(process.env.NICKNAME_CHANGE_INTERVAL_MS || '').trim()
|
const rawHours = String(process.env.NICKNAME_CHANGE_INTERVAL_HOURS || '').trim()
|
||||||
if (rawMs) {
|
if (rawHours) {
|
||||||
const parsed = Number(rawMs)
|
const parsed = Number(rawHours)
|
||||||
if (Number.isFinite(parsed) && parsed >= 0) return parsed
|
if (Number.isFinite(parsed) && parsed >= 0) return parsed * 60 * 60 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawDays = String(process.env.NICKNAME_CHANGE_INTERVAL_DAYS || '').trim()
|
const rawDays = String(process.env.NICKNAME_CHANGE_INTERVAL_DAYS || '').trim()
|
||||||
@@ -44,7 +44,7 @@ function resolveNicknameChangeIntervalMs() {
|
|||||||
if (Number.isFinite(parsed) && parsed >= 0) return parsed * 24 * 60 * 60 * 1000
|
if (Number.isFinite(parsed) && parsed >= 0) return parsed * 24 * 60 * 60 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
return 14 * 24 * 60 * 60 * 1000
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNicknameChangeIntervalLabel(intervalMs) {
|
function formatNicknameChangeIntervalLabel(intervalMs) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
- 닉네임 제한은 설정 본문에 계속 설명을 남기기보다, 버튼이 나타나는 조건과 모달 내부 안내로만 전달하는 편이 더 깔끔하다고 정리했다.
|
- 닉네임 제한은 설정 본문에 계속 설명을 남기기보다, 버튼이 나타나는 조건과 모달 내부 안내로만 전달하는 편이 더 깔끔하다고 정리했다.
|
||||||
- 이메일은 현재 시스템에서 개인 설정 수정 흐름보다 로그인 식별자 의미가 더 강하고, 인증/중복/세션 전환을 함께 다뤄야 하므로 일단 읽기 전용으로 분리해 두는 편이 맞다고 정리했다.
|
- 이메일은 현재 시스템에서 개인 설정 수정 흐름보다 로그인 식별자 의미가 더 강하고, 인증/중복/세션 전환을 함께 다뤄야 하므로 일단 읽기 전용으로 분리해 두는 편이 맞다고 정리했다.
|
||||||
- 환경변수 이름만 설명하는 것보다 실제 배포 파일에 샘플 값을 한 줄 남겨두는 편이 운영자 입장에서 훨씬 덜 헷갈리므로, `.env.production`에 20일 예시를 직접 두는 편이 낫다고 정리했다.
|
- 환경변수 이름만 설명하는 것보다 실제 배포 파일에 샘플 값을 한 줄 남겨두는 편이 운영자 입장에서 훨씬 덜 헷갈리므로, `.env.production`에 20일 예시를 직접 두는 편이 낫다고 정리했다.
|
||||||
|
- 모바일에서는 왼쪽 레일까지 상단에 고정해 둘 필요가 없고, 콘텐츠 영역을 넓히는 편이 더 중요하다고 판단했다. 그래서 `860px` 이하에서는 좌우 레일을 모두 오버레이로 띄우고, 목록 보기 전환 버튼도 모바일에서는 숨기는 쪽이 더 단순하다고 정리했다.
|
||||||
|
- 편집 화면 상단의 템플릿 제목은 같은 화면 안 스크롤보다 “이 주제의 다른 공개 티어표와 원본 템플릿으로 돌아가는 입구” 역할이 더 중요하다고 판단했다. 그래서 제목 클릭을 주제 허브 이동으로 바꾸되, 미저장 변경 보호는 기존 확인 모달을 재사용하는 편이 맞다고 정리했다.
|
||||||
|
|
||||||
## 2026-04-07 v1.1.17
|
## 2026-04-07 v1.1.17
|
||||||
- 가이드 모달은 같은 기능의 이동 수단을 중복으로 두기보다, 화살표와 점 네비게이션만 유지하는 편이 더 깔끔하다고 정리했다.
|
- 가이드 모달은 같은 기능의 이동 수단을 중복으로 두기보다, 화살표와 점 네비게이션만 유지하는 편이 더 깔끔하다고 정리했다.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
## `/editor/:topicId/new`, `/editor/:topicId/:tierListId`
|
## `/editor/:topicId/new`, `/editor/:topicId/:tierListId`
|
||||||
- 화면 파일: `frontend/src/views/TierEditorView.vue`
|
- 화면 파일: `frontend/src/views/TierEditorView.vue`
|
||||||
- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지하고 즐겨찾기 CTA도 함께 노출
|
- 역할: 주제 slug 기반 에디터 진입, 티어 그룹 편집, 티어 행 추가/삭제, 보드 옆 아이템 풀에서 관리자 아이템/커스텀 아이템 다중 드래그 앤 드롭 업로드, 아이템 클릭 선택 후 셀/풀 재배치, 아이템 우클릭 메뉴 기반 복제본 생성, 상단 템플릿 제목 클릭 시 해당 주제 허브로 이동, 공통 오른쪽 레일 안에 직접 배치되는 우측 편집 섹션에서 티어표 제목/설명/대표 썸네일/공개 여부/저장 제어와 커스텀 아이템 이름 정리, 읽기 전용 상태의 즐겨찾기 단독 CTA, PNG 다운로드, 저장된 티어표 기준 템플릿 등록/업데이트 요청, 댓글 카드 표시, `?preview=1` 진입 시 공통 앱 셸은 유지한 채 중앙 본문에서 완성본 프리뷰와 하단 댓글 카드를 렌더링하며, 우측 뷰어 카드(`공유 티어표 보기`)는 스폰서 카드 바로 아래에서 유지하고 즐겨찾기 CTA도 함께 노출
|
||||||
- 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/:id`, `GET /api/tierlists/:id/comments`, `POST /api/tierlists/:id/comments`, `DELETE /api/tierlists/:id/comments/:commentId`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`, `POST /api/tierlists/template-request`
|
- 연동 API: `GET /api/topics/:topicId`, `GET /api/tierlists/:id`, `GET /api/tierlists/:id/comments`, `POST /api/tierlists/:id/comments`, `DELETE /api/tierlists/:id/comments/:commentId`, `POST /api/tierlists/:id/favorite`, `DELETE /api/tierlists/:id/favorite`, `POST /api/tierlists/thumbnail`, `POST /api/tierlists/custom-items`, `POST /api/tierlists`, `POST /api/tierlists/template-request`
|
||||||
|
|
||||||
## `/comments`
|
## `/comments`
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
## 공통 레이아웃
|
## 공통 레이아웃
|
||||||
- 앱 셸 파일: `frontend/src/App.vue`
|
- 앱 셸 파일: `frontend/src/App.vue`
|
||||||
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, `홈 / 템플릿 / 댓글 관리` 네비게이션과 화면별 검색 placeholder 전환, `preview=1` 공유 프리뷰에서도 같은 좌우 레일과 중앙 헤더 유지, 로그인 상태 반영, 최근 즐겨찾기 바로가기와 전역 검색 입력, 댓글 알림 unread dot, 관리자 메뉴 노출 제어, 실제 SVG 에셋과 선형 SVG 아이콘이 혼합된 레일 UI, 전역 우측 상단 토스트 렌더링, 관리자/에디터 화면이 Teleport로 사용하는 `#local-right-rail-root` 대상 DOM을 상시 유지해 라우트 전환 중 우측 레일 언마운트 순서를 안정화, `S/ㄴ`, `G/ㅎ`, `L/ㅣ`, `A/ㅁ` 같은 전역 단축키 처리, 설정 가이드 모달 단계 이동/높이 안정화
|
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, `홈 / 템플릿 / 댓글 관리` 네비게이션과 화면별 검색 placeholder 전환, `preview=1` 공유 프리뷰에서도 같은 좌우 레일과 중앙 헤더 유지, 로그인 상태 반영, 최근 즐겨찾기 바로가기와 전역 검색 입력, 댓글 알림 unread dot, 관리자 메뉴 노출 제어, 실제 SVG 에셋과 선형 SVG 아이콘이 혼합된 레일 UI, 전역 우측 상단 토스트 렌더링, 관리자/에디터 화면이 Teleport로 사용하는 `#local-right-rail-root` 대상 DOM을 상시 유지해 라우트 전환 중 우측 레일 언마운트 순서를 안정화, `S/ㄴ`, `G/ㅎ`, `L/ㅣ`, `A/ㅁ` 같은 전역 단축키 처리, 설정 가이드 모달 단계 이동/높이 안정화
|
||||||
- 세부: 좌측 패널은 `248px` 기준 폭을 사용하되 축소 시 아이콘 중심의 좁은 레일로 전환하고, 우측 패널은 `320px` 기준 폭을 사용한다. 세 컬럼 모두 상단에 높이 `56px`의 헤더 블록을 유지한다. 중앙 헤더에는 고정 브랜드 `Tier Maker`와 서비스 설명이 표시되고, 우측 패널 토글은 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 아이콘만 표시된다. 좌우 레일의 주요 액션은 각각 하단 `56px` 푸터 안에서 항상 보이도록 유지하면서 아래쪽 패딩으로 여백을 확보한다.
|
- 세부: 좌측 패널은 `248px` 기준 폭을 사용하되 축소 시 아이콘 중심의 좁은 레일로 전환하고, 우측 패널은 `320px` 기준 폭을 사용한다. 모바일(`860px` 이하)에서는 좌우 패널 모두 오버레이로 뜨며, 중앙 헤더 오른쪽 버튼으로 각각 열고 닫는다. 중앙 헤더의 브랜드 `Tier Maker`는 홈(`/`)으로 이동하는 터치 타겟으로 유지한다. 좌우 레일의 주요 액션은 각각 하단 `56px` 푸터 안에서 항상 보이도록 유지하면서 아래쪽 패딩으로 여백을 확보한다.
|
||||||
|
|
||||||
## 백엔드 진입점
|
## 백엔드 진입점
|
||||||
- 서버 엔트리: `backend/index.js`
|
- 서버 엔트리: `backend/index.js`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- 프런트 앱 셸은 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 구조로 재정의되었고, `preview=1` 모드에서도 같은 셸을 유지한 채 중앙 본문만 완성본 프리뷰로 렌더링한다.
|
- 프런트 앱 셸은 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 구조로 재정의되었고, `preview=1` 모드에서도 같은 셸을 유지한 채 중앙 본문만 완성본 프리뷰로 렌더링한다.
|
||||||
- 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다.
|
- 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다.
|
||||||
- 좌측 패널은 필요 시 축소형 레일로 접을 수 있으며, 접힌 상태에서는 아이콘 중심 내비게이션과 축약된 바로가기만 유지한다.
|
- 좌측 패널은 필요 시 축소형 레일로 접을 수 있으며, 접힌 상태에서는 아이콘 중심 내비게이션과 축약된 바로가기만 유지한다.
|
||||||
|
- 모바일(`860px` 이하)에서는 좌측 패널도 고정 열을 차지하지 않고, 우측 패널과 같은 오버레이 방식으로 띄운다.
|
||||||
- 이 3단 셸 구조는 홈, 게임 허브, 에디터, 관리자 등 일반 페이지 전반의 공통 뼈대로 유지하고, 페이지별 차이는 중앙/우측에 어떤 콘텐츠를 넣는지만 달라지도록 관리한다.
|
- 이 3단 셸 구조는 홈, 게임 허브, 에디터, 관리자 등 일반 페이지 전반의 공통 뼈대로 유지하고, 페이지별 차이는 중앙/우측에 어떤 콘텐츠를 넣는지만 달라지도록 관리한다.
|
||||||
- 비로그인 상태의 로그인 유도는 좌측 하단 버튼으로만 노출하고, 좌측 상단 사용자 카드 영역에는 별도 게스트 안내 카드를 렌더링하지 않는다.
|
- 비로그인 상태의 로그인 유도는 좌측 하단 버튼으로만 노출하고, 좌측 상단 사용자 카드 영역에는 별도 게스트 안내 카드를 렌더링하지 않는다.
|
||||||
- 공통 셸의 좌측 내비, 우측 패널, 빠른 점프 버튼은 간단한 선형 SVG 아이콘과 두꺼운 카드형 버튼 문법을 공유한다.
|
- 공통 셸의 좌측 내비, 우측 패널, 빠른 점프 버튼은 간단한 선형 SVG 아이콘과 두꺼운 카드형 버튼 문법을 공유한다.
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
- `featuredTierLists`: 상단 추천 티어표
|
- `featuredTierLists`: 상단 추천 티어표
|
||||||
- `tierLists`: 추천 제외 최신 공개 티어표
|
- `tierLists`: 추천 제외 최신 공개 티어표
|
||||||
- 홈, 템플릿, 나의 티어표, 즐겨찾기, 팔로우 피드 화면은 공통 `viewToggle`로 `그리드 / 리스트` 보기를 전환하며, 상태는 현재 라우트의 `?view=list` 쿼리로 반영한다.
|
- 홈, 템플릿, 나의 티어표, 즐겨찾기, 팔로우 피드 화면은 공통 `viewToggle`로 `그리드 / 리스트` 보기를 전환하며, 상태는 현재 라우트의 `?view=list` 쿼리로 반영한다.
|
||||||
|
- 단, 모바일 브레이크포인트(`860px` 이하)에서는 `viewToggle`을 노출하지 않는다.
|
||||||
- 전역 단축키
|
- 전역 단축키
|
||||||
- `S/ㄴ`: 검색 포커스. 편집 화면에서는 아이템 검색창, 그 외 화면에서는 왼쪽 공통 검색창
|
- `S/ㄴ`: 검색 포커스. 편집 화면에서는 아이템 검색창, 그 외 화면에서는 왼쪽 공통 검색창
|
||||||
- `G/ㅎ`: 그리드 보기
|
- `G/ㅎ`: 그리드 보기
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
- 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다.
|
- 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다.
|
||||||
- 공통 토글 버튼은 패널이 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 각각 아이콘만 표시하는 방식으로 동작한다.
|
- 공통 토글 버튼은 패널이 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 각각 아이콘만 표시하는 방식으로 동작한다.
|
||||||
- 오른쪽 패널 토글은 열기/닫기 모두 `dock_to_left`, 왼쪽 패널 토글은 `dock_to_right` 아이콘으로 통일한다.
|
- 오른쪽 패널 토글은 열기/닫기 모두 `dock_to_left`, 왼쪽 패널 토글은 `dock_to_right` 아이콘으로 통일한다.
|
||||||
|
- 모바일에서는 중앙 `workspaceHead` 오른쪽에 좌/우 패널 버튼을 함께 두고, 브랜드 타이틀을 터치하면 홈(`/`)으로 이동한다.
|
||||||
- 좌우 레일의 주요 CTA는 스크롤되는 본문과 분리된 하단 `56px` 액션 영역에 배치한다.
|
- 좌우 레일의 주요 CTA는 스크롤되는 본문과 분리된 하단 `56px` 액션 영역에 배치한다.
|
||||||
- 하단 액션은 화면 바닥에 바로 붙지 않도록 푸터 내부에 추가 하단 여백을 둔다.
|
- 하단 액션은 화면 바닥에 바로 붙지 않도록 푸터 내부에 추가 하단 여백을 둔다.
|
||||||
- 홈 화면 기준 우측 패널은 임시 정보 카드 여러 개보다 핵심 CTA 하나만 남겨, 시안처럼 단순한 보조 레일 역할을 우선 유지한다.
|
- 홈 화면 기준 우측 패널은 임시 정보 카드 여러 개보다 핵심 CTA 하나만 남겨, 시안처럼 단순한 보조 레일 역할을 우선 유지한다.
|
||||||
@@ -91,6 +94,7 @@
|
|||||||
- 공통 `workspaceBody` 카드 컨테이너를 벗기고, 중앙 보드 영역은 메인 컬럼에, 우측 `320px` 편집 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다.
|
- 공통 `workspaceBody` 카드 컨테이너를 벗기고, 중앙 보드 영역은 메인 컬럼에, 우측 `320px` 편집 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다.
|
||||||
- 공통 상단 토글 버튼은 Teleport로 이동한 로컬 편집 패널의 접힘/펼침 상태와도 연결되어, 우측 패널을 숨기면 중앙 보드 영역이 확장된다.
|
- 공통 상단 토글 버튼은 Teleport로 이동한 로컬 편집 패널의 접힘/펼침 상태와도 연결되어, 우측 패널을 숨기면 중앙 보드 영역이 확장된다.
|
||||||
- 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다. 템플릿 등록/업데이트 요청 버튼은 저장된 티어표가 있을 때만 노출하며, 제목이 비어 있는 상태에서 저장하면 랜덤 고유 제목을 먼저 부여해 저장본을 만든다.
|
- 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다. 템플릿 등록/업데이트 요청 버튼은 저장된 티어표가 있을 때만 노출하며, 제목이 비어 있는 상태에서 저장하면 랜덤 고유 제목을 먼저 부여해 저장본을 만든다.
|
||||||
|
- 상단 템플릿 제목은 해당 주제 허브로 이동하는 액션으로 사용하며, 미저장 변경이 있으면 이동 전에 확인 모달을 띄운다.
|
||||||
- 보드 바로 옆에는 드래그용 아이템 풀을 별도 패널로 두고, 커스텀 아이템 이름 정리 목록은 우측 편집 패널 내부에서 관리한다.
|
- 보드 바로 옆에는 드래그용 아이템 풀을 별도 패널로 두고, 커스텀 아이템 이름 정리 목록은 우측 편집 패널 내부에서 관리한다.
|
||||||
- 관리자 화면
|
- 관리자 화면
|
||||||
- 공통 우측 패널 대신 전용 로컬 운영 패널을 사용한다.
|
- 공통 우측 패널 대신 전용 로컬 운영 패널을 사용한다.
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
- 닉네임 변경 제한 중일 때 설정 카드에서 아이콘이 완전히 사라지는 흐름이 사용성 측면에서 충분히 명확한지 실제 계정으로 확인한다.
|
- 닉네임 변경 제한 중일 때 설정 카드에서 아이콘이 완전히 사라지는 흐름이 사용성 측면에서 충분히 명확한지 실제 계정으로 확인한다.
|
||||||
- 이메일 카드의 읽기 전용 문구만으로도 “로그인용 계정 이메일은 여기서 바꾸지 않는다”는 점이 충분히 전달되는지 확인한다.
|
- 이메일 카드의 읽기 전용 문구만으로도 “로그인용 계정 이메일은 여기서 바꾸지 않는다”는 점이 충분히 전달되는지 확인한다.
|
||||||
- 운영 배포 환경에서 닉네임 변경 주기를 바꿀 때는 `.env.production`의 `NICKNAME_CHANGE_INTERVAL_DAYS` 값만 바꾸면 된다는 점을 배포 문서에도 추후 분리해둘지 검토한다.
|
- 운영 배포 환경에서 닉네임 변경 주기를 바꿀 때는 `.env.production`의 `NICKNAME_CHANGE_INTERVAL_DAYS` 값만 바꾸면 된다는 점을 배포 문서에도 추후 분리해둘지 검토한다.
|
||||||
|
- `860px` 이하 모바일 폭에서 좌우 레일이 모두 오버레이로 동작할 때, 헤더 버튼으로 열기/닫기와 바깥 영역 탭 닫기가 자연스러운지 확인한다.
|
||||||
|
- 모바일에서는 목록 화면 `viewToggle`을 숨기도록 바뀌었으니 홈/템플릿/나의 티어표/즐겨찾기/팔로우 피드에서 헤더 액션 영역이 더 여유롭게 보이는지 확인한다.
|
||||||
|
- 편집 화면 상단 템플릿 제목 클릭 시 해당 주제 허브로 이동하고, 미저장 변경이 있을 때는 `저장 없이 이동` 확인 모달이 먼저 뜨는지 확인한다.
|
||||||
- 오래전에 가입한 기존 계정은 `nickname_updated_at` 백필 후에도 바로 변경 가능하고, 최근 가입/최근 변경 계정은 정확히 14일 제한이 걸리는지 서버 기준으로 확인한다.
|
- 오래전에 가입한 기존 계정은 `nickname_updated_at` 백필 후에도 바로 변경 가능하고, 최근 가입/최근 변경 계정은 정확히 14일 제한이 걸리는지 서버 기준으로 확인한다.
|
||||||
- 비밀번호 변경이 요약 카드의 작은 액션으로만 열리더라도 접근성이 떨어지지 않는지, 모달 `Esc` 닫기와 포커스 이동이 자연스러운지 확인한다.
|
- 비밀번호 변경이 요약 카드의 작은 액션으로만 열리더라도 접근성이 떨어지지 않는지, 모달 `Esc` 닫기와 포커스 이동이 자연스러운지 확인한다.
|
||||||
- `v1.1.17` 이후 설정의 가이드 모달에서 페이지를 넘길 때 썸네일 영역 위치가 이전보다 안정적으로 유지되는지 확인한다.
|
- `v1.1.17` 이후 설정의 가이드 모달에서 페이지를 넘길 때 썸네일 영역 위치가 이전보다 안정적으로 유지되는지 확인한다.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
- 설정 가이드 마지막 단계에도 닉네임 변경 버튼은 제한 기간이 지나야 다시 나타난다는 안내를 추가했다.
|
- 설정 가이드 마지막 단계에도 닉네임 변경 버튼은 제한 기간이 지나야 다시 나타난다는 안내를 추가했다.
|
||||||
- 이메일 카드는 `현재 로그인에 사용하는 계정 이메일이며, 설정 화면에서는 변경할 수 없다`는 안내로 보정해 로그인 계정 이메일의 성격을 더 분명하게 드러냈다.
|
- 이메일 카드는 `현재 로그인에 사용하는 계정 이메일이며, 설정 화면에서는 변경할 수 없다`는 안내로 보정해 로그인 계정 이메일의 성격을 더 분명하게 드러냈다.
|
||||||
- 운영 환경에서 바로 이해할 수 있도록 `.env.production`에 `NICKNAME_CHANGE_INTERVAL_DAYS=20` 샘플 값을 추가했다. 닉네임 제한을 20일로 바꾸고 싶다면 이 값을 그대로 두고, 다른 기간을 원하면 숫자만 바꾸면 된다.
|
- 운영 환경에서 바로 이해할 수 있도록 `.env.production`에 `NICKNAME_CHANGE_INTERVAL_DAYS=20` 샘플 값을 추가했다. 닉네임 제한을 20일로 바꾸고 싶다면 이 값을 그대로 두고, 다른 기간을 원하면 숫자만 바꾸면 된다.
|
||||||
|
- 모바일 브레이크포인트(`860px` 이하)에서는 목록 화면의 `그리드/리스트` 전환 버튼을 숨기고, 왼쪽 레일도 오른쪽 레일처럼 오버레이 패널로 띄우도록 앱 셸 구조를 바꿨다. 모바일 상단 헤더 오른쪽에는 왼쪽 패널 열기와 오른쪽 패널 열기/닫기 버튼을 함께 배치한다.
|
||||||
|
- 티어표 편집 화면 상단의 템플릿 제목은 더 이상 본문 상단 스크롤 버튼이 아니다. 이제 제목을 누르면 해당 주제 템플릿 화면으로 이동하고, 편집 중 미저장 변경이 있으면 기존 `저장 없이 이동` 확인 모달을 먼저 보여준다.
|
||||||
- 확인: `node --check backend/src/db.js`, `node --check backend/src/routes/auth.js`, `npm run build`
|
- 확인: `node --check backend/src/db.js`, `node --check backend/src/routes/auth.js`, `npm run build`
|
||||||
|
|
||||||
## 2026-04-07 v1.1.17
|
## 2026-04-07 v1.1.17
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const isGuideNextDisabled = computed(() => guideStepIndex.value >= guideSteps.le
|
|||||||
const isLightTheme = computed(() => themeMode.value === 'light')
|
const isLightTheme = computed(() => themeMode.value === 'light')
|
||||||
const themeToggleLabel = computed(() => (isLightTheme.value ? '다크 모드' : '라이트 모드'))
|
const themeToggleLabel = computed(() => (isLightTheme.value ? '다크 모드' : '라이트 모드'))
|
||||||
const showSettingsThemePanel = computed(() => route.name === 'profile')
|
const showSettingsThemePanel = computed(() => route.name === 'profile')
|
||||||
const showTopicViewToggle = computed(() => ['home', 'templates', 'topicHub', 'me', 'favorites', 'followingFeed'].includes(String(route.name || '')))
|
const showTopicViewToggle = computed(() => !isMobileLayout.value && ['home', 'templates', 'topicHub', 'me', 'favorites', 'followingFeed'].includes(String(route.name || '')))
|
||||||
const topicViewMode = computed(() => (route.query.view === 'list' ? 'list' : 'grid'))
|
const topicViewMode = computed(() => (route.query.view === 'list' ? 'list' : 'grid'))
|
||||||
const showBackendFallback = computed(() => !isPreviewMode.value && ['maintenance', 'offline'].includes(backendState.value))
|
const showBackendFallback = computed(() => !isPreviewMode.value && ['maintenance', 'offline'].includes(backendState.value))
|
||||||
const shouldLockRightRailBodyScroll = computed(() => isRightRailOverlay.value && rightRailOpen.value && !showBackendFallback.value)
|
const shouldLockRightRailBodyScroll = computed(() => isRightRailOverlay.value && rightRailOpen.value && !showBackendFallback.value)
|
||||||
@@ -692,9 +692,11 @@ function reloadApp() {
|
|||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<aside class="leftRail">
|
<button v-if="isMobileLayout && mobileLeftNavOpen" class="leftRailBackdrop" type="button" aria-label="왼쪽 패널 닫기" @click="toggleLeftRail"></button>
|
||||||
|
|
||||||
|
<aside class="leftRail" :class="{ 'leftRail--overlay': isMobileLayout }" :aria-hidden="isMobileLayout && !mobileLeftNavOpen">
|
||||||
<div class="leftRail__top railHeader">
|
<div class="leftRail__top railHeader">
|
||||||
<button v-if="!isMobileLayout" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="왼쪽 패널 토글" @click="toggleLeftRail">
|
<button class="ghostIcon ghostIcon--iconOnly" type="button" :aria-label="isMobileLayout ? '왼쪽 패널 닫기' : '왼쪽 패널 토글'" @click="toggleLeftRail">
|
||||||
<SvgIcon :src="iconDockToRight" :size="24" />
|
<SvgIcon :src="iconDockToRight" :size="24" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,16 +711,6 @@ function reloadApp() {
|
|||||||
<div class="appUserCard__name">{{ accountName }}</div>
|
<div class="appUserCard__name">{{ accountName }}</div>
|
||||||
<div class="appUserCard__email" :class="{ 'appUserCard__email--hint': isAccountEmailHint }">{{ accountEmail }}</div>
|
<div class="appUserCard__email" :class="{ 'appUserCard__email--hint': isAccountEmailHint }">{{ accountEmail }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
v-if="isMobileLayout"
|
|
||||||
class="appUserCard__navToggle"
|
|
||||||
type="button"
|
|
||||||
:aria-label="mobileLeftNavOpen ? '네비게이션 메뉴 닫기' : '네비게이션 메뉴 열기'"
|
|
||||||
:aria-expanded="mobileLeftNavOpen"
|
|
||||||
@click="toggleLeftRail"
|
|
||||||
>
|
|
||||||
<SvgIcon :src="mobileLeftNavOpen ? iconDockToLeft : iconDockToRight" :size="24" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -794,9 +786,15 @@ function reloadApp() {
|
|||||||
<SvgIcon :src="iconLists" :size="24" />
|
<SvgIcon :src="iconLists" :size="24" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button v-if="isMobileLayout" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="왼쪽 패널 열기" @click="toggleLeftRail">
|
||||||
|
<SvgIcon :src="iconDockToRight" :size="24" />
|
||||||
|
</button>
|
||||||
<button v-if="!rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 열기" @click="toggleRightRail">
|
<button v-if="!rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 열기" @click="toggleRightRail">
|
||||||
<SvgIcon :src="iconDockToLeft" :size="24" />
|
<SvgIcon :src="iconDockToLeft" :size="24" />
|
||||||
</button>
|
</button>
|
||||||
|
<button v-else-if="isMobileLayout" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="오른쪽 패널 닫기" @click="toggleRightRail">
|
||||||
|
<SvgIcon :src="iconDockToLeft" :size="24" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="workspaceBody" :class="{ 'workspaceBody--localRail': usesLocalRightRail }">
|
<div class="workspaceBody" :class="{ 'workspaceBody--localRail': usesLocalRightRail }">
|
||||||
@@ -2151,10 +2149,10 @@ function reloadApp() {
|
|||||||
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr);
|
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightRailBackdrop {
|
.rightRailBackdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: block;
|
display: block;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
z-index: 29;
|
z-index: 29;
|
||||||
@@ -2263,19 +2261,49 @@ function reloadApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.leftRail {
|
.leftRail {
|
||||||
min-height: auto;
|
min-height: 100dvh;
|
||||||
height: auto;
|
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid var(--theme-border);
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftRailBackdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
border: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftRail--overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: min(340px, calc(100vw - 20px));
|
||||||
|
height: 100dvh;
|
||||||
|
z-index: 30;
|
||||||
|
background: var(--theme-shell-bg);
|
||||||
|
border-right: 1px solid var(--theme-border);
|
||||||
|
box-shadow: 18px 0 36px rgba(0, 0, 0, 0.34);
|
||||||
|
transition:
|
||||||
|
transform 220ms ease,
|
||||||
|
opacity 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appShell--mobileNavClosed .leftRail--overlay {
|
||||||
|
transform: translateX(calc(-100% - 24px));
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftRail__top {
|
.leftRail__top {
|
||||||
display: none;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftRail__body {
|
.leftRail__body {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px calc(18px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.appUserCard {
|
.appUserCard {
|
||||||
@@ -2290,10 +2318,6 @@ function reloadApp() {
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appUserCard__navToggle {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspaceHead .ghostIcon--iconOnly,
|
.workspaceHead .ghostIcon--iconOnly,
|
||||||
.rightRail__top .ghostIcon--iconOnly {
|
.rightRail__top .ghostIcon--iconOnly {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
@@ -2392,18 +2416,6 @@ function reloadApp() {
|
|||||||
margin: 14px 14px 0;
|
margin: 14px 14px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appShell--mobileNavClosed .leftRail__mobileMenu {
|
|
||||||
max-height: 0;
|
|
||||||
margin-top: -8px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-8px);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appShell--mobileNavClosed .leftRail__bottom {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightRail--overlay .rightRail__body {
|
.rightRail--overlay .rightRail__body {
|
||||||
padding: 14px 20px calc(32px + env(safe-area-inset-bottom));
|
padding: 14px 20px calc(32px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ watch(() => route.query.q, loadHomeFeed)
|
|||||||
<div class="pageHead__main">
|
<div class="pageHead__main">
|
||||||
<div class="pageHead__eyebrow">Feed</div>
|
<div class="pageHead__eyebrow">Feed</div>
|
||||||
<h1 class="pageHead__title">홈</h1>
|
<h1 class="pageHead__title">홈</h1>
|
||||||
<div class="pageHead__desc">사용자가 공개한 티어표를 최신순으로 살펴보고, 추천 티어표는 상단에서 바로 볼 수 있어요.</div>
|
<div class="pageHead__desc">다른 사용자들이 공개한 티어표를 살펴볼 수 있습니다.</div>
|
||||||
<div v-if="query" class="pageHead__searchState">"{{ query }}"에 맞는 공개 티어표만 보고 있어요.</div>
|
<div v-if="query" class="pageHead__searchState">"{{ query }}"에 맞는 공개 티어표만 보고 있어요.</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function openList(t) {
|
|||||||
<div class="pageHead__main">
|
<div class="pageHead__main">
|
||||||
<div class="pageHead__eyebrow">Tier Lists</div>
|
<div class="pageHead__eyebrow">Tier Lists</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>
|
||||||
|
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ async function logout() {
|
|||||||
<div class="pageHead__main">
|
<div class="pageHead__main">
|
||||||
<div class="pageHead__eyebrow">Account</div>
|
<div class="pageHead__eyebrow">Account</div>
|
||||||
<h2 class="pageHead__title">설정</h2>
|
<h2 class="pageHead__title">설정</h2>
|
||||||
<div class="pageHead__desc">수시로 바꾸지 않는 정보는 요약해서 보여주고, 필요할 때만 모달로 열어 바꾸는 흐름으로 정리했어요.</div>
|
<div class="pageHead__desc">닉네임은 변경은 쿨타임이 있으니 신중하게 설정해주세요.</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -368,12 +368,10 @@ async function logout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settingsSummaryItem">
|
<div class="settingsSummaryItem">
|
||||||
<div class="settingsSummaryItem__label">이메일</div>
|
<div class="settingsSummaryItem__label">ID (이메일)</div>
|
||||||
<div class="settingsSummaryItem__valueRow">
|
<div class="settingsSummaryItem__valueRow">
|
||||||
<div class="settingsSummaryItem__value">{{ authEmail }}</div>
|
<div class="settingsSummaryItem__value">{{ authEmail }}</div>
|
||||||
<span class="settingsSummaryItem__status">읽기 전용</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="settingsSummaryItem__meta">현재 로그인에 사용하는 계정 이메일이며, 설정 화면에서는 변경할 수 없습니다.</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -381,7 +379,7 @@ async function logout() {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="settingsThemeCard">
|
<article class="settingsThemeCard settingsThemeCard--compact">
|
||||||
<div class="settingsThemeCard__eyebrow">Security</div>
|
<div class="settingsThemeCard__eyebrow">Security</div>
|
||||||
<div class="settingsThemeCard__title">보안 설정</div>
|
<div class="settingsThemeCard__title">보안 설정</div>
|
||||||
<div class="settingsCompactRow">
|
<div class="settingsCompactRow">
|
||||||
@@ -395,7 +393,7 @@ async function logout() {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="settingsThemeCard">
|
<article class="settingsThemeCard settingsThemeCard--compact">
|
||||||
<div class="settingsThemeCard__eyebrow">Session</div>
|
<div class="settingsThemeCard__eyebrow">Session</div>
|
||||||
<div class="settingsThemeCard__title">계정 상태</div>
|
<div class="settingsThemeCard__title">계정 상태</div>
|
||||||
<div class="settingsCompactList">
|
<div class="settingsCompactList">
|
||||||
@@ -537,6 +535,10 @@ async function logout() {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settingsThemeCard--compact {
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.settingsThemeCard__eyebrow {
|
.settingsThemeCard__eyebrow {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
@@ -691,17 +693,6 @@ async function logout() {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsSummaryItem__status {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid var(--theme-border);
|
|
||||||
background: var(--theme-pill-bg);
|
|
||||||
color: var(--theme-text-soft);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsActionRow {
|
.settingsActionRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -712,6 +703,7 @@ async function logout() {
|
|||||||
.settingsCompactList {
|
.settingsCompactList {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsCompactRow {
|
.settingsCompactRow {
|
||||||
@@ -719,6 +711,7 @@ async function logout() {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-height: 100%;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
border: 1px solid var(--theme-border);
|
border: 1px solid var(--theme-border);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function templateThumbUrl(template) {
|
|||||||
<div class="pageHead__main">
|
<div class="pageHead__main">
|
||||||
<div class="pageHead__eyebrow">Topic</div>
|
<div class="pageHead__eyebrow">Topic</div>
|
||||||
<h1 class="pageHead__title">템플릿</h1>
|
<h1 class="pageHead__title">템플릿</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>
|
||||||
|
|||||||
@@ -367,11 +367,6 @@ function closeItemContextMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollWorkspaceBodyToTop() {
|
|
||||||
const workspaceBody = document.querySelector('.workspaceBody')
|
|
||||||
workspaceBody?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateEditorSidebarMaxHeight() {
|
function updateEditorSidebarMaxHeight() {
|
||||||
if (typeof window === 'undefined' || !sidebarEl.value) return
|
if (typeof window === 'undefined' || !sidebarEl.value) return
|
||||||
const bottomGap = 14
|
const bottomGap = 14
|
||||||
@@ -1104,6 +1099,11 @@ function confirmNavigationDiscard() {
|
|||||||
router.push(nextPath)
|
router.push(nextPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openTemplateTopic() {
|
||||||
|
if (!templateId.value) return
|
||||||
|
requestEditorNavigation(topicPath(templateId.value))
|
||||||
|
}
|
||||||
|
|
||||||
function openSourceTierList() {
|
function openSourceTierList() {
|
||||||
if (!sourceTierListId.value) return
|
if (!sourceTierListId.value) return
|
||||||
requestEditorNavigation(editorPath(templateId.value, sourceTierListId.value))
|
requestEditorNavigation(editorPath(templateId.value, sourceTierListId.value))
|
||||||
@@ -1535,7 +1535,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div v-if="isNavigationConfirmModalOpen" class="modalOverlay" @click.self="closeNavigationConfirmModal">
|
<div v-if="isNavigationConfirmModalOpen" class="modalOverlay" @click.self="closeNavigationConfirmModal">
|
||||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="navigationConfirmTitle">
|
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="navigationConfirmTitle">
|
||||||
<div id="navigationConfirmTitle" class="modalCard__title">원본 티어표로 이동</div>
|
<div id="navigationConfirmTitle" class="modalCard__title">다른 화면으로 이동</div>
|
||||||
<div class="modalCard__desc">
|
<div class="modalCard__desc">
|
||||||
아직 저장하지 않은 수정 내용이 있어요. 이대로 이동하면 현재 변경 내용은 사라집니다.
|
아직 저장하지 않은 수정 내용이 있어요. 이대로 이동하면 현재 변경 내용은 사라집니다.
|
||||||
</div>
|
</div>
|
||||||
@@ -1656,9 +1656,9 @@ onUnmounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="editorMain__title editorMain__titleButton"
|
class="editorMain__title editorMain__titleButton"
|
||||||
type="button"
|
type="button"
|
||||||
title="본문을 화면 위로 이동"
|
title="이 템플릿 화면으로 이동"
|
||||||
@click="scrollWorkspaceBodyToTop"
|
@click="openTemplateTopic"
|
||||||
@keydown.space.prevent="scrollWorkspaceBodyToTop"
|
@keydown.space.prevent="openTemplateTopic"
|
||||||
>
|
>
|
||||||
{{ templateName || templateId }}
|
{{ templateName || templateId }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user