This commit is contained in:
2025-07-18 19:00:00 +09:00
8 changed files with 246 additions and 48 deletions
+29 -16
View File
@@ -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>
);
+86 -5
View File
@@ -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">
+29 -16
View File
@@ -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>
);
+1
View File
@@ -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(() => {