Skip to content
Merged
9 changes: 8 additions & 1 deletion src/api/timeline/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ICommonResponse } from "@/types/common/common";
import {
type ITimelineDetail,
type ITimelineListItem,
type ITimelineListParams,
type ITimelineMutationResponse,
type ITimelineUpsertRequest,
type TTimelineEmptyResponse,
Expand All @@ -23,10 +24,16 @@ export const getTimelineDetail = async (
//타임라인 목록 조회 API
export const getTimelineList = async (
orgId: number,
params: ITimelineListParams = {},
): Promise<ITimelineListItem[]> => {
const { data } = await axiosInstance.get<
ICommonResponse<ITimelineListItem[]>
>(`/api/org/${orgId}/timeline`);
>(`/api/org/${orgId}/timeline`, {
params: {
...(params.status ? { status: params.status } : {}),
...(params.sort ? { sort: params.sort } : {}),
},
});
return data.data;
};

Expand Down
73 changes: 73 additions & 0 deletions src/components/timeline/TimelineFilterSortMenus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { twMerge } from "tailwind-merge";

import type { TTimelineSort } from "@/types/timeline/api";
import {
TIMELINE_SORT_OPTIONS,
TIMELINE_STATUS_FILTER_OPTIONS,
type TTimelineStatusFilter,
} from "@/constants/timeline/filterSort";

import {
DropdownMenu,
type TMenuItem,
} from "../common/dropdownmenu/DropdownMenu";
Comment thread
jjjsun marked this conversation as resolved.
Resolved

import FilterIcon from "@/assets/icon/timeline/filter.svg?react";
import SortIcon from "@/assets/icon/timeline/sort.svg?react";

const TOOLBAR_ACTIONS_CLASS =
"flex cursor-pointer items-center gap-1.5 rounded-lg px-2 py-1.5 font-caption text-text-muted transition-all hover:bg-surface-200 hover:text-text-body";

interface ITimelineFilterSortMenusProps {
statusFilter: TTimelineStatusFilter;
sort: TTimelineSort;
onStatusFilterChange: (value: TTimelineStatusFilter) => void;
onSortChange: (value: TTimelineSort) => void;
className?: string;
}

export default function TimelineFilterSortMenus({
statusFilter,
sort,
onStatusFilterChange,
onSortChange,
className,
}: ITimelineFilterSortMenusProps) {
const sortItems: TMenuItem[] = TIMELINE_SORT_OPTIONS.map((opt) => ({
label: opt.label,
active: sort === opt.value,
onClick: () => onSortChange(opt.value),
}));

const filterItems: TMenuItem[] = TIMELINE_STATUS_FILTER_OPTIONS.map(
(opt) => ({
label: opt.label,
active: statusFilter === opt.value,
onClick: () => onStatusFilterChange(opt.value),
}),
);
return (
<div className={twMerge("flex items-center gap-3", className)}>
<DropdownMenu
aria-label="타임라인 정렬"
items={sortItems}
trigger={
<span className={TOOLBAR_ACTIONS_CLASS}>
<SortIcon className="h-4 w-4" />
Sort
</span>
}
/>
<DropdownMenu
aria-label="성과 상태 필터"
items={filterItems}
trigger={
<span className={TOOLBAR_ACTIONS_CLASS}>
<FilterIcon className="h-4 w-4" />
Filter
</span>
}
/>
</div>
);
}
36 changes: 36 additions & 0 deletions src/constants/timeline/filterSort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type {
TTimelinePerformanceStatus,
TTimelineSort,
} from "@/types/timeline/api";

import { TIMELINE_PERFORMANCE_STATUS_STYLE } from "./statusStyle";

export type TTimelineStatusFilter = TTimelinePerformanceStatus | "ALL";

export const TIMELINE_STATUS_FILTER_OPTIONS: {
value: TTimelineStatusFilter;
label: string;
}[] = [
{ value: "ALL", label: "전체" },
{
value: "ON_TRACK",
label: TIMELINE_PERFORMANCE_STATUS_STYLE.ON_TRACK.label,
},
{
value: "ABOVE_AVG",
label: TIMELINE_PERFORMANCE_STATUS_STYLE.ABOVE_AVG.label,
},
{
value: "UNDERPERFORM",
label: TIMELINE_PERFORMANCE_STATUS_STYLE.UNDERPERFORM.label,
},
];

export const TIMELINE_SORT_OPTIONS: {
value: TTimelineSort;
label: string;
}[] = [
{ value: "DISPLAY_ORDER", label: "기본 순서" },
{ value: "LATEST", label: "종료일 먼 순" },
{ value: "OLDEST", label: "종료일 가까운 순" },
];
17 changes: 14 additions & 3 deletions src/hooks/timeline/useTimelineList.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import type { ITimelineListParams } from "@/types/timeline/api";

import { useCoreQuery } from "@/hooks/customQuery";

import { getTimelineList } from "@/api/timeline/timeline";
import { QUERY_KEYS } from "@/lib/queryKeys";
import useWorkspaceStore from "@/store/useWorkspaceStore";

export function useTimelineList() {
export function useTimelineList(params: ITimelineListParams = {}) {
const orgId = useWorkspaceStore((s) => s.selectedOrgId);
const sort = params.sort ?? "DISPLAY_ORDER";

return useCoreQuery(
QUERY_KEYS.timeline.list(orgId),
() => getTimelineList(orgId!),
QUERY_KEYS.timeline.listWithParams(orgId, {
status: params.status ?? null,
sort,
}),
() =>
getTimelineList(orgId!, {
status: params.status,
sort,
}),
{
enabled: orgId != null,
},
Expand Down
13 changes: 13 additions & 0 deletions src/lib/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,20 @@ export const QUERY_KEYS = {
},

timeline: {
/*invalidate용 prefix - status/sort 없이 */
list: (orgId: number | null) => ["timeline", "list", orgId] as const,
/*실제 useQuery용*/
listWithParams: (
orgId: number | null,
params: { status?: string | null; sort?: string } = {},
) =>
[
"timeline",
"list",
orgId,
params.status ?? null,
params.sort ?? "DISPLAY_ORDER",
] as const,
detail: (orgId: number | null, timelineId: number | null) =>
["timeline", "detail", orgId, timelineId] as const,
},
Expand Down
57 changes: 28 additions & 29 deletions src/pages/dashboard/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { twMerge } from "tailwind-merge";

import type { TTimelineSort } from "@/types/timeline/api";
import type { ITimelineSummaryPanelData } from "@/types/timeline/summary";
import type {
ITimelineCampaignBar,
TTimelineViewUnit,
} from "@/types/timeline/ui";
import type { TTimelineStatusFilter } from "@/constants/timeline/filterSort";
import {
TIMELINE_COL_WIDTH,
TIMELINE_PAGE_HEIGHT,
Expand All @@ -31,6 +33,7 @@ import TimelineAxis from "@/components/timeline/TimelineAxis";
import TimelineBar from "@/components/timeline/TimelineBar";
import TimelineCreateModal from "@/components/timeline/TimelineCreateModal";
import TimelineEmptyState from "@/components/timeline/TimelineEmptyState";
import TimelineFilterSortMenus from "@/components/timeline/TimelineFilterSortMenus";
import TimelineGrid from "@/components/timeline/TimelineGrid";
import TimelinePerformancePanel from "@/components/timeline/TimelinePerformancePanel";
import {
Expand All @@ -41,15 +44,10 @@ import TimelineStatusLegend from "@/components/timeline/TimelineStatusLegend";

import PlusIcon from "@/assets/icon/common/plus.svg?react";
import TrashIcon from "@/assets/icon/common/trash.svg?react";
import FilterIcon from "@/assets/icon/timeline/filter.svg?react";
import SortIcon from "@/assets/icon/timeline/sort.svg?react";

const SUMMARY_POLL_INTERVAL_MS = 1500;
const SUMMARY_POLL_TIMEOUT_MS = 90000;

const TOOLBAR_ACTION_CLASS =
"flex items-center gap-1.5 rounded-lg px-2 py-1.5 font-caption text-text-muted opacity-50 cursor-not-allowed";

export default function Timeline() {
const scrollRef = useRef<HTMLDivElement>(null);

Expand All @@ -71,12 +69,9 @@ export default function Timeline() {
number | null
>(null);

const {
data: timelineList = [],
isLoading,
isError,
error,
} = useTimelineList();
const [statusFilter, setStatusFilter] =
useState<TTimelineStatusFilter>("ALL");
const [sort, setSort] = useState<TTimelineSort>("DISPLAY_ORDER");
Comment thread
jjjsun marked this conversation as resolved.
Resolved

const { mutate: deleteTimeline, isPending: isDeleting } = useDeleteTimeline();
const { mutate: requestSummary, isPending: isSummaryPending } =
Expand All @@ -103,6 +98,21 @@ export default function Timeline() {
};
}, [editDetail]);

const listParams = useMemo(
() => ({
status: statusFilter === "ALL" ? undefined : statusFilter,
sort,
}),
[statusFilter, sort],
);

const {
data: timelineList = [],
isLoading,
isError,
error,
} = useTimelineList(listParams);

const gridData = useMemo(
() => buildTimelineGrid({ items: timelineList, viewUnit, periodIndex }),
[timelineList, viewUnit, periodIndex],
Expand Down Expand Up @@ -325,24 +335,13 @@ export default function Timeline() {
onGoToToday={handleGoToToday}
className="justify-self-center"
/>
<div className="flex items-center justify-self-end gap-3 tablet:justify-self-end">
<span
className={TOOLBAR_ACTION_CLASS}
title="준비 중"
aria-disabled
>
<SortIcon className="h-4 w-4" />
Sort
</span>
<span
className={TOOLBAR_ACTION_CLASS}
title="준비 중"
aria-disabled
>
<FilterIcon className="h-4 w-4" />
Filter
</span>
</div>
<TimelineFilterSortMenus
statusFilter={statusFilter}
sort={sort}
onStatusFilterChange={setStatusFilter}
onSortChange={setSort}
className="justify-self-end"
/>
</div>
</div>
{hasNoTimelines ? (
Expand Down
14 changes: 14 additions & 0 deletions src/types/timeline/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export const TIMELINE_PERFORMANCE_STATUS = [
export type TTimelinePerformanceStatus =
(typeof TIMELINE_PERFORMANCE_STATUS)[number];

export const TIMELINE_SORTS = ["DISPLAY_ORDER", "LATEST", "OLDEST"] as const;
export type TTimelineSort = (typeof TIMELINE_SORTS)[number];

/** API 3종 + UI 전용 미정(PENDING). null/미지 값은 UI에서 PENDING으로 정규화 */
export type TTimelinePerformanceStatusUi =
| TTimelinePerformanceStatus
Expand All @@ -44,6 +47,7 @@ export interface ITimelineListItem {
endDate: string;
/** 성과 미산출 시 null일 수 있음 */
performanceStatus: TTimelinePerformanceStatus | null;
displayOrder: number;
}

export interface ITimelineDailyTrend {
Expand Down Expand Up @@ -102,3 +106,13 @@ export interface IUpdateTimelineVariables {
timelineId: number;
body: ITimelineUpsertRequest;
}

/*목록 조회 쿼리. status가 생략되면 전체보이도록*/
export interface ITimelineListParams {
status?: TTimelinePerformanceStatus;
sort?: TTimelineSort;
}

export interface IUpdateTimelineDisplayOrderRequest {
timelineIds: number[];
}
2 changes: 2 additions & 0 deletions src/types/timeline/timeline.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ export const TIMELINE_LIST_MOCK: ITimelineListItem[] = [
startDate: "2026-06-01",
endDate: "2026-06-30",
performanceStatus: "ON_TRACK",
displayOrder: 1,
},
{
timelineId: 2,
name: "리타겟팅 집중 기간",
startDate: "2026-06-10",
endDate: "2026-06-30",
performanceStatus: "UNDERPERFORM",
displayOrder: 2,
},
];

Expand Down
Loading