first commit
This commit is contained in:
552
app/components/Dashboard.tsx
Normal file
552
app/components/Dashboard.tsx
Normal file
@@ -0,0 +1,552 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||
import { DateRangeEnum } from "@/app/constants/dateRange";
|
||||
import { useSession, signIn } from "next-auth/react";
|
||||
|
||||
import CalenderSelector from "@/app/components/CalenderSelector";
|
||||
import SvgChannelFilter from "@/app/components/svgs/svgChannelFilter";
|
||||
import ChannelFilter from "@/app/components/ChannelFilter";
|
||||
|
||||
// App.tsx
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
// Chart.js 모듈 등록 (꼭 필요)
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
type ListChannelRawItem = {
|
||||
id: string;
|
||||
email?: string;
|
||||
handle: string;
|
||||
isApproved: boolean;
|
||||
createtime: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
type ListChannelItem = {
|
||||
id: string;
|
||||
email?: string;
|
||||
handle: string;
|
||||
createtime: string;
|
||||
is_approved: boolean;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export default function Page({user}: {user: any}) {
|
||||
const [myChannelList, setMyChannelList] = useState<ListChannelItem[]>([]);
|
||||
const [visibleChannelIds, setVisibleChannelIds] = useState<Set<string>>(new Set());
|
||||
const [channelsReady, setChannelsReady] = useState<boolean>(false);
|
||||
const fetchListChannel = useCallback(async (options?: {
|
||||
cache?: RequestCache;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<ListChannelItem[]> => {
|
||||
const response = await fetch('/api/list_channel', {
|
||||
cache: options?.cache ?? 'no-store',
|
||||
signal: options?.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = '';
|
||||
try {
|
||||
message = await response.text();
|
||||
} catch {}
|
||||
throw new Error(`list_channel 요청 실패: ${response.status} ${message}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { items: ListChannelRawItem[] } | unknown;
|
||||
const items = (data as { items: ListChannelRawItem[] })?.items ?? [];
|
||||
|
||||
return items.map((item) => ({
|
||||
id: item.id,
|
||||
email: item.email,
|
||||
handle: item.handle,
|
||||
createtime: item.createtime,
|
||||
is_approved: item.isApproved,
|
||||
icon: item.icon,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const [chanellist, setChanellist] = useState<string[]>([]);
|
||||
const [contentlist, setContentlist] = useState<{
|
||||
id: string;
|
||||
subject: string;
|
||||
pubDate: Date;
|
||||
views: number;
|
||||
validViews: number;
|
||||
premiumViews: number;
|
||||
watchTime: number;
|
||||
publisher?: string;
|
||||
}[]>([]);
|
||||
const [selectedChannels, setSelectedChannels] = useState<string[]>(chanellist);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [dateRange, setDateRange] = useState<{
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}>({
|
||||
startDate: new Date(),
|
||||
endDate: new Date()
|
||||
});
|
||||
|
||||
const [dateString, setDateString] = useState<string>("2025.03.01 ~ 2025.03.31");
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const [hasRun, setHasRun] = useState(false);
|
||||
|
||||
// 전역 상수는 공용 파일에서 import
|
||||
|
||||
const [globalDateRangeType, setGlobalDateRangeType] = useState<number>(DateRangeEnum.ALL);
|
||||
const [dateRangeBlock1, setDateRangeBlock1] = useState<number>(DateRangeEnum.ALL);
|
||||
const [dateRangeBlock2, setDateRangeBlock2] = useState<number>(DateRangeEnum.ALL);
|
||||
const [dateRangeBlock3, setDateRangeBlock3] = useState<number>(DateRangeEnum.ALL);
|
||||
const [dateRangeBlock4, setDateRangeBlock4] = useState<number>(DateRangeEnum.ALL);
|
||||
const [dateRangeGraph, setDateRangeGraph] = useState<number>(DateRangeEnum.ALL);
|
||||
|
||||
const [startDate, setStartDate] = useState<Date>(new Date(Date.now()-30*24*60*60*1000));
|
||||
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||
const [cpv, setCpv] = useState<number>(0);
|
||||
const [rows, setRows] = useState<Array<{
|
||||
id: string;
|
||||
subject: string;
|
||||
pubDate: string;
|
||||
views: number;
|
||||
premiumViews: number;
|
||||
watchTime: number;
|
||||
handle: string;
|
||||
handleId: string | null;
|
||||
icon: string;
|
||||
validViews: number;
|
||||
expectedRevenue: number;
|
||||
}>>([]);
|
||||
const [checkedIds, setCheckedIds] = useState<Set<string>>(new Set());
|
||||
const [sortKey, setSortKey] = useState<'handle'|'subject'|'pubDate'|'watchTime'|'views'|'premiumViews'|'validViews'|'expectedRevenue'>('pubDate');
|
||||
const [sortDir, setSortDir] = useState<'asc'|'desc'>('desc');
|
||||
const handleSort = useCallback((key: 'handle'|'subject'|'pubDate'|'watchTime'|'views'|'premiumViews'|'validViews'|'expectedRevenue') => {
|
||||
setSortDir(prev => (sortKey === key ? (prev === 'asc' ? 'desc' : 'asc') : 'desc'));
|
||||
setSortKey(key);
|
||||
}, [sortKey]);
|
||||
|
||||
const fetchMyContents = useCallback(async (s: Date, e: Date) => {
|
||||
const qs = `start=${encodeURIComponent(s.toISOString())}&end=${encodeURIComponent(e.toISOString())}`;
|
||||
const res = await fetch(`/api/my_contents?${qs}`, { cache: 'no-store' });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.error ?? 'LOAD_FAILED');
|
||||
const items = data.items ?? [];
|
||||
setCpv(data.cpv ?? 0);
|
||||
setRows(items);
|
||||
// 초기 로드 시 전체 선택
|
||||
setCheckedIds(new Set(items.map((it: any) => it.id)));
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const data = {
|
||||
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
|
||||
datasets: [
|
||||
{
|
||||
label: "",
|
||||
data: [120000, 150000, 200000, 190000, 230000],
|
||||
borderColor: "#F94B37", // 새로운 색상 적용
|
||||
backgroundColor: "rgba(249, 75, 55, 0.5)", // 배경색도 변경
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false, text: "정산 수익 추이" },
|
||||
},
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: true,
|
||||
},
|
||||
ticks: {
|
||||
stepSize: 20000,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4, // 곡선으로 변경
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasRun) {
|
||||
if (chartRef.current) {
|
||||
setHasRun(true);
|
||||
chartRef.current.style.width = '10px';
|
||||
setTimeout(() => {
|
||||
chartRef.current!.style.width = '100%';
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [hasRun, chartRef.current]);
|
||||
|
||||
const handleCheckboxChange = (channel: string) => {
|
||||
setSelectedChannels(prev =>
|
||||
prev.includes(channel)
|
||||
? prev.filter(c => c !== channel)
|
||||
: [...prev, channel]
|
||||
);
|
||||
};
|
||||
|
||||
const filteredContentList = contentlist.filter(content =>
|
||||
selectedChannels.length === 0 || (content.publisher && selectedChannels.includes(content.publisher))
|
||||
);
|
||||
|
||||
const formatNumberWithCommas = (number: number): string => {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
};
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// setLoading(true);
|
||||
// fetch('/api/channels')
|
||||
// .then(response => response.json())
|
||||
// .then(data => {
|
||||
// setChanellist(data);
|
||||
// setSelectedChannels(data); // 기본값으로 모든 채널 선택
|
||||
// })
|
||||
// .finally(() => setLoading(false));
|
||||
// }, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// setLoading(true);
|
||||
// fetch('/api/contents')
|
||||
// .then(response => response.json())
|
||||
// .then(data => {
|
||||
// const sortedData = data.sort((a: any, b: any) => b.views - a.views);
|
||||
// setContentlist(sortedData);
|
||||
// })
|
||||
// .finally(() => setLoading(false));
|
||||
// }, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
window.resizeTo(500, 500);
|
||||
setTimeout(() => {
|
||||
window.resizeTo(window.screen.availWidth, window.screen.availHeight);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
}, []);
|
||||
|
||||
// 내 채널 목록: 최초 1회만 호출
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const items = await fetchListChannel({ cache: 'no-store' });
|
||||
console.log('list_channel:', items);
|
||||
setMyChannelList(items);
|
||||
// 초기에는 전체 채널 가시
|
||||
setVisibleChannelIds(new Set(items.map(i => i.id)));
|
||||
setChannelsReady(true);
|
||||
} catch (e) {
|
||||
console.error('list_channel 요청 에러:', e);
|
||||
}
|
||||
})();
|
||||
}, [fetchListChannel]);
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <div className="spinner">Loading...</div>;
|
||||
}
|
||||
|
||||
// 빈 집합은 "아무 채널도 선택되지 않음"으로 간주하여 행을 숨깁니다
|
||||
const visibleRows = rows.filter(r => visibleChannelIds.size > 0 && r.handleId && visibleChannelIds.has(r.handleId));
|
||||
const sortedVisibleRows = useMemo(() => {
|
||||
const copy = [...visibleRows];
|
||||
const factor = sortDir === 'asc' ? 1 : -1;
|
||||
copy.sort((a,b) => {
|
||||
let va: any, vb: any;
|
||||
switch (sortKey) {
|
||||
case 'pubDate': va = new Date(a.pubDate).getTime(); vb = new Date(b.pubDate).getTime(); break;
|
||||
case 'watchTime': va = a.watchTime; vb = b.watchTime; break;
|
||||
case 'views': va = a.views; vb = b.views; break;
|
||||
case 'premiumViews': va = a.premiumViews; vb = b.premiumViews; break;
|
||||
case 'validViews': va = a.validViews; vb = b.validViews; break;
|
||||
case 'expectedRevenue': va = a.expectedRevenue; vb = b.expectedRevenue; break;
|
||||
case 'handle': va = a.handle; vb = b.handle; break;
|
||||
case 'subject': va = a.subject; vb = b.subject; break;
|
||||
default: va = 0; vb = 0;
|
||||
}
|
||||
if (typeof va === 'string' && typeof vb === 'string') return va.localeCompare(vb) * factor;
|
||||
return ((va as number) - (vb as number)) * factor;
|
||||
});
|
||||
return copy;
|
||||
}, [visibleRows, sortKey, sortDir]);
|
||||
const allVisibleChecked = visibleRows.length > 0 && visibleRows.every(r => checkedIds.has(r.id));
|
||||
const totals = useMemo(() => {
|
||||
let views = 0, premium = 0, valid = 0, revenue = 0;
|
||||
for (const r of sortedVisibleRows) {
|
||||
if (checkedIds.has(r.id)) {
|
||||
views += r.views;
|
||||
premium += r.premiumViews;
|
||||
valid += r.validViews;
|
||||
revenue += r.expectedRevenue;
|
||||
}
|
||||
}
|
||||
return { views, premium, valid, revenue: Math.round(revenue) };
|
||||
}, [sortedVisibleRows, checkedIds]);
|
||||
|
||||
// 채널 필터가 변경되면, 보이지 않는 콘텐츠는 선택 해제
|
||||
useEffect(() => {
|
||||
const visibleIdSet = new Set(visibleRows.map(r => r.id));
|
||||
setCheckedIds(prev => {
|
||||
const next = new Set(Array.from(prev).filter(id => visibleIdSet.has(id)));
|
||||
return next;
|
||||
});
|
||||
}, [visibleChannelIds, rows]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="
|
||||
grid
|
||||
grid-cols-[1fr_1fr_1fr_1fr]
|
||||
grid-rows-[36px_minmax(300px,auto)_135px_135px_135px_135px]
|
||||
sm:grid-rows-[36px_minmax(400px,600px)_minmax(200px,250px)_minmax(200px,250px)]
|
||||
gap-2
|
||||
w-full
|
||||
h-full
|
||||
lg:gap-5
|
||||
py-2
|
||||
">
|
||||
<div className="order-1 col-[1/5] row-[1/2] flex flex-row">
|
||||
<div className="grow flex flex-row items-center overflow-y-hidden overflow-x-auto mx-1">
|
||||
{myChannelList.length === 0 ? (
|
||||
<div className=" h-[36px] border-1 border-border-pale rounded-xl flex items-center justify-center bg-white mx-1 px-2 min-w-[fit-content] font-semibold text-[#848484]">
|
||||
등록된 채널이 없습니다.
|
||||
</div>
|
||||
) : visibleChannelIds.size === 0 ? (
|
||||
<div className=" h-[36px] border-1 border-border-pale rounded-xl flex items-center justify-center bg-white mx-1 px-2 min-w-[fit-content] font-semibold text-[#F94B37]">
|
||||
선택된 채널이 없습니다. 오른쪽 필터에서 채널을 선택하세요.
|
||||
</div>
|
||||
) : (
|
||||
myChannelList.filter(c => visibleChannelIds.has(c.id)).map((channel, index) => (
|
||||
<div key={index} className=" h-[36px] border-1 border-border-pale rounded-xl flex items-center justify-center bg-white mx-1 px-2 min-w-[fit-content] font-semibold">
|
||||
<div className="w-[24px] h-[24px] rounded-full border-1 border-[#e6e9ef] overflow-hidden mr-2">
|
||||
{channel.icon ? (
|
||||
<img src={channel.icon} alt="channel icon" className="w-[24px] h-[24px] rounded-full" />
|
||||
) : (
|
||||
<div className="w-[24px] h-[24px] bg-[#e6e9ef] rounded-full border-1 border-[#848484]" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{channel.handle}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className='flex flex-row items-center'>
|
||||
<CalenderSelector
|
||||
dateString={dateString}
|
||||
is_ri={true}
|
||||
is_small={false}
|
||||
onRangeChange={(s,e)=>{
|
||||
setStartDate(s); setEndDate(e);
|
||||
setDateString(`${s.toISOString().slice(0,10)} ~ ${e.toISOString().slice(0,10)}`);
|
||||
fetchMyContents(s,e).catch(console.error);
|
||||
}}
|
||||
/>
|
||||
<div className="w-2"> </div>
|
||||
<ChannelFilter
|
||||
channel_list={myChannelList}
|
||||
visibleSet={visibleChannelIds}
|
||||
onChangeVisible={(next) => setVisibleChannelIds(new Set(next))}
|
||||
/>
|
||||
<div className="w-2"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="
|
||||
border-1 border-[#e6e9ef] rounded-lg bg-white col-[1/5] row-[2/3]
|
||||
min-h-[300px]
|
||||
overflow-auto
|
||||
">
|
||||
<table className="w-full h-full border-separate border-spacing-y-1 table-auto px-[10px]">
|
||||
<colgroup>
|
||||
<col className="w-[56px]" />
|
||||
<col className="min-w-[160px] max-w-[180px]" />
|
||||
<col className="min-w-[140px] w-[160px]" />
|
||||
<col className="min-w-[280px]" />
|
||||
<col className="min-w-[120px] w-[120px]" />
|
||||
<col className="min-w-[120px] w-[120px]" />
|
||||
<col className="min-w-[160px] w-[160px]" />
|
||||
<col className="min-w-[140px] w-[140px]" />
|
||||
<col className="w-[300px]" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="sticky top-0 bg-white h-[49px] z-1">
|
||||
<th className="border-b-1 border-[#e6e9ef] pl-2 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-[18px] h-[18px]"
|
||||
checked={allVisibleChecked}
|
||||
onChange={() => {
|
||||
const next = new Set(checkedIds);
|
||||
if (allVisibleChecked) {
|
||||
visibleRows.forEach(r => next.delete(r.id));
|
||||
} else {
|
||||
visibleRows.forEach(r => next.add(r.id));
|
||||
}
|
||||
setCheckedIds(next);
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none" onClick={()=> handleSort('handle')}>핸들
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='handle' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none" onClick={()=> handleSort('pubDate')}>게시일
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='pubDate' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none" onClick={()=> handleSort('subject')}>영상 제목
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='subject' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none" onClick={()=> handleSort('watchTime')}>시청시간
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='watchTime' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none" onClick={()=> handleSort('views')}>조회수
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='views' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none" onClick={()=> handleSort('premiumViews')}>premium 조회수
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='premiumViews' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 right-border border-[#e6e9ef] whitespace-nowrap cursor-pointer select-none bg-[#FFF3F2]" onClick={()=> handleSort('validViews')}>유효 조회수
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='validViews' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
<th className="border-b-1 border-[#e6e9ef] whitespace-nowrap min-w-[180px] cursor-pointer select-none bg-[#FFF3F2]" onClick={()=> handleSort('expectedRevenue')}>예상수익
|
||||
<span className={`ml-1 inline-block w-3 text-[10px] ${sortKey==='expectedRevenue' ? 'opacity-100' : 'opacity-0'}`}>{sortDir==='asc'?'▲':'▼'}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleChannelIds.size === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-6 text-[#848484]">선택된 채널이 없습니다. 채널 필터에서 채널을 선택하세요.</td>
|
||||
</tr>
|
||||
)}
|
||||
{visibleChannelIds.size > 0 && visibleRows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-6 text-[#848484]">표시할 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
)}
|
||||
{sortedVisibleRows.map((r) => (
|
||||
<tr key={r.id} className="h-[54px] border-1 border-[#e6e9ef] rounded-lg font-semibold">
|
||||
<td className="justify-center items-center rounded-l-lg border-l-1 border-t-1 border-b-1 border-[#e6e9ef] pl-2">
|
||||
<input type="checkbox" className="w-[18px] h-[18px]"
|
||||
checked={checkedIds.has(r.id)}
|
||||
onChange={(e)=>{
|
||||
const next = new Set(checkedIds);
|
||||
if (e.target.checked) next.add(r.id); else next.delete(r.id);
|
||||
setCheckedIds(next);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">
|
||||
<div className="flex items-center justify-center">
|
||||
{r.icon && <img src={r.icon} alt="icon" className="w-6 h-6 mr-2" />}
|
||||
{r.handle}
|
||||
</div>
|
||||
</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">{new Date(r.pubDate).toISOString().slice(0,10)}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">{r.subject}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center">{formatNumberWithCommas(r.watchTime)}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center">{formatNumberWithCommas(r.views)}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center">{formatNumberWithCommas(r.premiumViews)}</td>
|
||||
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center bg-[#FFF3F2]">{formatNumberWithCommas(r.validViews)}</td>
|
||||
<td className=" border-[#e6e9ef] text-center border-r-1 border-b-1 border-t-1 rounded-r-lg whitespace-nowrap px-2 bg-[#FFF3F2]">
|
||||
<span className="inline-block min-w-[180px] text-center">{formatNumberWithCommas(Math.round(r.expectedRevenue))}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="border-1 border-[#e6e9ef] col-[1/5] sm:col-[1/3] sm:row-[3/4] xl:col-[1/2] xl:row-[3/4] bg-white rounded-lg flex flex-col justify-around items-start p-3">
|
||||
<div>
|
||||
<div className="text-xl font-bold mb-2">총 조회수</div>
|
||||
{/* <div className=""> <CalenderSelector dateString={dateString} is_ri={false} is_small={true} /> </div> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold"> {formatNumberWithCommas(totals.views)} <span className="text-sm text-[#848484]"> 회</span></div>
|
||||
{/* <div className="text-xs font-normal text-[#F94B37]"> + 0.24%</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-1 border-[#e6e9ef] col-[1/5] sm:col-[3/5] sm:row-[3/4] xl:col-[2/3] xl:row-[3/4] bg-white rounded-lg flex flex-col justify-around items-start p-3">
|
||||
|
||||
<div>
|
||||
<div className="text-xl font-bold mb-2"> 프리미엄 조회수</div>
|
||||
{/* <div className=""> <CalenderSelector dateString={dateString} is_ri={false} is_small={true} /> </div> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold"> {formatNumberWithCommas(totals.premium)} <span className="text-sm text-[#848484]"> 회</span></div>
|
||||
{/* <div className="text-xs font-normal text-[#F94B37]"> + 0.24%</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-1 border-[#F94B37] col-[1/5] sm:col-[1/3] sm:row-[4/5] xl:col-[1/2] xl:row-[4/5] bg-white rounded-lg flex flex-col justify-around items-start p-3">
|
||||
<div>
|
||||
<div className="text-xl font-bold mb-2"> 유효 조회수</div>
|
||||
{/* <div className=""> <CalenderSelector dateString={dateString} is_ri={false} is_small={true} /> </div> */}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-3xl font-bold"> {formatNumberWithCommas(totals.valid)} <span className="text-sm text-[#848484]"> 회</span></div>
|
||||
{/* <div className="text-xs font-normal text-[#F94B37]"> + 0.24%</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-1 border-[#e6e9ef] col-[1/5] sm:col-[3/5] sm:row-[4/5] xl:col-[2/3] xl:row-[4/5] bg-white rounded-lg flex flex-col justify-around items-start p-3">
|
||||
|
||||
<div>
|
||||
<div className="text-xl font-bold mb-2"> 예상 수익 </div>
|
||||
{/* <div className=""> <CalenderSelector dateString={dateString} is_ri={false} is_small={true} /> </div> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold"> {formatNumberWithCommas(totals.revenue)} <span className="text-sm text-[#848484]"> 원</span></div>
|
||||
{/* <div className="text-xs font-normal text-[#0000FF]"> - 1.24%</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-1 border-[#F94B37] hidden xl:col-[3/5] xl:row-[3/5] bg-white rounded-lg p-4 xl:flex xl:flex-col">
|
||||
<div className="text-xl font-bold"> 유효 조회수 추이 </div>
|
||||
<div className="text-normal text-gray-500"> 상위 테이블에서 설정한 데이터를 토대로 그래프가 표출됩니다.</div>
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
<div className="font-bold text-3xl my-4"> 33,500 </div>
|
||||
{/* <div className=" p-2 pt-5 text-sm font-normal text-[#F94B37]"> + 0.24%</div> */}
|
||||
</div>
|
||||
<div className="w-full flex-1 items-center justify-center flex" ref={chartRef}>
|
||||
<Line data={data} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user