From 9c0aeb73e8b516f18dd3fac78c96b36160f3177d Mon Sep 17 00:00:00 2001 From: motaju Date: Wed, 15 Oct 2025 12:17:36 +0000 Subject: [PATCH] =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/CalenderSelector.tsx | 108 +++++++++++++++++++++++----- app/components/ChannelFilter.tsx | 12 +++- app/components/Dashboard.tsx | 75 ++++++++++++++----- 3 files changed, 157 insertions(+), 38 deletions(-) diff --git a/app/components/CalenderSelector.tsx b/app/components/CalenderSelector.tsx index b964e6a..cb8adeb 100644 --- a/app/components/CalenderSelector.tsx +++ b/app/components/CalenderSelector.tsx @@ -4,11 +4,18 @@ import Arrow from "./svgs/arrow"; import { DateRangeEnum } from "@/app/constants/dateRange"; import { useState,useEffect } from "react"; -export default function CalenderSelector( {dateString, is_small, onRangeChange}: {dateString: string, is_ri: boolean, is_small: boolean, onRangeChange?: (start: Date, end: Date) => void} ) { - const [isOpen, setIsOpen] = useState(false); +export default function CalenderSelector({ dateString, is_small, onRangeChange, isOpen: controlledOpen, onOpenChange }: { dateString: string, is_ri: boolean, is_small: boolean, onRangeChange?: (start: Date, end: Date) => void, isOpen?: boolean, onOpenChange?: (open: boolean) => void }) { + const CUSTOM = -1; // 커스텀(년/월) 선택 표기용 + const [internalOpen, setInternalOpen] = useState(false); + const isOpen = typeof controlledOpen === 'boolean' ? controlledOpen : internalOpen; + const setOpen = (next: boolean) => { if (onOpenChange) onOpenChange(next); else setInternalOpen(next); }; const [selected, setSelected] = useState(DateRangeEnum.ONE_MONTH); const [rangeStart, setRangeStart] = useState(new Date()); const [rangeEnd, setRangeEnd] = useState(new Date()); + const [yearInput, setYearInput] = useState(''); + const [monthInput, setMonthInput] = useState(''); + const [customLabel, setCustomLabel] = useState(''); + const [isCustomActive, setIsCustomActive] = useState(false); useEffect(() => { const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); @@ -16,6 +23,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(startDate); setRangeEnd(endDate); onRangeChange?.(startDate, endDate); + const now = new Date(); + setYearInput(String(now.getFullYear())); + setMonthInput(String(now.getMonth() + 1).padStart(2,'0')); }, []); return ( @@ -27,7 +37,7 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: border-1 rounded-lg flex flex-row items-center justify-between gap-2 px-2 cursor-pointer`} onClick={() => { console.log('click'); - setIsOpen(!isOpen); + setOpen(!isOpen); }} >
@@ -38,13 +48,15 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: )}
- {selected === DateRangeEnum.ONE_MONTH&& '최근 1개월'} - {selected === DateRangeEnum.ONE_WEEK&& '최근 1주일'} - {selected === DateRangeEnum.TWO_MONTHS&& '최근 2개월'} - {selected === DateRangeEnum.THREE_MONTHS&& '최근 3개월'} - {selected === DateRangeEnum.SIX_MONTHS&& '최근 6개월'} - {selected === DateRangeEnum.ONE_YEAR&& '최근 1년'} - {selected === DateRangeEnum.ALL&& '전체'} + {customLabel || ( + selected === DateRangeEnum.ONE_MONTH ? '최근 1개월' : + selected === DateRangeEnum.ONE_WEEK ? '최근 1주일' : + selected === DateRangeEnum.TWO_MONTHS ? '최근 2개월' : + selected === DateRangeEnum.THREE_MONTHS ? '최근 3개월' : + selected === DateRangeEnum.SIX_MONTHS ? '최근 6개월' : + selected === DateRangeEnum.ONE_YEAR ? '최근 1년' : + selected === DateRangeEnum.ALL ? '전체' : '' + )}
@@ -59,11 +71,13 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: onClick={() => { setSelected(DateRangeEnum.ALL); const end = new Date(); - const start = new Date(0); + const start = new Date(Date.UTC(2025, 0, 1)); setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -79,7 +93,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -95,7 +111,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -111,7 +129,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -127,7 +147,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -143,7 +165,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -159,7 +183,9 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}: setRangeStart(start); setRangeEnd(end); onRangeChange?.(start, end); - setIsOpen(false); + setCustomLabel(''); + setIsCustomActive(false); + setOpen(false); }} > @@ -167,6 +193,52 @@ export default function CalenderSelector( {dateString, is_small, onRangeChange}:
+
+
+ setYearInput(e.target.value.replace(/[^0-9]/g,''))} + /> + + { + const v = e.target.value.replace(/[^0-9]/g,''); + setMonthInput(v); + }} + /> + + +
+
)} diff --git a/app/components/ChannelFilter.tsx b/app/components/ChannelFilter.tsx index 219d6c7..bcd7497 100644 --- a/app/components/ChannelFilter.tsx +++ b/app/components/ChannelFilter.tsx @@ -7,12 +7,18 @@ export default function ChanalFilter({ channel_list, visibleSet, onChangeVisible, + isOpen: controlledOpen, + onOpenChange, }:{ channel_list: Channel[]; visibleSet: Set; onChangeVisible: (nextVisibleIds: string[]) => void; + isOpen?: boolean; + onOpenChange?: (open: boolean) => void; }) { - const [isOpen, setIsOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); + const isOpen = typeof controlledOpen === 'boolean' ? controlledOpen : internalOpen; + const setOpen = (next: boolean) => { if (onOpenChange) onOpenChange(next); else setInternalOpen(next); }; const allIds = useMemo(() => channel_list.map(c => c.id), [channel_list]); const allChecked = useMemo(() => allIds.every(id => visibleSet.has(id)) && allIds.length > 0, [allIds, visibleSet]); @@ -45,7 +51,7 @@ export default function ChanalFilter({
{setIsOpen(!isOpen)}} + onClick={()=>{setOpen(!isOpen)}} >
@@ -83,7 +89,7 @@ export default function ChanalFilter({
diff --git a/app/components/Dashboard.tsx b/app/components/Dashboard.tsx index 313d761..bd2e2cb 100644 --- a/app/components/Dashboard.tsx +++ b/app/components/Dashboard.tsx @@ -109,6 +109,8 @@ export default function Page({user}: {user: any}) { const [dateString, setDateString] = useState("2025.03.01 ~ 2025.03.31"); const chartRef = useRef(null); const [hasRun, setHasRun] = useState(false); + const [calendarOpen, setCalendarOpen] = useState(false); + const [filterOpen, setFilterOpen] = useState(false); // 전역 상수는 공용 파일에서 import @@ -316,9 +318,16 @@ export default function Page({user}: {user: any}) { expectedRevenue: { border: '#16A34A', bg: 'rgba(22, 163, 74, 0.25)' }, } const selectedRows = useMemo(() => sortedVisibleRows.filter(r => checkedIds.has(r.id)), [sortedVisibleRows, checkedIds]) - const [seriesMode, setSeriesMode] = useState<'items'|'series'>('items') + const selectedIdsKey = useMemo(() => { + const ids = Array.from(checkedIds) + if (ids.length === 0) return '' + ids.sort() + return ids.join(',') + }, [checkedIds]) + const [seriesMode, setSeriesMode] = useState<'items'|'series'>('series') const [seriesInterval, setSeriesInterval] = useState<'day'|'week'|'month'>('day') const [seriesItems, setSeriesItems] = useState>([]) + const [seriesLoading, setSeriesLoading] = useState(false) const chartData = useMemo(() => { const col = metricColor[selectedMetric] if (seriesMode === 'series') { @@ -337,6 +346,37 @@ export default function Page({user}: {user: any}) { elements: { line: { tension: 0.35 } }, }), []) + // 시리즈 모드일 때 옵션/기간 변경 시 자동 데이터 로드 + useEffect(() => { + if (seriesMode !== 'series') return; + const s = startDate; const e = endDate; + const qs = new URLSearchParams(); + qs.set('start', s.toISOString()); + qs.set('end', e.toISOString()); + qs.set('mode', 'series'); + qs.set('interval', seriesInterval); + if (selectedIdsKey) qs.set('contentIds', selectedIdsKey); + let cancelled = false; + setSeriesLoading(true); + (async () => { + try { + const res = await fetch(`/api/contents/mycontent?${qs.toString()}`, { cache: 'no-store' }); + const data = await res.json(); + if (res.ok && !cancelled) setSeriesItems(data.items || []); + } catch (e) { + if (!cancelled) console.error(e); + } finally { + if (!cancelled) setSeriesLoading(false); + } + })(); + return () => { cancelled = true; }; + }, [seriesMode, seriesInterval, startDate, endDate, selectedIdsKey]); + + // 시리즈 모드 해제 시 스피너 숨김 보장 + useEffect(() => { + if (seriesMode !== 'series') setSeriesLoading(false); + }, [seriesMode]); + // 채널 필터가 변경되면, 보이지 않는 콘텐츠는 선택 해제하되 // 초기 로딩 등으로 가시 채널이 비어있을 땐 해제하지 않음. // 가시 채널이 채워지고 선택이 비어있으면 전체 선택으로 초기화. @@ -364,7 +404,7 @@ export default function Page({user}: {user: any}) { py-2 ">
-
+
{myChannelList.length === 0 ? (
등록된 채널이 없습니다. @@ -394,6 +434,8 @@ export default function Page({user}: {user: any}) { dateString={dateString} is_ri={true} is_small={false} + isOpen={calendarOpen} + onOpenChange={(open)=>{ setCalendarOpen(open); if (open) setFilterOpen(false); }} onRangeChange={(s,e)=>{ setStartDate(s); setEndDate(e); setDateString(`${s.toISOString().slice(0,10)} ~ ${e.toISOString().slice(0,10)}`); @@ -405,6 +447,8 @@ export default function Page({user}: {user: any}) { channel_list={myChannelList} visibleSet={visibleChannelIds} onChangeVisible={(next) => setVisibleChannelIds(new Set(next))} + isOpen={filterOpen} + onOpenChange={(open)=>{ setFilterOpen(open); if (open) setCalendarOpen(false); }} />
@@ -596,20 +640,11 @@ export default function Page({user}: {user: any}) { {seriesMode === 'series' && ( - <> - - - + )}
@@ -621,7 +656,13 @@ export default function Page({user}: {user: any}) { {/*
+ 0.24%
*/}
- + {seriesMode === 'series' && seriesLoading ? ( +
+
+
+ ) : ( + + )}