[as] feat: 결재선 관리 페이지 구현
This commit is contained in:
@@ -4,6 +4,13 @@ export const ENDPOINT = {
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
USERS: "/user-service/api/v1/users",
|
||||
},
|
||||
AS_ELAPR_SERVICE: {
|
||||
ELAPR_TMPLT: "/as-elapr-service/api/v1/elapr-tmplt",
|
||||
ELAPR_APLN: "/as-elapr-service/api/v1/elapr-apln",
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
CODE: "/cms-service/api/v1/code/cache",
|
||||
},
|
||||
};
|
||||
|
||||
export const JWT_EXPIRATION_TIME = 1800000;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** TBD: elaprTmpltId / aplnId 서버 발급 정책 확정 후 클라이언트 생성 규칙 교체 */
|
||||
export function generateClientId(prefix: string): string {
|
||||
const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 8).toUpperCase();
|
||||
return `${prefix}-${suffix}`;
|
||||
}
|
||||
@@ -2,3 +2,6 @@ export { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
export { dayjs } from "./dayjs";
|
||||
export { queryClient } from "./query-client";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export { generateClientId } from "./generate-client-id";
|
||||
export { matchQueryStatus } from "./match-query-status";
|
||||
export { useDataTable } from "./use-data-table";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
|
||||
interface MatchQueryStatusOptions {
|
||||
Pending: ReactNode;
|
||||
Empty: ReactNode;
|
||||
Success: () => ReactNode;
|
||||
Error?: ReactNode;
|
||||
}
|
||||
|
||||
export function matchQueryStatus<T>(
|
||||
query: UseQueryResult<T>,
|
||||
options: MatchQueryStatusOptions,
|
||||
): ReactNode {
|
||||
if (query.isPending) return options.Pending;
|
||||
if (query.isError) return options.Error ?? null;
|
||||
|
||||
const isEmpty =
|
||||
query.data == null ||
|
||||
(Array.isArray(query.data) && query.data.length === 0) ||
|
||||
(typeof query.data === "object" &&
|
||||
query.data !== null &&
|
||||
"content" in query.data &&
|
||||
Array.isArray((query.data as { content: unknown[] }).content) &&
|
||||
(query.data as { content: unknown[] }).content.length === 0);
|
||||
|
||||
if (isEmpty) return options.Empty;
|
||||
|
||||
return options.Success();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import { useReactTable, type TableOptions } from "@tanstack/react-table";
|
||||
import { DataTable } from "@common/components";
|
||||
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
|
||||
import { matchQueryStatus } from "./match-query-status";
|
||||
|
||||
interface DataTableOptions<T> {
|
||||
variant?: "default" | "secondary" | "line";
|
||||
className?: HTMLAttributes<HTMLTableElement>["className"];
|
||||
viewPagination?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
}
|
||||
|
||||
interface UseDataTableOptions<T> {
|
||||
query: UseQueryResult<PageResponse<T>>;
|
||||
tableOptions: Omit<TableOptions<T>, "data" | "rowCount">;
|
||||
dataTableOptions?: DataTableOptions<T>;
|
||||
}
|
||||
|
||||
export function useDataTable<T>({
|
||||
query,
|
||||
tableOptions,
|
||||
dataTableOptions,
|
||||
}: UseDataTableOptions<T>) {
|
||||
const table = useReactTable({
|
||||
...tableOptions,
|
||||
data: query.data?.content ?? [],
|
||||
rowCount: query.data?.totalElements ?? 0,
|
||||
});
|
||||
|
||||
return {
|
||||
tableComponent: matchQueryStatus(query, {
|
||||
Pending: <p className="py-8 text-center text-sm text-muted-foreground">로딩 중…</p>,
|
||||
Empty: <NoData />,
|
||||
Success: () => <DataTable table={table} {...dataTableOptions} />,
|
||||
}),
|
||||
tableInstance: table,
|
||||
};
|
||||
}
|
||||
@@ -27,3 +27,5 @@ export interface ApiError {
|
||||
code?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export type { PageableParams, PageResponse } from "./page";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
export interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
export interface PageResponse<T> {
|
||||
content: T[];
|
||||
pageable: {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
};
|
||||
sort: SortConfig;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
numberOfElements: number;
|
||||
size: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { PageLayout } from "./page-layout";
|
||||
export { NoData } from "./no-data";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
function NoData({ message = "조회된 데이터가 없습니다." }: { message?: string }) {
|
||||
return (
|
||||
<div className="flex min-h-[120px] items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { NoData };
|
||||
Reference in New Issue
Block a user