Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182af7cf09 | ||
|
|
84caaa3977 |
@@ -44,12 +44,6 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
|
||||
```json
|
||||
{
|
||||
// 기타 설정 ...
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/components": "workspace:*", // 추가
|
||||
"react": "^19.0.0", // 제거
|
||||
@@ -149,9 +143,7 @@ export default nextConfig;
|
||||
### 6. globals.css(또는 전역 css) 수정
|
||||
|
||||
```css
|
||||
@import "@common/assets/styles/base.css";
|
||||
@import "@common/assets/styles/diveicon.css";
|
||||
@import "@common/assets/styles/components.css";
|
||||
@import "@common/assets/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -192,8 +184,3 @@ http://dev.onasys.co.kr:8000
|
||||
|
||||
## Swagger
|
||||
http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service
|
||||
|
||||
## 개발 가이드
|
||||
- 사이드바 메뉴 클릭 시 "menuCd" 데이터 쿠키 저장 처리
|
||||
- 쿠키명 : menu-code-key
|
||||
- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://localhost:8000
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ApimanPage } from "@/pages/apiman";
|
||||
|
||||
export default function Page() {
|
||||
return <ApimanPage />;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/apiman/ui/modify-page";
|
||||
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: number}>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <VModifyPage authorizationNo={id} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CreatePage } from "@/pages/apiman/ui/create-page";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { DashboardPage } from "@/pages/dashboard";
|
||||
export default DashboardPage;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { GroupPage } from "@/pages/group";
|
||||
|
||||
export default function Page() {
|
||||
return <GroupPage />;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/group/ui/modify-page";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <VModifyPage groupId={id} />;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CreatePage } from "@/pages/group/ui/create-page";
|
||||
export default CreatePage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/image";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ serial: string }>;
|
||||
}) {
|
||||
const { serial } = await params;
|
||||
return <VModifyPage serial={serial} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/image";
|
||||
export default CreatePage;
|
||||
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { MenuItem } from "@common/components/api";
|
||||
import { BaseLayout } from "@common/components/ui";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
}: {
|
||||
@@ -14,8 +12,6 @@ export default function WithBaseLayout({
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { setCurrentMenuInfo } = useMenuStore((state) => state);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
@@ -24,12 +20,8 @@ export default function WithBaseLayout({
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
<BaseLayout currentPath={pathname ?? ""} handleMenuClick={handleMenuClick}>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
</QueryClientProvider>
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
{children}
|
||||
<AlertDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export { GroupApi } from "./api/group-api";
|
||||
export {
|
||||
type GroupParams,
|
||||
type Group,
|
||||
type GroupFormData,
|
||||
} from "./model/types";
|
||||
@@ -1,29 +0,0 @@
|
||||
export interface Group {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string | null;
|
||||
mdfr: string | null;
|
||||
authrtId: string;
|
||||
siteId: string | null;
|
||||
}
|
||||
|
||||
export interface GroupParams {
|
||||
groupNm?: string;
|
||||
groupSeCd?: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
export interface GroupFormData {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
authrtId: string;
|
||||
siteId: string;
|
||||
}
|
||||
@@ -2,26 +2,27 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT, SITE_ID } from "@/shared/config";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const pathname = usePathname();
|
||||
const { data: menuRoles } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => MenuApi.getMenuRoles(SITE_ID),
|
||||
});
|
||||
const { setCurrentMenuInfo } = useMenuStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
if (!menuRoles) return;
|
||||
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
|
||||
|
||||
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
|
||||
if (!currentMenu) return;
|
||||
|
||||
setCurrentMenuInfo(currentMenu);
|
||||
})();
|
||||
}, []);
|
||||
if (!currentMenu) return;
|
||||
setCurrentMenuInfo(currentMenu);
|
||||
}, [menuRoles]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export class SiteApiService implements SiteService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateSite(siteId: string, data: SiteFormData): Promise<void> {
|
||||
public async modifySite(siteId: string, data: SiteFormData): Promise<void> {
|
||||
const validatedData = siteFormSchema.parse(data);
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface SiteDetail extends SiteFormData {
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
rgTrNm: string;
|
||||
mdfrNm: string;
|
||||
mdFrNm: string;
|
||||
}
|
||||
|
||||
export const siteFormSchema = z.object({
|
||||
@@ -54,6 +54,6 @@ export type SiteList = PageableList<Site>;
|
||||
export interface SiteService {
|
||||
getSiteList(params: SiteParams): Promise<SiteList>;
|
||||
siteDetail(siteId: string): Promise<SiteDetail>;
|
||||
updateSite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
modifySite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
createSite(data: SiteFormData): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { Page } from "@/shared/types";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface Api {
|
||||
authorizationNo: number; // API 일련번호
|
||||
authorizationName: string; // API 명
|
||||
urlPatternValue: string; // URL 패턴
|
||||
httpMethodCode: string; // 하이퍼텍스트 전송 프로토콜 메쏘드 값 (예: GET, POST)
|
||||
sortSeq: number; // 정렬 순서
|
||||
useYn: string; // 사용 여부 (Y/N)
|
||||
regDt: string; // 등록 일시
|
||||
rgtr: string; // 등록자 UUID
|
||||
mdfcnDt: string | null; // 갱신 일시
|
||||
mdfr: string | null; // 갱신자 UUID
|
||||
}
|
||||
|
||||
export interface ApiParams {
|
||||
authorizationName?: string; // API 명 검색
|
||||
httpMethodCode?: string; // HTTP 메소드 검색
|
||||
useYn?: string; // 사용 여부 검색
|
||||
page?: number; // 페이지 번호
|
||||
size?: number; // 페이지 크기
|
||||
}
|
||||
|
||||
export interface ApiFormData {
|
||||
authorizationNo: number | null; // API 일련번호 (신규 등록 시 null)
|
||||
authorizationName: string; // API 명
|
||||
urlPatternValue: string; // URL 패턴
|
||||
httpMethodCode: string; // HTTP 메소드
|
||||
sortSeq: number; // 정렬 순서
|
||||
useYn: string; // 사용 여부
|
||||
}
|
||||
|
||||
export const ApimanApi = {
|
||||
getApiList: async (params: ApiParams) => {
|
||||
const { data } = await client.get<Page<Api>>(
|
||||
`${ENDPOINT.USER_SERVICE.AUTHRTS}`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
getApi: async (authorizationNo: number) => {
|
||||
const { data } = await client.get<Api>(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
createApi: async (apiData: ApiFormData) => {
|
||||
const { data } = await client.post<Api>(ENDPOINT.USER_SERVICE.AUTHRTS, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateApi: async (authorizationNo: number, apiData: ApiFormData) => {
|
||||
const { data } = await client.put<Api>(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteApi: async (authorizationNo: number) => {
|
||||
const { data } = await client.delete<void>(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export { ApimanPage } from "./ui/apiman-page";
|
||||
export { ApimanForm } from "./ui/apiman-form";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { ApimanApi } from "./api/apiman-api";
|
||||
@@ -1,111 +0,0 @@
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Button } from "@common/components";
|
||||
import type { Api } from "../api/apiman-api";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// API 권한 관리를 위한 타입 정의
|
||||
type ApiTableMeta = {
|
||||
openRoleDialog: (api: Api) => void;
|
||||
};
|
||||
|
||||
export const columns: ColumnDef<Api>[] = [
|
||||
{
|
||||
accessorKey: "authorizationNo",
|
||||
header: "번호",
|
||||
cell: ({ row }) => <div>{row.original.authorizationNo}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "authorizationName",
|
||||
header: "API 명",
|
||||
cell: ({ row }) => <div>{row.original.authorizationName}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "urlPatternValue",
|
||||
header: "URL 패턴",
|
||||
cell: ({ row }) => (
|
||||
<div className="max-w-md truncate text-left" title={row.original.urlPatternValue}>
|
||||
{row.original.urlPatternValue}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "httpMethodCode",
|
||||
header: "HTTP 메소드",
|
||||
cell: ({ row }) => {
|
||||
const method = row.original.httpMethodCode;
|
||||
let color = "bg-gray-500";
|
||||
|
||||
switch (method) {
|
||||
case "GET":
|
||||
color = "bg-blue-500";
|
||||
break;
|
||||
case "POST":
|
||||
color = "bg-green-500";
|
||||
break;
|
||||
case "PUT":
|
||||
color = "bg-orange-500";
|
||||
break;
|
||||
case "DELETE":
|
||||
color = "bg-red-500";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`${color} text-white`}>
|
||||
{method}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "sortSeq",
|
||||
header: "정렬 순서",
|
||||
cell: ({ row }) => <div className="text-center">{row.original.sortSeq}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용 여부",
|
||||
cell: ({ row }) => {
|
||||
const useYn = row.original.useYn;
|
||||
return (
|
||||
<Badge className={useYn === "Y" ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}>
|
||||
{useYn === "Y" ? "사용" : "미사용"}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "수정일시",
|
||||
cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "권한",
|
||||
cell: ({ row, table }) => {
|
||||
// 메타 데이터에서 권한 다이얼로그 열기 함수 가져오기
|
||||
const meta = table.options.meta as ApiTableMeta | undefined;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-16 bg-blue-500 text-white hover:bg-blue-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // 행 클릭 이벤트 전파 방지
|
||||
if (meta?.openRoleDialog) {
|
||||
// console.log(row.original);
|
||||
meta.openRoleDialog(row.original);
|
||||
}
|
||||
}}
|
||||
>
|
||||
권한
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,201 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Input,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import { ApiRoleApi, type ApiRoleByApi } from "../../apirole/api/apirole-api";
|
||||
|
||||
// 페이지네이션 응답을 위한 인터페이스
|
||||
interface Page<T> {
|
||||
content: T[];
|
||||
pageable: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
};
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// API 역할 인터페이스 정의
|
||||
interface Role extends ApiRoleByApi {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
interface ApiRoleDialogProps {
|
||||
api: ApiRoleByApi | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (roleIds: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ApiRoleDialog({ api, isOpen, onClose, onSave }: ApiRoleDialogProps): JSX.Element {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// API에서 역할 데이터 가져오기
|
||||
useEffect(() => {
|
||||
if (isOpen && api && api.authorizationNo) {
|
||||
// 실제 API 호출로 데이터 가져오기
|
||||
ApiRoleApi.getApiRoleByApiList({ authorizationNo: api.authorizationNo })
|
||||
.then((data: Page<ApiRoleByApi>) => {
|
||||
// 데이터를 Role 인터페이스에 맞게 변환
|
||||
const rolesWithSelected = data.content.map(role => ({
|
||||
...role,
|
||||
mode: role.useYn ==null?"C":"R",
|
||||
selected: role.useYn === "Y"
|
||||
}));
|
||||
setRoles(rolesWithSelected);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("역할 데이터 가져오기 오류:", error);
|
||||
// 오류 발생 시 빈 배열 설정
|
||||
setRoles([]);
|
||||
});
|
||||
}
|
||||
}, [isOpen, api]);
|
||||
|
||||
const handleToggleRole = (roleId: string) => {
|
||||
setRoles(
|
||||
roles.map((role) =>
|
||||
role.roleId === roleId ? { ...role, useYn: role.useYn === "Y" ? "N" : "Y", mode: role.mode === "R" ? "U" : "C" } : role
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
//const selectedRoleIds = roles.filter((role) => role.useYn === "Y").map((role) => role.roleId ?? "");
|
||||
await onSave(roles);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("권한 저장 중 오류 발생:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Dialog가 닫힐 때 호출되는 함수
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
API 권한 설정 - {api?.authorizationName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="card mb-4">
|
||||
<Table>
|
||||
<colgroup>
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell as="th">API 명</TableCell>
|
||||
<TableCell className="border">
|
||||
{api?.authorizationName || ""}
|
||||
</TableCell>
|
||||
<TableCell as="th">HTTP 메소드</TableCell>
|
||||
<TableCell className="border">
|
||||
{api?.httpMethodCode || ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell as="th">URL 패턴</TableCell>
|
||||
<TableCell colSpan={3} className="border">
|
||||
{api?.urlPatternValue || ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[400px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px]">허용</TableHead>
|
||||
<TableHead className="w-[150px]">권한 ID</TableHead>
|
||||
<TableHead>권한명</TableHead>
|
||||
<TableHead>권한내용</TableHead>
|
||||
<TableHead className="w-[80px]">사용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{roles.map((role) => (
|
||||
<TableRow key={role.roleId}>
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
checked={role.useYn === "Y"}
|
||||
onCheckedChange={() => handleToggleRole(role.roleId)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{role.roleId}</TableCell>
|
||||
<TableCell>{role.roleName}</TableCell>
|
||||
<TableCell>{role.roleContent}</TableCell>
|
||||
<TableCell>
|
||||
<span className={role.useYn === "Y" ? "text-green-600" : "text-red-600"}>
|
||||
{role.useYn === "Y" ? "사용" : "미사용"}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{roles.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-4">
|
||||
권한 정보가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "저장 중..." : "저장"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
// apps/cs/src/pages/apiman/ui/apiman-form.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import type { ApiFormData } from "../api/apiman-api";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
interface ApimanFormProps {
|
||||
form: UseFormReturn<ApiFormData>;
|
||||
authorizationNo?: number;
|
||||
handleSave: (data: ApiFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function ApimanForm({ form, authorizationNo, handleSave, handleDelete }: ApimanFormProps) {
|
||||
const isModifyMode = !!authorizationNo;
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ApiFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{isModifyMode && (
|
||||
<TableRow>
|
||||
<TableHead>API 일련번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authorizationNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value?.toString() ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>API 명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authorizationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="API 명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>URL 패턴</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="urlPatternValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="URL 패턴을 입력하세요 (예: /api/users/{id})"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>요청 메소드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="httpMethodCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value || undefined}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="!text-[1.3rem]">
|
||||
<SelectValue placeholder="요청 메소드를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||
<SelectItem value="OPTIONS">OPTIONS</SelectItem>
|
||||
<SelectItem value="HEAD">TRACE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬 순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
value={field.value?.toString() ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" variant="destructive" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ApimanForm };
|
||||
@@ -1,213 +0,0 @@
|
||||
"use client";
|
||||
import "@common/assets/styles/grid.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Pagination
|
||||
} from "@common/components";
|
||||
|
||||
import { ApimanApi, type Api, type ApiParams } from "../api/apiman-api";
|
||||
import { type ApiRoleByApi } from "../../apirole/api/apirole-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/apiman-table-columns";
|
||||
import { ApiRoleDialog } from "./api-role-dialog";
|
||||
|
||||
function ApimanPage() {
|
||||
const router = useRouter();
|
||||
const [apiQuery, setApiQuery] = useState<ApiParams>({
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 10,
|
||||
});
|
||||
|
||||
// API 역할 다이얼로그 상태 관리
|
||||
const [apiRoleDialog, setApiRoleDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
api: ApiRoleByApi | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
api: null,
|
||||
});
|
||||
|
||||
const { data: apiList, refetch, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS, apiQuery],
|
||||
queryFn: () => ApimanApi.getApiList(apiQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: apiList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
pageCount: apiList?.totalPages ?? -1,
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: apiQuery.page ?? 0,
|
||||
pageSize: apiQuery.size ?? 10,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
openRoleDialog: (api: ApiRoleByApi) => {
|
||||
setApiRoleDialog({
|
||||
isOpen: true,
|
||||
api,
|
||||
});
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
const newPagination =
|
||||
typeof updater === "function"
|
||||
? updater({ pageIndex: apiQuery.page ?? 0, pageSize: apiQuery.size ?? 10 })
|
||||
: updater;
|
||||
|
||||
setApiQuery((prev) => ({
|
||||
...prev,
|
||||
page: newPagination.pageIndex,
|
||||
size: newPagination.pageSize,
|
||||
}));
|
||||
},
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [apiQuery, refetch]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setApiQuery(prev => ({ ...prev, page: 0 }));
|
||||
refetch();
|
||||
};
|
||||
|
||||
|
||||
const handleRowClick = (api: Api) => {
|
||||
router.push(`/apiman/register/${api.authorizationNo}`);
|
||||
};
|
||||
|
||||
// API 역할 저장 핸들러
|
||||
const handleSaveRoles = async (roles: ApiRoleByApi[]) => {
|
||||
try {
|
||||
// 실제 구현에서는 API 호출로 대체
|
||||
console.log(`API ${apiRoleDialog.api?.authorizationNo}에 권한 저장:`, roles);
|
||||
// 성공 시 다이얼로그 닫기
|
||||
setApiRoleDialog({ isOpen: false, api: null });
|
||||
// 목록 새로고침
|
||||
refetch();
|
||||
} catch (error) {
|
||||
console.error("권한 저장 중 오류 발생:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
{/* API 역할 다이얼로그 */}
|
||||
<ApiRoleDialog
|
||||
api={apiRoleDialog.api}
|
||||
isOpen={apiRoleDialog.isOpen}
|
||||
onClose={() => setApiRoleDialog({ isOpen: false, api: null })}
|
||||
onSave={handleSaveRoles}
|
||||
/>
|
||||
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
|
||||
<Label htmlFor="httpMethodCode" className="mr-2 text-label">요청 메소드</Label>
|
||||
<Select
|
||||
value={apiQuery.httpMethodCode || undefined}
|
||||
onValueChange={(value) => setApiQuery( prev => ({ ...prev, httpMethodCode: value === "ALL" ? "" : value }))}
|
||||
>
|
||||
<SelectTrigger id="httpMethodCode" className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]">
|
||||
<SelectValue placeholder="요청 메소드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="OPTIONS">OPTIONS</SelectItem>
|
||||
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||
<SelectItem value="HEAD">HEAD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label htmlFor="authorizationName" className="mr-2 text-label">API 명</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="authorizationName"
|
||||
value={apiQuery.authorizationName || ""}
|
||||
onChange={(e) => setApiQuery( prev => ({ ...prev, authorizationName: e.target.value }))}
|
||||
placeholder="API 명 입력"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">모듈API 목록</h3>
|
||||
<Button onClick={() => router.push("/apiman/register")}>+등록</Button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
table={table}
|
||||
isLoading={isLoading}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-lm text-muted-foreground">
|
||||
총 {apiList?.totalElements ?? 0} 건
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={apiQuery.page + 1}
|
||||
totalPages={apiList?.totalPages ?? 0}
|
||||
onPageChange={(page) =>
|
||||
setApiQuery(prev => ({ ...prev, page: page - 1 }))
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={String(apiQuery.size)}
|
||||
onValueChange={(value) =>
|
||||
setApiQuery(prev => ({ ...prev, page: 0, size: Number(value) }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[2,10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={String(pageSize)}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ApimanPage };
|
||||
@@ -1,76 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApimanApi, type ApiFormData } from "../api/apiman-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ApimanForm } from "./apiman-form";
|
||||
import { z } from "zod";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// API 스키마 정의
|
||||
const apiFormSchema = z.object({
|
||||
authorizationNo: z.number().nullable(),
|
||||
authorizationName: z.string().min(1, "API 명을 입력해주세요"),
|
||||
urlPatternValue: z.string().min(1, "URL 패턴을 입력해주세요"),
|
||||
httpMethodCode: z.string().min(1, "HTTP 메소드를 선택해주세요"),
|
||||
sortSeq: z.number().min(0, "정렬 순서는 0 이상이어야 합니다"),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createApi } = useMutation({
|
||||
mutationFn: (data: ApiFormData) => ApimanApi.createApi(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS],
|
||||
});
|
||||
router.push("/apiman");
|
||||
});
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
if (axiosError.response?.data) {
|
||||
alert(axiosError.response.data.message);
|
||||
} else if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
} else {
|
||||
alert("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ApiFormData>({
|
||||
resolver: zodResolver(apiFormSchema),
|
||||
defaultValues: {
|
||||
authorizationNo: null,
|
||||
authorizationName: "",
|
||||
urlPatternValue: "",
|
||||
httpMethodCode: "",
|
||||
sortSeq: 0,
|
||||
useYn: "Y"
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">API 등록</h3>
|
||||
</div>
|
||||
<ApimanForm
|
||||
form={form}
|
||||
handleSave={createApi}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApimanApi, type ApiFormData } from "../api/apiman-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ApimanForm } from "./apiman-form";
|
||||
import { z } from "zod";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// API 스키마 정의
|
||||
const apiFormSchema = z.object({
|
||||
authorizationNo: z.number().nullable(),
|
||||
authorizationName: z.string().min(1, "API 명을 입력해주세요"),
|
||||
urlPatternValue: z.string().min(1, "URL 패턴을 입력해주세요"),
|
||||
httpMethodCode: z.string().min(1, "HTTP 메소드를 선택해주세요"),
|
||||
sortSeq: z.number(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
function ModifyPage({ authorizationNo }: { authorizationNo: number }) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: apiDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS, authorizationNo],
|
||||
queryFn: () => ApimanApi.getApi(authorizationNo),
|
||||
select: (data) => {
|
||||
const keys = Object.keys(apiFormSchema.shape);
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([key]) => keys.includes(key)),
|
||||
);
|
||||
return filteredData;
|
||||
},
|
||||
enabled: !!authorizationNo,
|
||||
});
|
||||
|
||||
const { mutateAsync: updateApi } = useMutation({
|
||||
mutationFn: (data: ApiFormData) => ApimanApi.updateApi(authorizationNo, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS],
|
||||
});
|
||||
router.back();
|
||||
});
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
if (axiosError.response?.data) {
|
||||
alert(axiosError.response.data.message);
|
||||
} else if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
} else {
|
||||
alert("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteApi } = useMutation({
|
||||
mutationFn: () => ApimanApi.deleteApi(authorizationNo),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
if (axiosError.response?.data) {
|
||||
alert(axiosError.response.data.message);
|
||||
} else if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
} else {
|
||||
alert("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ApiFormData>({
|
||||
resolver: zodResolver(apiFormSchema),
|
||||
values: apiDetail as ApiFormData,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">API 수정</h3>
|
||||
</div>
|
||||
<ApimanForm
|
||||
form={form}
|
||||
authorizationNo={authorizationNo}
|
||||
handleSave={async (data) => {
|
||||
await updateApi(data);
|
||||
return;
|
||||
}}
|
||||
handleDelete={deleteApi}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { Page } from "@/shared/types";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface ApiRole {
|
||||
roleId?: string;
|
||||
/** @format int64 */
|
||||
authorizationNo?: number;
|
||||
authorizationName?: string;
|
||||
urlPatternValue?: string;
|
||||
httpMethodCode?: string;
|
||||
/** @format int32 */
|
||||
sortSeq?: number;
|
||||
createdAt?: boolean;
|
||||
}
|
||||
/** API 에서 역할 추가 (API에 대한 역할) */
|
||||
export interface ApiRoleByApi {
|
||||
roleId?: string;
|
||||
roleName?: string;
|
||||
roleContent?: string;
|
||||
useYn?: string;
|
||||
/** @format int64 */
|
||||
authorizationNo?: number;
|
||||
mode?:string;
|
||||
}
|
||||
|
||||
export interface ApiRoleParams {
|
||||
authorizationNo?: number; // API 일련번호 검색
|
||||
roleId?: string; // API 역할 검색
|
||||
page?: number; // 페이지 번호
|
||||
size?: number; // 페이지 크기
|
||||
}
|
||||
|
||||
export interface ApiRoleFormData {
|
||||
authorizationNo: number | null; // API 일련번호 (신규 등록 시 null)
|
||||
authorizationName: string; // API 명
|
||||
urlPatternValue: string; // URL 패턴
|
||||
httpMethodCode: string; // HTTP 메소드
|
||||
sortSeq: number; // 정렬 순서
|
||||
useYn: string; // 사용 여부
|
||||
}
|
||||
|
||||
export const ApiRoleApi = {
|
||||
getApiRoleList: async (params: ApiRoleParams) => {
|
||||
const { data } = await client.get<Page<ApiRole>>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
getApiRoleByApiList: async (params: ApiRoleParams) => {
|
||||
const { data } = await client.get<Page<ApiRoleByApi>>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/api`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
getApiRole: async (authorizationNo: number) => {
|
||||
const { data } = await client.get<ApiRole>(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
createApiRole: async (apiData: ApiRoleFormData) => {
|
||||
const { data } = await client.post<ApiRole>(ENDPOINT.USER_SERVICE.ROLES_AUTHRTS, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateApiRole: async (authorizationNo: number, apiData: ApiRoleFormData) => {
|
||||
const { data } = await client.put<ApiRole>(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteApiRole: async (authorizationNo: number) => {
|
||||
const { data } = await client.delete<void>(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -4,8 +4,10 @@ import Image from "next/image";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
function LoginPage() {
|
||||
const router = useRouter();
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -14,7 +16,7 @@ function LoginPage() {
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/dashboard";
|
||||
router.push("/site");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { DashboardPage } from "./ui/dashboard-page";
|
||||
@@ -1,5 +0,0 @@
|
||||
function DashboardPage() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
@@ -1,64 +0,0 @@
|
||||
//import { http } from "@/libs/axios";
|
||||
import type { Page } from "@/shared/types";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface Group {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string | null;
|
||||
mdfr: string | null;
|
||||
authrtId: string;
|
||||
siteId: string | null;
|
||||
}
|
||||
|
||||
export interface GroupParams {
|
||||
groupNm?: string;
|
||||
groupSeCd?: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
export interface GroupFormData {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
authrtId: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
|
||||
export const GroupApi = {
|
||||
getGroupList: async (params: GroupParams) => {
|
||||
const { data } = await client.get<Page<Group>>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP}`
|
||||
, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
getGroup: async (groupId: string) => {
|
||||
const { data } = await client.get<Group>(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
createGroup: async (groupData: GroupFormData) => {
|
||||
const { data } = await client.post<Group>(ENDPOINT.CMS_SERVICE.GROUP, groupData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateGroup: async (groupId: string, groupData: GroupFormData) => {
|
||||
const { data } = await client.put<Group>(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`, groupData);
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteGroup: async (groupId: string) => {
|
||||
const { data } = await client.delete<void>(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { GroupPage } from "./ui/group-page";
|
||||
@@ -1,73 +0,0 @@
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { type Group } from "../api/group-api";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const columns: ColumnDef<Group>[] = [
|
||||
/* {
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}, */
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
accessorFn: (_, index) => index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
header: "그룹 ID",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "groupSeNm",
|
||||
header: "그룹구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "delYn",
|
||||
header: "삭제여부",
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.original.delYn === "Y"}
|
||||
disabled
|
||||
aria-label="삭제여부"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "siteId",
|
||||
header: "사이트 ID",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "roleNm",
|
||||
header: "권한",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "수정일시",
|
||||
cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"),
|
||||
}
|
||||
];
|
||||
@@ -1,71 +0,0 @@
|
||||
// apps/cs/src/pages/group/ui/create-page.tsx
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { GroupApi, type GroupFormData } from "../api/group-api";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { GroupForm } from "./group-form";
|
||||
import { z } from "zod";
|
||||
|
||||
const groupFormSchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string().min(1, "그룹명을 입력해주세요"),
|
||||
groupSeCd: z.string().min(1, "그룹구분을 선택해주세요"),
|
||||
groupExpln: z.string(),
|
||||
delYn: z.string(),
|
||||
authrtId: z.string(),
|
||||
siteId: z.string(),
|
||||
});
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createGroup } = useMutation({
|
||||
mutationFn: (data: GroupFormData) => GroupApi.createGroup(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
onError: (error) => {
|
||||
/*{
|
||||
"timestamp": "2025-04-07T14:30:15.471581",
|
||||
"message": "해당 권한이 존재하지 않습니다. ID = sd",
|
||||
"status": 400,
|
||||
"code": "B001",
|
||||
"errors": []
|
||||
}*/
|
||||
if(error.response.data){
|
||||
alert(error.response.data.message);
|
||||
}else{
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<GroupFormData>({
|
||||
resolver: zodResolver(groupFormSchema),
|
||||
defaultValues: {
|
||||
groupId: "",
|
||||
groupNm: "",
|
||||
groupSeCd: "",
|
||||
groupExpln: "",
|
||||
delYn: "N",
|
||||
authrtId: "",
|
||||
siteId: "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 등록</h3>
|
||||
</div>
|
||||
<GroupForm form={form} handleSave={createGroup} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,258 +0,0 @@
|
||||
// apps/cs/src/pages/group/ui/group-form.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import type { GroupFormData } from "../api/group-api";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
interface GroupFormProps {
|
||||
form: UseFormReturn<GroupFormData>;
|
||||
groupId?: string;
|
||||
handleSave: (data: GroupFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function GroupForm({ form, groupId, handleSave, handleDelete }: GroupFormProps) {
|
||||
const isModifyMode = !!groupId;
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: GroupFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>그룹 ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="그룹명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupSeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="GROUPSECD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="그룹설명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 ID를 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>권한 ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authrtId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="권한 ID를 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>삭제여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="delYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="delYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="delY" />
|
||||
<Label htmlFor="delY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="delN" />
|
||||
<Label htmlFor="delN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupForm };
|
||||
@@ -1,145 +0,0 @@
|
||||
"use client";
|
||||
import "@common/assets/styles/grid.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Pagination
|
||||
} from "@common/components";
|
||||
|
||||
import { GroupApi, type Group, type GroupParams } from "../api/group-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/group-table-columns";
|
||||
|
||||
function GroupPage() {
|
||||
const router = useRouter();
|
||||
const [groupQuery, setGroupQuery] = useState<GroupParams>({
|
||||
groupNm: "",
|
||||
groupSeCd: "",
|
||||
page: 0,
|
||||
size: 2,
|
||||
});
|
||||
|
||||
const { data: groupList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP, groupQuery],
|
||||
queryFn: () => GroupApi.getGroupList(groupQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: groupList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
pageCount: groupList?.totalPages ?? -1,
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: groupQuery.page,
|
||||
pageSize: groupQuery.size,
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
if (typeof updater === 'function') {
|
||||
const newPagination = updater({
|
||||
pageIndex: groupQuery.page,
|
||||
pageSize: groupQuery.size,
|
||||
});
|
||||
setGroupQuery(prev => ({
|
||||
...prev,
|
||||
page: newPagination.pageIndex,
|
||||
size: newPagination.pageSize,
|
||||
}));
|
||||
}
|
||||
},
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setGroupQuery(prev => ({ ...prev, page: 0 })); // 검색 시 첫 페이지로 이동
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Group) => {
|
||||
router.push(`/group/register/${row.groupId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [groupQuery, refetch]); //[groupQuery.page, groupQuery.size]); // 페이지나 사이즈 변경 시 자동 조회
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
그룹명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={groupQuery.groupNm}
|
||||
onChange={(e) => setGroupQuery(prev => ({ ...prev, groupNm: e.target.value }))}
|
||||
/>
|
||||
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 목록</h3>
|
||||
<Button onClick={() => router.push("/group/register")}>+등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-lm text-muted-foreground">
|
||||
총 {groupList?.totalElements ?? 0} 건
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={groupQuery.page + 1}
|
||||
totalPages={groupList?.totalPages ?? 0}
|
||||
onPageChange={(page) =>
|
||||
setGroupQuery(prev => ({ ...prev, page: page - 1 }))
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={String(groupQuery.size)}
|
||||
onValueChange={(value) =>
|
||||
setGroupQuery(prev => ({ ...prev, page: 0, size: Number(value) }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[2,10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={String(pageSize)}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupPage };
|
||||
@@ -1,95 +0,0 @@
|
||||
// apps/cs/src/pages/group/ui/modify-page.tsx
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { GroupApi, type GroupFormData } from "../api/group-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { GroupForm } from "./group-form";
|
||||
import { z } from "zod";
|
||||
|
||||
// Group 스키마 정의
|
||||
const groupFormSchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string().min(1, "그룹명을 입력해주세요"),
|
||||
groupSeCd: z.string().min(1, "그룹구분을 선택해주세요"),
|
||||
groupExpln: z.string(),
|
||||
delYn: z.string(),
|
||||
authrtId: z.string(),
|
||||
siteId: z.string(),
|
||||
});
|
||||
|
||||
function ModifyPage({ groupId }: { groupId: string }) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: groupDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP, groupId],
|
||||
queryFn: () => GroupApi.getGroup(groupId),
|
||||
select: (data) => {
|
||||
const keys = Object.keys(groupFormSchema.shape);
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([key]) => keys.includes(key)),
|
||||
);
|
||||
return filteredData;
|
||||
},
|
||||
enabled: !!groupId,
|
||||
});
|
||||
|
||||
const { mutateAsync: updateGroup } = useMutation({
|
||||
mutationFn: (data: GroupFormData) => GroupApi.updateGroup(groupId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP],
|
||||
});
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if(error.response.data){
|
||||
alert(error.response.data.message);
|
||||
}else{
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteGroup } = useMutation({
|
||||
mutationFn: () => GroupApi.deleteGroup(groupId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
onError: (error) => {
|
||||
if(error.response.data){
|
||||
alert(error.response.data.message);
|
||||
}else{
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<GroupFormData>({
|
||||
resolver: zodResolver(groupFormSchema),
|
||||
values: groupDetail as GroupFormData,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 수정</h3>
|
||||
</div>
|
||||
<GroupForm
|
||||
form={form}
|
||||
groupId={groupId}
|
||||
handleSave={updateGroup}
|
||||
handleDelete={deleteGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1,47 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Image,
|
||||
ImageFormData,
|
||||
ImageList,
|
||||
ImageParams,
|
||||
ImageService,
|
||||
} from "../model/image-interface";
|
||||
|
||||
export class ImageApiService implements ImageService {
|
||||
public async getImageList(params: ImageParams): Promise<ImageList> {
|
||||
const response = await client.get<ImageList>(
|
||||
`${ENDPOINT.CMS_SERVICE.IMAGES}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getImageInfo(imgSn: string): Promise<Image> {
|
||||
const response = await client.get<Image>(
|
||||
`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createImage(data: ImageFormData): Promise<void> {
|
||||
const response = await client.post<void>(ENDPOINT.CMS_SERVICE.IMAGES, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateImage(imgSn: string, data: ImageFormData): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteImage(imgSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageApi = new ImageApiService();
|
||||
@@ -1,3 +1 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { ImagePage } from "./ui/image-page";
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Upload, type HttpResponse } from "tus-js-client";
|
||||
import { getCookie, setCookie } from "@/shared/lib";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
|
||||
type UploadOptions = {
|
||||
handleUploadStart?: (upload: Upload) => void;
|
||||
handleProgress?: (percentage: string) => void;
|
||||
handleSuccess?: (upload: Upload) => void;
|
||||
handleResponse?: (res: HttpResponse) => void;
|
||||
};
|
||||
|
||||
export const handleUpload = (files: FileList, options?: UploadOptions) => {
|
||||
let metadata = {};
|
||||
|
||||
const file = files.item(0);
|
||||
if (!file) return;
|
||||
metadata = {
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
};
|
||||
|
||||
const atflCd = getCookie("atfl-cd-key");
|
||||
if (atflCd) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
atflCd,
|
||||
};
|
||||
}
|
||||
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024,
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
metadata,
|
||||
onError(error) {
|
||||
console.error(`Failed because: ${error}`);
|
||||
},
|
||||
onProgress(bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
options?.handleProgress?.(percentage);
|
||||
},
|
||||
onAfterResponse(req, res) {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? "");
|
||||
options?.handleResponse?.(res);
|
||||
},
|
||||
onSuccess() {
|
||||
options?.handleSuccess?.(upload);
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
options?.handleUploadStart?.(upload);
|
||||
|
||||
console.log("upload start >>>>>", upload);
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const IMAGE_TYPE = {
|
||||
IMGSUB: "IMGSUB",
|
||||
IMGMAIN: "IMGMAIN",
|
||||
IMGLOGIN: "IMGLOGIN",
|
||||
} as const;
|
||||
|
||||
export type ImageType = (typeof IMAGE_TYPE)[keyof typeof IMAGE_TYPE];
|
||||
|
||||
export interface ImageParams extends PageableParams {
|
||||
keyword: string;
|
||||
imgTypeCd: string;
|
||||
useYn: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
imgSn: number;
|
||||
imgTypeCd: string;
|
||||
imgTypeNm: string;
|
||||
imgTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
atflYn: string;
|
||||
sortSeq: number;
|
||||
atflCd: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const imageFormSchema = z.object({
|
||||
imgTtl: z.string(),
|
||||
sortSeq: z.number(),
|
||||
imgTypeCd: z.string(),
|
||||
atflYn: z.string(),
|
||||
atflCd: z.string().nullish(),
|
||||
lnkgUrlAddr: z.string().nullish(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type ImageFormData = z.infer<typeof imageFormSchema>;
|
||||
export type ImageList = PageableList<Image>;
|
||||
|
||||
export interface ImageService {
|
||||
getImageList: (params: ImageParams) => Promise<ImageList>;
|
||||
getImageInfo: (imgSn: string) => Promise<Image>;
|
||||
createImage: (data: ImageFormData) => Promise<void>;
|
||||
updateImage: (imgSn: string, data: ImageFormData) => Promise<void>;
|
||||
deleteImage: (imgSn: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { type Image } from "./image-interface";
|
||||
import { IMAGE_TYPE } from "./image-interface";
|
||||
|
||||
export const columns: ColumnDef<Image>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "imgSn",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "imgTypeCd",
|
||||
header: "이미지구분",
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue("imgTypeCd") as string;
|
||||
switch (type) {
|
||||
case IMAGE_TYPE.IMGSUB:
|
||||
return "서브이미지";
|
||||
case IMAGE_TYPE.IMGMAIN:
|
||||
return "메인이미지";
|
||||
case IMAGE_TYPE.IMGLOGIN:
|
||||
return "로그인이미지";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "imgTtl",
|
||||
header: "이미지명",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
cell: ({ row }) => (row.getValue("useYn") === "Y" ? "사용" : "미사용"),
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.createImage(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
defaultValues: {
|
||||
atflYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 등록</h3>
|
||||
</div>
|
||||
<ImageForm form={form} handleSave={createImage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,317 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
Progress,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
import type { ImageFormData } from "../model/image-interface";
|
||||
|
||||
type ImageFormProps = {
|
||||
form: UseFormReturn<ImageFormData>;
|
||||
imgSn?: string;
|
||||
handleSave: (data: ImageFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
const isModifyMode = !!imgSn;
|
||||
const atflYn = form.watch("atflYn");
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ImageFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
form.setValue("atflCd", JSON.parse(fileInfo)?.atflCd);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>이미지명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이미지명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="정렬순서를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이미지유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="IMGTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="atflYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="atflY" />
|
||||
<Label htmlFor="atflY">이미지파일</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="atflN" />
|
||||
<Label htmlFor="atflN">외부링크</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{atflYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>이미지파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
handleUpload(files, {
|
||||
handleUploadStart: setUpload,
|
||||
handleProgress,
|
||||
handleSuccess: setUpload,
|
||||
handleResponse: handleUploadResponse,
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
|
||||
<p className="absolute left-0 top-0 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<div></div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{atflYn === "N" && (
|
||||
<TableRow>
|
||||
<TableHead>외부링크</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="외부링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ImageForm };
|
||||
@@ -1,90 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
import { Upload } from "tus-js-client";
|
||||
import { FileUploader } from "@common/components/ui";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.item(0);
|
||||
if (!file) return;
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
imgTypeCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 100,
|
||||
});
|
||||
//const p_atflCd = store.getState().upload.atflCd; // Redux 스토어에서 atflCd 가져오기
|
||||
|
||||
const { data: imageList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES],
|
||||
queryFn: () => ImageApi.getImageList(imageQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: imageList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024, // 5MB chunk size
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
filetype: file.type
|
||||
// atflCd: p_atflCd,
|
||||
},
|
||||
onError: function (error) {
|
||||
console.log("Failed because: " + error);
|
||||
},
|
||||
onProgress: function (bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
console.log(bytesUploaded, bytesTotal, percentage + "%");
|
||||
},
|
||||
onSuccess: function () {
|
||||
console.log(
|
||||
"Download %s from %s",
|
||||
(upload.file as File).name,
|
||||
upload.url,
|
||||
);
|
||||
console.log("upload._req:", upload._req);
|
||||
|
||||
if(upload._req){
|
||||
const xhr = upload._req._xhr; // 업로드에 사용된 XMLHttpRequest 객체
|
||||
const xFileInfo = xhr.getResponseHeader('X-file-info'); // X-file-info 헤더 가져오기
|
||||
if (xFileInfo) {
|
||||
const fileInfo = JSON.parse(xFileInfo);
|
||||
console.log('fileInfo :', fileInfo);
|
||||
const downloadUrl = `${ENDPOINT.ATTACH_SERVICE.TUS_DOWNLOAD}/${fileInfo.atflId}`;
|
||||
console.log('Download URL:', downloadUrl);
|
||||
// store.dispatch(setAtflCd(fileInfo.atflCd)); // atflCd 저장
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
upload.findPreviousUploads().then(function (previousUploads) {
|
||||
if (previousUploads.length) {
|
||||
upload.resumeFromPreviousUpload(previousUploads[0]!);
|
||||
}
|
||||
upload.start();
|
||||
});
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Image) => {
|
||||
router.push(`/image/register/${row.imgSn}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
이미지명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={imageQuery.keyword}
|
||||
onChange={(e) =>
|
||||
setImageQuery({ ...imageQuery, keyword: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<Button onClick={() => router.push("/image/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
</div>
|
||||
<FileUploader id="image-uploader" onUpload={handleUpload} />
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { Upload } from "tus-js-client";
|
||||
import { FileUploader } from "@common/components/ui";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
|
||||
function ImagePage() {
|
||||
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.item(0);
|
||||
if (!file) return;
|
||||
|
||||
//const p_atflCd = store.getState().upload.atflCd; // Redux 스토어에서 atflCd 가져오기
|
||||
|
||||
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024, // 5MB chunk size
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
filetype: file.type
|
||||
// atflCd: p_atflCd,
|
||||
},
|
||||
onError: function (error) {
|
||||
console.log("Failed because: " + error);
|
||||
},
|
||||
onProgress: function (bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
console.log(bytesUploaded, bytesTotal, percentage + "%");
|
||||
},
|
||||
onSuccess: function () {
|
||||
console.log(
|
||||
"Download %s from %s",
|
||||
(upload.file as File).name,
|
||||
upload.url,
|
||||
);
|
||||
console.log("upload._req:", upload._req);
|
||||
|
||||
if(upload._req){
|
||||
const xhr = upload._req._xhr; // 업로드에 사용된 XMLHttpRequest 객체
|
||||
const xFileInfo = xhr.getResponseHeader('X-file-info'); // X-file-info 헤더 가져오기
|
||||
if (xFileInfo) {
|
||||
const fileInfo = JSON.parse(xFileInfo);
|
||||
console.log('fileInfo :', fileInfo);
|
||||
const downloadUrl = `${ENDPOINT.ATTACH_SERVICE.TUS_DOWNLOAD}/${fileInfo.atflId}`;
|
||||
console.log('Download URL:', downloadUrl);
|
||||
// store.dispatch(setAtflCd(fileInfo.atflCd)); // atflCd 저장
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
upload.findPreviousUploads().then(function (previousUploads) {
|
||||
if (previousUploads.length) {
|
||||
upload.resumeFromPreviousUpload(previousUploads[0]!);
|
||||
}
|
||||
upload.start();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
</div>
|
||||
<FileUploader id="image-uploader" onUpload={handleUpload} />
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export { ImagePage };
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
function ModifyPage({ serial }: { serial: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: imageDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, serial],
|
||||
queryFn: () => ImageApi.getImageInfo(serial),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(serial, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteImage } = useMutation({
|
||||
mutationFn: () => ImageApi.deleteImage(serial),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
values: imageDetail,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
|
||||
</div>
|
||||
<ImageForm
|
||||
form={form}
|
||||
imgSn={serial}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -24,7 +24,7 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateMenu(
|
||||
public async modifyMenu(
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
): Promise<void> {
|
||||
@@ -49,7 +49,7 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
|
||||
public async modifyTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
@@ -57,7 +57,7 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateMenuName(menuCd: number, name: string): Promise<void> {
|
||||
public async modifyMenuName(menuCd: number, name: string): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
|
||||
);
|
||||
|
||||
@@ -75,12 +75,12 @@ export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||
export interface MenuService {
|
||||
getMenuInfo: (menuCd: number) => Promise<Menu>;
|
||||
createMenu: (data: MenuFormData) => Promise<void>;
|
||||
updateMenu: (
|
||||
modifyMenu: (
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
) => Promise<void>;
|
||||
deleteMenu: (menuCd: number) => Promise<void>;
|
||||
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
|
||||
updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||
updateMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Content } from "./content-interface";
|
||||
import type { Content } from "../model/content-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<Content>();
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
|
||||
|
||||
interface BaseMenuFormProps {
|
||||
@@ -52,6 +57,7 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
placeholder="메뉴명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -73,6 +79,7 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
placeholder="메뉴 영문명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -111,6 +118,7 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ContentApi } from "../api/content-service";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import { columns } from "../model/content-table-colums";
|
||||
import { columns } from "../model/table-colums";
|
||||
|
||||
type ContentSearchDialogProps = {
|
||||
handleContentSelect: (content: Content) => void;
|
||||
|
||||
@@ -13,7 +13,7 @@ interface MenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuCd?: number;
|
||||
handleCreate: (data: MenuFormData) => void;
|
||||
handleUpdate: ({
|
||||
handleModify: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
@@ -27,7 +27,7 @@ function MenuForm({
|
||||
form,
|
||||
menuCd,
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
handleModify,
|
||||
handleDelete,
|
||||
}: MenuFormProps) {
|
||||
const isModifyMode = !!menuCd;
|
||||
@@ -35,15 +35,15 @@ function MenuForm({
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
const handleConfirm = async (fn: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
await fn();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
if (isModifyMode) {
|
||||
await onUpdate(data);
|
||||
await onModify(data);
|
||||
} else {
|
||||
await onCreate(data);
|
||||
}
|
||||
@@ -57,10 +57,10 @@ function MenuForm({
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdate = async (data: MenuFormData) => {
|
||||
const onModify = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
handleModify({ menuCd: menuCd!, data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,10 +34,10 @@ function MenuNode({
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: updateMenuName } = useMutation({
|
||||
const { mutateAsync: modifyMenuName } = useMutation({
|
||||
mutationFn: (nodeName: string) => {
|
||||
const encodedName = encodeURIComponent(nodeName);
|
||||
return MenuApi.updateMenuName(id as number, encodedName);
|
||||
return MenuApi.modifyMenuName(id as number, encodedName);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -53,7 +53,7 @@ function MenuNode({
|
||||
const handleSelect = () => onSelect(node);
|
||||
|
||||
const handleSave = async () => {
|
||||
await updateMenuName(nodeName);
|
||||
await modifyMenuName(nodeName);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@common/components/ui";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
@@ -80,14 +81,14 @@ function MenuPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateMenu } = useMutation({
|
||||
const { mutate: modifyMenu } = useMutation({
|
||||
mutationFn: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
menuCd: number;
|
||||
data: Omit<MenuFormData, "siteId">;
|
||||
}) => MenuApi.updateMenu(menuCd, data),
|
||||
}) => MenuApi.modifyMenu(menuCd, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
@@ -196,65 +197,67 @@ function MenuPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] gap-2">
|
||||
<div className="card card-border w-[24rem]">
|
||||
<Select value={siteId} onValueChange={handleSiteIdChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{siteId && (
|
||||
<>
|
||||
<div className="my-4 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="flex-1"
|
||||
onClick={handleAddMenu}
|
||||
>
|
||||
메뉴 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
className="flex-1"
|
||||
onClick={handleNodeDelete}
|
||||
disabled={!selectedMenu}
|
||||
>
|
||||
메뉴 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree
|
||||
tree={tree}
|
||||
siteId={siteId}
|
||||
selectedMenu={selectedMenu}
|
||||
additionalNodeForm={additionalNodeForm}
|
||||
onSelectNode={handleNodeSelect}
|
||||
onAdditionalNodeSave={handleAdditionalNodeSave}
|
||||
onAdditionalNodeCancel={resetAdditionalNode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<PageLayout>
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] gap-2">
|
||||
<div className="card card-border w-[24rem]">
|
||||
<Select value={siteId} onValueChange={handleSiteIdChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{siteId && (
|
||||
<>
|
||||
<div className="my-4 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="flex-1"
|
||||
onClick={handleAddMenu}
|
||||
>
|
||||
메뉴 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
className="flex-1"
|
||||
onClick={handleNodeDelete}
|
||||
disabled={!selectedMenu}
|
||||
>
|
||||
메뉴 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree
|
||||
tree={tree}
|
||||
siteId={siteId}
|
||||
selectedMenu={selectedMenu}
|
||||
additionalNodeForm={additionalNodeForm}
|
||||
onSelectNode={handleNodeSelect}
|
||||
onAdditionalNodeSave={handleAdditionalNodeSave}
|
||||
onAdditionalNodeCancel={resetAdditionalNode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
menuCd={selectedMenu?.menuCd}
|
||||
handleCreate={createMenu}
|
||||
handleModify={modifyMenu}
|
||||
handleDelete={deleteMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
menuCd={selectedMenu?.menuCd}
|
||||
handleCreate={createMenu}
|
||||
handleUpdate={updateMenu}
|
||||
handleDelete={deleteMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -41,9 +41,9 @@ function MenuTree({
|
||||
const queryClient = useQueryClient();
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
const { mutate: updateTreeMenu } = useMutation({
|
||||
const { mutate: modifyTreeMenu } = useMutation({
|
||||
mutationFn: (newTree: TreeNode) =>
|
||||
MenuApi.updateTreeMenu(siteId, [newTree]),
|
||||
MenuApi.modifyTreeMenu(siteId, [newTree]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
@@ -96,7 +96,7 @@ function MenuTree({
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={handleDropNode(updateTreeMenu)}
|
||||
onDrop={handleDropNode(modifyTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import type { User } from "../model/organization-interface";
|
||||
@@ -112,6 +117,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -147,6 +153,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -179,6 +186,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -203,6 +211,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
placeholder="사이트맵에서 다른 이름으로 사용할 경우 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -246,6 +255,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -277,6 +287,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -311,6 +322,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { SiteForm } from "./site-form";
|
||||
|
||||
function CreatePage() {
|
||||
@@ -37,12 +38,9 @@ function CreatePage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 등록</h3>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<SiteForm form={form} handleSave={createSite} />
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,14 @@ import {
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { DomainApi } from "../api/domain-service";
|
||||
import {
|
||||
@@ -228,6 +233,7 @@ function DomainManagementDialog({
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -247,6 +253,7 @@ function DomainManagementDialog({
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -268,19 +275,20 @@ function DomainManagementDialog({
|
||||
value="Y"
|
||||
id={`useY-${i}`}
|
||||
/>
|
||||
<Label htmlFor={`useY-${i}`}>예</Label>
|
||||
<label htmlFor={`useY-${i}`}>예</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`useN-${i}`}
|
||||
/>
|
||||
<Label htmlFor={`useN-${i}`}>
|
||||
<label htmlFor={`useN-${i}`}>
|
||||
아니오
|
||||
</Label>
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -22,9 +22,14 @@ import {
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { IpApi } from "../api/ip-service";
|
||||
import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
|
||||
@@ -212,6 +217,7 @@ function IPManagementDialog({
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -233,19 +239,20 @@ function IPManagementDialog({
|
||||
value="Y"
|
||||
id={`prmY-${i}`}
|
||||
/>
|
||||
<Label htmlFor={`prmY-${i}`}>예</Label>
|
||||
<label htmlFor={`prmY-${i}`}>예</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`prmN-${i}`}
|
||||
/>
|
||||
<Label htmlFor={`prmN-${i}`}>
|
||||
<label htmlFor={`prmN-${i}`}>
|
||||
아니오
|
||||
</Label>
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -6,10 +6,13 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { SiteForm } from "./site-form";
|
||||
|
||||
function ModifyPage({ siteId }: { siteId: string }) {
|
||||
const isModifyMode = !!siteId;
|
||||
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
@@ -28,11 +31,11 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
bscSiteYn: data.basicSiteYn,
|
||||
};
|
||||
},
|
||||
enabled: !!siteId,
|
||||
enabled: isModifyMode,
|
||||
});
|
||||
|
||||
const { mutateAsync: updateSite } = useMutation({
|
||||
mutationFn: (data: SiteFormData) => SiteApi.updateSite(siteId, data),
|
||||
const { mutateAsync: modifySite } = useMutation({
|
||||
mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -45,7 +48,12 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
const { mutateAsync: deleteSite } = useMutation({
|
||||
mutationFn: () => SiteApi.deleteSite(siteId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
alert("삭제되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
});
|
||||
router.back();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -55,17 +63,14 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 수정</h3>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<SiteForm
|
||||
form={form}
|
||||
siteId={siteId}
|
||||
handleSave={updateSite}
|
||||
handleSave={modifySite}
|
||||
handleDelete={deleteSite}
|
||||
/>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,13 @@ import {
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SiteFormData } from "@/entities/site";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
Form,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import type { Domain } from "../model/domain-interface";
|
||||
import { IPManagementDialog } from "./ip-management-dialog";
|
||||
@@ -38,12 +44,6 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
|
||||
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleSaveDomain = (rows: Domain[]) => {
|
||||
form.setValue(
|
||||
"dmnAddr",
|
||||
@@ -56,329 +56,350 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
setOnConfirm(true);
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>사이트 코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{isModifyMode ? "사이트 수정" : "사이트 등록"}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>사이트 코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteKndCd"
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteKndCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 디자인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteSkn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 디자인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteSkn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<TableRow>
|
||||
<TableHead>사이트 도메인</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("dmnAddr")}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 주소</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 전화번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="telNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 팩스번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="faxNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lwndCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>북마크 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bkmkIcon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>기본 사이트 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bscSiteYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="bscSiteYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="bscSiteY" />
|
||||
<Label htmlFor="bscSiteY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="bscSiteN" />
|
||||
<Label htmlFor="bscSiteN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TableRow>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<TableRow>
|
||||
<TableHead>사이트 도메인</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{form.watch("dmnAddr")}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 주소</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 전화번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="telNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 팩스번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="faxNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lwndCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>북마크 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bkmkIcon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>기본 사이트 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bscSiteYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="bscSiteYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="bscSiteY" />
|
||||
<Label htmlFor="bscSiteY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="bscSiteN" />
|
||||
<Label htmlFor="bscSiteN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
|
||||
@@ -6,9 +6,10 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { Button, DataTable, Input } from "@common/components";
|
||||
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function SitePage() {
|
||||
@@ -43,32 +44,34 @@ function SitePage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label htmlFor="" className="mr-2 text-label">
|
||||
사이트명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={siteQuery.siteNm}
|
||||
onChange={(e) => setSiteQuery({ siteNm: e.target.value })}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 목록</h3>
|
||||
<Button onClick={() => router.push("/site/register")}>등록</Button>
|
||||
<PageLayout>
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<label htmlFor="" className="mr-2 text-label">
|
||||
사이트명
|
||||
</label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={siteQuery.siteNm}
|
||||
onChange={(e) => setSiteQuery({ siteNm: e.target.value })}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 목록</h3>
|
||||
<Button onClick={() => router.push("/site/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ export const ENDPOINT = {
|
||||
LOGIN: "/user-service/login",
|
||||
USERS: "/user-service/api/v1/users",
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
AUTHRTS: "/user-service/api/v1/authorizations",
|
||||
ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations",
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
@@ -22,8 +20,6 @@ export const ENDPOINT = {
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
USER_OGNZ: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
GROUP: "/cms-service/api/v1/groups",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
+1
-1
@@ -49,4 +49,4 @@ import { useWrStore } from '../../../store';
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.qodo
|
||||
@@ -35,7 +35,6 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface SiteInfo {
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
rgTrNm: string;
|
||||
mdfrNm: string;
|
||||
mdFrNm: string;
|
||||
}
|
||||
|
||||
export const getSiteInfo = async (siteId: string) => {
|
||||
|
||||
@@ -2,17 +2,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
export function QueryProvider({
|
||||
children,
|
||||
queryClient,
|
||||
client,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
client?: QueryClient;
|
||||
}) {
|
||||
if (!queryClient) {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
const queryClient = client ?? new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type RowData,
|
||||
type Table as TableType,
|
||||
} from "@tanstack/react-table";
|
||||
import { cn } from "../../lib";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "../table";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
@@ -56,7 +55,7 @@ function DataTable<T>({
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(onRowClick && "cursor-pointer hover:bg-accent")}
|
||||
className={onRowClick ? "cursor-pointer hover:bg-accent" : ""}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const { meta } = cell.column.columnDef;
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
@layer components {
|
||||
.file-uploader-container {
|
||||
@apply flex h-[3.2rem] w-full max-w-sm items-center gap-2;
|
||||
@apply flex w-full max-w-sm items-center gap-2;
|
||||
}
|
||||
.file-uploader-preview {
|
||||
@apply h-full w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem];
|
||||
@apply h-12 w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem];
|
||||
}
|
||||
.file-uploader-label {
|
||||
@apply inline-flex h-full flex-shrink-0 cursor-pointer items-center rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground;
|
||||
@apply flex-shrink-0 cursor-pointer rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,18 @@ import "./file-uploader.css";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Input } from "../input";
|
||||
import { Label } from "../label";
|
||||
import { combineFileName, validFileType } from "./utils";
|
||||
|
||||
type FileUploaderProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
id: string;
|
||||
files?: ReadonlyArray<File>;
|
||||
customPreview?: React.ReactNode;
|
||||
onUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
export function FileUploader({
|
||||
id,
|
||||
files,
|
||||
customPreview,
|
||||
className,
|
||||
files,
|
||||
onUpload,
|
||||
...props
|
||||
}: FileUploaderProps) {
|
||||
@@ -29,14 +26,12 @@ export function FileUploader({
|
||||
{...props}
|
||||
onChange={onUpload}
|
||||
/>
|
||||
{customPreview || (
|
||||
<div className="file-uploader-preview">
|
||||
{files?.filter(validFileType).map(combineFileName).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
<Label htmlFor={id} className="file-uploader-label">
|
||||
<div className="file-uploader-preview">
|
||||
{files?.filter(validFileType).map(combineFileName).join(", ")}
|
||||
</div>
|
||||
<label htmlFor={id} className="file-uploader-label">
|
||||
파일 선택
|
||||
</Label>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,9 @@ export * from "./layout";
|
||||
export * from "./loading";
|
||||
export * from "./page-title";
|
||||
export * from "./popover";
|
||||
export * from "./progress";
|
||||
export * from "./radio-group";
|
||||
export * from "./select";
|
||||
export * from "./slider";
|
||||
export * from "./switch";
|
||||
export * from "./table";
|
||||
export * from "./tabs";
|
||||
export * from "./pagination";
|
||||
@@ -19,7 +19,7 @@ export function BaseLayout({
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<QueryProvider client={queryClient}>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// apps/cs/src/common/components/ui/pagination.tsx
|
||||
import { Button } from "../button";
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
|
||||
// 표시할 페이지 번호 범위 계산
|
||||
const getPageRange = () => {
|
||||
const range: number[] = [];
|
||||
const maxButtons = 5; // 한 번에 보여줄 최대 페이지 버튼 수
|
||||
const halfMaxButtons = Math.floor(maxButtons / 2);
|
||||
|
||||
let startPage = Math.max(currentPage - halfMaxButtons, 1);
|
||||
let endPage = Math.min(startPage + maxButtons - 1, totalPages);
|
||||
|
||||
if (endPage - startPage + 1 < maxButtons) {
|
||||
startPage = Math.max(endPage - maxButtons + 1, 1);
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{/* 첫 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{"<<"}
|
||||
</Button>
|
||||
|
||||
{/* 이전 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{"<"}
|
||||
</Button>
|
||||
|
||||
{/* 페이지 번호 버튼들 */}
|
||||
{getPageRange().map((pageNum) => (
|
||||
<Button
|
||||
key={pageNum}
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* 다음 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
{">"}
|
||||
</Button>
|
||||
|
||||
{/* 마지막 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
{">>"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Pagination };
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@layer components {
|
||||
.table-ui tr {
|
||||
@apply border-0;
|
||||
@apply border-0 hover:bg-accent;
|
||||
}
|
||||
.table-ui th {
|
||||
@apply px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2];
|
||||
|
||||
Generated
+5
-29
@@ -390,9 +390,6 @@ importers:
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-radio-group':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -1626,7 +1623,7 @@ packages:
|
||||
magic-string: 0.27.0
|
||||
react-docgen-typescript: 2.2.2(typescript@5.6.3)
|
||||
typescript: 5.6.3
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
dev: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.8:
|
||||
@@ -2298,27 +2295,6 @@ packages:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-progress@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.3.18
|
||||
'@types/react-dom': ^18.3.5
|
||||
react: ^18.3.1
|
||||
react-dom: ^18.3.1
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
|
||||
peerDependencies:
|
||||
@@ -3083,7 +3059,7 @@ packages:
|
||||
browser-assert: 1.2.1
|
||||
storybook: 8.6.0(prettier@3.5.2)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
dev: true
|
||||
|
||||
/@storybook/components@8.6.0(storybook@8.6.0):
|
||||
@@ -3215,7 +3191,7 @@ packages:
|
||||
resolve: 1.22.10
|
||||
storybook: 8.6.0(prettier@3.5.2)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
@@ -3661,7 +3637,7 @@ packages:
|
||||
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.2
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -8438,7 +8414,7 @@ packages:
|
||||
debug: 4.4.0
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.5(typescript@5.6.3)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
Reference in New Issue
Block a user