Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
@@ -20,22 +21,34 @@ function EditableNode({
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
aria-label="저장"
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>저장</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
aria-label="취소"
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>취소</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { client } from "@/shared/api";
|
||||
import type {
|
||||
SiteAuthorityCreatePayload,
|
||||
SiteAuthorityList,
|
||||
SiteAuthoritySearchParams,
|
||||
SiteAuthorityService,
|
||||
} from "../model/site-authority-interface";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export class SiteAuthorityApiService implements SiteAuthorityService {
|
||||
public async getSiteAuthorityList(
|
||||
params: SiteAuthoritySearchParams,
|
||||
): Promise<SiteAuthorityList> {
|
||||
const response = await client.get<SiteAuthorityList>(
|
||||
ENDPOINT.CMS_SERVICE.SITE_AUTHORITY,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createSiteAuthority(
|
||||
data: SiteAuthorityCreatePayload[],
|
||||
): Promise<void> {
|
||||
await client.post(ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const SiteAuthorityApi = new SiteAuthorityApiService();
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface SiteAuthoritySearchParams extends PageableParams {
|
||||
siteId?: string;
|
||||
authrtId?: string;
|
||||
}
|
||||
|
||||
export interface SiteAuthority {
|
||||
siteId: string;
|
||||
authrtId: string;
|
||||
siteNm: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export type SiteAuthorityCreatePayload = Omit<SiteAuthority, "siteNm">;
|
||||
|
||||
export type SiteAuthorityList = PageableList<SiteAuthority>;
|
||||
|
||||
export interface SiteAuthorityService {
|
||||
getSiteAuthorityList: (
|
||||
params: SiteAuthoritySearchParams,
|
||||
) => Promise<SiteAuthorityList>;
|
||||
createSiteAuthority: (data: SiteAuthorityCreatePayload[]) => Promise<void>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
|
||||
import { useState, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
DialogTitle,
|
||||
Button,
|
||||
Switch,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import {
|
||||
RoleAuthorizationsApi,
|
||||
@@ -31,12 +33,18 @@ function MappingRoleDialog({
|
||||
}: MappingRoleDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
keyword: "",
|
||||
keywordType: "authorizationName",
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, searchParams],
|
||||
queryFn: () =>
|
||||
RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({
|
||||
roleId,
|
||||
...searchParams,
|
||||
}),
|
||||
enabled: open,
|
||||
});
|
||||
@@ -46,7 +54,7 @@ function MappingRoleDialog({
|
||||
RoleAuthorizationsApi.createRoleAuthorizations(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS],
|
||||
});
|
||||
alert("권한 맵핑이 완료되었습니다.");
|
||||
},
|
||||
@@ -66,6 +74,26 @@ function MappingRoleDialog({
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const keyword = formData.get("keyword") as string;
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
@@ -79,21 +107,31 @@ function MappingRoleDialog({
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">권한맵핑</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>모듈 목록</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[40rem] overflow-y-auto">
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button size="sm" onClick={() => handleApplyAll("Y")}>
|
||||
일괄적용
|
||||
<div className="flex items-center justify-between">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name="keyword"
|
||||
className="w-[32rem]"
|
||||
/>
|
||||
<Button type="submit">검색</Button>
|
||||
<Button type="button" color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button size="sm" color="gray" onClick={() => handleApplyAll("N")}>
|
||||
</form>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button onClick={() => handleApplyAll("Y")}>일괄적용</Button>
|
||||
<Button color="gray" onClick={() => handleApplyAll("N")}>
|
||||
일괄해제
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</div>
|
||||
<div className="max-h-[60rem] overflow-y-auto">{tableComponent}</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
@@ -16,6 +18,9 @@ import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { RoleApi, type RoleFormData } from "@/entities/role";
|
||||
import { FormField, FormItem, FormControl } from "@/shared/ui/form";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { SiteAuthorityApi } from "../api/site-authority-service";
|
||||
import type { SiteAuthorityCreatePayload } from "../model/site-authority-interface";
|
||||
|
||||
type RoleFormProps = {
|
||||
roleId?: string;
|
||||
@@ -24,12 +29,24 @@ type RoleFormProps = {
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function RoleForm({ roleId, form, ...props }: RoleFormProps) {
|
||||
function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
const isModifyMode = !!roleId;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [isValidRoleId, setIsValidRoleId] = useState(false);
|
||||
const [checkedSiteAuthority, setCheckedSiteAuthority] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const { data: siteAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, roleId],
|
||||
queryFn: () =>
|
||||
SiteAuthorityApi.getSiteAuthorityList({
|
||||
authrtId: roleId,
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
});
|
||||
|
||||
const { mutateAsync: checkDuplicateRoleId } = useMutationWrapper({
|
||||
mutationFn: (roleId: string) => RoleApi.checkDuplicateRoleId(roleId),
|
||||
@@ -43,8 +60,49 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSiteAuthorityChecked = (checked: boolean, siteId: string) => {
|
||||
if (checked) {
|
||||
setCheckedSiteAuthority((prev) => [...prev, siteId]);
|
||||
} else {
|
||||
setCheckedSiteAuthority((prev) => prev.filter((id) => id !== siteId));
|
||||
}
|
||||
};
|
||||
|
||||
const saveInterceptor = async (data: RoleFormData) => {
|
||||
const siteAuthorityPayload: SiteAuthorityCreatePayload[] =
|
||||
checkedSiteAuthority.map((siteId) => ({
|
||||
siteId,
|
||||
authrtId: data.roleId,
|
||||
mode: "C",
|
||||
}));
|
||||
|
||||
try {
|
||||
await handleSave(data);
|
||||
if (data.mngrYn === "N") return;
|
||||
await SiteAuthorityApi.createSiteAuthority(siteAuthorityPayload);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!siteAuthorityList?.content.length) return;
|
||||
const checkedSiteAuthority = siteAuthorityList.content.filter(
|
||||
(item) => item.mode === "R",
|
||||
);
|
||||
setCheckedSiteAuthority(checkedSiteAuthority.map((item) => item.siteId));
|
||||
}, [siteAuthorityList]);
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
|
||||
<FormWrapper
|
||||
form={form}
|
||||
isModifyMode={isModifyMode}
|
||||
handleSave={saveInterceptor}
|
||||
{...props}
|
||||
>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
@@ -165,6 +223,29 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{form.watch("mngrYn") === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead data-required>관리사이트</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{siteAuthorityList?.content.map((item) => (
|
||||
<div
|
||||
key={item.siteId}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={item.mode === "R"}
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
handleSiteAuthorityChecked(checked, item.siteId);
|
||||
}}
|
||||
/>
|
||||
<span className="whitespace-nowrap">{item.siteNm}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
@@ -21,22 +22,34 @@ function EditableNode({
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
aria-label="저장"
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>저장</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
aria-label="취소"
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>취소</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ export const ENDPOINT = {
|
||||
GROUP_USERS: "/cms-service/api/v1/group-users",
|
||||
MANAGER_IP: "/cms-service/api/v1/mgrip",
|
||||
APILOG: "/cms-service/api/v1/api-logs",
|
||||
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -63,7 +63,7 @@ export function LNB({
|
||||
|
||||
function isPathMatched(path: string) {
|
||||
if (!path) return false;
|
||||
return currentPath === path;
|
||||
return currentPath?.startsWith(path);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user