Merge branch 'iipbs/common/main' into iipbs/sn/ii

This commit is contained in:
swcho
2025-07-16 10:04:51 +09:00
245 changed files with 7714 additions and 8817 deletions
+8
View File
@@ -0,0 +1,8 @@
# 디폴트 무시된 파일
/shelf/
/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
@@ -77,7 +77,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
</colgroup>
<TableBody>
<TableRow>
<TableHead>제목</TableHead>
<TableHead data-required>제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -120,7 +120,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
</TableRow>
{!userInfo?.userUnqId && (
<TableRow>
<TableHead>비밀번호</TableHead>
<TableHead data-required>비밀번호</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -44,7 +44,9 @@ function PostPage({ bbsId }: { bbsId: string }) {
const thumbRefs = useRef<Map<number, string>>(new Map());
const [tableSort, setTableSort] = useState("");
const [postParams, setPostParams] = useState<PostParams>({
const [keywordType, setKeywordType] = useState("title");
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PostParams>({
keywordType: "title",
keyword: "",
});
@@ -58,7 +60,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo],
queryFn: () =>
PostApi.getPostList(parseInt(bbsId), {
...postParams,
...searchParams,
upndFixYn: bbsInfo?.upndFixUseYn,
}),
select: (data) => {
@@ -79,10 +81,16 @@ function PostPage({ bbsId }: { bbsId: string }) {
isLoading,
refetch,
} = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pagination, tableSort],
queryKey: [
ENDPOINT.BOARD_SERVICE.POSTS_USER,
bbsId,
searchParams,
pagination,
tableSort,
],
queryFn: () =>
PostApi.getPostList(parseInt(bbsId), {
...postParams,
...searchParams,
ansUseYn: bbsInfo?.ansUseYn,
page: pagination.pageIndex,
size: pagination.pageSize,
@@ -128,7 +136,29 @@ function PostPage({ bbsId }: { bbsId: string }) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keywordType,
keyword,
});
};
const handleReset = () => {
setKeywordType("title");
setKeyword("");
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keywordType: "title",
keyword: "",
});
};
const handleRowClick = (row: Post) => {
@@ -141,12 +171,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<Select value={keywordType} onValueChange={setKeywordType}>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -157,15 +182,21 @@ function PostPage({ bbsId }: { bbsId: string }) {
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
PORT=13000
LOG_FILE="start_13000.log"
# 1. 기존 포트 사용 프로세스 종료
PID=$(lsof -t -i :$PORT)
if [ -n "$PID" ]; then
echo "Killing process on port $PORT (PID: $PID)"
kill -9 $PID
fi
# 2. 백그라운드로 앱 시작 (로그 파일로 리디렉션)
echo "Starting app on port $PORT..."
nohup pnpm start > $LOG_FILE 2>&1 &
# 3. PID 확인
sleep 1
NEW_PID=$(lsof -t -i :$PORT)
if [ -n "$NEW_PID" ]; then
echo "App started successfully (PID: $NEW_PID)"
echo "Logs: tail -f $LOG_FILE"
else
echo "Failed to start app. Check logs: $LOG_FILE"
fi
@@ -9,12 +9,11 @@ import type {
export class ApiLogApiService implements ApiLogService {
public async getApiLogList(params: ApiLogParams): Promise<ApiLogList> {
const response = await client.get<ApiLogList>(
`${ENDPOINT.USER_SERVICE.APILOG}`,
`${ENDPOINT.CMS_SERVICE.APILOG}`,
{ params },
);
return response.data;
}
}
export const ApiLogApi = new ApiLogApiService();
@@ -2,8 +2,9 @@ import type { PageableList, PageableParams } from "@/shared/model";
export interface ApiLogParams extends PageableParams {
siteId?: string;
userId?: string;
userUnqId?: string;
userNm?: string;
userId?: string;
apiHttpPmhd?: string;
dmndUrl?: string;
ipAddr?: string;
@@ -85,6 +85,15 @@ export class ContentsApiService implements ContentsService {
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`,
);
}
public async rejectContents(
contsSn: number,
contsVer: number,
): Promise<void> {
await client.put(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/reject`,
);
}
}
export const ContentsApi = new ContentsApiService();
@@ -97,4 +97,5 @@ export interface ContentsService {
deleteContents(contsSn: number, contsVer: number): Promise<void>;
requestApprove(contsSn: number, contsVer: number): Promise<void>;
approveContents(contsSn: number, contsVer: number): Promise<void>;
rejectContents(contsSn: number, contsVer: number): Promise<void>;
}
@@ -3,6 +3,7 @@ import type { PageableList, PageableParams } from "@/shared/model";
// 그룹 사용자 검색 파라미터 인터페이스
export interface GroupUserSearchParams extends PageableParams {
keyword?: string;
userNm?: string;
}
// 그룹 사용자 목록 응답 인터페이스
+1
View File
@@ -3,4 +3,5 @@ export {
groupFormSchema,
type GroupFormData,
type GroupListResponseDto,
type GroupSearchParams,
} from "./model/group-interface";
@@ -56,7 +56,10 @@ export const groupFormSchema = z.object({
groupNm: z.string().min(1, { message: "그룹명을 입력해주세요." }),
groupSeCd: z.string().min(1, { message: "그룹구분을 선택해주세요." }),
groupExpln: z.string().optional(),
delYn: z.string().default("N"),
delYn: z
.string()
.transform((val) => (val === "" || val === undefined ? "N" : val))
.default("N"),
authrtId: z.string().min(1, { message: "권한ID를 선택해주세요." }),
siteId: z.string().optional(),
mngrYn: z.string().default("N"),
@@ -74,7 +74,7 @@ function ManagementAuthorityDialog({ menuCd }: ManagementAuthorityDialogProps) {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="gap-3" disabled={!menuCd}>
<i className="diveicon di-personal-information" />
<i className="diveicon di-key" />
권한관리
</Button>
</DialogTrigger>
+57 -1
View File
@@ -1,6 +1,12 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { MenuItem, MenuService } from "../model/menu-interface";
import type {
MenuService,
Menu,
MenuItem,
MenuFormData,
MenuTemporarilyStoredData,
} from "../model/menu-interface";
export class MenuApiService implements MenuService {
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
@@ -9,6 +15,56 @@ export class MenuApiService implements MenuService {
);
return response.data;
}
public async getMenuInfo(menuCd: number): Promise<Menu> {
const response = await client.get<Menu>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
);
return response.data;
}
public async createMenu(data: MenuFormData): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}`,
data,
);
return response.data;
}
public async updateMenu(
menuCd: number,
data: Omit<MenuFormData, "siteId">,
): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
data,
);
return response.data;
}
public async deleteMenu(menuCd: number): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`);
}
public async menuTemporaryStorage(
menuCd: number,
data: MenuFormData,
): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE}/${menuCd}`,
data,
);
return response.data;
}
public async getMenuTemporarilyStoredData(
menuCd: number,
): Promise<MenuTemporarilyStoredData[]> {
const response = await client.get<MenuTemporarilyStoredData[]>(
`${ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE}/menu/${menuCd}/user`,
);
return response.data;
}
}
export const MenuApi = new MenuApiService();
+8 -1
View File
@@ -1,2 +1,9 @@
export { MenuApi } from "./api/menu-service";
export {
MENU_TYPE,
menuFormSchema,
type MenuType,
type MenuItem,
type MenuFormData,
} from "./model/menu-interface";
export { MenuWrapper } from "./lib/menu-wrapper";
export type { MenuItem } from "./model/menu-interface";
@@ -1,3 +1,16 @@
import { z } from "zod";
export const MENU_TYPE = {
EMPTY: "EMPTY",
CONTENTS: "CONTENTS",
BOARD: "BOARD",
INSIDE: "INSIDE",
OUTSIDE: "OUTSIDE",
BOTTOM: "BOTTOM",
} as const;
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
export interface MenuItem {
menuCd: number;
menuKornNm: string;
@@ -18,6 +31,65 @@ export interface MenuItem {
requiredUrlPath: boolean;
}
export interface MenuTemporarilyStoredData {
tmprStrgSn: number;
menuCd: number;
menuTmprStrg: string;
regDt: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
}
export const menuFormSchema = z.object({
siteId: z.string(),
menuKornNm: z.string(),
menuSubKornNm: z.string().nullish(),
menuEngNm: z.string().nullish(),
menuSubEngNm: z.string().nullish(),
levelNo: z.number(),
urlPath: z.string().nullish(),
iconNm: z.string().nullish(),
menuLnkgTypeCd: z.enum([
MENU_TYPE.EMPTY,
MENU_TYPE.CONTENTS,
MENU_TYPE.BOARD,
MENU_TYPE.INSIDE,
MENU_TYPE.OUTSIDE,
MENU_TYPE.BOTTOM,
]),
menuLnkgSn: z.number().nullish(),
otptYn: z.string().default("Y"),
prntMenuCd: z.number().nullish(),
sortSeq: z.number(),
evlUseYn: z.string().default("Y"),
qrcd: z.string().nullish(),
useYn: z.string().default("Y"),
newWndwYn: z.string().nullish(),
menuLnkgSnNm: z.string().nullish(),
menuPicUserUnqId: z.string().nullish(),
menuPicUserUnqIdNm: z.string().nullish(),
menuPicDsgnYmd: z.string().nullish(),
});
export type MenuFormData = z.infer<typeof menuFormSchema>;
export type Menu = Omit<MenuFormData, "iconNm"> & {
menuCd: number;
iconName?: string;
};
export interface MenuService {
getMenuRoles(siteId: string): Promise<MenuItem[]>;
getMenuInfo: (menuCd: number) => Promise<Menu>;
createMenu: (data: MenuFormData) => Promise<void>;
updateMenu: (
menuCd: number,
data: Omit<MenuFormData, "siteId">,
) => Promise<void>;
deleteMenu: (menuCd: number) => Promise<void>;
menuTemporaryStorage: (menuCd: number, data: MenuFormData) => Promise<void>;
getMenuTemporarilyStoredData: (
menuCd: number,
) => Promise<MenuTemporarilyStoredData[]>;
}
+42 -16
View File
@@ -38,10 +38,14 @@ function AdminPage() {
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
const [adminQuery, setAdminQuery] = useState<UserSearchParams>({
const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState("");
const [grpId, setGrpId] = useState("");
const [searchParams, setSearchParams] = useState<UserSearchParams>({
keywordType: "",
keyword: "",
grpId: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -66,10 +70,10 @@ function AdminPage() {
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination],
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParams, pagination],
queryFn: () =>
UserApi.findPageAdminUser({
...adminQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -123,7 +127,28 @@ function AdminPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keywordType,
keyword,
grpId,
});
};
const handleReset = () => {
setKeywordType("");
setKeyword("");
setGrpId("");
setSearchParams({
...searchParams,
keywordType: "",
keyword: "",
grpId: "",
});
};
const handleDeleteAdmins = async () => {
@@ -161,11 +186,7 @@ function AdminPage() {
<form onSubmit={handleSubmit} className="flex items-center gap-6 p-2">
<div className="flex items-center gap-2">
<Label className="mr-2 text-label">관리자구분</Label>
<Select
onValueChange={(value) =>
setAdminQuery({ ...adminQuery, grpId: value })
}
>
<Select value={grpId} onValueChange={(value) => setGrpId(value)}>
<SelectTrigger className="h-[3.2rem] w-[16rem] !text-[1.3rem]">
<SelectValue placeholder="그룹 선택" />
</SelectTrigger>
@@ -183,9 +204,8 @@ function AdminPage() {
관리자
</Label>
<Select
onValueChange={(value) =>
setAdminQuery({ ...adminQuery, keywordType: value })
}
value={keywordType}
onValueChange={(value) => setKeywordType(value)}
>
<SelectTrigger className="h-[3.2rem] w-[10rem] !text-[1.3rem]">
<SelectValue placeholder="선택" />
@@ -197,16 +217,22 @@ function AdminPage() {
</Select>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={adminQuery.keyword}
onChange={(e) =>
setAdminQuery({ ...adminQuery, keyword: e.target.value })
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="검색어 입력"
/>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+48 -37
View File
@@ -26,36 +26,32 @@ type DateRange = {
to?: Date | undefined;
};
function ApiLogPage({}) {
// 1주일 전부터 오늘까지 기본 날짜 설정
function ApiLogPage() {
const defaultDateRange: DateRange = {
from: dayjs().subtract(7, "day").toDate(),
to: dayjs().toDate(),
};
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [loginlogPostQuery, setLoginlogPostQuery] = useState<ApiLogParams>({
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [userId, setUserId] = useState("");
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
const [searchParams, setSearchParams] = useState<ApiLogParams>({
userId: "",
siteId: "",
apiHttpPmhd: "",
dmndUrl: "",
ipAddr: "",
dmndDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
dmndDtEnd: dayjs().format("YYYY-MM-DD"),
sort: "lgnDt,asc",
sort: "regDt,desc",
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.APILOG, searchParams, pagination],
queryFn: () =>
ApiLogApi.getApiLogList({
...loginlogPostQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -81,13 +77,31 @@ function ApiLogPage({}) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
userId,
apiHttpPmhd,
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
});
};
const handleLoginlogParamsChange =
(key: keyof ApiLogParams) => (value: unknown) => {
setLoginlogPostQuery({ ...loginlogPostQuery, [key]: value });
};
const handleReset = () => {
setUserId("");
setApiHttpPmhd("");
setDateRange(defaultDateRange);
setSearchParams({
...searchParams,
userId: "",
apiHttpPmhd: "",
dmndDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
dmndDtEnd: dayjs().format("YYYY-MM-DD"),
});
};
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange | undefined;
@@ -113,19 +127,7 @@ function ApiLogPage({}) {
default:
presetDateRange = undefined;
}
handleDateRangeChange(presetDateRange);
};
const handleDateRangeChange = (value: DateRange | undefined) => {
setDateRange(value || defaultDateRange);
setLoginlogPostQuery({
...loginlogPostQuery,
dmndDtStart:
dayjs(value?.from).format("YYYY-MM-DD") ??
dayjs().subtract(7, "day").format("YYYY-MM-DD"),
dmndDtEnd:
dayjs(value?.to).format("YYYY-MM-DD") ?? dayjs().format("YYYY-MM-DD"),
});
setDateRange(presetDateRange || defaultDateRange);
};
return (
@@ -135,7 +137,7 @@ function ApiLogPage({}) {
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={handleDateRangeChange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
@@ -152,14 +154,15 @@ function ApiLogPage({}) {
</DateRangePicker>
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input
value={userId}
onChange={(e) => setUserId(e.target.value)}
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={loginlogPostQuery.userId}
onChange={(e) =>
handleLoginlogParamsChange("userId")(e.target.value)
}
/>
<Label className="ml-4 mr-2 text-label">상태</Label>
<Select onValueChange={handleLoginlogParamsChange("apiHttpPmhd")}>
<Select
value={apiHttpPmhd}
onValueChange={(value) => setApiHttpPmhd(value)}
>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -173,12 +176,20 @@ function ApiLogPage({}) {
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
<h3 className="text-2xl font-medium text-label">작업로그 목록</h3>
<div className="flex gap-2">
<Select
value={table.getState().pagination.pageSize.toString()}
+4 -4
View File
@@ -84,7 +84,7 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>배너명</TableHead>
<TableHead data-required>배너명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -105,7 +105,7 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>배너구분</TableHead>
<TableHead data-required>배너구분</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -210,7 +210,7 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>링크 열기</TableHead>
<TableHead data-required>링크 열기</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -260,7 +260,7 @@ function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableHead data-required>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+34 -9
View File
@@ -28,17 +28,21 @@ function BannerPage() {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const [bannerQuery, setBannerQuery] = useState<BannerParams>({ bnrTtl: "" });
const [bnrTtl, setBnrTtl] = useState("");
const [searchParams, setSearchParams] = useState<BannerParams>({
bnrTtl: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
pageSize: 10,
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, searchParams, pagination],
queryFn: () =>
BannerApi.getBannerList({
...bannerQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -46,7 +50,22 @@ function BannerPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
bnrTtl,
});
};
const handleReset = () => {
setBnrTtl("");
setSearchParams({
...searchParams,
bnrTtl: "",
});
};
const handleRowClick = (row: Banner) => {
@@ -107,14 +126,20 @@ function BannerPage() {
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={bannerQuery.bnrTtl}
onChange={(e) =>
setBannerQuery({ ...bannerQuery, bnrTtl: e.target.value })
}
value={bnrTtl}
onChange={(e) => setBnrTtl(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+14 -14
View File
@@ -54,7 +54,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>게시판명</TableHead>
<TableHead data-required>게시판명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -75,7 +75,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>게시판 설명</TableHead>
<TableHead data-required>게시판 설명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -96,7 +96,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>게시판 유형</TableHead>
<TableHead data-required>게시판 유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -116,7 +116,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>참조 사용 여부</TableHead>
<TableHead data-required>참조 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -146,7 +146,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일 사용 여부</TableHead>
<TableHead data-required>첨부파일 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -236,7 +236,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>답글 사용 여부</TableHead>
<TableHead data-required>답글 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -295,7 +295,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>댓글 사용 여부</TableHead>
<TableHead data-required>댓글 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -329,7 +329,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>게시물평가 사용 여부</TableHead>
<TableHead data-required>게시물평가 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -359,7 +359,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>팝업 사용 여부</TableHead>
<TableHead data-required>팝업 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -389,7 +389,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>SMS알림 사용 여부</TableHead>
<TableHead data-required>SMS알림 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -419,7 +419,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>상단고정 사용 여부</TableHead>
<TableHead data-required>상단고정 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -449,7 +449,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>게시기간 사용 여부</TableHead>
<TableHead data-required>게시기간 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -479,7 +479,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>개인정보 표시 여부</TableHead>
<TableHead data-required>개인정보 표시 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -508,7 +508,7 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableHead data-required>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+37 -12
View File
@@ -29,9 +29,12 @@ function BbsPage() {
const router = useRouter();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [boardParams, setBoardParams] = useState<BoardParams>({
const [bbsTypeCd, setBbsTypeCd] = useState("");
const [bbsNm, setBbsNm] = useState("");
const [searchParams, setSearchParams] = useState<BoardParams>({
bbsTypeCd: "",
bbsNm: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -39,10 +42,10 @@ function BbsPage() {
});
const query = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, pagination],
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, searchParams, pagination],
queryFn: () =>
BoardApi.getBoardList({
...boardParams,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -50,7 +53,25 @@ function BbsPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
bbsTypeCd,
bbsNm,
});
};
const handleReset = () => {
setBbsTypeCd("");
setBbsNm("");
setSearchParams({
...searchParams,
bbsTypeCd: "",
bbsNm: "",
});
};
const handleRowClick = (row: Board) => {
@@ -86,10 +107,8 @@ function BbsPage() {
</Label>
<CodeSelect
upCd="BBSTYPE"
value={boardParams.bbsTypeCd}
onValueChange={(value) =>
setBoardParams({ ...boardParams, bbsTypeCd: value })
}
value={bbsTypeCd}
onValueChange={(value) => setBbsTypeCd(value)}
/>
<Label htmlFor="bbsNm" className="ml-4 mr-2 text-label">
게시판명
@@ -97,14 +116,20 @@ function BbsPage() {
<Input
id="bbsNm"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={boardParams.bbsNm}
onChange={(e) =>
setBoardParams({ ...boardParams, bbsNm: e.target.value })
}
value={bbsNm}
onChange={(e) => setBbsNm(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -5,56 +5,62 @@ import type { LwpoCode } from "@/entities/code";
import { LwpoCodeManagementDialog } from "../ui/lwpocode-dialog";
import { useState } from "react";
export const columns: ColumnDef<LwpoCode>[] = [
{
id: "no",
header: "NO",
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const { pageIndex, pageSize } = table.getState().pagination;
return totalRows - (pageIndex * pageSize + row.index);
// columns를 함수로 변경하여 onAfterSave를 주입받음
export function getColumns(onAfterSave: () => void) : ColumnDef<LwpoCode>[] {
return [
{
id: "no",
header: "NO",
cell: ({ row, table }) => {
const totalRows = table.getRowCount();
const { pageIndex, pageSize } = table.getState().pagination;
return totalRows - (pageIndex * pageSize + row.index);
},
},
},
{
accessorKey: "lwpoCd",
header: "하위코드",
},
{
accessorKey: "lwpoCdNm",
header: "하위코드명",
},
{
accessorKey: "useYn",
header: "사용 여부",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => {
const date = dayjs(row.original.regDt);
return date.format("YYYY-MM-DD");
{
accessorKey: "lwpoCd",
header: "하위코드",
},
},
{
id: "actions",
header: "관리",
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<LwpoCodeManageButton
lwpoCd={row.original.lwpoCd}
upCd={row.original.upCd}
/>
</div>
),
},
];
{
accessorKey: "lwpoCdNm",
header: "하위코드명",
},
{
accessorKey: "useYn",
header: "사용 여부",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => {
const date = dayjs(row.original.regDt);
return date.format("YYYY-MM-DD");
},
},
{
id: "actions",
header: "관리",
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<LwpoCodeManageButton
lwpoCd={row.original.lwpoCd}
upCd={row.original.upCd}
onAfterSave={onAfterSave}
/>
</div>
),
},
];
}
const LwpoCodeManageButton = ({
lwpoCd,
upCd,
onAfterSave,
}: {
lwpoCd: string;
upCd: string;
onAfterSave: () => void;
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
@@ -63,6 +69,7 @@ const LwpoCodeManageButton = ({
upCd={upCd}
open={dialogOpen}
onOpenChange={setDialogOpen}
onAfterSave={onAfterSave}
>
<Button size="sm">관리</Button>
</LwpoCodeManagementDialog>
+64 -18
View File
@@ -1,7 +1,7 @@
import { memo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
@@ -38,6 +38,7 @@ type ManagementLwpoCodeDialogProps = {
children?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onAfterSave?: () => void; // 콜백 추가
};
const LwpoCodeManagementDialog = memo(
@@ -47,17 +48,18 @@ const LwpoCodeManagementDialog = memo(
children,
open,
onOpenChange,
onAfterSave, // 콜백 사용
...props
}: ManagementLwpoCodeDialogProps) => {
const isRegistration = !lwpoCd;
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const { data: detailData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd],
queryFn: () => LwpoCodeApi.lwpoCodeDetail(upCd!, lwpoCd!),
enabled: !!upCd && !isRegistration,
enabled: !!upCd && !isRegistration && open,
});
const { data: upCodeData } = useQuery({
@@ -69,7 +71,13 @@ const LwpoCodeManagementDialog = memo(
const { mutateAsync: createLwpoCode } = useMutationWrapper({
mutationFn: (data: LwpoCodeFormData) => LwpoCodeApi.createLwpoCode(data),
onSuccess: () => {
alert("등록되었습니다.", () => onOpenChange?.(false));
alert("등록되었습니다.", () => {
onOpenChange?.(false);
onAfterSave?.();
});
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd],
});
},
});
@@ -77,7 +85,10 @@ const LwpoCodeManagementDialog = memo(
mutationFn: (data: LwpoCodeFormData) =>
LwpoCodeApi.updateLwpoCode(upCd!, lwpoCd!, data),
onSuccess: () => {
alert("수정되었습니다.", () => onOpenChange?.(false));
alert("수정되었습니다.", () => {
onOpenChange?.(false);
onAfterSave?.();
});
},
});
@@ -96,15 +107,38 @@ const LwpoCodeManagementDialog = memo(
},
});
const onSubmit =
(callback: (data: LwpoCodeFormData) => void) =>
async (data: LwpoCodeFormData) => {
const confirmMessage = isRegistration
? "등록하시겠습니까?"
: "수정하시겠습니까?";
if (await confirm(confirmMessage)) callback(data);
};
const handleRegister = async () => {
if (!upCd) {
alert("상위코드가 없습니다.");
return;
}
const confirmMessage = "등록하시겠습니까?";
if (await confirm(confirmMessage)) {
const rawValues = form.getValues();
const values = {
...rawValues,
upCd: upCd,
sortSeq: Number(rawValues.sortSeq) || 0,
};
await createLwpoCode(values);
}
};
const handleSave = async () => {
if (!upCd) {
alert("상위코드가 없습니다.");
return;
}
const confirmMessage = "수정하시겠습니까?";
if (await confirm(confirmMessage)) {
const rawValues = form.getValues();
const values = {
...rawValues,
upCd: upCd,
sortSeq: Number(rawValues.sortSeq) || 0,
};
await updateLwpoCode(values);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
@@ -117,9 +151,14 @@ const LwpoCodeManagementDialog = memo(
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(
onSubmit(isRegistration ? createLwpoCode : updateLwpoCode),
)}
onSubmit={(e) => {
e.preventDefault();
if (isRegistration) {
handleRegister();
} else {
handleSave();
}
}}
className="space-y-4"
>
<Table variant="secondary">
@@ -251,6 +290,9 @@ const LwpoCodeManagementDialog = memo(
{...field}
type="number"
value={field.value ?? 0}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
className="!text-[1.3rem]"
placeholder="정렬순서를 입력해주세요"
/>
@@ -292,7 +334,11 @@ const LwpoCodeManagementDialog = memo(
</TableBody>
</Table>
<DialogFooter className="!justify-center">
<Button size="lg" type="submit">
<Button
size="lg"
type="submit"
onClick={isRegistration ? handleRegister : handleSave}
>
{isRegistration ? "등록" : "저장"}
</Button>
<DialogClose asChild>
+40 -12
View File
@@ -5,7 +5,7 @@ import { Button, Input, Label } from "@common/components";
import { LwpoCodeApi, type LwpoCodeParams } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable } from "@/shared/lib";
import { columns } from "../model/lwpocode-table-columns";
import { getColumns } from "../model/lwpocode-table-columns";
import { LwpoCodeManagementDialog } from "./lwpocode-dialog";
type LwpoCodeSectionProps = {
@@ -15,8 +15,10 @@ type LwpoCodeSectionProps = {
function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
const alert = useAlertStore((state) => state.setMessage);
const [dialogOpen, setDialogOpen] = useState(false);
const [lwpoCodeQuery, setLwpoCodeQuery] = useState<LwpoCodeParams>({
const [lwpoCdNm, setLwpoCdNm] = useState("");
const [searchParams, setSearchParams] = useState<LwpoCodeParams>({
lwpoCdNm: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -24,10 +26,10 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, searchParams, pagination],
queryFn: () =>
LwpoCodeApi.getLwpoCodeList({
...lwpoCodeQuery,
...searchParams,
upCd,
page: pagination.pageIndex,
size: pagination.pageSize,
@@ -36,6 +38,12 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
placeholderData: keepPreviousData,
});
// 등록/수정 후 목록 재조회 콜백
const handleAfterSave = () => {
query.refetch();
};
const columns = getColumns(handleAfterSave);
const { mutateAsync: codeRefreshAll } = useMutation({
mutationFn: LwpoCodeApi.codeRefreshAll,
onSuccess: () => {
@@ -59,7 +67,22 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
lwpoCdNm,
});
};
const handleReset = () => {
setLwpoCdNm("");
setSearchParams({
...searchParams,
lwpoCdNm: "",
});
};
useEffect(() => {
@@ -75,17 +98,20 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={lwpoCodeQuery.lwpoCdNm}
onChange={(e) =>
setLwpoCodeQuery({
...lwpoCodeQuery,
lwpoCdNm: e.target.value,
})
}
value={lwpoCdNm}
onChange={(e) => setLwpoCdNm(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -96,8 +122,10 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
코드갱신
</Button>
<LwpoCodeManagementDialog
upCd={upCd}
open={dialogOpen}
onOpenChange={setDialogOpen}
onAfterSave={handleAfterSave}
>
<Button>등록</Button>
</LwpoCodeManagementDialog>
+31 -11
View File
@@ -14,8 +14,10 @@ type UpCodeSectionProps = {
function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [upCodeQuery, setUpCodeQuery] = useState<UpCodeParams>({
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<UpCodeParams>({
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -23,10 +25,10 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, searchParams, pagination],
queryFn: () =>
UpCodeApi.getUpCodeList({
...upCodeQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -40,7 +42,22 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
};
const { tableComponent, tableInstance: table } = useDataTable({
@@ -66,17 +83,20 @@ function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) {
</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={upCodeQuery.keyword}
onChange={(e) =>
setUpCodeQuery({
...upCodeQuery,
keyword: e.target.value,
})
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -60,7 +60,7 @@ function ContentsForm({
</colgroup>
<TableBody>
<TableRow>
<TableHead>콘텐츠 제목</TableHead>
<TableHead data-required>콘텐츠 제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -82,7 +82,7 @@ function ContentsForm({
</TableCell>
</TableRow>
<TableRow>
<TableHead>콘텐츠 내용</TableHead>
<TableHead data-required>콘텐츠 내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -228,7 +228,7 @@ function ContentsForm({
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableHead data-required>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -17,19 +17,21 @@ import { columns } from "../model/table-columns";
function ContentsPage() {
const router = useRouter();
const [contentsQuery, setContentsQuery] = useState<ContentsParams>({
const [contsTtl, setContsTtl] = useState("");
const [searchParams, setSearchParams] = useState<ContentsParams>({
contsTtl: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
pageSize: 10,
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, searchParams, pagination],
queryFn: () =>
ContentsApi.getContentsList({
...contentsQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -38,7 +40,22 @@ function ContentsPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
contsTtl,
});
};
const handleReset = () => {
setContsTtl("");
setSearchParams({
...searchParams,
contsTtl: "",
});
};
const handleRowClick = (row: Contents) => {
@@ -72,12 +89,20 @@ function ContentsPage() {
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={contentsQuery.contsTtl}
onChange={(e) => setContentsQuery({ contsTtl: e.target.value })}
value={contsTtl}
onChange={(e) => setContsTtl(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+24 -6
View File
@@ -97,6 +97,16 @@ function ModifyPage({ sequential }: { sequential: string }) {
},
});
const { mutateAsync: rejectContents } = useMutationWrapper({
mutationFn: () => ContentsApi.rejectContents(contsSn, contsVer),
onSuccess: () => {
alert("반려되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS],
});
},
});
const form = useForm<ContentsFormData>({
resolver: zodResolver(contentsFormSchema),
values: contentsDetail,
@@ -107,14 +117,20 @@ function ModifyPage({ sequential }: { sequential: string }) {
(contentsVersionList?.content?.[0]?.contsVer ?? 0) > 1;
const onRequestApprove = async () => {
const message =
"승인 요청하면 더이상 내용을 수정할 수 없습니다.\n요청하시겠습니까?";
if (await confirm(message)) requestApprove();
if (
await confirm(
"승인 요청하면 더이상 내용을 수정할 수 없습니다.\n요청하시겠습니까?",
)
)
requestApprove();
};
const onApprove = async () => {
const message = "승인하시겠습니까?";
if (await confirm(message)) approveContents();
if (await confirm("승인하시겠습니까?")) approveContents();
};
const onReject = async () => {
if (await confirm("반려하시겠습니까?")) rejectContents();
};
useEffect(() => {
@@ -158,7 +174,9 @@ function ModifyPage({ sequential }: { sequential: string }) {
{approveStatus === CONTENTS_APPROVE_STATUS.REQUESTED && (
<>
<Button onClick={onApprove}>승인</Button>
<Button color="gray">반려</Button>
<Button color="gray" onClick={onReject}>
반려
</Button>
</>
)}
</div>
+31 -13
View File
@@ -14,8 +14,10 @@ function FilePage() {
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [attachmentQuery, setAttachmentQuery] = useState<AttachmentParams>({
const [orgnlFileNm, setOrgnlFileNm] = useState("");
const [searchParams, setSearchParams] = useState<AttachmentParams>({
orgnlFileNm: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -23,10 +25,10 @@ function FilePage() {
});
const query = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, pagination],
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, searchParams, pagination],
queryFn: () =>
AttachmentApi.getAttachmentsList({
...attachmentQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -64,14 +66,24 @@ function FilePage() {
},
});
const handleAttachmentQueryChange =
(key: keyof AttachmentParams) => (value: string) => {
setAttachmentQuery({ ...attachmentQuery, [key]: value });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
orgnlFileNm,
});
};
const handleReset = () => {
setOrgnlFileNm("");
setSearchParams({
...searchParams,
orgnlFileNm: "",
});
};
return (
@@ -86,10 +98,8 @@ function FilePage() {
<Input
id="keyword"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={attachmentQuery.orgnlFileNm || ""}
onChange={(e) =>
handleAttachmentQueryChange("orgnlFileNm")(e.target.value)
}
value={orgnlFileNm}
onChange={(e) => setOrgnlFileNm(e.target.value)}
placeholder="파일명을 입력하세요"
/>
</div>
@@ -97,6 +107,14 @@ function FilePage() {
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -4,7 +4,11 @@ import { type GroupListResponseDto } from "@/entities/group";
import { MenuAuthorityDialog } from "../ui/menu-authority-dialog";
import { GroupsDialog } from "../ui/groups-dialog";
export const columns: ColumnDef<GroupListResponseDto>[] = [
export const createColumns = ({
onDelete,
}: {
onDelete: (groupId: string) => void;
}): ColumnDef<GroupListResponseDto>[] => [
{
accessorKey: "groupId",
header: "그룹코드",
@@ -20,18 +24,28 @@ export const columns: ColumnDef<GroupListResponseDto>[] = [
{
id: "actions",
header: "관리",
cell: ({ row }) => (
<div
onClick={(e) => e.stopPropagation()}
className="flex justify-center gap-2"
>
<MenuAuthorityDialog groupId={row.original.groupId} />
<GroupsDialog groupId={row.original.groupId}>
<Button size="sm" color="white">
수정
cell: ({ row }) => {
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
onDelete(row.original.groupId);
};
return (
<div
onClick={(e) => e.stopPropagation()}
className="flex justify-center gap-2"
>
<MenuAuthorityDialog groupId={row.original.groupId} />
<GroupsDialog groupId={row.original.groupId}>
<Button size="sm" color="white">
수정
</Button>
</GroupsDialog>
<Button size="sm" color="red" onClick={handleDelete}>
삭제
</Button>
</GroupsDialog>
</div>
),
</div>
);
},
},
];
+5 -5
View File
@@ -37,7 +37,7 @@ function GroupForm({ form }: GroupFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>그룹코드</TableHead>
<TableHead data-required>그룹코드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -59,7 +59,7 @@ function GroupForm({ form }: GroupFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>그룹명</TableHead>
<TableHead data-required>그룹명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -81,7 +81,7 @@ function GroupForm({ form }: GroupFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>그룹구분</TableHead>
<TableHead data-required>그룹구분</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -146,7 +146,7 @@ function GroupForm({ form }: GroupFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>관리권한</TableHead>
<TableHead data-required>관리권한</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -176,7 +176,7 @@ function GroupForm({ form }: GroupFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>권한</TableHead>
<TableHead data-required>권한</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -12,7 +12,7 @@ import {
type RowSelectionState,
} from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button } from "@common/components";
import { Button,Input, Label } from "@common/components";
import { GroupUsersApi } from "@/entities/group-user";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable } from "@/shared/lib";
@@ -32,13 +32,17 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
pageIndex: 0,
pageSize: 10,
});
const [userNm, setUserNm] = useState("");
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.GROUP_USERS, groupId, pagination, userNm],
queryFn: () =>
GroupUsersApi.findPageGroupUser({
keyword: groupId,
userNm: userNm,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -46,6 +50,20 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
placeholderData: keepPreviousData,
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
// userNm이 이미 state에 반영되어 있으므로 별도 처리 불필요
// 쿼리키에 userNm이 포함되어 있어 자동 refetch됨
};
const handleReset = () => {
setUserNm("");
};
const { tableComponent, tableInstance: table } = useDataTable({
query,
tableOptions: {
@@ -97,6 +115,29 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
return (
<div className="flex-1">
<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-[20rem] rounded-sm !text-[1.3rem]"
value={userNm}
onChange={(e) => setUserNm(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">그룹 사용자 목록</h3>
@@ -43,7 +43,7 @@ const GroupsDialog = memo(
const { data: group } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
queryFn: () => GroupApi.findGroupById(groupId!),
enabled: !!groupId,
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
});
const { mutateAsync: createGroup } = useMutationWrapper({
+98 -7
View File
@@ -1,14 +1,20 @@
"use client";
import { useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
keepPreviousData,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button } from "@common/components";
import { GroupApi } from "@/entities/group";
import { Button, Input, Label } from "@common/components";
import { GroupApi , type GroupSearchParams} from "@/entities/group";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { columns } from "../model/groups-table-columns";
import { createColumns } from "../model/groups-table-columns";
import { GroupsDialog } from "./groups-dialog";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
type GroupsSectionProps = {
handleSelectedGroup: (groupId: string) => void;
@@ -20,25 +26,73 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
pageSize: 10,
});
const [groupNm, setGroupNm] = useState("");
const [searchParams, setSearchParams] = useState<GroupSearchParams>({
groupNm: "",
delYn :"N",
sort : "groupId,desc",
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS,searchParams, pagination],
queryFn: () =>
GroupApi.findPageGroup({
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const queryClient = useQueryClient();
const { mutateAsync: deleteGroup } = useMutationWrapper({
mutationFn: (groupId: string) => GroupApi.deleteGroup(groupId),
onSuccess: () => {
alert("삭제되었습니다.");
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === ENDPOINT.CMS_SERVICE.GROUPS,
});
},
});
const handleDelete = async (groupId: string) => {
if (await confirm("삭제하시겠습니까?")) {
await deleteGroup(groupId);
}
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
groupNm,
});
};
const handleReset = () => {
setGroupNm("");
setSearchParams({
...searchParams,
groupNm: "",
delYn:"N"
});
};
const handleRowClick = (row: { groupId: string }) => {
table.setRowSelection({ [row.groupId]: true });
handleSelectedGroup(row.groupId);
handleSelectedGroup(row.groupId); // 1. 리스트에서 선택된 groupId 저장
};
const { tableComponent, tableInstance: table } = useDataTable({
query,
tableOptions: {
columns,
columns: createColumns({ onDelete: handleDelete }),
state: { pagination },
manualPagination: true,
enableMultiRowSelection: false,
@@ -54,6 +108,43 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
return (
<div className="flex-1">
<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-[20rem] rounded-sm !text-[1.3rem]"
value={groupNm}
onChange={(e) => setGroupNm(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
<Label htmlFor="delYn" className="ml-4 mr-2 text-label">
삭제여부
</Label>
<input
id="delYn"
type="checkbox"
checked={searchParams.delYn === "Y"}
onChange={(e) =>
setSearchParams({
...searchParams,
delYn: e.target.checked ? "Y" : "N",
})
}
/>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">그룹 목록</h3>
+26 -23
View File
@@ -42,8 +42,9 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
file: uploadFile,
response: uploadResponse,
progress,
isValid,
setFile,
} = useTus();
} = useTus({ accept: ".jpeg,.jpg,.png" });
const setAttachmentFileCode = useAttachmentStore(
(state) => state.setAttachmentFileCode,
);
@@ -90,7 +91,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>이미지명</TableHead>
<TableHead data-required>이미지명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -111,7 +112,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>정렬순서</TableHead>
<TableHead data-required>정렬순서</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -137,7 +138,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>이미지유형</TableHead>
<TableHead data-required>이미지유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -157,7 +158,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableHead data-required>첨부파일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -208,24 +209,26 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
customPreview={
<>
<Label htmlFor="atflCd">
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
<div className="relative h-full w-full">
{imageUrl && (
<Image
src={imageUrl}
className="object-contain"
fill
alt="업로드된 이미지"
<div className="h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
{isValid && (
<div className="relative size-full">
{imageUrl && (
<Image
src={imageUrl}
className="object-contain"
fill
alt="업로드된 이미지"
/>
)}
<p className="absolute bottom-3 left-0 z-10 p-2 text-base leading-none">
{uploadFile?.name}
</p>
<Progress
value={progress}
className="absolute bottom-0 left-0 z-10"
/>
)}
</div>
<p className="absolute bottom-3 left-0 z-10 p-2 text-base leading-none">
{uploadFile?.name}
</p>
<Progress
value={progress}
className="absolute bottom-0 left-0 z-10"
/>
</div>
)}
</div>
</Label>
<div className="mb-2 text-lg font-medium text-label">
@@ -269,7 +272,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
</TableRow>
)}
<TableRow>
<TableHead>사용여부</TableHead>
<TableHead data-required>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+35 -13
View File
@@ -24,22 +24,23 @@ type PostingTarget = {
function ImagePage() {
const router = useRouter();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const [imageQuery, setImageQuery] = useState<ImageParams>({
keyword: "" ,
sort:"regDt,desc"
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<ImageParams>({
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, searchParams, pagination],
queryFn: () =>
ImageApi.getImageList({
...imageQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -47,7 +48,22 @@ function ImagePage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
};
const handleRowClick = (row: Image) => {
@@ -99,14 +115,20 @@ function ImagePage() {
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={imageQuery.keyword}
onChange={(e) =>
setImageQuery({ ...imageQuery, keyword: e.target.value })
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+45 -33
View File
@@ -27,8 +27,7 @@ type DateRange = {
to?: Date | undefined;
};
function LoginlogPage({}) {
// 1주일 전부터 오늘까지 기본 날짜 설정
function LoginlogPage() {
const defaultDateRange: DateRange = {
from: dayjs().subtract(7, "day").toDate(),
to: dayjs().toDate(),
@@ -41,24 +40,21 @@ function LoginlogPage({}) {
pageIndex: 0,
pageSize: 10,
});
const [loginlogPostQuery, setLoginlogPostQuery] = useState<LoginlogParams>({
const [userId, setUserId] = useState("");
const [errOcrnYn, setErrOcrnYn] = useState("N");
const [searchParams, setSearchParams] = useState<LoginlogParams>({
userId: "",
siteId: "",
errOcrnYn: "N",
remoteIp: "",
lgnMthd: "",
lgnErrCd: "",
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
searchDateTo: dayjs().format("YYYY-MM-DD"),
sort: "lgnDt,desc",
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, pagination],
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
queryFn: () =>
LoginlogApi.getLoginlogList({
...loginlogPostQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -85,15 +81,34 @@ function LoginlogPage({}) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsManualLoading(true);
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
userId,
errOcrnYn,
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"),
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"),
});
query.refetch().finally(() => {
setIsManualLoading(false);
});
};
const handleLoginlogParamsChange =
(key: keyof LoginlogParams) => (value: unknown) => {
setLoginlogPostQuery({ ...loginlogPostQuery, [key]: value });
};
const handleReset = () => {
setUserId("");
setErrOcrnYn("N");
setDateRange(defaultDateRange);
setSearchParams({
...searchParams,
userId: "",
errOcrnYn: "N",
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
searchDateTo: dayjs().format("YYYY-MM-DD"),
});
};
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange | undefined;
@@ -119,19 +134,7 @@ function LoginlogPage({}) {
default:
presetDateRange = undefined;
}
handleDateRangeChange(presetDateRange);
};
const handleDateRangeChange = (value: DateRange | undefined) => {
setDateRange(value || defaultDateRange);
setLoginlogPostQuery({
...loginlogPostQuery,
searchDateFrom:
dayjs(value?.from).format("YYYY-MM-DD") ??
dayjs().subtract(7, "day").format("YYYY-MM-DD"),
searchDateTo:
dayjs(value?.to).format("YYYY-MM-DD") ?? dayjs().format("YYYY-MM-DD"),
});
setDateRange(presetDateRange || defaultDateRange);
};
return (
@@ -141,7 +144,7 @@ function LoginlogPage({}) {
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={handleDateRangeChange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
@@ -159,13 +162,14 @@ function LoginlogPage({}) {
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={loginlogPostQuery.userId}
onChange={(e) =>
handleLoginlogParamsChange("userId")(e.target.value)
}
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<Label className="ml-4 mr-2 text-label">상태</Label>
<Select onValueChange={handleLoginlogParamsChange("errOcrnYn")}>
<Select
value={errOcrnYn}
onValueChange={(value) => setErrOcrnYn(value)}
>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -183,6 +187,14 @@ function LoginlogPage({}) {
? "조회 중..."
: "조회"}
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -0,0 +1,28 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageableList } from "@/shared/model";
import type {
MenuHistory,
MenuHistorySearchParams,
MenuHistoryService,
} from "../model/menu-history-interface";
export class MenuHistoryApiService implements MenuHistoryService {
public async getMenuHistoryList(
params: MenuHistorySearchParams,
): Promise<PageableList<MenuHistory>> {
const response = await client.get<PageableList<MenuHistory>>(
`${ENDPOINT.CMS_SERVICE.MENU_HISTORY}`,
{ params },
);
return response.data;
}
public async rollbackMenu(hstrySn: number): Promise<void> {
await client.post(`${ENDPOINT.CMS_SERVICE.MENU_HISTORY}/rollback`, {
hstrySn,
});
}
}
export const MenuHistoryApi = new MenuHistoryApiService();
@@ -1,65 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
Menu,
MenuFormData,
MenuService,
TreeMenu,
TreeNode,
} from "../model/menu-interface";
export class MenuApiService implements MenuService {
public async getMenuInfo(menuCd: number): Promise<Menu> {
const response = await client.get<Menu>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
);
return response.data;
}
public async createMenu(data: MenuFormData): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}`,
data,
);
return response.data;
}
public async updateMenu(
menuCd: number,
data: Omit<MenuFormData, "siteId">,
): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
data,
);
return response.data;
}
public async deleteMenu(menuCd: number): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`);
}
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
const response = await client.get<TreeMenu[]>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
);
return response.data;
}
public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
data,
);
return response.data;
}
public async updateMenuName(menuCd: number, name: string): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
);
return response.data;
}
}
export const MenuApi = new MenuApiService();
@@ -0,0 +1,33 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
TreeMenuService,
TreeMenu,
TreeNode,
} from "../model/tree-menu-interface";
export class TreeMenuApiService implements TreeMenuService {
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
const response = await client.get<TreeMenu[]>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
);
return response.data;
}
public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
data,
);
return response.data;
}
public async updateMenuName(menuCd: number, name: string): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
);
return response.data;
}
}
export const TreeMenuApi = new TreeMenuApiService();
+2 -1
View File
@@ -3,7 +3,7 @@ import type {
TreeUserOrganization,
TreeUser,
} from "@/entities/user-organization";
import type { TreeMenu, TreeNode } from "../model/menu-interface";
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
export const generateMenuTreeData = (
data: TreeMenu[],
@@ -153,6 +153,7 @@ export const filterUserOrganizationTreeDataByKeyword = (
): NodeModel<TreeUserOrganization | TreeUser>[] => {
return Array.from(
treeData.reduce((matchedNodes, node) => {
if (node.data && "ognzSn" in node.data) return matchedNodes; // 조직 노드는 필터링 제외
if (!node.text.includes(keyword)) return matchedNodes;
matchedNodes.add(node);
@@ -0,0 +1,71 @@
import type { ColumnDef } from "@tanstack/react-table";
import { Checkbox } from "@common/components";
import type { MenuHistory } from "./menu-history-interface";
import dayjs from "dayjs";
export const columns: ColumnDef<MenuHistory>[] = [
{
id: "select",
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="행 선택"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "chgTypeNm",
header: "이력구분",
meta: {
width: 80,
},
},
{
accessorKey: "menuKornNm",
header: "메뉴명",
meta: {
width: 100,
},
},
{
accessorKey: "bchgData",
header: "이전 데이터",
cell: ({ row }) => {
const parsed = JSON.parse(row.original.bchgData);
const stringified = JSON.stringify(parsed, null, 2);
return <pre className="break-all text-left">{stringified}</pre>;
},
meta: {
width: 250,
},
},
{
accessorKey: "achgData",
header: "변경 데이터",
cell: ({ row }) => {
const parsed = JSON.parse(row.original.achgData);
const stringified = JSON.stringify(parsed, null, 2);
return <pre className="break-all text-left">{stringified}</pre>;
},
meta: {
width: 250,
},
},
{
accessorKey: "regDt",
header: "변경일시",
cell: ({ row }) => {
return dayjs(row.original.regDt).format("YYYY-MM-DD HH:mm:ss");
},
},
{
accessorKey: "rgtrNm",
header: "변경자",
meta: {
width: 80,
},
},
];
@@ -0,0 +1,30 @@
import type { PageableParams, PageableList } from "@/shared/model";
export interface MenuHistorySearchParams extends PageableParams {
keyword?: string;
keywordType?: string;
menuCd?: number;
chgTypeCd?: string;
startDt?: string;
endDt?: string;
}
export interface MenuHistory {
hstrySn: number;
menuCd: number;
chgTypeCd: string;
chgTypeNm: string;
bchgData: string;
achgData: string;
rgtr: string;
regDt: string;
rgtrNm: string;
menuKornNm: string;
}
export interface MenuHistoryService {
getMenuHistoryList: (
params: MenuHistorySearchParams,
) => Promise<PageableList<MenuHistory>>;
rollbackMenu: (hstrySn: number) => Promise<void>;
}
@@ -1,86 +0,0 @@
import { z } from "zod";
export const MENU_TYPE = {
EMPTY: "EMPTY",
CONTENTS: "CONTENTS",
BOARD: "BOARD",
INSIDE: "INSIDE",
OUTSIDE: "OUTSIDE",
BOTTOM: "BOTTOM",
} as const;
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
export interface TreeNode {
menuCd?: number;
name?: string;
sortSeq?: number;
parentId?: number | null;
levelNo?: number;
icon?: string;
children?: TreeNode[];
}
export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
menuKornNm: string;
menuEngNm?: string;
iconNm?: string;
children?: TreeMenu[];
urlPath?: string;
menuLnkgTypeCd: string;
menuLnkgSn?: number;
qrcd?: string;
useYn: string;
otptYn: string;
requiredUrlPath?: boolean;
}
export const menuFormSchema = z.object({
siteId: z.string(),
menuKornNm: z.string(),
menuSubKornNm: z.string().nullish(),
menuEngNm: z.string().nullish(),
menuSubEngNm: z.string().nullish(),
levelNo: z.number(),
urlPath: z.string().nullish(),
iconNm: z.string().nullish(),
menuLnkgTypeCd: z.enum([
MENU_TYPE.EMPTY,
MENU_TYPE.CONTENTS,
MENU_TYPE.BOARD,
MENU_TYPE.INSIDE,
MENU_TYPE.OUTSIDE,
MENU_TYPE.BOTTOM,
]),
menuLnkgSn: z.number().nullish(),
otptYn: z.string().default("Y"),
prntMenuCd: z.number().nullish(),
sortSeq: z.number(),
evlUseYn: z.string().default("Y"),
qrcd: z.string().nullish(),
useYn: z.string().default("Y"),
newWndwYn: z.string().nullish(),
menuLnkgSnNm: z.string().nullish(),
menuPicUserUnqId: z.string().nullish(),
menuPicUserUnqIdNm: z.string().nullish(),
menuPicDsgnYmd: z.string().nullish(),
});
export type MenuFormData = z.infer<typeof menuFormSchema>;
export type Menu = Omit<MenuFormData, "iconNm"> & {
menuCd: number;
iconName?: string;
};
export interface MenuService {
getMenuInfo: (menuCd: number) => Promise<Menu>;
createMenu: (data: MenuFormData) => Promise<void>;
updateMenu: (
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>;
}
@@ -0,0 +1,29 @@
export interface TreeNode {
menuCd?: number;
name?: string;
sortSeq?: number;
parentId?: number | null;
levelNo?: number;
icon?: string;
children?: TreeNode[];
}
export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
menuKornNm: string;
menuEngNm?: string;
iconNm?: string;
children?: TreeMenu[];
urlPath?: string;
menuLnkgTypeCd: string;
menuLnkgSn?: number;
qrcd?: string;
useYn: string;
otptYn: string;
requiredUrlPath?: boolean;
}
export interface TreeMenuService {
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
updateMenuName: (menuCd: number, name: string) => Promise<void>;
}
+2 -2
View File
@@ -10,8 +10,8 @@ import {
TableHead,
TableRow,
} from "@common/components";
import { MENU_TYPE, type MenuFormData } from "@/entities/menu";
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
import { InfoTooltip } from "@/shared/ui/info-tooltip";
interface BaseMenuFormProps {
@@ -85,7 +85,7 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>메뉴 유형</TableHead>
<TableHead data-required>메뉴 유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+120 -28
View File
@@ -1,11 +1,23 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import type { UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button } from "@common/components";
import { Form } from "@/shared/ui/form";
import {
Button,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
LNB,
} from "@common/components";
import { MenuApi, menuFormSchema, type MenuFormData } from "@/entities/menu";
import { ManagementAuthorityDialog } from "@/entities/menu-authority";
import type { MenuFormData } from "../model/menu-interface";
import { Form } from "@/shared/ui/form";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { BaseMenuForm } from "./base-menu-form";
import { TypeMenuForm } from "./type-menu-form";
import { MenuHistoryDialog } from "./menu-history-dialog";
interface MenuFormProps {
form: UseFormReturn<MenuFormData>;
@@ -29,8 +41,22 @@ function MenuForm({
handleDelete,
}: MenuFormProps) {
const isModifyMode = !!menuCd;
const [open, setOpen] = useState(false);
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { data: menuTemporarilyStoredData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE, menuCd],
queryFn: () => MenuApi.getMenuTemporarilyStoredData(menuCd!),
enabled: !!menuCd,
});
const { mutate: menuTemporaryStorage } = useMutationWrapper({
mutationFn: () => MenuApi.menuTemporaryStorage(menuCd!, form.getValues()),
onSuccess: () => {
alert("메뉴 임시 저장이 완료되었습니다.");
},
});
const onSubmit = async (data: MenuFormData) => {
if (isModifyMode) {
@@ -58,36 +84,102 @@ function MenuForm({
}
};
const handleMenuTemporaryStorage = () => {
if (!menuCd || !menuFormSchema.parse(form.getValues())) return;
menuTemporaryStorage();
};
const handleLoadTemporaryData = () => {
if (!menuTemporarilyStoredData?.[0]?.menuTmprStrg) {
alert("불러올 임시 저장 데이터가 없습니다.");
return;
}
const storedData = menuTemporarilyStoredData[0].menuTmprStrg;
const parsedData = JSON.parse(storedData);
form.reset(parsedData);
};
const menuType = form.watch("menuLnkgTypeCd");
const isSubmitDisabled = !form.watch("siteId");
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
</h3>
<ManagementAuthorityDialog menuCd={menuCd} />
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
</h3>
<div className="flex items-center gap-2">
<MenuHistoryDialog menuCd={menuCd} />
<ManagementAuthorityDialog menuCd={menuCd} />
</div>
</div>
<div className="card">
<BaseMenuForm form={form} menuCd={menuCd} />
<TypeMenuForm form={form} menuType={menuType} />
</div>
</div>
<div className="card">
<BaseMenuForm form={form} menuCd={menuCd} />
<TypeMenuForm form={form} menuType={menuType} />
</div>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
<div className="flex justify-center gap-4 pb-12 pt-10">
{isModifyMode && (
<Button
size="lg"
color="white"
type="button"
onClick={handleLoadTemporaryData}
>
불러오기
</Button>
)}
{isModifyMode && (
<Button
size="lg"
color="gray"
type="button"
onClick={handleMenuTemporaryStorage}
>
임시저장
</Button>
)}
{isModifyMode && (
<Button
size="lg"
color="emerald"
type="button"
onClick={() => setOpen(true)}
>
미리보기
</Button>
)}
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
저장
</Button>
)}
</div>
</form>
</Form>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-h-[90vh]">
<DialogHeader>
<DialogTitle>메뉴 미리보기</DialogTitle>
</DialogHeader>
<div className="flex h-full max-h-[80vh] min-h-[60vh] flex-col gap-4 overflow-auto">
<LNB
currentPath={form.watch("urlPath") ?? ""}
propSiteId={form.watch("siteId")}
previewMode
classNames={{ wrapper: "static h-full border-none p-0" }}
/>
</div>
</DialogContent>
</Dialog>
</>
);
}
@@ -0,0 +1,163 @@
import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
getCoreRowModel,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Input,
Label,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { MenuHistoryApi } from "../api/menu-history-service";
import type { MenuHistorySearchParams } from "../model/menu-history-interface";
import { columns } from "../model/menu-history-columns";
type MenuHistoryDialogProps = {
menuCd?: number;
} & React.ComponentProps<typeof Dialog>;
function MenuHistoryDialog({ menuCd }: MenuHistoryDialogProps) {
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [searchParams, setSearchParams] = useState<MenuHistorySearchParams>({
keyword: "",
keywordType: "menuKornNm",
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const selectedHistoryId = parseInt(Object.keys(rowSelection)[0] ?? "0");
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_HISTORY, pagination],
queryFn: () =>
MenuHistoryApi.getMenuHistoryList({
...searchParams,
menuCd,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
enabled: !!menuCd && open,
});
const { mutateAsync: rollbackMenu } = useMutationWrapper({
mutationFn: (hstrySn: number) => MenuHistoryApi.rollbackMenu(hstrySn),
onSuccess: () => {
alert("메뉴가 성공적으로 복원되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
});
setOpen(false);
},
});
const handleRollback = async (hstrySn: number) => {
if (!(await confirm("메뉴를 '이전 데이터'로 되돌리시겠습니까?"))) return;
await rollbackMenu(hstrySn);
};
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
state: { pagination, rowSelection },
manualPagination: true,
enableRowSelection: true,
enableMultiRowSelection: false,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.hstrySn.toString(),
},
dataTableOptions: {
viewPagination: true,
},
});
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
query.refetch();
};
useEffect(() => {
if (open) {
setSearchParams({
...searchParams,
keyword: "",
});
setPagination({
...pagination,
pageIndex: 0,
});
setRowSelection({});
}
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button color="white" disabled={!menuCd}>
<i className="diveicon di-history" />
이력보기
</Button>
</DialogTrigger>
<DialogContent className="max-w-[100rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>메뉴 이력 관리</DialogTitle>
</DialogHeader>
<form onSubmit={handleSearch}>
<div className="mb-4 flex items-center gap-2">
<Label>메뉴명</Label>
<Input
placeholder="검색어를 입력하세요"
value={searchParams.keyword ?? ""}
onChange={(e) =>
setSearchParams({ ...searchParams, keyword: e.target.value })
}
className="w-[36rem]"
/>
<Button type="submit">조회</Button>
</div>
</form>
<div className="max-h-[56rem] overflow-auto">{tableComponent}</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button size="lg" color="white">
닫기
</Button>
</DialogClose>
<Button
size="lg"
onClick={() => handleRollback(selectedHistoryId)}
disabled={!selectedHistoryId}
>
되돌리기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { MenuHistoryDialog };
+3 -3
View File
@@ -4,8 +4,8 @@ import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
import { Button, cn } from "@common/components";
import { useMutationWrapper } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { MenuApi } from "../api/menu-service";
import type { TreeMenu } from "../model/menu-interface";
import { TreeMenuApi } from "../api/tree-menu-service";
import type { TreeMenu } from "../model/tree-menu-interface";
import { EditableNode } from "./editable-node";
type MenuNodeProps = {
@@ -37,7 +37,7 @@ function MenuNode({
const { mutateAsync: updateMenuName } = useMutationWrapper({
mutationFn: (nodeName: string) => {
const encodedName = encodeURIComponent(nodeName);
return MenuApi.updateMenuName(id as number, encodedName);
return TreeMenuApi.updateMenuName(id as number, encodedName);
},
onSuccess: () => {
queryClient.invalidateQueries({
+9 -8
View File
@@ -14,19 +14,20 @@ import {
SelectTrigger,
SelectValue,
} from "@common/components";
import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import type { TreeMenu } from "../model/menu-interface";
import { MenuApi } from "../api/menu-service";
import {
MenuApi,
MENU_TYPE,
menuFormSchema,
type MenuFormData,
} from "../model/menu-interface";
} from "@/entities/menu";
import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { generateMenuTreeData } from "../lib/utils";
import { TreeMenuApi } from "../api/tree-menu-service";
import type { TreeMenu } from "../model/tree-menu-interface";
import { MenuTree } from "./menu-tree";
import { MenuForm } from "./menu-form";
import { generateMenuTreeData } from "../lib/utils";
function MenuPage() {
const queryClient = useQueryClient();
@@ -46,7 +47,7 @@ function MenuPage() {
const { data: menuTreeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
queryFn: () => MenuApi.getTreeMenuList(siteId),
queryFn: () => TreeMenuApi.getTreeMenuList(siteId),
select: (data) => {
const treeData = generateMenuTreeData(data);
const defaultSortSeq =
+4 -3
View File
@@ -11,8 +11,9 @@ import {
import { Button } from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useMutationWrapper } from "@/shared/lib";
import { MenuApi } from "../api/menu-service";
import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface";
import type { MenuFormData } from "@/entities/menu";
import { TreeMenuApi } from "../api/tree-menu-service";
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
import { handleDropNode, handleSelectNode } from "../lib/utils";
import { MenuNode } from "./menu-node";
import { EditableNode } from "./editable-node";
@@ -41,7 +42,7 @@ function MenuTree({
const { mutate: updateTreeMenu } = useMutationWrapper({
mutationFn: (newTree: TreeNode) =>
MenuApi.updateTreeMenu(siteId, [newTree]),
TreeMenuApi.updateTreeMenu(siteId, [newTree]),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
@@ -12,7 +12,9 @@ import {
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -52,10 +54,22 @@ function OrganizationSearchDialog({
});
const handleSelectNode = (node: NodeModel<TreeModel>) => {
if (node.data && "ognzSn" in node.data) return;
setSelectedNode(node);
handleUserSelect(node.data as TreeUser);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
setTree(filterUserOrganizationTreeDataByKeyword(treeData ?? [], keyword));
};
const handleReset = () => {
setKeyword("");
setTree(treeData ?? []);
};
useEffect(() => {
setTree(treeData ?? []);
}, [treeData]);
@@ -73,21 +87,25 @@ function OrganizationSearchDialog({
<DialogTitle>담당자 조회</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="card card-border !m-0 flex w-full items-center gap-2">
<Input
className="h-[2.5rem] !text-[1.3rem]"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button
size="sm"
onClick={() =>
setTree(filterUserOrganizationTreeDataByKeyword(tree, keyword))
}
>
<i className="diveicon di-search text-base" />
조회
</Button>
<div className="card card-border !m-0 w-full">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<Input
className="h-[2.5rem] !text-[1.3rem]"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button type="submit" size="sm">
조회
</Button>
<Button
type="button"
size="sm"
color="white"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="h-[36rem] overflow-auto">
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
@@ -116,6 +134,16 @@ function OrganizationSearchDialog({
</DndProvider>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button color="white" onClick={() => setSelectedNode(null)}>
취소
</Button>
</DialogClose>
<DialogClose asChild>
<Button disabled={!selectedNode}>확인</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
+4 -4
View File
@@ -16,8 +16,8 @@ import {
import type { BoardPopup } from "@/entities/board";
import type { Contents } from "@/entities/content";
import type { TreeUser } from "@/entities/user-organization";
import type { MenuFormData, MenuType } from "@/entities/menu";
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
import type { MenuFormData, MenuType } from "../model/menu-interface";
import { OrganizationSearchDialog } from "./organization-search-dialog";
import { ContentSearchDialog } from "./content-search-dialog";
import { BoardSearchDialog } from "./board-search-dialog";
@@ -171,7 +171,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
)}
{(menuType === "CONTENTS" || menuType === "BOARD") && (
<TableRow>
<TableHead>메뉴 평가 사용 여부</TableHead>
<TableHead data-required>메뉴 평가 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -237,7 +237,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>출력 여부</TableHead>
<TableHead data-required>출력 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -268,7 +268,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableHead data-required>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+5 -5
View File
@@ -38,7 +38,7 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>API명</TableHead>
<TableHead data-required>API명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -59,7 +59,7 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>API URL</TableHead>
<TableHead data-required>API URL</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -80,7 +80,7 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>요청 메소드</TableHead>
<TableHead data-required>요청 메소드</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -100,7 +100,7 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>정렬순서</TableHead>
<TableHead data-required>정렬순서</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -126,7 +126,7 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableHead data-required>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+40 -20
View File
@@ -18,21 +18,23 @@ import { columns } from "../model/authorizations-table-columns";
function ModulePage() {
const router = useRouter();
const [authorizationQuery, setAuthorizationQuery] =
useState<AuthorizationsParams>({
authorizationName: "",
httpMethodCode: "",
});
const [authorizationName, setAuthorizationName] = useState("");
const [httpMethodCode, setHttpMethodCode] = useState("");
const [searchParams, setSearchParams] = useState<AuthorizationsParams>({
authorizationName: "",
httpMethodCode: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, pagination],
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, searchParams, pagination],
queryFn: () =>
AuthorizationsApi.getAuthorizationsList({
...authorizationQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -60,14 +62,27 @@ function ModulePage() {
},
});
const handleAuthorizationQueryChange =
(key: keyof AuthorizationsParams) => (value: string) => {
setAuthorizationQuery({ ...authorizationQuery, [key]: value });
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
authorizationName,
httpMethodCode,
});
};
const handleReset = () => {
setAuthorizationName("");
setHttpMethodCode("");
setSearchParams({
...searchParams,
authorizationName: "",
httpMethodCode: "",
});
};
return (
@@ -81,7 +96,8 @@ function ModulePage() {
</Label>
<CodeSelect
upCd="HTTPMHD"
onValueChange={handleAuthorizationQueryChange("httpMethodCode")}
value={httpMethodCode}
onValueChange={(value) => setHttpMethodCode(value)}
/>
</div>
<div className="flex items-center gap-2">
@@ -91,18 +107,22 @@ function ModulePage() {
<Input
id="keyword"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={authorizationQuery.authorizationName}
onChange={(e) =>
handleAuthorizationQueryChange("authorizationName")(
e.target.value,
)
}
value={authorizationName}
onChange={(e) => setAuthorizationName(e.target.value)}
/>
</div>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -87,7 +87,7 @@ function OrganizationForm({
</colgroup>
<TableBody>
<TableRow>
<TableHead>조직명</TableHead>
<TableHead data-required>조직명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -106,7 +106,7 @@ function OrganizationForm({
)}
/>
</TableCell>
<TableHead>정렬순서</TableHead>
<TableHead data-required>정렬순서</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -41,6 +41,10 @@ export class PopupApiService implements PopupService {
validatedData,
);
}
public async deletePopup(popupSn: number): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`);
}
}
export const PopupApi = new PopupApiService();
@@ -68,4 +68,5 @@ export interface PopupService {
getPopupInfo(popupSn: number): Promise<PopupInfo>;
createPopup(data: PopupFormData): Promise<void>;
updatePopup(popupSn: number, data: PopupUpdateFormData): Promise<void>;
deletePopup(popupSn: number): Promise<void>;
}
+13 -1
View File
@@ -52,6 +52,13 @@ function ModifyPage({ popupSn }: { popupSn: number }) {
},
});
const { mutateAsync: deletePopup } = useMutationWrapper({
mutationFn: () => PopupApi.deletePopup(popupSn),
onSuccess: () => {
alert("삭제되었습니다.", router.back);
},
});
const { mutateAsync: savePostingHistory } = useMutationWrapper({
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
onSuccess: () => {
@@ -101,7 +108,12 @@ function ModifyPage({ popupSn }: { popupSn: number }) {
<div className="card-header">
<h3 className="text-2xl font-medium text-label">팝업 수정</h3>
</div>
<PopupForm form={form} handleSave={updatePopup} />
<PopupForm
form={form}
popupSn={popupSn}
handleSave={updatePopup}
handleDelete={deletePopup}
/>
</div>
<div className="card card-border">
<div className="card-header">
+8 -8
View File
@@ -32,7 +32,7 @@ const Editor = dynamic(
type PopupFormProps<T extends PopupFormData | PopupUpdateFormData> = {
form: UseFormReturn<T>;
popupSn?: string;
popupSn?: number;
handleSave: (data: T) => Promise<void>;
handleDelete?: () => Promise<void>;
};
@@ -80,7 +80,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</colgroup>
<TableBody>
<TableRow>
<TableHead>제목</TableHead>
<TableHead data-required>제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -101,7 +101,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>팝업 크기</TableHead>
<TableHead data-required>팝업 크기</TableHead>
<TableCell className="border">
<div className="flex items-center gap-1">
<FormField
@@ -149,7 +149,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>팝업 위치</TableHead>
<TableHead data-required>팝업 위치</TableHead>
<TableCell className="border">
<div className="flex items-center gap-1">
<FormField
@@ -197,7 +197,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>팝업 유형</TableHead>
<TableHead data-required>팝업 유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -216,7 +216,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>콘텐츠 유형</TableHead>
<TableHead data-required>콘텐츠 유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -297,7 +297,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>팝업 그만 보기 사용 여부</TableHead>
<TableHead data-required>팝업 그만 보기 사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -327,7 +327,7 @@ function PopupForm<T extends PopupFormData | PopupUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableHead data-required>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+31 -8
View File
@@ -25,8 +25,10 @@ type PostingTarget = {
function PopupPage() {
const router = useRouter();
const [popupParams, setPopupParams] = useState<PopupParams>({
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PopupParams>({
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -36,10 +38,10 @@ function PopupPage() {
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, searchParams, pagination],
queryFn: () =>
PopupApi.getPopupList({
...popupParams,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -48,7 +50,22 @@ function PopupPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
};
const handleRowClick = (row: Popup) => {
@@ -99,14 +116,20 @@ function PopupPage() {
</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={popupParams.keyword}
onChange={(e) =>
setPopupParams({ ...popupParams, keyword: e.target.value })
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -27,7 +27,7 @@ export const columns: ColumnDef<PrivacyLog>[] = [
},
{
accessorKey: "rqstrNm",
header: "조회자명",
header: "요청자명",
},
{
accessorKey: "rqstrIp",
@@ -3,65 +3,54 @@
import dayjs from "dayjs";
import { useState } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
getCoreRowModel,
type PaginationState,
} from "@tanstack/react-table";
import {
Button,
DateRangePicker,
Input,
Button,
DateRangePicker,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
} from "@common/components";
import { PrivacyLogPrivacy } from "@/entities/privacylog";
import { type PrivacyLogParams} from "@/entities/privacylog/model/privacylog-interface";
import { useDataTable } from "@/shared/lib";
import type { PrivacyLogParams } from "@/entities/privacylog/model/privacylog-interface";
import { useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/privacylog/model/table-columns";
type DateRange = {
from: Date | undefined;
to?: Date | undefined;
};
function PrivacyLogPage({ }) {
// 1주일 전부터 오늘까지 기본 날짜 설정
function PrivacyLogPage() {
const defaultDateRange: DateRange = {
from: dayjs().subtract(7, "day").toDate(),
to: dayjs().toDate(),
};
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [privacylogPostQuery, setprivacylogPostQuery] = useState< PrivacyLogParams>({
siteId: "",
inqInfoKeywordType: "userName",
inqInfoKeyword: "",
rqstrIp: "",
inqDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
inqDtEnd: dayjs().format("YYYY-MM-DD"),
sort:"inqDt,asc"
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
inqInfoKeywordType: "userName",
inqInfoKeyword: "",
inqDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
inqDtEnd: dayjs().format("YYYY-MM-DD"),
sort: "inqDt,desc",
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.PRIVACYLOG, pagination],
queryKey: [ENDPOINT.USER_SERVICE.PRIVACYLOG, searchParams, pagination],
queryFn: () =>
PrivacyLogPrivacy.getPrivacyLogList({
...privacylogPostQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -87,14 +76,29 @@ function PrivacyLogPage({ }) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
inqInfoKeyword,
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
});
};
const handleprivacylogParamsChange =
(key: keyof PrivacyLogParams) => (value: unknown) => {
setprivacylogPostQuery({ ...privacylogPostQuery, [key]: value });
const handleReset = () => {
setInqInfoKeyword("");
setDateRange(defaultDateRange);
setSearchParams({
...searchParams,
inqInfoKeyword: "",
inqDtStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
inqDtEnd: dayjs().format("YYYY-MM-DD"),
});
};
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange | undefined;
switch (value) {
@@ -119,23 +123,9 @@ function PrivacyLogPage({ }) {
default:
presetDateRange = undefined;
}
handleDateRangeChange(presetDateRange);
setDateRange(presetDateRange || defaultDateRange);
};
const handleDateRangeChange = (value: DateRange | undefined) => {
setDateRange(value || defaultDateRange);
setprivacylogPostQuery({
...privacylogPostQuery,
inqDtStart: dayjs(value?.from).format("YYYY-MM-DD") ?? dayjs().subtract(7, "day").format("YYYY-MM-DD"),
inqDtEnd: dayjs(value?.to).format("YYYY-MM-DD") ?? dayjs().format("YYYY-MM-DD"),
});
};
return (
<div className="flex-1">
<div className="card card-border">
@@ -143,7 +133,7 @@ function PrivacyLogPage({ }) {
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={handleDateRangeChange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
@@ -158,24 +148,32 @@ function PrivacyLogPage({ }) {
</Select>
</div>
</DateRangePicker>
<Label className="ml-4 mr-2 text-label">조회자명</Label>
<Label className="ml-4 mr-2 text-label">조회정보</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={privacylogPostQuery.inqInfoKeyword}
onChange={(e) =>
handleprivacylogParamsChange("inqInfoKeyword")(e.target.value)
}
value={inqInfoKeyword}
onChange={(e) => setInqInfoKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">로그인로그 목록</h3>
<h3 className="text-2xl font-medium text-label">
개인정보 조회 로그 목록
</h3>
<div className="flex gap-2">
<Select
value={table.getState().pagination.pageSize.toString()}
@@ -196,7 +194,7 @@ function PrivacyLogPage({ }) {
</Select>
</div>
</div>
</div>
</div>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
</div>
</div>
+74 -32
View File
@@ -51,28 +51,43 @@ function RecentPage() {
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [dateRange, setDateRange] = useState<DateRange>();
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [tableSort, setTableSort] = useState("regDt,desc");
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [recentPostQuery, setRecentPostQuery] = useState<RecentPostParams>({
const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState("");
const [bbsId, setBbsId] = useState<string | null>(null);
const [delYn, setDelYn] = useState("N");
const [searchParams, setSearchParams] = useState<RecentPostParams>({
keywordType: "",
keyword: "",
bbsId: null,
delYn: "N",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc",
});
const [isDeletePostData, setIsDeletePostData] = useState(false);
const query = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, pagination, tableSort],
queryKey: [
ENDPOINT.BOARD_SERVICE.POSTS,
searchParams,
pagination,
tableSort,
],
queryFn: () =>
PostApi.getRecentPostList({
...recentPostQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
sort: tableSort,
@@ -115,14 +130,44 @@ function RecentPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsDeletePostData(recentPostQuery.delYn === "Y");
query.refetch();
setIsDeletePostData(delYn === "Y");
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keywordType,
keyword,
bbsId: bbsId ? parseInt(bbsId) : null,
delYn,
frDateYmd: dateRange?.from
? dayjs(dateRange.from).format("YYYYMMDD")
: "",
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
});
};
const handleRecentPostParamsChange =
(key: keyof RecentPostParams) => (value: unknown) => {
setRecentPostQuery({ ...recentPostQuery, [key]: value });
};
const handleReset = () => {
setKeywordType("");
setKeyword("");
setBbsId(null);
setDelYn("N");
setDateRange({
from: undefined,
to: undefined,
});
setSearchParams({
...searchParams,
keywordType: "",
keyword: "",
bbsId: null,
delYn: "N",
frDateYmd: "",
toDateYmd: "",
});
setIsDeletePostData(false);
};
const handlePresetDateRange = (value: string) => {
let presetDateRange: DateRange | undefined;
@@ -148,16 +193,7 @@ function RecentPage() {
default:
presetDateRange = undefined;
}
handleDateRangeChange(presetDateRange);
};
const handleDateRangeChange = (value: DateRange | undefined) => {
setDateRange(value);
setRecentPostQuery({
...recentPostQuery,
frDateYmd: dayjs(value?.from).format("YYYYMMDD") ?? "",
toDateYmd: dayjs(value?.to).format("YYYYMMDD") ?? "",
});
setDateRange(presetDateRange);
};
const onSoftDeletePost = async () => {
@@ -214,10 +250,7 @@ function RecentPage() {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">날짜</Label>
<DateRangePicker
value={dateRange}
onValueChange={handleDateRangeChange}
>
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
@@ -233,7 +266,10 @@ function RecentPage() {
</DateRangePicker>
<Label className="ml-4 mr-2 text-label">게시판</Label>
<Select onValueChange={handleRecentPostParamsChange("bbsId")}>
<Select
value={bbsId?.toString() || ""}
onValueChange={(value) => setBbsId(value ? value : null)}
>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -247,7 +283,7 @@ function RecentPage() {
</Select>
<Label className="ml-4 mr-2 text-label">상태</Label>
<Select onValueChange={handleRecentPostParamsChange("delYn")}>
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
@@ -259,8 +295,8 @@ function RecentPage() {
<Label className="ml-4 mr-2 text-label">검색</Label>
<Select
value={recentPostQuery.keywordType}
onValueChange={handleRecentPostParamsChange("keywordType")}
value={keywordType}
onValueChange={(value) => setKeywordType(value)}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
@@ -272,15 +308,21 @@ function RecentPage() {
</SelectContent>
</Select>
<Input
value={recentPostQuery.keyword}
onChange={(e) =>
handleRecentPostParamsChange("keyword")(e.target.value)
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+5 -5
View File
@@ -53,7 +53,7 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>권한ID</TableHead>
<TableHead data-required>권한ID</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -95,7 +95,7 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>권한명</TableHead>
<TableHead data-required>권한명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -116,7 +116,7 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>권한내용</TableHead>
<TableHead data-required>권한내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -137,7 +137,7 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>관리권한여부</TableHead>
<TableHead data-required>관리권한여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -166,7 +166,7 @@ function RoleForm({ roleId, form, ...props }: RoleFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableHead data-required>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+31 -8
View File
@@ -13,8 +13,10 @@ import { columns } from "../model/table-columns";
function RolePage() {
const router = useRouter();
const [roleQuery, setRoleQuery] = useState<RoleParams>({
const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<RoleParams>({
keyword: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -22,10 +24,10 @@ function RolePage() {
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.ROLES, pagination],
queryKey: [ENDPOINT.USER_SERVICE.ROLES, searchParams, pagination],
queryFn: () =>
RoleApi.getRoles({
...roleQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -54,7 +56,22 @@ function RolePage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
});
};
const handleReset = () => {
setKeyword("");
setSearchParams({
...searchParams,
keyword: "",
});
};
return (
@@ -66,14 +83,20 @@ function RolePage() {
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={roleQuery.keyword}
onChange={(e) =>
setRoleQuery({ ...roleQuery, keyword: e.target.value })
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -34,6 +34,10 @@ class ScheduleApiService implements ScheduleService {
): Promise<void> {
await client.put(`${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`, data);
}
public async deleteSchedule(schdlSn: number): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`);
}
}
export const ScheduleApi = new ScheduleApiService();
@@ -71,4 +71,5 @@ export interface ScheduleService {
getScheduleInfo(schdlSn: number): Promise<Schedule>;
createSchedule(data: ScheduleFormData): Promise<void>;
updateSchedule(schdlSn: number, data: ScheduleUpdateFormData): Promise<void>;
deleteSchedule(schdlSn: number): Promise<void>;
}
+16 -1
View File
@@ -35,6 +35,16 @@ function ModifyPage({ schdlSn }: { schdlSn: string }) {
},
});
const { mutateAsync: deleteSchedule } = useMutationWrapper({
mutationFn: () => ScheduleApi.deleteSchedule(parseInt(schdlSn)),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES],
});
alert("일정이 삭제되었습니다.", router.back);
},
});
const form = useForm<ScheduleUpdateFormData>({
resolver: zodResolver(scheduleUpdateFormSchema),
values: {
@@ -57,7 +67,12 @@ function ModifyPage({ schdlSn }: { schdlSn: string }) {
<div className="card-header">
<h3 className="text-2xl font-medium text-label">스케줄 수정</h3>
</div>
<ScheduleForm form={form} handleSave={handleSave} />
<ScheduleForm
form={form}
scheduleId={schdlSn}
handleSave={handleSave}
handleDelete={deleteSchedule}
/>
</div>
);
}
@@ -66,7 +66,7 @@ function ScheduleForm<T extends ScheduleFormData | ScheduleUpdateFormData>({
</colgroup>
<TableBody>
<TableRow>
<TableHead>제목</TableHead>
<TableHead data-required>제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -87,7 +87,7 @@ function ScheduleForm<T extends ScheduleFormData | ScheduleUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>일정 기간</TableHead>
<TableHead data-required>일정 기간</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
@@ -243,7 +243,7 @@ function ScheduleForm<T extends ScheduleFormData | ScheduleUpdateFormData>({
</TableCell>
</TableRow>
<TableRow>
<TableHead>일정 유형</TableHead>
<TableHead data-required>일정 유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+4 -4
View File
@@ -70,7 +70,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트 구분</TableHead>
<TableHead data-required>사이트 구분</TableHead>
<TableCell className="border">
<div className="flex gap-2">
<FormField
@@ -122,7 +122,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사이트명</TableHead>
<TableHead data-required>사이트명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -287,7 +287,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>기본 사이트 여부</TableHead>
<TableHead data-required>기본 사이트 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -316,7 +316,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableHead data-required>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+30 -10
View File
@@ -14,10 +14,10 @@ import { columns } from "../model/table-columns";
function SitePage() {
const router = useRouter();
const [siteQuery, setSiteQuery] = useState<SiteParams>({
const [siteNm, setSiteNm] = useState("");
const [searchParams, setSearchParams] = useState<SiteParams>({
siteNm: "",
sort:"regDt,desc"
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -25,10 +25,10 @@ function SitePage() {
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.SITES, searchParams, pagination],
queryFn: () =>
SiteApi.getSiteList({
...siteQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -37,7 +37,18 @@ function SitePage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setSearchParams({
...searchParams,
siteNm,
});
};
const handleReset = () => {
setSiteNm("");
setSearchParams({
...searchParams,
siteNm: "",
});
};
const handleRowClick = (row: Site) => {
@@ -65,17 +76,26 @@ function SitePage() {
<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 htmlFor="siteNm" className="mr-2 text-label">
사이트명
</Label>
<Input
name="siteNm"
value={siteNm}
onChange={(e) => setSiteNm(e.target.value)}
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 type="submit" className="h-[3.2rem]">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -45,6 +45,10 @@ export class SurveyApiService implements SurveyService {
await client.put(`${ENDPOINT.CMS_SERVICE.SURVEY}/${srvySn}`, data);
}
public async deleteSurvey(srvySn: number): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.SURVEY}/${srvySn}`);
}
public async getQuestionList(params: QuestionParams): Promise<QuestionList> {
const response = await client.get<QuestionList>(
`${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}`,
+16 -1
View File
@@ -29,6 +29,13 @@ function DetailPage({ srvySn }: { srvySn: string }) {
queryFn: () => SurveyApi.getSurveyInfo(parseInt(srvySn)),
});
const { mutateAsync: deleteSurvey } = useMutationWrapper({
mutationFn: () => SurveyApi.deleteSurvey(parseInt(srvySn)),
onSuccess: () => {
alert("삭제되었습니다.", router.back);
},
});
const postHistoryQuery = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, srvySn],
queryFn: () =>
@@ -77,6 +84,12 @@ function DetailPage({ srvySn }: { srvySn: string }) {
},
});
const handleDeleteSurvey = async () => {
if (await confirm("삭제하시겠습니까?")) {
deleteSurvey();
}
};
const handleDeletePostingHistory = async () => {
const data = postingTable.getFilteredSelectedRowModel().rows.map((row) => ({
...row.original,
@@ -101,7 +114,9 @@ function DetailPage({ srvySn }: { srvySn: string }) {
<Button onClick={() => router.push(`${pathname}/modify`)}>
수정
</Button>
<Button color="red">삭제</Button>
<Button color="red" onClick={handleDeleteSurvey}>
삭제
</Button>
</div>
</div>
<ul className="flex flex-col">
@@ -41,7 +41,7 @@ function QuestionForm({ form, actionSlot, handleSave }: QuestionFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>문항순번</TableHead>
<TableHead data-required>문항순번</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -66,7 +66,7 @@ function QuestionForm({ form, actionSlot, handleSave }: QuestionFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>문항유형</TableHead>
<TableHead data-required>문항유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -87,7 +87,7 @@ function QuestionForm({ form, actionSlot, handleSave }: QuestionFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>문항내용</TableHead>
<TableHead data-required>문항내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+2 -2
View File
@@ -29,7 +29,7 @@ function SurveyForm({ form, ...props }: SurveyFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>설문 제목</TableHead>
<TableHead data-required>설문 제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -50,7 +50,7 @@ function SurveyForm({ form, ...props }: SurveyFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>설문 목적</TableHead>
<TableHead data-required>설문 목적</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+32 -9
View File
@@ -25,21 +25,23 @@ type PostingTarget = {
function SurveyPage() {
const router = useRouter();
const [surveyParams, setSurveyParams] = useState<SurveyParams>({
const [srvyTtl, setSrvyTtl] = useState("");
const [searchParams, setSearchParams] = useState<SurveyParams>({
srvyTtl: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, searchParams, pagination],
queryFn: () =>
SurveyApi.getSurveyList({
...surveyParams,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -71,7 +73,22 @@ function SurveyPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
srvyTtl,
});
};
const handleReset = () => {
setSrvyTtl("");
setSearchParams({
...searchParams,
srvyTtl: "",
});
};
useEffect(() => {
@@ -99,14 +116,20 @@ function SurveyPage() {
</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={surveyParams.srvyTtl}
onChange={(e) =>
setSurveyParams({ ...surveyParams, srvyTtl: e.target.value })
}
value={srvyTtl}
onChange={(e) => setSrvyTtl(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -39,7 +39,7 @@ function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>템플릿 구분</TableHead>
<TableHead data-required>템플릿 구분</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -59,7 +59,7 @@ function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>템플릿명</TableHead>
<TableHead data-required>템플릿명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -80,7 +80,7 @@ function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>템플릿 내용</TableHead>
<TableHead data-required>템플릿 내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+35 -19
View File
@@ -14,22 +14,23 @@ import type { Template, TemplateParams } from "../model/template-interface";
function TemplatePage() {
const router = useRouter();
const [templateQuery, setTemplateQuery] = useState<TemplateParams>({
tmpltSeCd: "",
const [keyword, setKeyword] = useState("");
const [tmpltSeCd, setTmpltSeCd] = useState("");
const [searchParams, setSearchParams] = useState<TemplateParams>({
keyword: "",
sort:"regDt,desc"
tmpltSeCd: "",
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
pageSize: 10,
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, searchParams, pagination],
queryFn: () =>
TemplateApi.getTemplateList({
...templateQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -38,7 +39,21 @@ function TemplatePage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setSearchParams({
...searchParams,
keyword,
tmpltSeCd,
});
};
const handleReset = () => {
setKeyword("");
setTmpltSeCd("");
setSearchParams({
...searchParams,
keyword: "",
tmpltSeCd: "",
});
};
const handleRowClick = (row: Template) => {
@@ -73,10 +88,8 @@ function TemplatePage() {
</Label>
<CodeSelect
upCd="TMPLTCLCD"
value={templateQuery.tmpltSeCd}
onValueChange={(value) =>
setTemplateQuery({ ...templateQuery, tmpltSeCd: value })
}
value={tmpltSeCd}
onValueChange={(value) => setTmpltSeCd(value)}
/>
</div>
<div className="flex items-center gap-2">
@@ -85,19 +98,22 @@ function TemplatePage() {
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={templateQuery.keyword}
onChange={(e) =>
setTemplateQuery({
...templateQuery,
keyword: e.target.value,
})
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
+4 -4
View File
@@ -39,7 +39,7 @@ function TermsForm({ form, trmsSn, ...props }: TermsFormProps) {
</colgroup>
<TableBody>
<TableRow>
<TableHead>약관 구분</TableHead>
<TableHead data-required>약관 구분</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -59,7 +59,7 @@ function TermsForm({ form, trmsSn, ...props }: TermsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>약관명</TableHead>
<TableHead data-required>약관명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -80,7 +80,7 @@ function TermsForm({ form, trmsSn, ...props }: TermsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>약관 내용</TableHead>
<TableHead data-required>약관 내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -99,7 +99,7 @@ function TermsForm({ form, trmsSn, ...props }: TermsFormProps) {
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용 여부</TableHead>
<TableHead data-required>사용 여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+39 -18
View File
@@ -15,21 +15,23 @@ import type { TermsParams, Terms } from "../model/terms-interface";
function TermsPage() {
const router = useRouter();
const [termsQuery, setTermsQuery] = useState<TermsParams>({
const [keyword, setKeyword] = useState("");
const [trmsSeCd, setTrmsSeCd] = useState("");
const [searchParams, setSearchParams] = useState<TermsParams>({
keyword: "",
trmsSeCd: "",
keyword: "",
sort:"regDt,desc"
sort: "regDt,desc",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
pageSize: 10,
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, pagination],
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, searchParams, pagination],
queryFn: () =>
TermsApi.getTermsList({
...termsQuery,
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -38,7 +40,25 @@ function TermsPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keyword,
trmsSeCd,
});
};
const handleReset = () => {
setKeyword("");
setTrmsSeCd("");
setSearchParams({
...searchParams,
keyword: "",
trmsSeCd: "",
});
};
const handleRowClick = (row: Terms) => {
@@ -73,10 +93,8 @@ function TermsPage() {
</Label>
<CodeSelect
upCd="TRMSCLCD"
value={termsQuery.trmsSeCd}
onValueChange={(value) =>
setTermsQuery({ ...termsQuery, trmsSeCd: value })
}
value={trmsSeCd}
onValueChange={(value) => setTrmsSeCd(value)}
/>
</div>
<div className="flex items-center gap-2">
@@ -85,19 +103,22 @@ function TermsPage() {
</Label>
<Input
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={termsQuery.keyword}
onChange={(e) =>
setTermsQuery({
...termsQuery,
keyword: e.target.value,
})
}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="card card-border">
@@ -50,6 +50,7 @@ function AgencySearchDialog({ onSelectAgency }: AgencySearchDialogProps) {
queryFn: () =>
LwpoCodeApi.getLwpoCodeList({
...searchQuery,
upCd: "AGENCYCD",
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -70,6 +71,9 @@ function AgencySearchDialog({ onSelectAgency }: AgencySearchDialogProps) {
{
accessorKey: "lwpoCd",
header: "기관코드",
meta: {
width: 200,
},
},
{
accessorKey: "lwpoCdNm",
@@ -120,11 +124,13 @@ function AgencySearchDialog({ onSelectAgency }: AgencySearchDialogProps) {
<DialogTrigger asChild>
<Button className="px-6">기관 검색</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogContent
className="max-h-[80vh] max-w-[60rem] overflow-auto"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>기관 검색</DialogTitle>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
@@ -150,9 +156,7 @@ function AgencySearchDialog({ onSelectAgency }: AgencySearchDialogProps) {
</div>
</div>
</form>
{tableComponent}
<DialogFooter className="!justify-center">
<Button
size="lg"
+21 -9
View File
@@ -3,6 +3,7 @@
import dayjs from "dayjs";
import { usePathname, useRouter } from "next/navigation";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Table,
@@ -18,10 +19,11 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
function UserDetail({ userUnqId }: { userUnqId: string }) {
const router = useRouter();
const pathname = usePathname();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const { data } = useQuery({
const { data, refetch } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
queryFn: () => UserApi.findByUserUnqId(userUnqId),
enabled: !!userUnqId,
@@ -31,9 +33,7 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
mutationFn: () => UserApi.lockUser(userUnqId),
onSuccess: () => {
alert("계정 잠금이 활성화 되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
});
refetch();
},
});
@@ -41,9 +41,7 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
mutationFn: () => UserApi.unlockUser(userUnqId),
onSuccess: () => {
alert("계정 잠금이 해제되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
});
refetch();
},
});
@@ -51,12 +49,26 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
mutationFn: () => UserApi.applyUser(userUnqId),
onSuccess: () => {
alert("사용자 승인이 완료되었습니다.");
refetch();
},
});
const { mutateAsync: deleteUser } = useMutationWrapper({
mutationFn: () => UserApi.deleteUser(userUnqId),
onSuccess: () => {
alert("회원이 삭제되었습니다.", router.back);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
queryKey: [ENDPOINT.USER_SERVICE.USERS],
});
},
});
const handleDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
await deleteUser();
}
};
return (
<div className="card card-border !mt-6">
<div className="card-header">
@@ -164,7 +176,7 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
<Button size="lg" onClick={() => router.push(`${pathname}/modify`)}>
수정
</Button>
<Button size="lg" color="red">
<Button size="lg" color="red" onClick={handleDelete}>
삭제
</Button>
</div>
+7 -7
View File
@@ -140,7 +140,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
}, [laeSeCd, watchDtlInstCd]);
const enoCdSelectData = useMemo(
() => laeSeCd?.filter((code) => code.unit?.startsWith("G2")) ?? [],
() => laeSeCd?.filter((code) => code.lwpoCd?.startsWith("G2")) ?? [],
[laeSeCd],
);
@@ -179,7 +179,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
<TableBody>
{/* 아이디 */}
<TableRow>
<TableHead>아이디</TableHead>
<TableHead data-required>아이디</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -223,7 +223,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
{/* 비밀번호 */}
<TableRow>
<TableHead>비밀번호</TableHead>
<TableHead data-required>비밀번호</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -270,7 +270,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
{/* 비밀번호 힌트 */}
<TableRow>
<TableHead>비밀번호 힌트</TableHead>
<TableHead data-required>비밀번호 힌트</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -316,7 +316,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
{/* 이름 */}
<TableRow>
<TableHead>이름</TableHead>
<TableHead data-required>이름</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -489,7 +489,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
{/* 기관구분 */}
<TableRow>
<TableHead>기관구분</TableHead>
<TableHead data-required>기관구분</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -571,7 +571,7 @@ function UsersForm<T extends UserSaveData | UserUpdateData>({
{/* 소속 */}
<TableRow>
<TableHead>소속</TableHead>
<TableHead data-required>소속</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+100 -48
View File
@@ -2,7 +2,6 @@
import dayjs from "dayjs";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
@@ -40,19 +39,25 @@ function UsersPage() {
pageIndex: 0,
pageSize: 10,
});
const [dateRange, setDateRange] = useState<DateRange>();
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [keywordType, setKeywordType] = useState("all");
const [keyword, setKeyword] = useState("");
const [userStateCode, setUserStateCode] = useState("");
const [aprvYn, setAprvYn] = useState("");
const [grpId, setGrpId] = useState("");
const { register, watch, setValue, getValues } = useForm<UserSearchParams>({
defaultValues: {
keywordType: "all",
keyword: "",
userStateCode: "",
aprvYn: "",
grpId: "",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc"
},
const [searchParams, setSearchParams] = useState<UserSearchParams>({
keywordType: "all",
keyword: "",
userStateCode: "",
aprvYn: "",
grpId: "",
frDateYmd: "",
toDateYmd: "",
sort: "regDt,desc",
});
const { data: userSttsStats } = useQuery({
@@ -61,16 +66,16 @@ function UsersPage() {
});
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination, watch("sort")],
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParams, pagination],
queryFn: () =>
UserApi.findPageUser({
...getValues(),
...searchParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
});
const { data: userStateCode } = useQuery({
const { data: userStateCodes } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE],
queryFn: () => CodeApi.getCode("USERSTTS"),
});
@@ -98,7 +103,44 @@ function UsersPage() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
query.refetch();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParams({
...searchParams,
keywordType,
keyword,
userStateCode,
aprvYn,
grpId,
frDateYmd: dateRange?.from
? dayjs(dateRange.from).format("YYYYMMDD")
: "",
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
});
};
const handleReset = () => {
setKeywordType("all");
setKeyword("");
setUserStateCode("");
setAprvYn("");
setGrpId("");
setDateRange({
from: undefined,
to: undefined,
});
setSearchParams({
...searchParams,
keywordType: "all",
keyword: "",
userStateCode: "",
aprvYn: "",
grpId: "",
frDateYmd: "",
toDateYmd: "",
});
};
const handlePresetDateRange = (value: string) => {
@@ -126,8 +168,6 @@ function UsersPage() {
presetDateRange = undefined;
}
setDateRange(presetDateRange);
setValue("frDateYmd", dayjs(presetDateRange?.from).format("YYYYMMDD"));
setValue("toDateYmd", dayjs(presetDateRange?.to).format("YYYYMMDD"));
};
const getUserSttsStatsCnt = (code: string): number => {
@@ -135,6 +175,21 @@ function UsersPage() {
return userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
};
const handleUserStateCodeChange = (code: string, checked: boolean) => {
let newUserStateCode = userStateCode;
if (checked) {
newUserStateCode = userStateCode ? `${userStateCode},${code}` : code;
} else {
newUserStateCode = userStateCode
.split(",")
.filter((c) => c !== code)
.join(",");
}
setUserStateCode(newUserStateCode);
};
return (
<div className="card card-border !mt-6">
<div className="card card-border flex items-center justify-center gap-10 !bg-accent !py-6">
@@ -195,8 +250,8 @@ function UsersPage() {
회원정보
</Label>
<Select
{...register("keywordType")}
onValueChange={(value) => setValue("keywordType", value)}
value={keywordType}
onValueChange={(value) => setKeywordType(value)}
>
<SelectTrigger className="w-[18rem]">
<SelectValue placeholder="선택" />
@@ -210,7 +265,8 @@ function UsersPage() {
</SelectContent>
</Select>
<Input
{...register("keyword")}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="!text-[1.3rem]"
placeholder="검색어를 입력하세요"
/>
@@ -221,14 +277,7 @@ function UsersPage() {
</div>
<div className="flex items-center gap-2">
<Label className="mr-2 text-label">기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => {
setDateRange(value);
setValue("frDateYmd", dayjs(value?.from).format("YYYYMMDD"));
setValue("toDateYmd", dayjs(value?.to).format("YYYYMMDD"));
}}
>
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
@@ -250,24 +299,14 @@ function UsersPage() {
<div className="flex items-center gap-2">
<Label className="mr-2 text-label">회원상태</Label>
<div className="flex gap-4">
{userStateCode?.map((item) => (
{userStateCodes?.map((item) => (
<div key={item.lwpoCd} className="flex items-center gap-2">
<Checkbox
id={item.lwpoCd}
value={item.lwpoCd}
checked={watch("userStateCode")?.includes(item.lwpoCd)}
checked={userStateCode?.includes(item.lwpoCd)}
onCheckedChange={(checked) => {
setValue(
"userStateCode",
checked
? getValues("userStateCode")
? `${getValues("userStateCode")},${item.lwpoCd}`
: item.lwpoCd
: getValues("userStateCode")
?.split(",")
.filter((code) => code !== item.lwpoCd)
.join(","),
);
handleUserStateCodeChange(item.lwpoCd, !!checked);
}}
/>
<Label htmlFor={item.lwpoCd}>{item.lwpoCdNm}</Label>
@@ -276,10 +315,20 @@ function UsersPage() {
</div>
</div>
<hr />
<Button type="submit" className="mx-auto w-[16rem]">
<i className="diveicon di-search" />
검색
</Button>
<div className="flex justify-center gap-2">
<Button type="submit" className="w-[16rem]">
<i className="diveicon di-search" />
검색
</Button>
<Button
type="button"
color="white"
className="w-[16rem]"
onClick={handleReset}
>
초기화
</Button>
</div>
</form>
</div>
<div className="card card-border">
@@ -290,7 +339,10 @@ function UsersPage() {
<Select
defaultValue="regDt,desc"
onValueChange={(value) => {
setValue("sort", value);
setSearchParams({
...searchParams,
sort: value,
});
}}
>
<SelectTrigger>
+3 -2
View File
@@ -20,9 +20,7 @@ export const ENDPOINT = {
ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations",
ROLES: "/user-service/api/v1/roles",
LOGINLOG: "/user-service/api/v1/login-logs",
APILOG: "/user-service/api/v1/api-logs",
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
},
CMS_SERVICE: {
SITES: "/cms-service/api/v1/sites",
@@ -33,6 +31,8 @@ export const ENDPOINT = {
MENUS: "/cms-service/api/v1/menus",
MENU_ROLES: "/cms-service/api/v1/menu-roles",
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
MENU_TEMPORARY_STORAGE: "/cms-service/api/v1/menu-tmpr-strg",
MENU_HISTORY: "/cms-service/api/v1/menu-hstry",
CONTENTS: "/cms-service/api/v1/contents",
CONTENTS_DETAILS: "/cms-service/api/v1/contents-details",
ORGANIZATIONS: "/cms-service/api/v1/ognzs",
@@ -52,6 +52,7 @@ export const ENDPOINT = {
GROUPS: "/cms-service/api/v1/groups",
GROUP_USERS: "/cms-service/api/v1/group-users",
MANAGER_IP: "/cms-service/api/v1/mgrip",
APILOG: "/cms-service/api/v1/api-logs",
},
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",
@@ -17,15 +17,13 @@ export const useMutationWrapper = <
let errorMessage = "";
if (error instanceof AxiosError) {
if (error.response?.status)
errorMessage += `status: ${error.response.status}\n`;
if (error.response?.data?.message)
errorMessage += `${error.response.data.message}\n`;
if (error.response?.data?.errors[0]?.message)
errorMessage += `${error.response.data.errors[0].message}\n`;
}
alert(`[Error] ${errorMessage}`);
alert(`${errorMessage}`);
},
});
};
+27 -1
View File
@@ -2,9 +2,15 @@ import { useEffect, useState } from "react";
import { type HttpResponse, Upload } from "tus-js-client";
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "../config";
import { useAttachmentStore } from "./use-attachment-store";
import { useAlertStore } from "./use-alert-store";
import { getCookie } from "./cookie";
export const useTus = () => {
type TusProps = {
accept?: string;
};
export const useTus = ({ accept }: TusProps = {}) => {
const alert = useAlertStore((state) => state.setMessage);
const { attachmentFileCode, setAttachmentFileCode } = useAttachmentStore(
(state) => state,
);
@@ -13,6 +19,7 @@ export const useTus = () => {
const [upload, setUpload] = useState<Upload | null>(null);
const [response, setResponse] = useState<HttpResponse | null>(null);
const [progress, setProgress] = useState<number>(0);
const [isValid, setIsValid] = useState<boolean>(false);
const metadata = {
filename: file?.name ?? "",
@@ -20,6 +27,23 @@ export const useTus = () => {
atflCd: attachmentFileCode,
};
const validFileType = (file: File) => {
const allowedExtensions = accept?.split(",") ?? [];
const fileName = file.name;
const fileExtension = fileName
.substring(fileName.lastIndexOf("."))
.toLowerCase();
if (!allowedExtensions.includes(fileExtension)) {
alert("파일 형식이 올바르지 않습니다.");
setIsValid(false);
return false;
}
setIsValid(true);
return true;
};
const createUpload = () => {
return new Upload(file!, {
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
@@ -48,6 +72,7 @@ export const useTus = () => {
useEffect(() => {
if (!file) return;
if (!validFileType(file)) return;
const upload = createUpload();
upload.start();
@@ -59,6 +84,7 @@ export const useTus = () => {
upload,
response,
progress,
isValid,
setFile,
};
};
@@ -102,7 +102,7 @@ function GeneralFileUploader<T extends { atflCd?: string | null }>({
<span>{attachment.orgnlFileNm}</span>
<Button
color="lightGray"
className="flex size-7 items-center justify-center rounded-full p-0"
className="flex size-7 shrink-0 items-center justify-center rounded-full p-0"
onClick={() =>
setDeleteAttachmentIds([...deleteAttachmentIds, atflId])
}
+2 -2
View File
@@ -77,7 +77,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
</colgroup>
<TableBody>
<TableRow>
<TableHead>제목</TableHead>
<TableHead data-required>제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
@@ -120,7 +120,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
</TableRow>
{!userInfo?.userUnqId && (
<TableRow>
<TableHead>비밀번호</TableHead>
<TableHead data-required>비밀번호</TableHead>
<TableCell className="border">
<FormField
control={form.control}
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
PORT=3000
LOG_FILE="start_cs.log"
# 1. 기존 포트 사용 프로세스 종료
PID=$(lsof -t -i :$PORT)
if [ -n "$PID" ]; then
echo "Killing process on port $PORT (PID: $PID)"
kill -9 $PID
fi
# 2. 백그라운드로 앱 시작 (로그 파일로 리디렉션)
echo "Starting app on port $PORT..."
nohup pnpm start > $LOG_FILE 2>&1 &
# 3. PID 확인
sleep 1
NEW_PID=$(lsof -t -i :$PORT)
if [ -n "$NEW_PID" ]; then
echo "App started successfully (PID: $NEW_PID)"
echo "Logs: tail -f $LOG_FILE"
else
echo "Failed to start app. Check logs: $LOG_FILE"
fi
+2 -2
View File
@@ -3,8 +3,8 @@ NEXT_PUBLIC_PORT = 11302
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302
NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver_swl
NEXT_PUBLIC_RSONE_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터
NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
+1 -1
View File
@@ -3,7 +3,7 @@ NEXT_PUBLIC_PORT = 11301
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_TEMP_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
+2 -2
View File
@@ -3,12 +3,12 @@ NEXT_PUBLIC_PORT = 3001
NEXT_PUBLIC_PROXY_HOST = http://localhost:3001
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_TEMP_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+12
View File
@@ -0,0 +1,12 @@
NEXT_PUBLIC_SITE_ID = WR
NEXT_PUBLIC_PORT = 11301
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+18 -3
View File
@@ -3,23 +3,38 @@ import { BaseLayout } from "@common/components";
import "./globals.css";
import { StoreProvider } from "./store";
import { MainDiv } from "@common/business_components";
import koBase from '../public/locales/ko/message.json';
import i18next from "i18next";
import { I18nextProvider } from "react-i18next";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
i18next.init({
lng: 'ko',
fallbackLng: 'ko',
resources: {
ko: {
base: koBase,
},
}
});
return (
<html lang="en">
<body>
<I18nextProvider i18n={i18next}>
<StoreProvider>
<BaseLayout>
<MainDiv>
<MainDiv>
{children}
</MainDiv>
</MainDiv>
</BaseLayout>
</StoreProvider>
</I18nextProvider>
</body>
</html>
</html >
);
}
-201
View File
@@ -1,201 +0,0 @@
'use client'
import { getData, putData } from "../../../sn/app/components/lib/api";
import Modal from "../../../sn/app/components/ui/modal";
import { VerticalTable } from "../../../sn/app/components/ui/verticalTable";
import { useGisContext } from "@common/business_components/ui/map/Gis";
import { Button } from "@common/components/ui";
import { useEffect, useRef, useState } from "react";
import { getLayerContentComponent } from "../../../sn/app/map/components/common";
// TODO:
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
// const featureId = featureData.getId(); // 피처의 id 가져오기
const featureId = typeof featureData.getId === 'function'
? featureData.getId()
: featureData.fid;
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
const [tableData, setTableData] = useState<any>(null); // 피처 데이터
const [mntrngsData, setMntrngsData] = useState(false); // 수량/수질 측정 정보
const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태
const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태
const [buttonChk, setButtonChk] = useState(true)
const [fetchData, setfetchData] = useState<any>(); // api 호출여부
const contentRef = useRef<HTMLDivElement>(null);
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부
// 피쳐데이터의 현재 레이어 이름 가져오는 함수
const getSelectedLayers = () => {
let result = null;
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
let featureLayer: { get: (key: string) => any } = { get: (key: string) => null }; // 피처가 속한 레이어를 찾기 위한 변수
layers.forEach((layer: any) => {
const source = layer.getSource();
if (source && source.hasFeature && featureData
&& typeof featureData.getId === 'function' && source.hasFeature(featureData)) {
featureLayer = layer; // 피처가 속한 레이어 찾기
}
});
// SearchModal 클릭 이벤트와 Gis 클릭 이벤트 구분하기
if (featureLayer.get('lyrNm') === null) {
if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설';
else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역';
else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
}
else result = featureLayer.get('lyrNm');
return result;
// return featureLayer.get('lyrNm'); // 레이어 이름 출력
};
useEffect(() => {
const fetchData = async () => {
if (featureId) {
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
const layerId = featureId.split('.')[0]
let data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`);
setfetchData(data)
switch (selectedLayerName) {
case '하수처리시설':
setButtonChk(false)
break;
case '하수처리구역':
setButtonChk(false)
break;
case '중점우수토실':
setButtonChk(false)
setChkMainSsc(true)
// 데이터 전처리: 합쳐진 필드 추가
data = data ? {
...data,
discharge_pipe_spec: `직경: ${data.pipe_diameter || '-'} / 높이: ${data.pipe_hg || '-'} / 길이: ${data.pipe_lt || '-'}`
} : {};
// 수량/수질측정 정보 데이터 호출
const additionalResponse = await getData(`v1/onsemiro/swgRain/${data.adst_dstr_cd}/${data.manage_no}/mntrngs`);
setMntrngsData(additionalResponse.content);
break;
case '우수토실':
setButtonChk(true)
break;
case '하수관로':
setButtonChk(true)
break;
case '오수펌프':
setButtonChk(true)
break;
case '빗물펌프':
setButtonChk(true)
break;
case '하수맨홀':
setButtonChk(true)
break;
default:
break;
}
setTableData(data);
};
}
fetchData();
}, [featureId]);
// 저장 시 모달 띄우기
const handleSaveClick = () => {
setIsModalOpen(true); // 모달 열기
};
const handleModalSave = () => {
setIsModalOpen(false); // 모달 닫기
btnClickEvent(); // 저장 이벤트 실행
};
const handleModalClose = () => {
setIsModalOpen(false); // 모달 닫기
};
const btnClickEvent = async () => {
const bodyData: Record<string, any> = {}
if (buttonChk && contentRef.current) {
const inputElements = contentRef.current.querySelectorAll('input');
const selectElements = contentRef.current.querySelectorAll('select');
// 카멜케이스 변환 함수
const toCamelCase = (str: string) =>
str.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
// select 태그의 값 가져오기
selectElements.forEach((select) => {
const camelCaseId = toCamelCase(select.id);
bodyData[camelCaseId] = select.value;
});
// input 태그의 값 가져오기
inputElements.forEach((input) => {
const camelCaseId = toCamelCase(input.id);
bodyData[camelCaseId] = input.value;
});
if (layerNm === '하수관로') bodyData['lyrTypeNm'] = 'tb_ms_swage_ppl'
else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr'
else if (layerNm === '오수펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn'
else if (layerNm === '빗물펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn'
else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl'
bodyData['fid'] = featureId
const data = await putData(`v1/fclts/${featureId}`, bodyData);
if (data.status == 200) {
alert('저장되었습니다')
setTableData(data)
setfetchData(data)
}
}
}
type ContentComponentProps = {
featureData: any;
chkMainSsc: boolean;
additionalData: any;
};
const ContentComponent = layerNm ? getLayerContentComponent(layerNm) as React.ComponentType<ContentComponentProps> : null;
return (
<div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[102]">
<div className="card-header bg-primary flex items-center justify-between p-4">
<div className="flex mx-3 flex-col">
<h3 className="text-2xl font-medium text-white text-label">{layerNm} 정보</h3>
<div className="font-medium text-white text-label">(00구 00리 하수관로)</div>
</div>
<button
onClick={onClose}
className="text-white hover:text-gray-700 focus:outline-none"
aria-label="닫기"
>
✕
</button>
</div>
<div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}>
<VerticalTable
headName='LX 연계정보'
tableData={[tableData]}
headlist={headlist}
pageSize={20}
total={0}
children={undefined}
cellClick={undefined}
onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}
/>
<div className="w-full mx-5 flex items-center justify-end space-x-2">
{buttonChk ? <Button onClick={handleSaveClick} className="px-4 mr-5 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700">
저장
</Button> : null}
</div>
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
{ContentComponent &&
<ContentComponent
featureData={fetchData}
chkMainSsc={chkMainSsc}
additionalData={mntrngsData}
/>
}
</div>
</div>
)
}
export default FeatureClickEventPopup;
-621
View File
@@ -1,621 +0,0 @@
"use client";
import {
Form,
FormField,
FormUi,
SelectBox,
} from "@common/business_components/ui";
import { Button, Input } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useWrStore } from "@/app/store";
import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg";
import {
hdtflZod,
type customHtdflBdst,
} from "@/app/store/ruseFcltMng/fcltReg/htdflReg";
import { useEffect } from "react";
import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg";
import {
formatCommaNumber,
handleCommaNumberChange,
} from "@/components/lib/func/commom";
import { formH1Class } from "@/components/lib/class/comClass";
export default function HtdflPrfmncSttnPopup({
htdflForm,
isModal,
}: {
htdflForm: customHtdflBdst;
isModal: boolean;
}) {
const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state);
const defaultValues: z.infer<typeof hdtflZod> = hdtflZod.parse({});
const form = useForm<z.infer<typeof hdtflZod>>({
resolver: zodResolver(hdtflZod),
defaultValues: defaultValues,
});
const onSubmit = (values: z.infer<typeof hdtflZod>) => {
if (isModal) {
htdflBdstReg(values, '41');
} else {
htdflBdstUpdate(values, '41');
}
};
const cleanHtdflData = (raw: any): z.infer<typeof hdtflZod> => {
return {
...raw,
frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null),
archChgPrmsnDt: raw?.archChgPrmsnDt
? new Date(raw?.archChgPrmsnDt)
: null,
chgBuilding: (raw?.chgBuilding || []).map((item: any) => ({
...item,
archChgPrmsnDt:
item?.archChgPrmsnDt === null
? new Date()
: new Date(item?.archChgPrmsnDt),
bdstChgNm: item?.bdstChgNm ?? "", // null → ""
})),
reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({
...item,
instlCmptnDt: new Date(item?.instlCmptnDt),
htdflObj: {
...item?.htdflObj,
},
})),
};
};
useEffect(() => {
console.log("form errors:", form.formState.errors);
}, [form.formState.errors]);
useEffect(() => {
if (hdtflZod) form.reset(cleanHtdflData(htdflForm));
}, [htdflForm]);
return (
<div className="!mx-[1.6rem] !p-0r">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* <div className="flex justify-between items-center mx-3"> */}
<div>
<div className="grid grid-cols-12 flex items-center card-header">
<h1 className="text-2xl font-medium text-label col-span-10">온배수 시설 정보</h1>
</div>
</div>
{isModal && htdflForm != undefined ? <BdstReg form={form} /> : <></>}
{isModal ? (
<h1 className="text-xl col-span-10 mt-4">
온배수 시설 정보{" "}
</h1>
) : (
<></>
)}
<div className="grid grid-cols-6 gap-2 mt-4">
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.etgtType"
render={({ field }) => (
<FormUi label="발전유형(코드정비후 수정)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.fclCpct" // 이름 이상함.
render={({ field }) => (
<FormUi label="설비용량">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.plwtYn"
render={({ field }) => (
<FormUi label="담수화 여부">
<SelectBox
className={""}
label={""}
selectClass={selectClass}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-4">
<h1 className={formH1Class}>
온배수 시설 운영현황
</h1>
</div>
<div className="grid grid-cols-10 gap-1 mt-4">
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.orgwtWtritSpt"
render={({ field }) => (
<FormUi label="원수취수 지점(코드정비후 수정)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
value={field.value ?? 0}
onChange={(e) => {
field.onChange(Number(e.target.value));
}}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.orgwtOcrnProcs"
render={({ field }) => (
<FormUi label="원수발생 공정">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.wtritEntrcTp"
render={({ field }) => (
<FormUi label="취수구 온도">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.amtgnr"
render={({ field }) => (
<FormUi label="발생량(m³/년)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.prcsProcsYn"
render={({ field }) => (
<FormUi label="처리공정">
<SelectBox
className={""}
label={""}
selectClass={selectClass}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.prcsFcltCpct"
render={({ field }) => (
<FormUi label="처리시설 용량(m³/년)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.fmYn"
render={({ field }) => (
<FormUi label="유량계유무">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.utztnCpct"
render={({ field }) => (
<FormUi label="이용량(m³/년)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.utztnUsg"
render={({ field }) => (
<FormUi label="이용용도">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-2">
<h1 className={formH1Class}>
요구수질
</h1>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.grssClfg"
render={({ field }) => (
<FormUi label="총대장균군">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.tn"
render={({ field }) => (
<FormUi label="총질소">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.combResdch"
render={({ field }) => (
<FormUi label="결합잔류염소">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6 ">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.tph"
render={({ field }) => (
<FormUi label="총인">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.trbt"
render={({ field }) => (
<FormUi label="탁도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.ph"
render={({ field }) => (
<FormUi label="수소이온농도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.bod"
render={({ field }) => (
<FormUi label="생물학적 산소요구량">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.clIonQnt"
render={({ field }) => (
<FormUi label="염화물">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.smll"
render={({ field }) => (
<FormUi label="냄새">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.cdtnv"
render={({ field }) => (
<FormUi label="전기전도도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.crmty"
render={({ field }) => (
<FormUi label="색도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.emsnExitTp"
render={({ field }) => (
<FormUi label="배출구온도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
</form>
</Form>
</div>
);
}
+7 -9
View File
@@ -2,15 +2,12 @@
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import FeatureClickEventPopup from "../featureClickEventPopup";
import { mapService } from "../service";
import { MapSearchBar } from "../mapSearchBar";
import NewWindowPopup from "../newWindowPopup";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
@@ -46,7 +43,7 @@ const Map = () => {
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
let map = gisMaps.current;
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
@@ -60,7 +57,7 @@ const Map = () => {
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature = null;
let foundFeature: any = null;
if (
data &&
typeof data.getGeometry === "function" &&
@@ -85,9 +82,9 @@ const Map = () => {
// 3. 피처 선택
if (foundFeature) {
let geometry = foundFeature.getGeometry();
const geometry = foundFeature.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
@@ -136,7 +133,7 @@ const Map = () => {
// />
// )
// }
detailView={(props: any) => (
detailView={(props: any) =>(
<NewWindowPopup
onClose={handlePopupClose} // 메모이제이션된 함수 사용
checkedLayers={checkedLayers}
@@ -144,7 +141,7 @@ const Map = () => {
{...props}
/>
)}
onFeatureSelect={undefined}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
@@ -152,6 +149,7 @@ const Map = () => {
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
imgVisble={true}
/>
</div>
);
-414
View File
@@ -1,414 +0,0 @@
// "use client"
// import { useWrStore } from "@/app/store";
// import SearchInputText from "@/components/ui/searchInput/searchInput";
// import type { EnvPartSelectDataType, ReturnType, SelectDataType } from "@common/business_components/ui";
// import { SelectBox } from "@common/business_components/ui";
// import { Button, Input, Label } from "@common/components/ui";
// import React, { useEffect, useMemo, useRef, useState } from "react";
// import { MapSearchFormLeft } from "./mapSearchForm";
// // 건물 검색 타입 정의
// interface BuildingInfo {
// bdstInfoPk: number;
// ctpvCd: number;
// sggCd: number;
// daddr: string;
// bdstNm: string;
// frstArchPrmsnDt: string;
// archArea: number;
// ctpvNm: string | null;
// sggNm: string | null;
// lat: number | null;
// lot: number | null;
// regDt: string;
// rgtr: string | null;
// mdfcnDt: string;
// mdfr: string | null;
// }
// type SearchBaseType = {
// searchType: string;
// selectSidoData?: SelectDataType[] | [];
// selectSigunData?: SelectDataType[] | [];
// selectRegionData?: SelectDataType[] | [];
// selectUnitWaterShedData?: SelectDataType[] | [];
// selectMiddleRegionData?: SelectDataType[] | [];
// selectEnvPartData?: EnvPartSelectDataType[] | [];
// onFeatureSelect: () => void;
// btnClass?: string;
// ref?: React.RefObject<ReturnType>;
// isAdmin?: boolean;
// leftClassName?: string;
// rightClassName?: string;
// showQueryButton?: boolean;
// showInitButton?: boolean;
// showApplyButton?: boolean;
// checkedLayers?: string[];
// checkedLayersData?: any[];
// }
// const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`;
// const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`);
// interface SearchModalProps {
// open: boolean;
// onOpenChange: (open: boolean) => void;
// onFeatureSelect: (row: any) => void;
// }
// interface SelectedDataTypeWithSggNm {
// text: string;
// val: string;
// sggNm: string;
// }
// export function MapSearchBar({
// searchType,
// selectSidoData,
// selectSigunData,
// selectRegionData,
// selectUnitWaterShedData,
// selectMiddleRegionData,
// selectEnvPartData,
// btnClass,
// onFeatureSelect,
// isAdmin = false,
// leftClassName,
// rightClassName,
// showQueryButton = true,
// showInitButton = true,
// showApplyButton = true,
// checkedLayers = [],
// checkedLayersData = [],
// }: SearchBaseType) {
// // 로딩바
// const [isLoading, setIsLoading] = useState(false);
// // 상세 정보 조회를 위한 상태 추가
// const [popupFeature, setPopupFeature] = useState<any>(null);
// const [isChHead, setIsChHead] = useState(false);
// const ref = useRef<ReturnType>({} as ReturnType);
// // zustand store에서 건물 검색 함수와 데이터 가져오기
// const {
// code,
// bdst,
// findBdst,
// wrMap,
// buildingSearchRes,
// buildingNmSearch,
// buildingNmSearchMultiple
// } = useWrStore((state) => state);
// const [selectedBuilding, setSelectedBuilding] = useState<BuildingInfo | null>(null);
// const [buildingSearchText, setBuildingSearchText] = useState("");
// const [buildingList, setBuildingList] = useState<BuildingInfo[]>([]);
// // 전체 목록
// const [partValue, setPartValue] = useState<string>("A");
// const [sidoValue, setSidoValue] = useState<string>("0");
// const [sigunValue, setSigunValue] = useState<string>("0");
// const [regionValue, setRegionValue] = useState<string>("0");
// const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0");
// const [middleRegionValue, setMiddleRegionValue] = useState<string>("0");
// const [envPartValue, setEnvPartValue] = useState<string>("0");
// const [resetTrigger, setResetTrigger] = useState(false);
// const [sidoText, setSidoText] = useState<string>("0");
// const [sigunText, setSigunText] = useState<string>("0");
// // 공급처 후보지 조회 필터 목록
// const [selectedSido, setSelectedSido] = useState<SelectDataType>({ text: "전체", val: '0' });
// const [selectedSigun, setSelectedSigun] = useState<SelectDataType>({ text: "전체", val: '0' });
// const [selectedFclt, setSelectedFclt] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태
// // 전체 시설
// let allFclt: SelectedDataTypeWithSggNm[] = [{ text: "전체", val: '0', sggNm: '전체' }];
// const [selectedDong, setSelectedDong] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 행정동
// const filteredSigunData = useMemo(() => {
// if (!selectedSido || selectedSido.val === '0' || !selectedSido.val) return [{ text: "전체", val: '0' }];
// return [
// { text: "전체", val: '0' },
// ...code.sggCd.filter((item: { val: string; }) => item.val.substring(0, 2) === selectedSido.val.substring(0, 2))
// ];
// }, [selectedSido]);
// const filteredFcltData = useMemo(() => {
// if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }];
// return [
// { text: "전체", val: '0', sggNm: '전체' },
// ...allFclt.filter(item => item.sggNm === selectedSido.text)
// ];
// }, [selectedSigun]);
// const filteredDong = useMemo(() => {
// if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }];
// return [
// { text: "전체", val: '0', sggNm: '전체' },
// // ...code.dong.filter(item => item.sggNm === selectedSido.text)
// ];
// }, [selectedSigun]);
// // 조회 함수
// const queryVal = () => {
// // 46 : 하수처리시설
// // 47 : 하수재이용시설
// // 48 : 빗물이용시설
// // 49 : 중수도시설
// // 50 : 온배수시설
// console.log("조회버튼 클릭: ", checkedLayers);
// if (checkedLayers && checkedLayers.includes('48')) {
// // searchBuildings
// }
// }
// // 초기화 함수
// const initVal = () => {
// setPartValue("A");
// setSidoValue("0");
// setSigunValue("0");
// setRegionValue("0");
// setUntitWaterShedValue("0");
// setMiddleRegionValue("0");
// setEnvPartValue("0");
// setSidoText("전체");
// setSigunText("전체");
// setSelectedSido({ text: "전체", val: '0' });
// setSelectedSigun({ text: "전체", val: '0' });
// setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' });
// setSelectedDong({ text: "전체", val: '0', sggNm: '전체' });
// setResetTrigger(true);
// };
// // 적용 버튼
// const applyVal = () => {
// onFeatureSelect();
// };
// useEffect(() => {
// if (resetTrigger) {
// setResetTrigger(false);
// }
// }, [resetTrigger]);
// // 시설 조회
// useEffect(() => {
// const fetchData = async () => {
// setIsLoading(true);
// if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) {
// allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]);
// setIsLoading(false);
// return;
// }
// try {
// const queryParamsFacility = new URLSearchParams({
// lyrTypeNm: 'tb_il_swage_prcs_fclt',
// page: '0',
// size: '100000',
// sort: 'string',
// });
// const cqlFiltersFacility: string[] = [];
// if (selectedSigun.text && selectedSigun.val !== '0') {
// cqlFiltersFacility.push(`sgg_nm ILIKE '%${selectedSigun.text}%'`);
// }
// if (cqlFiltersFacility.length > 0) {
// queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and '));
// }
// console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`);
// const facilityRes = await fetch(`${API_URL}/v1/fclts?${queryParamsFacility.toString()}`, {
// method: "GET",
// headers: { accept: "application/json" }
// }).then(res => res.json());
// const facilities = (facilityRes.data || []).map((item: any) => ({
// text: item.swtrmtFcltNm,
// val: item.fid,
// sggNm: item.sggNm
// }));
// allFclt = (facilities);
// } catch (error) {
// console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error);
// allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]);
// } finally {
// setIsLoading(false);
// }
// };
// fetchData();
// }, [selectedSigun]);
// useEffect(() => {
// if (ref && ref.current) {
// const customRef = ref.current;
// customRef.partValue = partValue;
// customRef.sidoValue = sidoValue;
// customRef.sigunValue = sigunValue;
// customRef.regionValue = regionValue;
// customRef.unitWaterShedValue = untitWaterShedValue;
// customRef.middleRegionValue = middleRegionValue;
// customRef.envPartValue = envPartValue;
// customRef.sidoText = selectedSido.text;
// customRef.sigunText = selectedSigun.text;
// }
// }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]);
// // buildingSearchText가 변경될 때 API 호출
// useEffect(() => {
// if (buildingSearchText && buildingSearchText.length > 1) {
// if (checkedLayers && checkedLayers.length > 0) {
// // 다중 레이어 검색
// // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", buildingSearchText);
// buildingNmSearch("RNB", selectedSigun.val, buildingSearchText);
// } else {
// // 단일 레이어 검색
// // buildingNmSearch(checkedLayers[0], selectedSigun.val, buildingSearchText);
// }
// } else {
// // 검색 텍스트가 짧으면 리스트 초기화
// if (checkedLayers && checkedLayers.length > 0) {
// // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", "");
// } else {
// buildingNmSearch("RNB", selectedSigun.val || "", "");
// }
// }
// }, [buildingSearchText]);
// return (
// <div className="absolute left-1/2 -translate-x-1/2 bg-white flex items-center justify-center p-4 rounded-md shadow-lg w-auto min-w-fit">
// <div className={leftClassName ? leftClassName : `flex items-center justify-center space-x-2 `}>
// <div className="flex gap-8 items-center">
// {searchType == "공급처 후보지 조회" && (
// <>
// <SelectBox
// className="flex items-center"
// label={'시도'}
// selectArray={code.cityCd}
// onSelectValue={(val) => {
// const selected = code.cityCd.find((item: { val: string | number; }) => (item.val === val));
// setSelectedSido(selected || { text: "전체", val: '0' });
// setSelectedSigun({ text: "전체", val: '0' });
// }}
// value={selectedSido.val}
// selectClass={'w-32'}
// />
// <SelectBox
// className="flex items-center"
// label={'시군구'}
// selectArray={filteredSigunData}
// onSelectValue={(val) => {
// const selected = filteredSigunData.find(item => (item.val === val));
// setSelectedSigun(selected || { text: "전체", val: '0' });
// setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' });
// }}
// value={selectedSigun.val}
// selectClass={'w-32'}
// />
// <div className="flex items-center">
// <Label className="w-20 text-xl text-gray-600">시설명</Label>
// <div className="w-64">
// <SearchInputText
// label="시설명"
// initValue={buildingSearchText}
// setValue={(searchNm?: string) => {
// setBuildingSearchText(searchNm || "");
// }}
// setRtnData={function (rtnData: any): void {
// // 선택된 건물 데이터 처리
// // if (rtnData && rtnData.bdstInfoPk) {
// // setSelectedBuilding(rtnData);
// // setBuildingSearchText(rtnData.bdstNm || "");
// // }
// }}
// searchList={buildingSearchRes || []}
// />
// </div>
// </div>
// </>
// )}
// {searchType == "우리동네가뭄지도보기" && (
// <>
// <SelectBox
// className="flex items-center"
// label={'시도'}
// selectArray={code.cityCd}
// onSelectValue={(val) => {
// const selected = code.cityCd.find((item: { val: string | number; }) => (item.val === val));
// setSelectedSido(selected || { text: "전체", val: '0' });
// setSelectedSigun({ text: "전체", val: '0' });
// }}
// value={selectedSido.val}
// selectClass={'w-44'}
// />
// <SelectBox
// className="flex items-center"
// label={'시군구'}
// selectArray={filteredSigunData}
// onSelectValue={(val) => {
// const selected = filteredSigunData.find(item => (item.val === val));
// setSelectedSigun(selected || { text: "전체", val: '0' });
// setSelectedDong({ text: "전체", val: '0', sggNm: '전체' });
// }}
// value={selectedSigun.val}
// selectClass={'w-32'}
// />
// <SelectBox
// className="flex items-center mr-8"
// label={'행정동'}
// selectArray={filteredDong}
// onSelectValue={(val) => {
// const selected = filteredDong.find(item => (item.val === val));
// setSelectedDong(selected || { text: "전체", val: '0', sggNm: '전체' });
// }}
// value={selectedDong.val}
// selectClass={'w-32'}
// />
// </>
// )}
// {searchType == "지도보기" && (
// <div className="flex items-center gap-2 w-[800px]">
// <div className="flex min-w-0"> {/* SearchFormLeft 래퍼 */}
// <MapSearchFormLeft
// isShowPart={true}
// selectSidoData={code.cityCd}
// selectSigunData={filteredSigunData}
// selectRegionData={code.rvbsnCd}
// selectUnitWaterShedData={selectUnitWaterShedData}
// selectMiddleRegionData={selectMiddleRegionData}
// selectEnvPartData={selectEnvPartData}
// setRtnPartValue={setPartValue}
// setRtnSidoValue={setSidoValue}
// setRtnSigunValue={setSigunValue}
// setRtnRegionValue={setRegionValue}
// setRtnUnitWaterShedValue={setUntitWaterShedValue}
// setRtnMiddleRegionValue={setMiddleRegionValue}
// setRtnEnvPartValue={setEnvPartValue}
// setRtnEnvPartSidoValue={setSidoValue}
// setRtnEnvPartSigunValue={setSigunValue}
// setRtnSidoText={setSidoText}
// setRtnSigunText={setSigunText}
// resetTrigger={resetTrigger}
// isAdmin={isAdmin}
// children={undefined}
// />
// </div>
// <Label className="w-auto text-xl text-gray-600 whitespace-nowrap">시설명</Label>
// <div className="w-44">
// <Input className="mr-4" />
// </div>
// </div>
// )}
// </div>
// {/* 버튼 부분 */}
// <div className="flex items-center justify-center gap-4">
// {showQueryButton && <Button className={btnClass} onClick={queryVal}>조회</Button>}
// {showInitButton && <Button className={btnClass} onClick={initVal}>초기화</Button>}
// {showApplyButton && <Button className={btnClass} onClick={applyVal}>적용</Button>}
// </div>
// </div>
// </div>
// )
// }

Some files were not shown because too many files have changed in this diff Show More