팔로우 피드 보기 전환 추가
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-07 v1.1.13
|
||||||
|
- 팔로우 피드도 본질적으로는 “공개 티어표 목록 화면”이므로, 홈/템플릿/즐겨찾기와 같은 `viewToggle` 문법을 공유하는 편이 맞다고 정리했다.
|
||||||
|
|
||||||
## 2026-04-07 v1.1.12
|
## 2026-04-07 v1.1.12
|
||||||
- `viewToggle`은 특정 주제 화면에만 남겨둘 기능이 아니라, 카드형/리스트형을 공통 문법으로 쓰는 주요 목록 화면 전반에서 일관되게 제공하는 편이 맞다고 정리했다.
|
- `viewToggle`은 특정 주제 화면에만 남겨둘 기능이 아니라, 카드형/리스트형을 공통 문법으로 쓰는 주요 목록 화면 전반에서 일관되게 제공하는 편이 맞다고 정리했다.
|
||||||
- 현재 주요 목록 화면은 데이터 규모가 아직 크지 않아 전부 한 번에 조회하는 구조를 유지하되, 이후 공개 티어표와 즐겨찾기 수가 늘어나면 페이지네이션이나 점진 로딩을 후속 과제로 검토하기로 했다.
|
- 현재 주요 목록 화면은 데이터 규모가 아직 크지 않아 전부 한 번에 조회하는 구조를 유지하되, 이후 공개 티어표와 즐겨찾기 수가 늘어나면 페이지네이션이나 점진 로딩을 후속 과제로 검토하기로 했다.
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
## `/following`
|
## `/following`
|
||||||
- 화면 파일: `frontend/src/views/FollowingFeedView.vue`
|
- 화면 파일: `frontend/src/views/FollowingFeedView.vue`
|
||||||
- 역할: 팔로우한 작성자의 공개 티어표를 최신 업데이트순 카드 목록으로 모아보기, 제목/주제/작성자 검색, 티어표 상세 이동, 작성자 프로필 이동
|
- 역할: 팔로우한 작성자의 공개 티어표를 최신 업데이트순 카드 목록으로 모아보기, 상단 공통 `viewToggle`로 카드형/리스트형 전환, 제목/주제/작성자 검색, 티어표 상세 이동, 작성자 프로필 이동
|
||||||
- 연동 API: `GET /api/users/following-feed`
|
- 연동 API: `GET /api/users/following-feed`
|
||||||
|
|
||||||
## `/users/:userId`
|
## `/users/:userId`
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
- 홈 피드(`/`)는 `GET /api/tierlists/public?q=...`를 사용한다.
|
- 홈 피드(`/`)는 `GET /api/tierlists/public?q=...`를 사용한다.
|
||||||
- `featuredTierLists`: 상단 추천 티어표
|
- `featuredTierLists`: 상단 추천 티어표
|
||||||
- `tierLists`: 추천 제외 최신 공개 티어표
|
- `tierLists`: 추천 제외 최신 공개 티어표
|
||||||
- 홈, 템플릿, 나의 티어표, 즐겨찾기 화면은 공통 `viewToggle`로 `그리드 / 리스트` 보기를 전환하며, 상태는 현재 라우트의 `?view=list` 쿼리로 반영한다.
|
- 홈, 템플릿, 나의 티어표, 즐겨찾기, 팔로우 피드 화면은 공통 `viewToggle`로 `그리드 / 리스트` 보기를 전환하며, 상태는 현재 라우트의 `?view=list` 쿼리로 반영한다.
|
||||||
- 위 네 화면의 목록 데이터는 현재 페이지네이션이나 무한 스크롤 없이 조회 결과 전체를 한 번에 렌더링한다.
|
- 위 네 화면의 목록 데이터는 현재 페이지네이션이나 무한 스크롤 없이 조회 결과 전체를 한 번에 렌더링한다.
|
||||||
- 저장된 티어표에는 댓글 스레드가 붙을 수 있다. 작성자 본인 편집 화면에서는 `작업 팁` 아래, 작성자가 아닌 사용자의 보기 전용 화면에서는 `preview` 보드 아래에서 같은 댓글 카드를 사용한다.
|
- 저장된 티어표에는 댓글 스레드가 붙을 수 있다. 작성자 본인 편집 화면에서는 `작업 팁` 아래, 작성자가 아닌 사용자의 보기 전용 화면에서는 `preview` 보드 아래에서 같은 댓글 카드를 사용한다.
|
||||||
- 댓글 알림 메뉴는 좌측 사이드 `댓글 관리`로 노출하며, 읽지 않은 댓글이 하나라도 있으면 빨간 dot을 표시한다.
|
- 댓글 알림 메뉴는 좌측 사이드 `댓글 관리`로 노출하며, 읽지 않은 댓글이 하나라도 있으면 빨간 dot을 표시한다.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# 할 일 및 이슈
|
# 할 일 및 이슈
|
||||||
|
|
||||||
## 단기 확인
|
## 단기 확인
|
||||||
|
- `v1.1.13` 이후 팔로우 피드에서도 공통 `viewToggle`이 보이고, 리스트형 보기에서 작성자 카드와 썸네일 정렬이 어색하지 않은지 확인한다.
|
||||||
- `v1.1.12` 이후 홈/템플릿/나의 티어표/즐겨찾기에서 공통 `viewToggle`이 모두 같은 위치/같은 동작으로 보이는지 확인한다.
|
- `v1.1.12` 이후 홈/템플릿/나의 티어표/즐겨찾기에서 공통 `viewToggle`이 모두 같은 위치/같은 동작으로 보이는지 확인한다.
|
||||||
- 리스트형 보기에서 홈/템플릿/나의 티어표/즐겨찾기 카드가 데스크톱과 모바일 모두에서 썸네일 비율과 제목 overflow 없이 안정적으로 보이는지 확인한다.
|
- 리스트형 보기에서 홈/템플릿/나의 티어표/즐겨찾기 카드가 데스크톱과 모바일 모두에서 썸네일 비율과 제목 overflow 없이 안정적으로 보이는지 확인한다.
|
||||||
- 내가 만든 저장 티어표도 즐겨찾기에 추가되고 `/favorites`에 나타나는지, 비공개 내 티어표를 즐겨찾기했을 때 접근/표시 규칙이 자연스러운지 확인한다.
|
- 내가 만든 저장 티어표도 즐겨찾기에 추가되고 `/favorites`에 나타나는지, 비공개 내 티어표를 즐겨찾기했을 때 접근/표시 규칙이 자연스러운지 확인한다.
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-07 v1.1.13
|
||||||
|
- 팔로우 피드도 다른 티어표 목록 화면과 같은 성격이므로, 공통 `viewToggle` 대상에 포함했다.
|
||||||
|
- `FollowingFeedView`에 그리드형/리스트형 보기 전환을 추가했다. 이제 팔로우한 작성자의 공개 티어표도 상단 공통 토글로 카드형과 가로 리스트형을 오갈 수 있다.
|
||||||
|
- 팔로우 피드 리스트형 레이아웃에서는 썸네일 좌측, 정보 우측 구조로 정리해 다른 목록 화면과 같은 문법을 따른다.
|
||||||
|
- 확인: `npm run build`
|
||||||
|
|
||||||
## 2026-04-07 v1.1.12
|
## 2026-04-07 v1.1.12
|
||||||
- 티어표 즐겨찾기 프런트 제한을 풀었다. 이제 내가 만든 저장된 티어표도 즐겨찾기에 넣을 수 있고, 같은 즐겨찾기 목록(`/favorites`)에서 다시 확인할 수 있다.
|
- 티어표 즐겨찾기 프런트 제한을 풀었다. 이제 내가 만든 저장된 티어표도 즐겨찾기에 넣을 수 있고, 같은 즐겨찾기 목록(`/favorites`)에서 다시 확인할 수 있다.
|
||||||
- 홈, 템플릿, 나의 티어표, 즐겨찾기 화면에 공통 `viewToggle`을 다시 연결했다. 기존처럼 카드형 그리드와 가로 리스트형 보기 전환을 URL `?view=list` 기준으로 같은 방식으로 유지한다.
|
- 홈, 템플릿, 나의 티어표, 즐겨찾기 화면에 공통 `viewToggle`을 다시 연결했다. 기존처럼 카드형 그리드와 가로 리스트형 보기 전환을 URL `?view=list` 기준으로 같은 방식으로 유지한다.
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ const leftRailCollapsed = ref(false)
|
|||||||
const mobileLeftNavOpen = ref(false)
|
const mobileLeftNavOpen = ref(false)
|
||||||
const rightRailOpen = ref(true)
|
const rightRailOpen = ref(true)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const leftRailSearchPlaceholder = computed(() => (route.name === 'templates' ? '주제 템플릿 검색' : '공개 티어표 검색'))
|
const leftRailSearchPlaceholder = computed(() => {
|
||||||
|
if (route.name === 'templates') return '주제 템플릿 검색'
|
||||||
|
if (route.name === 'topicHub') return '이 템플릿의 공개 티어표 검색'
|
||||||
|
if (route.name === 'followingFeed') return '팔로우한 사람의 공개 티어표 검색'
|
||||||
|
if (route.name === 'favorites') return '즐겨찾기한 티어표 검색'
|
||||||
|
if (route.name === 'me') return '내 티어표 검색'
|
||||||
|
return '공개 티어표 검색'
|
||||||
|
})
|
||||||
const isCollapsedSearchOpen = ref(false)
|
const isCollapsedSearchOpen = ref(false)
|
||||||
const isGuideModalOpen = ref(false)
|
const isGuideModalOpen = ref(false)
|
||||||
const themeMode = ref('dark')
|
const themeMode = ref('dark')
|
||||||
@@ -157,7 +164,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'].includes(String(route.name || '')))
|
const showTopicViewToggle = computed(() => ['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)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import { editorPath, loginPath, userProfilePath } from '../lib/paths'
|
import { editorPath, loginPath, userProfilePath } from '../lib/paths'
|
||||||
import { toApiUrl } from '../lib/runtime'
|
import { toApiUrl } from '../lib/runtime'
|
||||||
@@ -8,6 +8,7 @@ import { useToast } from '../composables/useToast'
|
|||||||
import { displayInitialFrom } from '../lib/display'
|
import { displayInitialFrom } from '../lib/display'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const tierLists = ref([])
|
const tierLists = ref([])
|
||||||
@@ -15,6 +16,7 @@ const query = ref('')
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const brokenThumbnailIds = ref({})
|
const brokenThumbnailIds = ref({})
|
||||||
|
const isListView = computed(() => route.query.view === 'list')
|
||||||
|
|
||||||
watch(error, (message) => {
|
watch(error, (message) => {
|
||||||
if (!message) return
|
if (!message) return
|
||||||
@@ -95,9 +97,9 @@ onMounted(loadFollowingFeed)
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div v-if="isLoading" class="empty">팔로우 피드를 불러오고 있어요.</div>
|
<div v-if="isLoading" class="empty">팔로우 피드를 불러오고 있어요.</div>
|
||||||
<div v-else-if="tierLists.length === 0" class="empty">아직 팔로우한 작성자의 공개 티어표가 없어요.</div>
|
<div v-else-if="tierLists.length === 0" class="empty">아직 팔로우한 작성자의 공개 티어표가 없어요.</div>
|
||||||
<div v-else class="list">
|
<div v-else class="list" :class="{ 'list--table': isListView }">
|
||||||
<article v-for="tierList in tierLists" :key="tierList.id" class="boardCard">
|
<article v-for="tierList in tierLists" :key="tierList.id" class="boardCard" :class="{ 'boardCard--list': isListView }">
|
||||||
<button class="boardCard__body" type="button" @click="openTierList(tierList)">
|
<button class="boardCard__body" :class="{ 'boardCard__body--list': isListView }" type="button" @click="openTierList(tierList)">
|
||||||
<div class="boardCard__thumbWrap">
|
<div class="boardCard__thumbWrap">
|
||||||
<img
|
<img
|
||||||
v-if="tierListThumbnailUrl(tierList)"
|
v-if="tierListThumbnailUrl(tierList)"
|
||||||
@@ -174,6 +176,9 @@ onMounted(loadFollowingFeed)
|
|||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
.list--table {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
.boardCard {
|
.boardCard {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
@@ -200,6 +205,19 @@ onMounted(loadFollowingFeed)
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.boardCard__body--list {
|
||||||
|
grid-template-columns: 200px minmax(0, 1fr);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.boardCard__body--list .boardCard__thumbWrap {
|
||||||
|
height: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.boardCard__body--list .boardCard__thumb,
|
||||||
|
.boardCard__body--list .boardCard__thumbPlaceholder {
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
.boardCard__thumbWrap {
|
.boardCard__thumbWrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -230,6 +248,11 @@ onMounted(loadFollowingFeed)
|
|||||||
padding: 16px 18px 0;
|
padding: 16px 18px 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.boardCard--list .boardCard__head {
|
||||||
|
align-content: center;
|
||||||
|
padding: 16px 18px 16px 0;
|
||||||
}
|
}
|
||||||
.boardCard__titleRow {
|
.boardCard__titleRow {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -321,6 +344,14 @@ onMounted(loadFollowingFeed)
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.boardCard__body--list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardCard--list .boardCard__head {
|
||||||
|
padding: 16px 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user