[as] feat: 결재선 관리 페이지 구현

This commit is contained in:
2026-05-18 17:40:21 +09:00
parent 0e067bb5fc
commit 25affc06b9
43 changed files with 2071 additions and 236 deletions
+7
View File
@@ -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}`;
}
+3
View File
@@ -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();
}
+43
View File
@@ -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,
};
}
+2
View File
@@ -27,3 +27,5 @@ export interface ApiError {
code?: string;
status?: number;
}
export type { PageableParams, PageResponse } from "./page";
+32
View File
@@ -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
View File
@@ -1 +1,2 @@
export { PageLayout } from "./page-layout";
export { NoData } from "./no-data";
+9
View File
@@ -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 };