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
2 changes: 1 addition & 1 deletion src/entities/channel/kpiCard/ui/KpiCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function KpiCard({ icon, label, prefix, value, unit }: KpiCardProps) {
)}
<span className='text-ibm-title-md-thin'>{label}</span>
</div>
<div className='h-fit w-full pl-[4.4rem] text-text-and-icon-default'>
<div className='h-fit w-full pl-[4.4rem] text-text-and-icon-primary'>
{prefix && <span className='text-ibm-heading-md-normal'>{prefix}</span>}
<span className='text-ibm-heading-md-normal'>{value}</span>
<span className='text-ibm-heading-md-normal'>{unit}</span>
Expand Down
14 changes: 14 additions & 0 deletions src/entities/channel/subscriberDistribution/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export {
mockSubscriberDistribution,
mockSubscriber,
} from './mock/mockSubscriberDistribution'
export type {
SubscriberRatioDto,
DistributionItem,
SubscriberDistributionsResponseDto,
DistributionsFilter,
} from './model/types'

export { DistributionChart } from './ui/DistributionChart'
export { GenderChart } from './ui/GenderChart'
export { SubscriberChart } from './ui/SubscriberChart'
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {
SubscriberDistributionsResponseDto,
SubscriberRatioDto,
} from '../model/types'

export const mockSubscriberDistribution: SubscriberDistributionsResponseDto = {
gender: [
{ label: '남성', percentage: 62.4 },
{ label: '여성', percentage: 37.6 },
],
age: [
{ label: '13-17', percentage: 70 },
{ label: '18-24', percentage: 20.1 },
{ label: '25-34', percentage: 7.9 },
{ label: '35-44', percentage: 2.5 },
{ label: '45-54', percentage: 1.2 },
{ label: '55-64', percentage: 1.2 },
{ label: '65+', percentage: 1.2 },
],
country: [
{ label: '대한민국', percentage: 70 },
{ label: '일본', percentage: 20.1 },
{ label: '미국', percentage: 7.9 },
{ label: '남아프리카 공화국', percentage: 2.5 },
{ label: '중앙 아프리카 공화국', percentage: 1.2 },
],
}

export const mockSubscriber: SubscriberRatioDto = {
count: 1240,
ratio: 62.5,
}
16 changes: 16 additions & 0 deletions src/entities/channel/subscriberDistribution/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface DistributionItem {
label: string
percentage: number
}

export interface SubscriberDistributionsResponseDto {
gender: DistributionItem[]
age: DistributionItem[]
country: DistributionItem[]
}
export interface SubscriberRatioDto {
count: number
ratio: number
}

export type DistributionsFilter = 'countries' | 'ages' | 'genders'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { BaseBarChart } from '@/shared/ui/chart/barChart'
import { DistributionItem } from '../model/types'

export function DistributionChart({
data,
type,
}: {
data: DistributionItem[]
type: string
}) {
return (
<div className='flex gap-24 px-8 pb-20'>
{/* 순위 번호 + 항목 라벨 */}
<div className='flex w-[22.6rem] shrink-0 flex-col gap-32'>
{data.map((item, idx) => (
<div
className='flex h-24 w-full items-center gap-16 text-text-and-icon-primary'
key={item.label}>
<span className='text-noto-body-md-bold'>{idx + 1}</span>
<span className='text-noto-body-xs-bold'>
{item.label}
{type === 'ages'
? idx === data.length - 1
? '세 이상'
: '세'
: ''}
</span>
</div>
))}
</div>
{/* 가로 막대 차트 */}
<BaseBarChart data={data} />
{/* 항목별 (%) */}
<div className='flex w-[4.2rem] shrink-0 flex-col gap-32 text-right'>
{data.map((item) => (
<div
className='flex h-24 items-center justify-end text-noto-body-xs-normal text-text-and-icon-default'
key={item.label}>
{item.percentage}%
</div>
))}
</div>
</div>
)
}
38 changes: 38 additions & 0 deletions src/entities/channel/subscriberDistribution/ui/GenderChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client'

