Skip to content

Commit fec88cc

Browse files
authored
Merge pull request #127 from juuhye/feat/IN-287
feat(IN-287): 브랜드 협업 이력 분석 페이지(광고탭) 조회결과
2 parents aa206b0 + 4892016 commit fec88cc

20 files changed

Lines changed: 411 additions & 93 deletions

File tree

src/entities/influencerDetail/advertisementCard/model/types.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { ApiResponse } from '@/shared/api/types'
2+
import { axiosInstance } from '@/shared/api'
3+
import type {
4+
AdvertisementListParams,
5+
AdvertisementListResponseDto,
6+
} from '../model/types'
7+
8+
export async function fetchInfluencerBrand(
9+
channelId: string,
10+
params?: AdvertisementListParams
11+
): Promise<AdvertisementListResponseDto> {
12+
const response = await axiosInstance.get<
13+
ApiResponse<AdvertisementListResponseDto>
14+
>(`/brand-collaborations/channels/${channelId}`, { params })
15+
return response.data.responseDto
16+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { AdvertisementListResponseDto } from '../model/types'
2+
import mockImg from '@/shared/assets/mock/mockThumbnailImage.png'
3+
4+
export const mockAdvertisementList: AdvertisementListResponseDto = {
5+
content: [
6+
{
7+
videoId: '1',
8+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
9+
videoThumbnailUrl: mockImg.src,
10+
publishedAt: '2024-05-01T10:00:00Z',
11+
viewCount: 450000,
12+
likeCount: 12500,
13+
commentCount: 850,
14+
videoFormat: 'LONG_FORM',
15+
categoryName: '노하우/스타일',
16+
brands: ['메디큐브', 'APR'],
17+
},
18+
{
19+
videoId: '2',
20+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
21+
videoThumbnailUrl: mockImg.src,
22+
publishedAt: '2024-05-01T10:00:00Z',
23+
viewCount: 450000,
24+
likeCount: 12500,
25+
commentCount: 850,
26+
videoFormat: 'LONG_FORM',
27+
categoryName: '노하우/스타일',
28+
brands: ['메디큐브', 'APR'],
29+
},
30+
{
31+
videoId: '3',
32+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
33+
videoThumbnailUrl: mockImg.src,
34+
publishedAt: '2024-05-01T10:00:00Z',
35+
viewCount: 450000,
36+
likeCount: 12500,
37+
commentCount: 850,
38+
videoFormat: 'LONG_FORM',
39+
categoryName: '노하우/스타일',
40+
brands: ['메디큐브', 'APR'],
41+
},
42+
{
43+
videoId: '4',
44+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
45+
videoThumbnailUrl: mockImg.src,
46+
publishedAt: '2024-05-01T10:00:00Z',
47+
viewCount: 450000,
48+
likeCount: 12500,
49+
commentCount: 850,
50+
videoFormat: 'LONG_FORM',
51+
categoryName: '노하우/스타일',
52+
brands: ['메디큐브', 'APR'],
53+
},
54+
{
55+
videoId: '5',
56+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
57+
videoThumbnailUrl: mockImg.src,
58+
publishedAt: '2024-05-01T10:00:00Z',
59+
viewCount: 450000,
60+
likeCount: 12500,
61+
commentCount: 850,
62+
videoFormat: 'LONG_FORM',
63+
categoryName: '노하우/스타일',
64+
brands: ['메디큐브', 'APR'],
65+
},
66+
{
67+
videoId: '6',
68+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
69+
videoThumbnailUrl: mockImg.src,
70+
publishedAt: '2024-05-01T10:00:00Z',
71+
viewCount: 450000,
72+
likeCount: 12500,
73+
commentCount: 850,
74+
videoFormat: 'LONG_FORM',
75+
categoryName: '노하우/스타일',
76+
brands: ['메디큐브', 'APR'],
77+
},
78+
{
79+
videoId: '7',
80+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
81+
videoThumbnailUrl: mockImg.src,
82+
publishedAt: '2024-05-01T10:00:00Z',
83+
viewCount: 450000,
84+
likeCount: 12500,
85+
commentCount: 850,
86+
videoFormat: 'LONG_FORM',
87+
categoryName: '노하우/스타일',
88+
brands: ['메디큐브', 'APR'],
89+
},
90+
{
91+
videoId: '8',
92+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
93+
videoThumbnailUrl: mockImg.src,
94+
publishedAt: '2024-05-01T10:00:00Z',
95+
viewCount: 450000,
96+
likeCount: 12500,
97+
commentCount: 850,
98+
videoFormat: 'LONG_FORM',
99+
categoryName: '노하우/스타일',
100+
brands: ['메디큐브', 'APR'],
101+
},
102+
{
103+
videoId: '9',
104+
videoTitle: '[광고] 신상 앰플 리뷰 - 한 달 사용 후기',
105+
videoThumbnailUrl: mockImg.src,
106+
publishedAt: '2024-05-01T10:00:00Z',
107+
viewCount: 450000,
108+
likeCount: 12500,
109+
commentCount: 850,
110+
videoFormat: 'LONG_FORM',
111+
categoryName: '노하우/스타일',
112+
brands: ['메디큐브', 'APR'],
113+
},
114+
],
115+
pageInfo: {
116+
size: 9,
117+
numberOfElements: 9,
118+
nextCursor: '1',
119+
hasNext: false,
120+
},
121+
sort: {
122+
sorted: true,
123+
sortCriteria: 'LATEST',
124+
sortOrder: 'DESC',
125+
},
126+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { PageInfo } from '@/shared/api/types'
2+
3+
export type VideoFormat = 'ALL' | 'LONG_FORM' | 'SHORT_FORM'
4+
5+
export type SortCriteria = 'LATEST' | 'VIEW_COUNT' | 'LIKE_COUNT'
6+
7+
export type SortOrder = 'DESC'
8+
9+
export interface AdvertisementListParams {
10+
startDate?: string
11+
endDate?: string
12+
videoFormat?: VideoFormat
13+
categoryId?: string
14+
sortCriteria?: SortCriteria
15+
sortOrder?: SortOrder
16+
cursor?: string
17+
pageSize?: number
18+
}
19+
20+
export interface AdvertisementVideoItem {
21+
videoId: string
22+
videoTitle: string | null
23+
videoThumbnailUrl: string | null
24+
publishedAt: string | null
25+
viewCount: number
26+
likeCount: number
27+
commentCount: number
28+
videoFormat: Exclude<VideoFormat, 'ALL'>
29+
categoryName: string | null
30+
brands: string[]
31+
}
32+
33+
export interface SortInfo {
34+
sorted: boolean
35+
sortCriteria: SortCriteria
36+
sortOrder: SortOrder
37+
}
38+
39+
export interface AdvertisementListResponseDto {
40+
content: AdvertisementVideoItem[]
41+
pageInfo: PageInfo
42+
sort: SortInfo
43+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { useInfiniteScroll } from '@/shared/lib/hooks/useInfiniteScroll'
4+
import { fetchInfluencerBrand } from '../api/influencerBrandApi'
5+
import type { AdvertisementListParams } from './types'
6+
7+
type AdvertisementListFilter = Omit<AdvertisementListParams, 'cursor'>
8+
9+
const INFLUENCER_BRAND_QUERY_KEY = ['influencerBrand']
10+
11+
export function useInfluencerBrand(
12+
channelId: string,
13+
params?: AdvertisementListFilter
14+
) {
15+
return useInfiniteScroll({
16+
queryKey: [...INFLUENCER_BRAND_QUERY_KEY, channelId, params],
17+
queryFn: ({ pageParam }) =>
18+
fetchInfluencerBrand(channelId, {
19+
cursor: pageParam ?? undefined,
20+
...params,
21+
}),
22+
enabled: !!channelId,
23+
})
24+
}

src/entities/influencerDetail/advertisementCard/ui/AdvertisementCard.tsx renamed to src/features/influencerDetail/advertisementList/ui/AdvertisementCard.tsx

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
import Image from 'next/image'
2-
import { AdVideoCardItem } from '../model/types'
2+
import type { AdvertisementVideoItem } from '../model/types'
33
import { HashtagBox } from '@/shared/ui'
44
import {
55
format10Thousands,
66
formatThousands,
77
formatMonthAgo,
88
} from '@/shared/lib/format'
9-
import Eye from '@/shared/assets/eye-thin.svg'
10-
import Like from '@/shared/assets/like-thin.svg'
11-
import Comment from '@/shared/assets/comment-thin.svg'
12-
import Clock from '@/shared/assets/clock-thin.svg'
9+
import IconEye from '@/shared/assets/eye-thin.svg'
10+
import IconLike from '@/shared/assets/like-thin.svg'
11+
import IconComment from '@/shared/assets/comment-thin.svg'
12+
import IconClock from '@/shared/assets/clock-thin.svg'
1313

1414
export function AdvertisementCard({
15-
videoTitle,
16-
videoThumbnailUrl,
17-
publishedAt,
18-
viewCount,
19-
likeCount,
20-
commentCount,
21-
categoryName,
22-
brands,
23-
}: AdVideoCardItem) {
15+
video,
16+
}: {
17+
video: AdvertisementVideoItem
18+
}) {
19+
const {
20+
videoTitle,
21+
videoThumbnailUrl,
22+
publishedAt,
23+
viewCount,
24+
likeCount,
25+
commentCount,
26+
categoryName,
27+
brands,
28+
} = video
2429
return (
2530
<div className='h-fit overflow-hidden rounded-6 border border-stroke-border-gray-default'>
2631
{/* 카드 상단 (이미지 + AD 뱃지) */}
2732
<div className='relative min-h-[29.2rem]'>
2833
<Image
29-
src={videoThumbnailUrl}
30-
alt={videoTitle}
34+
src={videoThumbnailUrl ?? ''}
35+
alt={videoTitle ?? ''}
3136
fill
3237
className='object-cover'
3338
/>
@@ -40,7 +45,7 @@ export function AdvertisementCard({
4045
<div className='flex flex-col gap-8'>
4146
{/* 해시태그 */}
4247
<div>
43-
<HashtagBox label={categoryName} />
48+
<HashtagBox label={categoryName ?? ''} />
4449
</div>
4550
{/* 영상 타이틀 */}
4651
<p className='line-clamp-2 text-noto-title-sm-normal text-text-and-icon-default'>
@@ -49,16 +54,18 @@ export function AdvertisementCard({
4954
{/* 영상 정보 */}
5055
<div className='flex size-fit gap-12 py-2 text-noto-caption-md-normal text-text-and-icon-secondary'>
5156
<span className='flex size-fit items-center gap-4'>
52-
<Eye className='size-16' /> {format10Thousands(viewCount)}
57+
<IconEye className='size-16' /> {format10Thousands(viewCount)}
5358
</span>
5459
<span className='flex size-fit items-center gap-4'>
55-
<Like className='size-16' /> {format10Thousands(likeCount)}
60+
<IconLike className='size-16' /> {format10Thousands(likeCount)}
5661
</span>
5762
<span className='flex size-fit items-center gap-4'>
58-
<Comment className='size-16' /> {formatThousands(commentCount)}
63+
<IconComment className='size-16' />
64+
{formatThousands(commentCount)}
5965
</span>
6066
<span className='flex size-fit items-center gap-4'>
61-
<Clock className='size-16' /> {formatMonthAgo(publishedAt)}
67+
<IconClock className='size-16' />
68+
{formatMonthAgo(publishedAt ?? '')}
6269
</span>
6370
</div>
6471
</div>
Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,70 @@
1-
export function AdvertisementList() {
2-
return <div className='h-fit w-full'></div>
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { ContentType, TabGroup } from '@/shared/ui'
5+
import { InfiniteScrollList } from '@/shared/ui/infinite-scroll-list'
6+
import { AdvertisementCard } from './AdvertisementCard'
7+
import { useInfluencerBrand } from '../model/useInfluencerBrand'
8+
import type { VideoFormat, SortCriteria } from '../model/types'
9+
10+
const AD_FILTER_TABS = [
11+
{ id: 'ALL', label: '전체' },
12+
{ id: 'LONG_FORM', label: '롱폼' },
13+
{ id: 'SHORT_FORM', label: '숏폼' },
14+
] as const
15+
16+
const SORT_OPTIONS = [
17+
{ label: '최신순', filter: 'LATEST' },
18+
{ label: '조회수순', filter: 'VIEW_COUNT' },
19+
{ label: '좋아요순', filter: 'LIKE_COUNT' },
20+
] as const
21+
22+
type AdFilterTab = (typeof AD_FILTER_TABS)[number]['id']
23+
type SortOption = (typeof SORT_OPTIONS)[number]['filter']
24+
25+
export function AdvertisementList({ channelId }: { channelId: string }) {
26+
const [videoFormat, setVideoFormat] = useState<AdFilterTab>('ALL')
27+
const [sortCriteria, setSortCriteria] = useState<SortOption>('LATEST')
28+
29+
const { data, sentinelRef, isFetchingNextPage, hasNextPage } =
30+
useInfluencerBrand(channelId, {
31+
videoFormat: videoFormat as VideoFormat,
32+
sortCriteria: sortCriteria as SortCriteria,
33+
sortOrder: 'DESC',
34+
})
35+
36+
const videos = data?.pages.flatMap((page) => page.content) ?? []
37+
38+
return (
39+
<div className='flex h-fit w-full flex-col items-center p-24 pb-0'>
40+
{/* 컨텐츠 타입 탭 (전체/롱폼/숏폼) */}
41+
<TabGroup
42+
type='fit'
43+
tabs={AD_FILTER_TABS}
44+
activeTab={videoFormat}
45+
onTabChange={setVideoFormat}
46+
/>
47+
<div className='flex h-fit w-full flex-col gap-16'>
48+
{/* 정렬 (최신순/조회수순/좋아요순) */}
49+
<ContentType
50+
className='w-full justify-end'
51+
options={SORT_OPTIONS}
52+
filter={sortCriteria}
53+
onFilterChange={(value) => setSortCriteria(value as SortOption)}
54+
/>
55+
56+
{/* 광고 영상 카드 리스트 */}
57+
<InfiniteScrollList
58+
sentinelRef={sentinelRef}
59+
isFetchingNextPage={isFetchingNextPage}
60+
hasNextPage={hasNextPage}>
61+
<div className='grid h-fit w-full grid-cols-3 gap-24'>
62+
{videos.map((video) => (
63+
<AdvertisementCard key={video.videoId} video={video} />
64+
))}
65+
</div>
66+
</InfiniteScrollList>
67+
</div>
68+
</div>
69+
)
370
}

src/features/influencerDetail/tabGroup/index.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)