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
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 "@/components/common/dropdownmenu/DropdownMenu";

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 @@ -31,6 +31,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 +42,11 @@ 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";
import useTimelineStore from "@/store/useTimelineStore";

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 +68,10 @@ export default function Timeline() {
number | null
>(null);

const {
data: timelineList = [],
isLoading,
isError,
error,
} = useTimelineList();
const statusFilter = useTimelineStore((s) => s.statusFilter);
const sort = useTimelineStore((s) => s.sort);
const setStatusFilter = useTimelineStore((s) => s.setStatusFilter);
const setSort = useTimelineStore((s) => s.setSort);

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
20 changes: 20 additions & 0 deletions src/store/useTimelineStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { create } from "zustand";

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

interface ITimelineUiState {
statusFilter: TTimelineStatusFilter;
sort: TTimelineSort;
setStatusFilter: (statusFilter: TTimelineStatusFilter) => void;
setSort: (sort: TTimelineSort) => void;
}

const useTimelineStore = create<ITimelineUiState>((set) => ({
statusFilter: "ALL",
sort: "DISPLAY_ORDER",
setStatusFilter: (statusFilter) => set({ statusFilter }),
setSort: (sort) => set({ sort }),
}));

export default useTimelineStore;
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