diff --git a/apps/web/src/api/pages/valanse/trendingVoteApi.ts b/apps/web/src/api/pages/valanse/trendingVoteApi.ts index 7b1f3b2..aa045e4 100644 --- a/apps/web/src/api/pages/valanse/trendingVoteApi.ts +++ b/apps/web/src/api/pages/valanse/trendingVoteApi.ts @@ -1,27 +1,55 @@ import { isAxiosError } from 'axios' import { publicApi } from '../../instance/publicApi' -import { PinType } from '@/types/balanse/vote' -export type TrendingVoteResponse = { +/** trending 응답 내 선택지 */ +export type TrendingVoteOption = { + optionId: number + content: string + imageUrl: string | null + vote_count: number +} + +/** 기간별 인기 급상승 투표 1건 (최대 5위) */ +export type TrendingVoteItem = { + /** 노출 순위 (1부터) */ + displayOrder: number + /** PINNED: 관리자 고정 · RANKED: 반응성 순위 */ + displayType: 'PINNED' | 'RANKED' voteId: number title: string content: string category: string + reactivityScore: number + voteReactionCount: number + commentReactionCount: number totalParticipants: number createdBy: string creatorTitle: string | null createdAt: string - pinType: PinType - options: { - optionId: number - content: string - vote_count: number - }[] + options: TrendingVoteOption[] +} + +/** GET /votes/trending 응답 */ +export type TrendingVotesResponse = { + requestedDays: number + from: string + to: string + /** PERIOD: 기간 내 반응 기준 · ALL_TIME: 전체 누적 폴백 */ + scoreType: 'PERIOD' | 'ALL_TIME' + /** 기간 내 반응이 없어 전체 누적으로 폴백됐는지 */ + fallbackApplied: boolean + votes: TrendingVoteItem[] } -export async function fetchTrendingVotes() { +/** + * 기간별 인기 급상승 밸런스 게임 조회 (최대 5개). + * @param days 조회 기간(일). hotissue/trending 통합 API의 필수 파라미터. + */ +export async function fetchTrendingVotes(days: number) { try { - const res = await publicApi.get('/votes/trending') + const res = await publicApi.get('/votes/trending', { + params: { days }, + }) return res.data } catch (error) { if (isAxiosError(error) && error.response?.status === 404) { diff --git a/apps/web/src/components/pages/balanse/balansePage.tsx b/apps/web/src/components/pages/balanse/balansePage.tsx index 5866449..b46bae3 100644 --- a/apps/web/src/components/pages/balanse/balansePage.tsx +++ b/apps/web/src/components/pages/balanse/balansePage.tsx @@ -9,6 +9,7 @@ import { fetchVotes } from '@/api/pages/valanse/balanseListapi' import type { Vote } from '@/types/balanse/vote' import { useReportedContent } from '@/hooks/utils/useReportedContent' import BalanseVoteCard from './balanseVoteCard' +import HotTrendingBar from './hotTrendingBar' import { CATEGORIES } from '@/constants/category' const TABS = [ @@ -106,6 +107,8 @@ function BalancePageContent() { ))} + +
{error && (

diff --git a/apps/web/src/components/pages/balanse/hotTrendingBar.tsx b/apps/web/src/components/pages/balanse/hotTrendingBar.tsx new file mode 100644 index 0000000..3363f13 --- /dev/null +++ b/apps/web/src/components/pages/balanse/hotTrendingBar.tsx @@ -0,0 +1,85 @@ +'use client' + +import { useEffect, useState } from 'react' +import Link from 'next/link' +import { Icon } from '@iconify/react' +import { cn } from '@/lib/utils' +import { Chip } from '@/components/ui/chip' +import { + fetchTrendingVotes, + type TrendingVoteItem, +} from '@/api/pages/valanse/trendingVoteApi' +import { useReportedContent } from '@/hooks/utils/useReportedContent' + +/** 조회 기간(일) — hotissue/trending 통합 API의 days 파라미터 */ +const TRENDING_DAYS = 7 + +/** + * 밸런스 게임 리스트 상단 HOT(인기 급상승) 드롭다운. Figma 노드 6651:12395 참조. + * 접힘: 1위 제목만 노출 · 펼침: 1~5위 랭킹(각 항목 클릭 시 상세로 이동). + */ +export default function HotTrendingBar() { + const [votes, setVotes] = useState([]) + const [open, setOpen] = useState(false) + const { isReported } = useReportedContent() + + useEffect(() => { + fetchTrendingVotes(TRENDING_DAYS) + .then((res) => setVotes(res?.votes ?? [])) + .catch(() => {}) + }, []) + + // 내가 신고한 투표는 관리자 처리 전까지 랭킹에서도 감춘다 + const visibleVotes = votes.filter((v) => !isReported('VOTE', v.voteId)) + + if (visibleVotes.length === 0) return null + + const [top] = visibleVotes + + return ( +

+ + + {open && ( +
    + {visibleVotes.map((vote, index) => ( +
  • + + + {index + 1} + + + {vote.title} + + +
  • + ))} +
+ )} +
+ ) +} diff --git a/apps/web/src/components/pages/main/homeVoteCard.tsx b/apps/web/src/components/pages/main/homeVoteCard.tsx index 9c51d37..d21c80e 100644 --- a/apps/web/src/components/pages/main/homeVoteCard.tsx +++ b/apps/web/src/components/pages/main/homeVoteCard.tsx @@ -4,10 +4,10 @@ import { useState } from 'react' import Link from 'next/link' import { useVoteAction } from '@/hooks/utils/useVoteAction' import { cn } from '@/lib/utils' -import type { TrendingVoteResponse } from '@/api/pages/valanse/trendingVoteApi' +import type { TrendingVoteItem } from '@/api/pages/valanse/trendingVoteApi' interface Props { - data: TrendingVoteResponse + data: TrendingVoteItem } /** diff --git a/apps/web/src/components/pages/main/mainPage.tsx b/apps/web/src/components/pages/main/mainPage.tsx index b9c6c56..eb7f131 100644 --- a/apps/web/src/components/pages/main/mainPage.tsx +++ b/apps/web/src/components/pages/main/mainPage.tsx @@ -12,7 +12,7 @@ import HorizontalScroll from '@/components/_shared/horizontalScroll' import BalanseVoteCard from '@/components/pages/balanse/balanseVoteCard' import { fetchTrendingVotes, - type TrendingVoteResponse, + type TrendingVoteItem, } from '@/api/pages/valanse/trendingVoteApi' import { fetchVotes } from '@/api/pages/valanse/balanseListapi' import type { Vote } from '@/types/balanse/vote' @@ -21,7 +21,7 @@ import HomeVoteCard from './homeVoteCard' import { CATEGORIES } from '@/constants/category' const MainPage = () => { - const [featured, setFeatured] = useState(null) + const [featured, setFeatured] = useState(null) const [latest, setLatest] = useState([]) const { isReported } = useReportedContent() @@ -31,8 +31,8 @@ const MainPage = () => { const visibleLatest = latest.filter((v) => !isReported('VOTE', v.id)) useEffect(() => { - fetchTrendingVotes() - .then(setFeatured) + fetchTrendingVotes(7) + .then((res) => setFeatured(res?.votes[0] ?? null)) .catch(() => {}) fetchVotes({ category: 'ALL', sort: 'latest', size: 3 }) .then((data) => setLatest(data.votes)) diff --git a/docs/screens/SCR-BALANSE-001_balanse.md b/docs/screens/SCR-BALANSE-001_balanse.md index e6613a6..c10dcc5 100644 --- a/docs/screens/SCR-BALANSE-001_balanse.md +++ b/docs/screens/SCR-BALANSE-001_balanse.md @@ -9,6 +9,7 @@ | 경로 | `/balanse` | | 인증 필요 | ✗ | | 작성일 | 2026-05-16 | +| 최종 수정일 | 2026-08-27 | ## 🎯 화면 목적 @@ -23,20 +24,25 @@ ## 📐 레이아웃 구성 1. **헤더** -2. **인기 급상승 섹션** (SectionHeader + MockPollCard 슬라이드) -3. **카테고리/정렬 필터 탭 (FilterTabs)** +2. **카테고리/정렬 필터 탭 (FilterTabs)** +3. **HOT 인기 급상승 드롭다운 (HotTrendingBar)** — 필터 탭 바로 아래. 접힘 시 1위 제목만, 펼침 시 1~5위 랭킹 노출 4. **전체 목록 (BalanceList)** — 무한 스크롤 5. **하단 네비게이션 바** ## 🧩 섹션별 상세 명세 -### 1. 인기 급상승 (Trending) +### 1. HOT 인기 급상승 드롭다운 (HotTrendingBar) -**표시 데이터**: 상위 N개 토픽 +**표시 데이터**: 통합 트렌딩 API의 상위 1~5위 (`votes[]`). 각 항목은 순위(`displayOrder`)·`displayType`(PINNED 관리자 고정 / RANKED 반응성 순위)·제목을 가진다. -**사용 API**: `fetchTrendingVotes` +- **접힘 상태**: `[HOT]` 칩 + 1위(`votes[0]`) 제목 + 펼침 화살표(▼) +- **펼침 상태**: 1~5위 순위 리스트 -**사용자 액션**: 카드 클릭 → `/poll/[id]` +**사용 API**: `fetchTrendingVotes(7)` → `GET /votes/trending?days=7` (hotissue/trending 통합 API, 기간 7일) + +**사용자 액션**: 바 클릭 → 펼침/접힘 토글 · 랭킹 항목 클릭 → `/poll/[voteId]` + +**상태별 처리**: 노출 가능한 `votes`가 비면 바 자체를 렌더하지 않음 ### 2. FilterTabs @@ -81,13 +87,14 @@ | 상황 | 처리 | |---|---| | 내가 신고한 투표 | 신고자 화면에서만 목록에서 제외. 제외 후 비면 "해당 카테고리의 밸런스게임이 아직 없어요" 노출 (로컬 저장 기준, 기기별) | +| 내가 신고한 투표가 HOT 랭킹에 포함 | HotTrendingBar 랭킹에서도 제외. 전부 제외되면 바를 렌더하지 않음 | ## 🔌 사용 API | Method | Endpoint | 용도 | |---|---|---| | GET | `/votes` (필터 파라미터) | 목록 | -| GET | (`fetchTrendingVotes`) | 인기 급상승 | +| GET | `/votes/trending?days=7` (`fetchTrendingVotes`) | HOT 인기 급상승 1~5위 | | PUT | `/votes/{id}/pin` | 핀 고정 (관리자) | ## 📎 관련 문서