import { ChartLegend } from '@/features/channel/chartLegend'
import { DistributionItem } from '../model/types'
import { BasePieChart, type PieDataPoint } from '@/shared/ui/chart'

export function GenderChart({ data }: { data: DistributionItem[] }) {
const pieData: PieDataPoint[] = data.map((item, index) => ({
name: item.label,
value: item.percentage,
color: index === 0 ? 'bg-brand-primary' : 'bg-btn-primary-filled-disabled',
}))

return (
<div className='flex flex-col items-center gap-40'>
{/* 여성 / 남성 차트 */}
<BasePieChart<PieDataPoint>
data={pieData}
dataKey='value'
nameKey='name'
tooltipFormatter={(value) => `${value}%`}
/>

{/* 여성 / 남성 차트 범례 */}
<div className='flex flex-col gap-12'>
{pieData.map((item) => (
<ChartLegend
key={item.name}
value={item.value}
label={item.name}
variant={item.color}
unit='%'
/>
))}
</div>
</div>
)
}
45 changes: 45 additions & 0 deletions src/entities/channel/subscriberDistribution/ui/SubscriberChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import { ChartLegend } from '@/features/channel/chartLegend'
import { SubscriberRatioDto } from '../model/types'
import { BasePieChart, type PieDataPoint } from '@/shared/ui/chart'

