[as] feat: 결재선관리 페이지 작업
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
│ │ ⋮⋮ | APLN-0001 | [ 1 ] | [검토자 ▼] | [김민수 ][선택] | … | [저장][삭제] │ │
|
||||
│ │ ⋮⋮ | (신규) | [ 2 ] | [승인자 ▼] | [ ][선택] | — | [저장][취소] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ [행 추가] │
|
||||
│ [행 추가] │
|
||||
│ ※ 결재자 등록·수정은 그리드 셀에서 직접 입력. 검토인력만 [선택] 팝업/다이얼로그 │
|
||||
│ ※ 행 추가·수정 중: 해당 행만 편집 가능, 나머지 행·[행 추가]·드래그 비활성 │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"axios": "^1.8.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.8",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { BaseLayout } from "@common/components";
|
||||
@@ -18,11 +19,14 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) {
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
},
|
||||
[router],
|
||||
[router, setCurrentMenuInfo],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,9 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
import { queryClient } from "@/shared/lib";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
|
||||
interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
@@ -15,7 +17,10 @@ export function Providers({ children }: ProvidersProps) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<AuthWrapper>{children}</AuthWrapper>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
</AuthWrapper>
|
||||
</StoreProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export { codeService } from "./api/code-service";
|
||||
export { useCodeList, useCodeListByUseYn, useCodeName } from "./api/queries";
|
||||
export { findCodeLabel } from "./lib/find-code-label";
|
||||
export { UP_CD } from "./lib/up-cd-constants";
|
||||
export { CodeSelect } from "./ui/code-select";
|
||||
export type { Code, CodeService } from "./model/code-interface";
|
||||
export { CodeSelect } from "./ui/code-select";
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/** 결재선 관리 SPEC 상위코드 */
|
||||
export const UP_CD = {
|
||||
DOC_SE: "ASDOCSECD",
|
||||
ATRZ_SE: "ATRZSECD",
|
||||
} as const;
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
@@ -0,0 +1,19 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
MenuAuthorityService,
|
||||
PageMenuAuthority,
|
||||
} from "../model/menu-authority-interface";
|
||||
|
||||
export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
/** MenuAuthrtApiController GET /api/v1/menu-authrts/pageinfo/{menuCd} */
|
||||
async getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]> {
|
||||
const response = await client.get<PageMenuAuthority[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuAuthorityApi = new MenuAuthorityApiService();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { MenuAuthorityApi } from "./api/menu-authority-service";
|
||||
export type { PageMenuAuthority } from "./model/menu-authority-interface";
|
||||
@@ -0,0 +1,12 @@
|
||||
/** MenuAuthrtApiController GET /pageinfo/{menuCd} 응답 DTO */
|
||||
export interface PageMenuAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { mapMenuTreeResponse } from "../lib/map-menu-tree-response";
|
||||
import type { MenuTreeResponseDto } from "../model/menu-tree-response";
|
||||
import type { MenuItem } from "../model/types";
|
||||
|
||||
export class MenuApiService {
|
||||
/** MenuRoleApiController GET /api/v1/menu-roles/{siteId} */
|
||||
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
|
||||
const response = await client.get<MenuTreeResponseDto[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
|
||||
);
|
||||
return response.data.map(mapMenuTreeResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -1 +1,4 @@
|
||||
export { MenuApi } from "./api/menu-service";
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
export { getCurrentMenu } from "./lib/utils";
|
||||
export { type MenuItem } from "./model/types";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { MenuTreeResponseDto } from "../model/menu-tree-response";
|
||||
import type { MenuItem } from "../model/types";
|
||||
|
||||
function isRequiredUrlPath(menuLnkgTypeCd: string): boolean {
|
||||
return menuLnkgTypeCd === "BOARD" || menuLnkgTypeCd === "CONTENTS";
|
||||
}
|
||||
|
||||
export function mapMenuTreeResponse(dto: MenuTreeResponseDto): MenuItem {
|
||||
return {
|
||||
menuCd: dto.menuCd,
|
||||
menuKornNm: dto.menuKornNm,
|
||||
menuEngNm: dto.menuEngNm,
|
||||
parentId: dto.parentId,
|
||||
sortSeq: dto.sortSeq,
|
||||
iconNm: dto.iconNm,
|
||||
children: dto.children?.map(mapMenuTreeResponse) ?? null,
|
||||
levelNo: dto.levelNo,
|
||||
urlPath: dto.urlPath,
|
||||
menuLnkgTypeCd: dto.menuLnkgTypeCd,
|
||||
menuLnkgSn: dto.menuLnkgSn,
|
||||
menuPath: dto.menuPath,
|
||||
qrcd: dto.qrcd,
|
||||
useYn: dto.useYn,
|
||||
otptYn: dto.otptYn,
|
||||
newWndwYn: dto.newWndwYn,
|
||||
requiredUrlPath: isRequiredUrlPath(dto.menuLnkgTypeCd),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu";
|
||||
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
function MenuWrapper({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } =
|
||||
useMenuStore((state) => state);
|
||||
|
||||
const getCurrentMenuInfo = async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
return getCurrentMenu(menuRoles, pathname ?? "");
|
||||
};
|
||||
|
||||
const getPageMenuAuthority = async () => {
|
||||
const menuCd = getCookie("menu-code-key");
|
||||
if (!menuCd) return;
|
||||
const response = await MenuAuthorityApi.getPageMenuAuthority(
|
||||
parseInt(menuCd, 10),
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
const checkInquiryAuthority = () => {
|
||||
return pageMenuAuthority?.inqAuthrtYn === "Y";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const currentMenu = await getCurrentMenuInfo();
|
||||
if (currentMenu) setCurrentMenuInfo(currentMenu);
|
||||
|
||||
const response = await getPageMenuAuthority();
|
||||
if (response?.length) setPageMenuAuthority(response[0]!);
|
||||
})();
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageMenuAuthority) return;
|
||||
const inquiryAuthority = checkInquiryAuthority();
|
||||
if (!inquiryAuthority) {
|
||||
alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/"));
|
||||
}
|
||||
}, [pageMenuAuthority]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export { MenuWrapper };
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MenuItem } from "../model/types";
|
||||
|
||||
export function getCurrentMenu(
|
||||
menus: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem | null {
|
||||
for (const menu of menus) {
|
||||
if (
|
||||
menu.urlPath !== "" &&
|
||||
(menu.urlPath === pathname || pathname.startsWith(menu.urlPath))
|
||||
) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
if (menu.children) {
|
||||
const child = getCurrentMenu(menu.children, pathname);
|
||||
if (child) return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** MenuRoleApiController GET /api/v1/menu-roles/{siteId} 응답 DTO */
|
||||
export interface MenuTreeResponseDto {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuTreeResponseDto[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
menuHlpCn?: string;
|
||||
menuSubKornNm?: string;
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { generateClientId } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -14,10 +18,6 @@ import {
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { CodeSelect, UP_CD } from "@/entities/code";
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import { generateClientId } from "@/shared/lib";
|
||||
|
||||
export interface ElaprTmpltFormValues {
|
||||
elaprTmpltId: string;
|
||||
elaprTmpltNm: string;
|
||||
@@ -77,41 +77,40 @@ function ElaprTmpltFormDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className="max-w-[52rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "결재서식 등록" : "결재서식 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltId">전자결재서식아이디</Label>
|
||||
<Input
|
||||
id="elaprTmpltId"
|
||||
readOnly
|
||||
{...register("elaprTmpltId", { required: true })}
|
||||
/>
|
||||
<Input id="elaprTmpltId" disabled {...register("elaprTmpltId", { required: true })} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltNm">결재서식명</Label>
|
||||
<Input
|
||||
id="elaprTmpltNm"
|
||||
{...register("elaprTmpltNm", { required: true })}
|
||||
/>
|
||||
<Input id="elaprTmpltNm" {...register("elaprTmpltNm", { required: true })} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>문서구분</Label>
|
||||
<CodeSelect
|
||||
upCd={UP_CD.DOC_SE}
|
||||
upCd="ASDOCSECD"
|
||||
value={docSeComCd}
|
||||
onValueChange={(v) => setValue("docSeComCd", v)}
|
||||
placeholder="문서구분 선택"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltExpln">서식 설명</Label>
|
||||
<Textarea id="elaprTmpltExpln" rows={3} {...register("elaprTmpltExpln")} />
|
||||
<Textarea
|
||||
id="elaprTmpltExpln"
|
||||
rows={3}
|
||||
{...register("elaprTmpltExpln")}
|
||||
className="!text-xl"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
|
||||
@@ -5,23 +5,17 @@ import { useState } from "react";
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import { ElaprAplnGrid } from "@/widgets/elapr-apln-grid";
|
||||
import { ElaprTmpltList } from "@/widgets/elapr-tmplt-list";
|
||||
import { PageTitle } from "@common/components";
|
||||
|
||||
const MENU_PATHS = ["디지털행정지원", "시스템관리", "결재선 관리"];
|
||||
|
||||
function ApprovalLineManagementPage() {
|
||||
const [selectedTmplt, setSelectedTmplt] = useState<ElaprTmplt | null>(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-container pb-6">
|
||||
<PageTitle title="결재선 관리" menuPaths={MENU_PATHS} />
|
||||
<div className="space-y-6 px-[1.8rem]">
|
||||
<ElaprTmpltList
|
||||
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onSelectTmplt={setSelectedTmplt}
|
||||
/>
|
||||
<ElaprAplnGrid elaprTmpltId={selectedTmplt?.elaprTmpltId ?? null} />
|
||||
</div>
|
||||
<div className="space-y-6 px-[1.8rem] pb-6">
|
||||
<ElaprTmpltList
|
||||
selectedTmpltId={selectedTmplt?.elaprTmpltId ?? null}
|
||||
onSelectTmplt={setSelectedTmplt}
|
||||
/>
|
||||
<ElaprAplnGrid elaprTmpltId={selectedTmplt?.elaprTmpltId ?? null} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export const ENDPOINT = {
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
CODE: "/cms-service/api/v1/code/cache",
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 { queryClient } from "./query-client";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
export { useDataTable } from "./use-data-table";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { useStore } from "zustand";
|
||||
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
import type { AlertStore } from "../store/alert-store";
|
||||
|
||||
export const useAlertStore = <T>(selector: (store: AlertStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useAlertStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.alertStore, selector);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { useStore } from "zustand";
|
||||
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
import type { MenuStore } from "../store/menu-store";
|
||||
|
||||
export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useMenuStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.menuStore, selector);
|
||||
};
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { createContext, useRef } from "react";
|
||||
|
||||
import { createUserStore } from "../store";
|
||||
import { createAlertStore, createMenuStore, createUserStore } from "../store";
|
||||
|
||||
type StoreContextType = {
|
||||
alertStore: ReturnType<typeof createAlertStore>;
|
||||
menuStore: ReturnType<typeof createMenuStore>;
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
|
||||
@@ -14,6 +16,8 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const storeRef = useRef<StoreContextType | null>(null);
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
alertStore: createAlertStore(),
|
||||
menuStore: createMenuStore(),
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export type AlertState = {
|
||||
message: string;
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
export type AlertActions = {
|
||||
setMessage: (message: string, callback?: () => void) => void;
|
||||
};
|
||||
|
||||
export type AlertStore = AlertState & AlertActions;
|
||||
|
||||
export const defaultInitState: AlertState = {
|
||||
message: "",
|
||||
callback: undefined,
|
||||
};
|
||||
|
||||
export const createAlertStore = (initState: AlertState = defaultInitState) => {
|
||||
return createStore<AlertStore>()((set) => ({
|
||||
...initState,
|
||||
setMessage: (message: string, callback?: () => void) =>
|
||||
set(() => ({ message, callback })),
|
||||
}));
|
||||
};
|
||||
@@ -1 +1,3 @@
|
||||
export { type AlertStore,createAlertStore } from "./alert-store";
|
||||
export { createMenuStore, type MenuStore } from "./menu-store";
|
||||
export { createUserStore, type IUserInfo, type UserStore } from "./user-store";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
interface PageMenuAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export type MenuState = {
|
||||
currentMenuInfo: MenuItem | null;
|
||||
pageMenuAuthority: PageMenuAuthority | null;
|
||||
};
|
||||
|
||||
export type MenuActions = {
|
||||
setCurrentMenuInfo: (menu: MenuItem) => void;
|
||||
setPageMenuAuthority: (authority: PageMenuAuthority) => void;
|
||||
};
|
||||
|
||||
export type MenuStore = MenuState & MenuActions;
|
||||
|
||||
export const defaultInitState: MenuState = {
|
||||
currentMenuInfo: null,
|
||||
pageMenuAuthority: null,
|
||||
};
|
||||
|
||||
export const createMenuStore = (initState: MenuState = defaultInitState) => {
|
||||
return createStore<MenuStore>()((set) => ({
|
||||
...initState,
|
||||
setCurrentMenuInfo: (menu: MenuItem) => set({ currentMenuInfo: menu }),
|
||||
setPageMenuAuthority: (authority: PageMenuAuthority) =>
|
||||
set({ pageMenuAuthority: authority }),
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
function AlertDialog() {
|
||||
const { message, callback, setMessage } = useAlertStore((state) => state);
|
||||
const handleOpenChange = () => {
|
||||
setMessage("");
|
||||
callback?.();
|
||||
};
|
||||
return (
|
||||
<Dialog open={!!message} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-foreground whitespace-pre-line text-center text-[1.4rem]">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button>확인</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AlertDialog };
|
||||
@@ -1,32 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { PageTitle } from "@common/components";
|
||||
|
||||
const mockCurrentMenuInfo = {
|
||||
menuCd: 0,
|
||||
menuKornNm: "메인",
|
||||
menuEngNm: "Main",
|
||||
parentId: null,
|
||||
sortSeq: 0,
|
||||
children: null,
|
||||
levelNo: 0,
|
||||
urlPath: "/",
|
||||
menuLnkgTypeCd: "",
|
||||
menuLnkgSn: 0,
|
||||
menuPath: "메인",
|
||||
qrcd: "",
|
||||
useYn: "Y",
|
||||
otptYn: "Y",
|
||||
newWndwYn: "N",
|
||||
requiredUrlPath: false,
|
||||
};
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
const menuPaths = mockCurrentMenuInfo.menuPath.split(">").map((path) => path.trim());
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const menuPaths = currentMenuInfo?.menuPath
|
||||
?.split(">")
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="bg-container h-auto min-h-full pb-2">
|
||||
<PageTitle title={mockCurrentMenuInfo.menuKornNm} menuPaths={menuPaths} />
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
|
||||
import type { ElaprApln } from "@/entities/elapr";
|
||||
import { generateClientId } from "@/shared/lib";
|
||||
import type { ElaprApln } from "@/entities/elapr";
|
||||
|
||||
import type { ElaprAplnRow } from "../model/types";
|
||||
|
||||
@@ -60,11 +61,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
if (isEditingLocked) return;
|
||||
setEditingRowId(aplnId);
|
||||
setRows((prev) =>
|
||||
prev.map((row) =>
|
||||
row.aplnId === aplnId
|
||||
? { ...row, snapshot: { ...row } }
|
||||
: row,
|
||||
),
|
||||
prev.map((row) => (row.aplnId === aplnId ? { ...row, snapshot: { ...row } } : row)),
|
||||
);
|
||||
},
|
||||
[isEditingLocked],
|
||||
@@ -74,11 +71,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
(aplnId: string, patch: Partial<ElaprAplnRow>) => {
|
||||
if (editingRowId !== aplnId) return;
|
||||
setRows((prev) =>
|
||||
prev.map((row) =>
|
||||
row.aplnId === aplnId
|
||||
? { ...row, ...patch, isDirty: true }
|
||||
: row,
|
||||
),
|
||||
prev.map((row) => (row.aplnId === aplnId ? { ...row, ...patch, isDirty: true } : row)),
|
||||
);
|
||||
},
|
||||
[editingRowId],
|
||||
@@ -114,8 +107,11 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
}, []);
|
||||
|
||||
const reorderRows = useCallback(
|
||||
(activeId: string, overId: string) => {
|
||||
if (isEditingLocked) return;
|
||||
(activeId: string, overId: string): ElaprAplnRow[] => {
|
||||
if (isEditingLocked) return [];
|
||||
|
||||
let newRows: ElaprAplnRow[] = [];
|
||||
|
||||
setRows((prev) => {
|
||||
const oldIndex = prev.findIndex((r) => r.aplnId === activeId);
|
||||
const newIndex = prev.findIndex((r) => r.aplnId === overId);
|
||||
@@ -126,8 +122,11 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
atrzSeqNo: idx + 1,
|
||||
isDirty: true,
|
||||
}));
|
||||
newRows = moved;
|
||||
return moved;
|
||||
});
|
||||
|
||||
return newRows;
|
||||
},
|
||||
[isEditingLocked],
|
||||
);
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
useCreateElaprApln,
|
||||
useDeleteElaprApln,
|
||||
@@ -18,11 +13,21 @@ import {
|
||||
useUpdateElaprApln,
|
||||
} from "@/entities/elapr";
|
||||
import { AprvrPickerDialog, type ReviewerOption } from "@/features/aprvr-picker";
|
||||
import {
|
||||
Button,
|
||||
LoadingWithAnimation,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { useAplnRows } from "../lib/use-apln-rows";
|
||||
import { useDndSensors } from "../lib/use-dnd-sensors";
|
||||
import { validateAplnRow, validateUniqueSeqNo } from "../lib/validators";
|
||||
import type { ElaprAplnRow } from "../model/types";
|
||||
|
||||
import { SortableAplnRow } from "./sortable-apln-row";
|
||||
|
||||
interface ElaprAplnGridProps {
|
||||
@@ -32,7 +37,8 @@ interface ElaprAplnGridProps {
|
||||
function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [pickingRowId, setPickingRowId] = useState<string | null>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const aplnQuery = useElaprAplnPage({
|
||||
elaprTmpltId: elaprTmpltId ?? undefined,
|
||||
@@ -67,20 +73,18 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
reorderRows(String(active.id), String(over.id));
|
||||
const payloadRows = reorderRows(String(active.id), String(over.id));
|
||||
};
|
||||
|
||||
const handleRowSave = async (row: ElaprAplnRow) => {
|
||||
const fieldError = validateAplnRow(row);
|
||||
if (fieldError) {
|
||||
setStatusMessage(fieldError);
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
const dupError = validateUniqueSeqNo(
|
||||
rows.map((r) => (r.aplnId === row.aplnId ? row : r)),
|
||||
);
|
||||
const dupError = validateUniqueSeqNo(rows.map((r) => (r.aplnId === row.aplnId ? row : r)));
|
||||
if (dupError) {
|
||||
setStatusMessage(dupError);
|
||||
alert(dupError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,7 +111,6 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
}
|
||||
|
||||
clearEditing();
|
||||
setStatusMessage(null);
|
||||
await aplnQuery.refetch();
|
||||
};
|
||||
|
||||
@@ -130,7 +133,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
|
||||
const dupError = validateUniqueSeqNo(rows);
|
||||
if (dupError) {
|
||||
setStatusMessage(dupError);
|
||||
alert(dupError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,7 +142,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
for (const row of dirtyRows) {
|
||||
const fieldError = validateAplnRow(row);
|
||||
if (fieldError) {
|
||||
setStatusMessage(fieldError);
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
}
|
||||
}
|
||||
|
||||
setStatusMessage("결재선이 저장되었습니다.");
|
||||
alert("결재선이 저장되었습니다.");
|
||||
await aplnQuery.refetch();
|
||||
};
|
||||
|
||||
@@ -178,54 +181,41 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`rounded-lg border border-border bg-background p-4 ${isDisabled ? "pointer-events-none opacity-50" : ""}`}
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="text-lg font-semibold">결재선 관리</h3>
|
||||
<section className="card card-border mx-0 pt-0">
|
||||
<div className="card-header flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="text-2xl font-semibold">결재선 관리</h3>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{elaprTmpltId
|
||||
? `선택된 서식: ${elaprTmpltId}`
|
||||
: "서식을 선택하면 결재선을 관리할 수 있습니다."}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
color="white"
|
||||
disabled={isDisabled || isEditingLocked}
|
||||
onClick={addRow}
|
||||
>
|
||||
행 추가
|
||||
+ 행 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isDisabled || isEditingLocked}
|
||||
onClick={handleBulkSave}
|
||||
>
|
||||
<Button type="button" disabled={isDisabled || isEditingLocked} onClick={handleBulkSave}>
|
||||
결재선 저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{statusMessage && (
|
||||
<p className="mb-3 text-sm text-primary" role="status">
|
||||
{statusMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{aplnQuery.isPending && elaprTmpltId ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">로딩 중…</p>
|
||||
<div className="flex h-full w-full items-center justify-center py-8">
|
||||
<div className="relative size-[4rem]">
|
||||
<LoadingWithAnimation />
|
||||
</div>
|
||||
</div>
|
||||
) : !elaprTmpltId ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
상단 목록에서 결재서식을 선택해 주세요.
|
||||
</p>
|
||||
) : rows.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
등록된 결재선이 없습니다. [행 추가]로 결재자를 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table variant="line" className="w-full">
|
||||
<Table className="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-10" />
|
||||
@@ -237,7 +227,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<tbody>
|
||||
<TableBody>
|
||||
<SortableContext
|
||||
items={rows.map((r) => r.aplnId)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
@@ -261,7 +251,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</tbody>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
)}
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { Equal } from "lucide-react";
|
||||
|
||||
import { CodeSelect, findCodeLabel, UP_CD, useCodeList } from "@/entities/code";
|
||||
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { Button, Input, TableCell, TableRow } from "@common/components";
|
||||
|
||||
import type { ElaprAplnRow } from "../model/types";
|
||||
|
||||
@@ -38,16 +34,12 @@ function SortableAplnRow({
|
||||
onChange,
|
||||
onPickAprvr,
|
||||
}: SortableAplnRowProps) {
|
||||
const { data: atrzSeCodes } = useCodeList(UP_CD.ATRZ_SE);
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSECD");
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: row.aplnId, disabled: dragDisabled });
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: row.aplnId,
|
||||
disabled: dragDisabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -56,26 +48,24 @@ function SortableAplnRow({
|
||||
};
|
||||
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel =
|
||||
row.atrzSeComNm ?? findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
const atrzSeLabel = row.atrzSeComNm ?? findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
|
||||
return (
|
||||
<TableRow ref={setNodeRef} style={style} className={isEditing ? "bg-accent/30" : undefined}>
|
||||
<TableCell align="center" className="w-10">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="cursor-grab disabled:cursor-not-allowed disabled:opacity-40"
|
||||
size="icon"
|
||||
className="cursor-grab !bg-transparent text-muted-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={dragDisabled}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
aria-label="순서 변경"
|
||||
>
|
||||
<span className="select-none text-muted-foreground" aria-hidden>
|
||||
⋮⋮
|
||||
</span>
|
||||
</button>
|
||||
<Equal className="!size-6" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell align="left">{row.aplnId}</TableCell>
|
||||
<TableCell align="center">{row.aplnId}</TableCell>
|
||||
<TableCell align="center">
|
||||
{isEditing ? (
|
||||
<Input
|
||||
@@ -88,13 +78,13 @@ function SortableAplnRow({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
(row.atrzSeqNo ?? "—")
|
||||
(row.atrzSeqNo ?? "-")
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{isEditing ? (
|
||||
<CodeSelect
|
||||
upCd={UP_CD.ATRZ_SE}
|
||||
upCd="ATRZSECD"
|
||||
className="min-w-[120px]"
|
||||
value={row.atrzSeComCd ?? ""}
|
||||
onValueChange={(v) =>
|
||||
@@ -106,22 +96,20 @@ function SortableAplnRow({
|
||||
placeholder="결재구분"
|
||||
/>
|
||||
) : (
|
||||
atrzSeLabel || "—"
|
||||
atrzSeLabel || "-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-[80px] text-sm">
|
||||
{row.aprvrNm || row.aprvrUuid || "—"}
|
||||
</span>
|
||||
<TableCell align="center">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<span>{row.aprvrNm || row.aprvrUuid || "-"}</span>
|
||||
{isEditing && (
|
||||
<Button type="button" size="sm" variant="outline" onClick={onPickAprvr}>
|
||||
<Button type="button" size="sm" onClick={onPickAprvr}>
|
||||
선택
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">{row.isNew ? "—" : (row.regDt ?? "—")}</TableCell>
|
||||
<TableCell align="center">{row.isNew ? "-" : (row.regDt ?? "-")}</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{isEditing ? (
|
||||
@@ -129,29 +117,16 @@ function SortableAplnRow({
|
||||
<Button type="button" size="sm" onClick={onSave}>
|
||||
저장
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="outline" onClick={onCancel}>
|
||||
<Button type="button" size="sm" color="white" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={rowDisabled}
|
||||
onClick={onStartEdit}
|
||||
>
|
||||
<Button type="button" size="sm" disabled={rowDisabled} onClick={onStartEdit}>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="red"
|
||||
disabled={rowDisabled}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Button type="button" size="sm" color="red" disabled={rowDisabled} onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<ElaprTmplt>();
|
||||
|
||||
@@ -34,20 +34,11 @@ export function createTmpltColumns({ onEdit, onDelete }: CreateTmpltColumnsOptio
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div
|
||||
className="flex justify-center gap-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => onEdit(row.original)}>
|
||||
<div className="flex justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button type="button" size="sm" onClick={() => onEdit(row.original)}>
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="red"
|
||||
onClick={() => onDelete(row.original)}
|
||||
>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type ElaprTmplt,
|
||||
useCreateElaprTmplt,
|
||||
useDeleteElaprTmplt,
|
||||
useElaprTmpltPage,
|
||||
useUpdateElaprTmplt,
|
||||
type ElaprTmplt,
|
||||
} from "@/entities/elapr";
|
||||
import {
|
||||
ElaprTmpltFormDialog,
|
||||
type ElaprTmpltFormValues,
|
||||
} from "@/features/elapr-tmplt-dialog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ElaprTmpltFormDialog, type ElaprTmpltFormValues } from "@/features/elapr-tmplt-dialog";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { createTmpltColumns } from "../model/columns";
|
||||
|
||||
@@ -66,23 +64,19 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => row.elaprTmpltId,
|
||||
onRowSelectionChange: (updater) => {
|
||||
const next =
|
||||
typeof updater === "function" ? updater(rowSelection) : updater;
|
||||
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
||||
setRowSelection(next);
|
||||
const selectedId = Object.keys(next)[0];
|
||||
if (!selectedId) {
|
||||
onSelectTmplt(null);
|
||||
return;
|
||||
}
|
||||
const selected = tmpltQuery.data?.content.find(
|
||||
(t) => t.elaprTmpltId === selectedId,
|
||||
);
|
||||
const selected = tmpltQuery.data?.content.find((t) => t.elaprTmpltId === selectedId);
|
||||
onSelectTmplt(selected ?? null);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
@@ -113,9 +107,9 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-border bg-background p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">전자결재선 관리 목록</h3>
|
||||
<section className="card card-border mx-0 bg-background pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">전자결재선 관리 목록</h3>
|
||||
<Button type="button" onClick={() => setDialogMode("create")}>
|
||||
서식 등록
|
||||
</Button>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
import presetConfig from "../../tailwind.config.preset";
|
||||
|
||||
const config: Config = {
|
||||
presets: [presetConfig],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./shared/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./entities/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./features/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./widgets/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"../../packages/common/components/dist/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
|
||||
Generated
+3
@@ -171,6 +171,9 @@ importers:
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
lucide-react:
|
||||
specifier: ^0.474.0
|
||||
version: 0.474.0(react@19.2.4)
|
||||
next:
|
||||
specifier: 15.3.8
|
||||
version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
||||
Reference in New Issue
Block a user