Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions apps/web/src/api/pages/valanse/trendingVoteApi.ts
Original file line number Diff line number Diff line change
@@ -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<TrendingVoteResponse>('/votes/trending')
const res = await publicApi.get<TrendingVotesResponse>('/votes/trending', {
params: { days },
})
return res.data
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/pages/balanse/balansePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -106,6 +107,8 @@ function BalancePageContent() {
))}
</TabBar>

<HotTrendingBar />

<div className="flex flex-col gap-3 px-4 pt-4">
{error && (
<p className="typo-body-b-01 py-8 text-center text-destructive">
Expand Down
85 changes: 85 additions & 0 deletions apps/web/src/components/pages/balanse/hotTrendingBar.tsx
Original file line number Diff line number Diff line change
@@ -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<TrendingVoteItem[]>([])
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 (
<div className="border-b-8 border-brand-gray-50 bg-card px-4">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
className="flex w-full items-center gap-2 py-3 text-left"
>
<Chip size="s" status="primary" className="shrink-0 bg-brand-violet-200">
HOT
</Chip>
<span className="typo-title-04 min-w-0 flex-1 truncate text-foreground">
{top.title}
</span>
<Icon
icon="mingcute:down-line"
width={24}
className={cn(
'shrink-0 text-brand-gray-100 transition-transform',
open && 'rotate-180',
)}
aria-hidden
/>
</button>

{open && (
<ul className="flex flex-col pb-2 duration-200 animate-in fade-in slide-in-from-top-1">
{visibleVotes.map((vote, index) => (
<li key={vote.voteId}>
<Link
href={`/poll/${vote.voteId}`}
className="flex items-center gap-3 rounded-lg px-1 py-2.5 hover:bg-brand-gray-50"
>
<span className="typo-label-01 w-5 shrink-0 text-center text-primary">
{index + 1}
</span>
<span className="typo-body-b-01 min-w-0 flex-1 truncate text-foreground">
{vote.title}
</span>
</Link>
</li>
))}
</ul>
)}
</div>
)
}
4 changes: 2 additions & 2 deletions apps/web/src/components/pages/main/homeVoteCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/pages/main/mainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,7 +21,7 @@ import HomeVoteCard from './homeVoteCard'
import { CATEGORIES } from '@/constants/category'

const MainPage = () => {
const [featured, setFeatured] = useState<TrendingVoteResponse | null>(null)
const [featured, setFeatured] = useState<TrendingVoteItem | null>(null)
const [latest, setLatest] = useState<Vote[]>([])
const { isReported } = useReportedContent()

Expand All @@ -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))
Expand Down
21 changes: 14 additions & 7 deletions docs/screens/SCR-BALANSE-001_balanse.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
| 경로 | `/balanse` |
| 인증 필요 | ✗ |
| 작성일 | 2026-05-16 |
| 최종 수정일 | 2026-08-27 |

## 🎯 화면 목적

Expand All @@ -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

Expand Down Expand Up @@ -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` | 핀 고정 (관리자) |

## 📎 관련 문서
Expand Down