export function SubscriberChart({ data }: { data: SubscriberRatioDto }) {
const { ratio } = data

const pieData: PieDataPoint[] = [
{
name: '기존 구독자',
value: 100 - ratio,
color: 'bg-brand-primary',
},
{
name: '신규 구독자',
value: ratio,
color: 'bg-btn-primary-filled-disabled',
},
]
return (
<div className='flex flex-col items-center gap-40'>
{/* 기존 / 신규 구독자 차트 */}
<BasePieChart<PieDataPoint>
data={pieData}
dataKey='value'
nameKey='name'
tooltipFormatter={(value) => `${value}%`}
/>
{/* 기존 / 신규 구독자 차트 범례 */}
<div className='flex flex-col gap-12'>
{pieData.map((item) => (
<ChartLegend
key={item.name}
value={item.value}
label={item.name}
variant={item.color}
unit='%'
/>
))}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { ChartLegend } from '@/features/channel/chartLegend'
import type { TypeEngagementSummaryDto } from '../model/types'
import { BasePieChart, type PieDataPoint } from '@/shared/ui/chart'

export function TypeEngagementChart({ data }: { data: TypeEngagementSummaryDto }) {
export function TypeEngagementChart({
data,
}: {
data: TypeEngagementSummaryDto
}) {
const { longFormEngagementRate, shortFormEngagementRate } = data

const pieData: PieDataPoint[] = [
Expand Down Expand Up @@ -37,6 +41,7 @@ export function TypeEngagementChart({ data }: { data: TypeEngagementSummaryDto }
value={item.value}
label={item.name}
variant={item.color}
unit='%'
/>
))}
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/features/channel/chartLegend/ui/ChartLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ interface ChartLegendProps {
label: string
value: number | string
variant?: string
unit?: string
}

export function ChartLegend({ label, value, variant }: ChartLegendProps) {
export function ChartLegend({ label, value, variant, unit }: ChartLegendProps) {
return (
<div className='flex items-center gap-10'>
<div className='p-2'>
<div className={cn('h-12 w-12 rounded-full', variant)}></div>
</div>
<div className='flex items-center gap-12 text-noto-body-xs-bold'>
<span>{label}</span>
<span>{value}</span>
<span>
{value}
{unit}
</span>
</div>
</div>
)
Expand Down
37 changes: 24 additions & 13 deletions src/features/channel/contentType/ui/ContentType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,37 @@

import { cn } from '@/shared/lib/utils'

const FILTER_OPTIONS = [
{ label: '롱폼', isShort: false },
{ label: '숏폼', isShort: true },
] as const
interface FilterOption<T> {
label: string
filter: T
}

interface Props {
isShort: boolean
onIsShortChange: (isShort: boolean) => void
interface Props<T> {
options: FilterOption<T>[]
filter: T
onFilterChange: (filter: T) => void
className?: string
}

export function ContentType({ isShort, onIsShortChange }: Props) {
export function ContentType<T>({
options,
filter,
onFilterChange,
className,
}: Props<T>) {
return (
<div className='flex h-full w-fit gap-8 text-noto-label-sm-normal text-text-and-icon-disabled'>
{FILTER_OPTIONS.map((option, index) => (
<div
className={cn(
'flex h-full w-fit gap-8 text-noto-label-sm-normal text-text-and-icon-disabled',
className
)}>
{options.map((option, index) => (
<button
key={option.label}
onClick={() => onIsShortChange(option.isShort)}
key={String(option.filter)}
onClick={() => onFilterChange(option.filter)}
className={cn(
'flex cursor-pointer items-center px-8 py-4',
isShort === option.isShort && 'text-text-and-icon-primary',
filter === option.filter && 'text-text-and-icon-primary',
index > 0 &&
'relative after:absolute after:top-1/2 after:-left-4 after:h-2 after:w-2 after:-translate-y-1/2 after:rounded-full after:bg-text-and-icon-disabled after:content-[""]'
)}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ApiResponse } from '@/shared/api/types'
import { axiosInstance } from '@/shared/api'
import {
SubscriberDistributionsResponseDto,
DistributionsFilter,
} from '@/entities/channel/subscriberDistribution'

export async function fetchSubscriberDistribution(
channelId: string,
filters: DistributionsFilter[]
): Promise<SubscriberDistributionsResponseDto> {
const response = await axiosInstance.get<
ApiResponse<SubscriberDistributionsResponseDto>
>(`/channels/${channelId}/subscriber-distribution`, {
params: { filter: filters.join(',') },
})

return response.data.responseDto
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiResponse } from '@/shared/api/types'
import { axiosInstance } from '@/shared/api'
import { SubscriberRatioDto } from '@/entities/channel/subscriberDistribution'

export async function fetchSubscriberChart(
channelId: string
): Promise<SubscriberRatioDto> {
const response = await axiosInstance.get<ApiResponse<SubscriberRatioDto>>(
`/channels/${channelId}/subscriber-pattern`
)

return response.data.responseDto
}
2 changes: 2 additions & 0 deletions src/features/channel/subscriberDistribution/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useSubscriberChart } from './model/useSubscriberChart'
export { useDistributionChart } from './model/useDistributionChart'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { fetchSubscriberDistribution } from '../api/DistributionChartApi'

type DistributionToggle = 'countries' | 'ages'

export function useDistributionChart(channelId: string) {
const [filter, setFilter] = useState<DistributionToggle>('countries')

const query = useQuery({
queryKey: ['distributionChart', channelId, filter],
queryFn: () => fetchSubscriberDistribution(channelId, ['genders', filter]),
enabled: !!channelId,
})

return { ...query, filter, setFilter }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query'
import { fetchSubscriberChart } from '../api/subscriberChartApi'

export function useSubscriberChart(channelId: string) {
return useQuery({
queryKey: ['subscriberChart', channelId],
queryFn: () => fetchSubscriberChart(channelId),
enabled: !!channelId,
})
}
2 changes: 2 additions & 0 deletions src/pages/channel/ui/ChannelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SubscriberGrowthSection } from '@/widgets/channel/SubscriberGrowth'
import { TrendingVideoSection } from '@/widgets/channel/trendingVideo'
import { NewInflowSection } from '@/widgets/channel/newInflow'
import { TypeEngagementSection } from '@/widgets/channel/typeEngagement'
import { SubscriberDemographicsSection } from '@/widgets/channel/subscriberDemographics'

export function ChannelPage() {
const { user } = useAuth()
Expand Down Expand Up @@ -37,6 +38,7 @@ export function ChannelPage() {
<div className='flex items-start gap-24'>
{/* 롱폼/숏폼 평균 참여율 TOP5 */}
<TypeEngagementSection channelId={id} />
<SubscriberDemographicsSection channelId={id} />
</div>
</div>
</div>
Expand Down
Loading
Loading