Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72fa450bc | ||
|
|
400d6cf732 | ||
|
|
39e2734a49 | ||
|
|
d92d4d3949 | ||
|
|
b93efa5503 | ||
|
|
0b8559891a | ||
|
|
ffa1781005 | ||
|
|
b47c0dff2b | ||
|
|
83f5d878e7 | ||
|
|
e815b9662f | ||
|
|
2e6cbad49c | ||
|
|
ede5ef6a21 | ||
|
|
621d222799 | ||
|
|
01df37d0b3 | ||
|
|
072a5337a3 | ||
|
|
2a0f5dce25 | ||
|
|
26f2c60f4c | ||
|
|
28d0e21815 | ||
|
|
49c475709f | ||
|
|
31b8152d37 | ||
|
|
b9f5eb50e5 | ||
|
|
c7772a0a23 | ||
|
|
392ccefb16 | ||
|
|
aeaf507c47 | ||
|
|
2f4cfa0d78 | ||
|
|
ebfc520d67 | ||
|
|
4df286b086 | ||
|
|
262cf21546 | ||
|
|
056b7bae3c | ||
|
|
428f617802 | ||
|
|
e797bdbdf7 | ||
|
|
cb11ec839e | ||
|
|
772dd1c1aa | ||
|
|
579e599eb1 | ||
|
|
3cc8fcb2eb | ||
|
|
f644bd6b7e | ||
|
|
f685884ed7 | ||
|
|
a2af60d52e | ||
|
|
395590bbe4 | ||
|
|
6c0d65a69a | ||
|
|
a6b34780ae | ||
|
|
a83ea455fb | ||
|
|
ab63cbe992 | ||
|
|
b2a6285475 | ||
|
|
6808430c9a | ||
|
|
dad8b9925e | ||
|
|
07c7947da1 | ||
|
|
35e1203168 | ||
|
|
bec3132417 | ||
|
|
e7a46ac3a2 | ||
|
|
6fb39d6384 | ||
|
|
0ad7d6a2c4 | ||
|
|
8fbecc41a5 | ||
|
|
711adc2e17 | ||
|
|
4920b4c1a2 | ||
|
|
c4f2af0bfe | ||
|
|
190adb6bc1 | ||
|
|
59bccfe1d6 | ||
|
|
84ff24c5b5 | ||
|
|
e1b48ab7c8 | ||
|
|
4c3be448f0 | ||
|
|
8ede32f4c4 | ||
|
|
2862fdc9fd | ||
|
|
a446739432 | ||
|
|
ea125605d1 | ||
|
|
8353800dac |
Generated
+8
@@ -0,0 +1,8 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 에디터 기반 HTTP 클라이언트 요청
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -185,15 +185,6 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
...prev,
|
||||
[siteId]: value,
|
||||
}));
|
||||
|
||||
// 상시 선택 시 게시 기간 미설정
|
||||
if (value === "99") {
|
||||
handleUpdateFields(siteId)({
|
||||
pstgPrdSeCd: value,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setRememberPeriodDate((prev) => ({
|
||||
...prev,
|
||||
[siteId]: { from, to },
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi, PostPageGuard, type PostParams } from "@/entities/post";
|
||||
import { PostApi, type PostParams } from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import dayjs from "dayjs";
|
||||
@@ -103,147 +103,143 @@ function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<PostPageGuard bbsId={bbsId}>
|
||||
<div className="card card-border !mt-6">
|
||||
<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 });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
<div className="card card-border !mt-6">
|
||||
<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 });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</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>
|
||||
<div className="flex gap-2">
|
||||
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
||||
<Button onClick={() => router.push(`${pathname}/register`)}>
|
||||
등록
|
||||
</Button>
|
||||
)}
|
||||
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div className="flex gap-2">
|
||||
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
||||
<Button onClick={() => router.push(`${pathname}/register`)}>
|
||||
등록
|
||||
</Button>
|
||||
)}
|
||||
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={selectedPostNumbers.length === postList?.content.length}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={
|
||||
selectedPostNumbers.length === postList?.content.length
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PostPageGuard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+8
-27
@@ -1,12 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CLAIM_NAME,
|
||||
AUTH_USER_ID,
|
||||
PORT,
|
||||
PROXY_HOST,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "./src/shared/config";
|
||||
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
@@ -14,23 +7,17 @@ const serverApiUrl =
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
const logMessages: string[] = [
|
||||
`[Middleware Request] ${new Date().toISOString()}`,
|
||||
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
|
||||
`[Request]\n Original Pathname: ${pathname}`,
|
||||
];
|
||||
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const queryParams = searchParams.toString();
|
||||
|
||||
const decodedParams = Array.from(searchParams.entries())
|
||||
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
decodedParams ? `?${decodedParams}` : ""
|
||||
}`;
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
@@ -43,10 +30,6 @@ export function middleware(req: NextRequest) {
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
@@ -56,8 +39,6 @@ export function middleware(req: NextRequest) {
|
||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
}
|
||||
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,22 +67,11 @@ export const contentsDetailSchema = z.object({
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export const contentsFormSchema = contentsDetailSchema
|
||||
.omit({
|
||||
contsVer: true,
|
||||
aprvDt: true,
|
||||
autzr: true,
|
||||
})
|
||||
.extend({
|
||||
contsSn: z.number().optional(),
|
||||
aprvCd: z
|
||||
.enum([
|
||||
CONTENTS_APPROVE_STATUS.DRAFT,
|
||||
CONTENTS_APPROVE_STATUS.REQUESTED,
|
||||
CONTENTS_APPROVE_STATUS.APPROVED,
|
||||
])
|
||||
.optional(),
|
||||
});
|
||||
export const contentsFormSchema = contentsDetailSchema.omit({
|
||||
contsVer: true,
|
||||
aprvDt: true,
|
||||
autzr: true,
|
||||
});
|
||||
|
||||
export type Contents = z.infer<typeof contentsSchema>;
|
||||
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
MenuItem,
|
||||
MenuFormData,
|
||||
MenuTemporarilyStoredData,
|
||||
SiteMapFormData,
|
||||
} from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
@@ -66,14 +65,6 @@ export class MenuApiService implements MenuService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async publishSiteMap(data: SiteMapFormData["siteMap"]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/sitemap/publish`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
|
||||
@@ -2,11 +2,8 @@ export { MenuApi } from "./api/menu-service";
|
||||
export {
|
||||
MENU_TYPE,
|
||||
menuFormSchema,
|
||||
siteMapFormSchema,
|
||||
type MenuType,
|
||||
type MenuItem,
|
||||
type MenuFormData,
|
||||
type SiteMapFormData,
|
||||
type SiteMapPayloadData,
|
||||
} from "./model/menu-interface";
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
|
||||
@@ -72,19 +72,8 @@ export const menuFormSchema = z.object({
|
||||
menuPicDsgnYmd: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const siteMapPayloadSchema = z.object({
|
||||
menuCd: z.number(),
|
||||
menuSubKornNm: z.string(),
|
||||
otptYn: z.string(),
|
||||
});
|
||||
|
||||
export const siteMapFormSchema = z.object({
|
||||
siteMap: z.array(siteMapPayloadSchema),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
export type SiteMapFormData = z.infer<typeof siteMapFormSchema>;
|
||||
export type SiteMapPayloadData = z.infer<typeof siteMapPayloadSchema>;
|
||||
|
||||
export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||
menuCd: number;
|
||||
iconName?: string;
|
||||
@@ -103,5 +92,4 @@ export interface MenuService {
|
||||
getMenuTemporarilyStoredData: (
|
||||
menuCd: number,
|
||||
) => Promise<MenuTemporarilyStoredData[]>;
|
||||
publishSiteMap: (data: SiteMapFormData["siteMap"]) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -194,15 +194,6 @@ function PostingDialog({
|
||||
...prev,
|
||||
[siteId]: value,
|
||||
}));
|
||||
|
||||
// 상시 선택 시 게시 기간 미설정
|
||||
if (value === "99") {
|
||||
handleUpdateFields(siteId)({
|
||||
pstgPrdSeCd: value,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setRememberPeriodDate((prev) => ({
|
||||
...prev,
|
||||
[siteId]: { from, to },
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { TreeMenuApi } from "./api/tree-menu-service";
|
||||
export type { TreeMenu, TreeNode } from "./model/tree-menu-interface";
|
||||
@@ -4,7 +4,6 @@ export {
|
||||
userSaveSchema,
|
||||
userPasswordUpdateSchema,
|
||||
userUpdateSchema,
|
||||
type UserStateCode,
|
||||
type UserSearchParams,
|
||||
type UserListResponse,
|
||||
type UserSaveData,
|
||||
|
||||
@@ -9,8 +9,7 @@ export const USER_STATE_CODE = {
|
||||
삭제: "09",
|
||||
} as const;
|
||||
|
||||
export type UserStateCode =
|
||||
(typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
|
||||
type UserStateCode = (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
|
||||
|
||||
// 사용자 검색 파라미터
|
||||
export interface UserSearchParams extends PageableParams {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import {
|
||||
PostApi,
|
||||
PostPageGuard,
|
||||
deleteReasonMessages,
|
||||
type PostParams,
|
||||
} from "@/entities/post";
|
||||
@@ -121,147 +120,143 @@ function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<PostPageGuard bbsId={bbsId}>
|
||||
<div className="card card-border !mt-6">
|
||||
<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 });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</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>
|
||||
<div className="flex gap-2">
|
||||
<AuthorityButton
|
||||
authority="regAuthrtYn"
|
||||
onClick={() => router.push(`${pathname}/register`)}
|
||||
>
|
||||
등록
|
||||
</AuthorityButton>
|
||||
<AuthorityButton
|
||||
authority="delAuthrtYn"
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
>
|
||||
선택삭제
|
||||
</AuthorityButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={
|
||||
selectedPostNumbers.length === postList?.content.length
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
<div className="card card-border !mt-6">
|
||||
<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 });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</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>
|
||||
<div className="flex gap-2">
|
||||
<AuthorityButton
|
||||
authority="regAuthrtYn"
|
||||
onClick={() => router.push(`${pathname}/register`)}
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
등록
|
||||
</AuthorityButton>
|
||||
<AuthorityButton
|
||||
authority="delAuthrtYn"
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
>
|
||||
선택삭제
|
||||
</AuthorityButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={selectedPostNumbers.length === postList?.content.length}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PostPageGuard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function AdminPage() {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [keywordType, setKeywordType] = useState("userId");
|
||||
const [keywordType, setKeywordType] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [grpId, setGrpId] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<UserSearchParams>({
|
||||
|
||||
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
|
||||
import { ApiLogApi } from "@/entities/apilog";
|
||||
import { type ApiLogParams } from "@/entities/apilog/model/apilog-interface";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/apilog/model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -35,7 +36,6 @@ function ApiLogPage() {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
|
||||
@@ -85,10 +85,8 @@ function ApiLogPage() {
|
||||
...searchParams,
|
||||
userId,
|
||||
apiHttpPmhd,
|
||||
dmndDtStart: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -129,7 +127,6 @@ function ApiLogPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
@@ -138,31 +135,23 @@ function ApiLogPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||
<Input
|
||||
value={userId}
|
||||
|
||||
@@ -35,14 +35,9 @@ export const columns: ColumnDef<Post>[] = [
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
|
||||
if (pstgPrdSeCd === "99") {
|
||||
return pstgPrdSeNm;
|
||||
}
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ function CreatePage() {
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
|
||||
</div>
|
||||
<ContentsForm isEditable form={form} handleSave={createContents} />
|
||||
<ContentsForm form={form} handleSave={createContents} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
@@ -25,7 +24,6 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ContentsForm } from "./contents-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
@@ -53,11 +51,9 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
||||
onSuccess: () => {
|
||||
setContsVer(0);
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
router.back();
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -66,7 +62,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
mutationFn: (data: ContentsFormData) =>
|
||||
ContentsApi.updateContents(contsSn, contsVer, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -74,11 +70,9 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
setContsVer(0);
|
||||
alert("삭제되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
router.back();
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
|
||||
|
||||
export class DashboardApiService implements DashboardService {
|
||||
public async getDashboard(): Promise<Dashboard> {
|
||||
const response = await client.get<Dashboard>(
|
||||
ENDPOINT.CMS_SERVICE.DASHBOARD,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardApi = new DashboardApiService();
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { ChartData, ChartDataset } from "chart.js";
|
||||
import { getCssVariableValue } from "@common/components";
|
||||
import type {
|
||||
MenuStatisticsContent,
|
||||
SiteStatisticsContent,
|
||||
} from "../model/dashboard-interface";
|
||||
|
||||
export function processSiteAccessData(
|
||||
siteAccessList: SiteStatisticsContent[],
|
||||
): ChartData<"line">["datasets"] {
|
||||
const siteGroups: Record<string, ChartDataset> = siteAccessList.reduce(
|
||||
(acc, item) => {
|
||||
if (!acc[item.siteNm]) {
|
||||
acc[item.siteNm] = {
|
||||
label: item.siteNm,
|
||||
data: Array(24).fill(0), // 24시간 데이터 초기화
|
||||
borderColor: getCssVariableValue(
|
||||
`--chart-${Math.floor(Math.random() * 5) + 1}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 시간대별 접속 카운트 누적
|
||||
const hourIndex = parseInt(item.accessHour, 10);
|
||||
if (!isNaN(hourIndex) && hourIndex >= 0 && hourIndex < 24) {
|
||||
acc[item.siteNm]!.data[hourIndex] = item.accessCnt;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ChartDataset>,
|
||||
);
|
||||
|
||||
return Object.values(siteGroups) as ChartData<"line">["datasets"];
|
||||
}
|
||||
|
||||
export function processMenuAccessData(
|
||||
menuAccessList: MenuStatisticsContent[],
|
||||
): ChartData<"bar">["datasets"] {
|
||||
const data = menuAccessList.map((item) => item.accessCnt);
|
||||
|
||||
return [
|
||||
{
|
||||
label: "메뉴별 접속 횟수",
|
||||
data,
|
||||
backgroundColor: "rgb(32, 148, 250)",
|
||||
},
|
||||
] as ChartData<"bar">["datasets"];
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { type ChartData, type ChartOptions } from "chart.js";
|
||||
|
||||
const commonData = {
|
||||
labels: ["1", "2", "3", "4", "5", "6", "7"],
|
||||
datasets: [
|
||||
{
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
tension: 0.5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commonOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const chartData: Record<
|
||||
string,
|
||||
{ data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> }
|
||||
> = {
|
||||
memberChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [8, 10, 7, 12, 9, 13, 12],
|
||||
borderColor: "rgba(255, 255, 255, 0.6)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
scales: {
|
||||
...commonOptions.scales,
|
||||
y: {
|
||||
...commonOptions.scales.y,
|
||||
min: 5,
|
||||
max: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
postChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [5200, 5350, 5450, 5520, 5600, 5650, 5678],
|
||||
borderColor: "rgba(86, 138, 255, 1)",
|
||||
backgroundColor: "rgba(86, 138, 255, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
},
|
||||
},
|
||||
visitorChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [1800, 2100, 1950, 2300, 2150, 2250, 2345],
|
||||
borderColor: "rgba(149, 131, 252)",
|
||||
backgroundColor: "rgba(149, 131, 252, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
},
|
||||
},
|
||||
apiChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [41000, 42500, 43200, 44100, 44800, 45200, 45678],
|
||||
borderColor: "rgba(220, 103, 206)",
|
||||
backgroundColor: "rgba(220, 103, 206, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
},
|
||||
},
|
||||
siteAccessChartData: {
|
||||
data: {
|
||||
labels: Array.from({ length: 24 }, (_, i) =>
|
||||
i.toString().padStart(2, "0"),
|
||||
),
|
||||
datasets: [],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
},
|
||||
tooltip: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
text: "시간대",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
text: "접속자 수",
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
menuAccessChartData: {
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
},
|
||||
tooltip: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
text: "메뉴명",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
text: "접속 횟수",
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
export interface SiteStatisticsContent {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
accessHour: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface MenuStatisticsContent {
|
||||
menuCd: number;
|
||||
menuNm: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
memberTotCnt: number;
|
||||
memberTodayCnt: number;
|
||||
pstTotCnt: number;
|
||||
pstTodayCnt: number;
|
||||
accessTodayCnt: number;
|
||||
accessBefDayPer: number;
|
||||
apiTodayCnt: number;
|
||||
waitMemberCnt: number;
|
||||
waitCntnCnt: number;
|
||||
notiList: [];
|
||||
schdlList: [];
|
||||
siteAccessList: SiteStatisticsContent[];
|
||||
menuAccessList: MenuStatisticsContent[];
|
||||
}
|
||||
|
||||
export interface DashboardService {
|
||||
getDashboard(): Promise<Dashboard>;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge } from "@common/components";
|
||||
import type { RecentPost } from "@/entities/post";
|
||||
|
||||
const noticePostTableColumns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
header: "번호",
|
||||
cell: ({ row, table }) => {
|
||||
if (row.original.upndFixYn === "Y") {
|
||||
return (
|
||||
<Badge
|
||||
color="orange"
|
||||
className="flex size-8 items-center justify-center"
|
||||
>
|
||||
<i className="diveicon di-volume-up-fill" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
},
|
||||
{
|
||||
accessorKey: "wrtrNm",
|
||||
header: "작성자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "작성일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
accessorKey: "inqNocs",
|
||||
header: "조회수",
|
||||
},
|
||||
];
|
||||
|
||||
const recentPostTableColumns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
header: "번호",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
},
|
||||
{
|
||||
accessorKey: "wrtrNm",
|
||||
header: "작성자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "작성일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
accessorKey: "inqNocs",
|
||||
header: "조회수",
|
||||
},
|
||||
];
|
||||
|
||||
export { noticePostTableColumns, recentPostTableColumns };
|
||||
@@ -1,409 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
type ChartData,
|
||||
} from "chart.js";
|
||||
import { Bar, Line } from "react-chartjs-2";
|
||||
import { PostApi, type RecentPost } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { DashboardApi } from "../api/dashboard-service";
|
||||
import { chartData } from "../model/chart-data";
|
||||
import {
|
||||
noticePostTableColumns,
|
||||
recentPostTableColumns,
|
||||
} from "../model/table-columns";
|
||||
import { processMenuAccessData, processSiteAccessData } from "../lib/utils";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
);
|
||||
|
||||
function DashboardPage() {
|
||||
const router = useRouter();
|
||||
const { data } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD],
|
||||
queryFn: () => DashboardApi.getDashboard(),
|
||||
});
|
||||
|
||||
const noticePostQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
page: 0,
|
||||
size: 5,
|
||||
keywordType: "bbsTypeCd",
|
||||
keyword: "BBSNOTICE",
|
||||
}),
|
||||
});
|
||||
|
||||
const recentPostQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
page: 0,
|
||||
size: 5,
|
||||
}),
|
||||
});
|
||||
|
||||
const handleNoticePostRowClick = (row: RecentPost) => {
|
||||
router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`);
|
||||
};
|
||||
|
||||
const handleRecentPostRowClick = (row: RecentPost) => {
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { tableComponent: noticePostTableComponent } = useDataTable({
|
||||
query: noticePostQuery,
|
||||
tableOptions: {
|
||||
columns: noticePostTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleNoticePostRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
const { tableComponent: recentPostTableComponent } = useDataTable({
|
||||
query: recentPostQuery,
|
||||
tableOptions: {
|
||||
columns: recentPostTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleRecentPostRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* 첫번째 라인 */}
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{/* 총 회원수 카드 */}
|
||||
<div className="card card-border h-80 !bg-primary !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter text-white">
|
||||
<h3 className="text-2xl font-medium">총 회원수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.memberTotCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
금일 신규 가입자 {data?.memberTodayCnt ?? "-"}명
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.memberChartData!.data as ChartData<"line">}
|
||||
options={chartData.memberChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 전체 게시물 카드 */}
|
||||
<div className="card card-border h-80 !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||
<h3 className="text-2xl font-medium">전체 게시물</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.pstTotCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
금일 +{data?.pstTodayCnt ?? "-"} 등록
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.postChartData!.data as ChartData<"line">}
|
||||
options={chartData.postChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 오늘 접속자 수 카드 */}
|
||||
<div className="card card-border h-80 !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.accessTodayCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
전일 대비 +{data?.accessBefDayPer ?? "-"}%
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.visitorChartData!.data as ChartData<"line">}
|
||||
options={chartData.visitorChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* API 호출 건수 카드 */}
|
||||
<div className="card card-border h-80 !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||
<h3 className="text-2xl font-medium">API 호출 건수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.apiTodayCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.apiChartData!.data as ChartData<"line">}
|
||||
options={chartData.apiChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 두번째 라인 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 승인 대기 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">승인 대기 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex gap-4">
|
||||
{/* 사용자 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-red-500"
|
||||
>
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-medium">사용자 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">
|
||||
{data?.waitMemberCnt}건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
신규 가입 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 매뉴 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-blue-500"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-medium">메뉴 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">3건</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
매뉴 수정 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 컨텐츠 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-yellow-500"
|
||||
>
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-medium">컨텐츠 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">
|
||||
{data?.waitCntnCnt}건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
컨텐츠 수정 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 실시간 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">신규 사용자 가입</span>
|
||||
<span>1분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">메뉴 수정 요청</span>
|
||||
<span>5분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">게시물 신고</span>
|
||||
<span>10분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">컨텐츠 승인 요청</span>
|
||||
<span>15분 전</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 일정 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">일정 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">3월 정기 시스템 점검</span>
|
||||
<span>내일</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">서버 업데이트 예정</span>
|
||||
<span>3일 후</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">백업 스케줄</span>
|
||||
<span>5일 후</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">정기 보안 점검</span>
|
||||
<span>7일 후</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 세번째 라인 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 접속자 통계 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">접속자 통계</h3>
|
||||
</div>
|
||||
<div className="h-80 p-4">
|
||||
<Line
|
||||
data={{
|
||||
...chartData.siteAccessChartData!.data,
|
||||
datasets: processSiteAccessData(data?.siteAccessList ?? []),
|
||||
}}
|
||||
options={chartData.siteAccessChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 메뉴별 접속 통계 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
|
||||
</div>
|
||||
<div className="h-80 p-4">
|
||||
<Bar
|
||||
data={{
|
||||
labels:
|
||||
data?.menuAccessList?.map((item) => item.menuNm) || [],
|
||||
datasets: processMenuAccessData(data?.menuAccessList || []),
|
||||
}}
|
||||
options={chartData.menuAccessChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 네번째 라인 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 공지사항 */}
|
||||
<div className="card card-border !p-0">
|
||||
<div className="card-header !m-0 !border-none">
|
||||
<h3 className="text-2xl font-medium">공지사항</h3>
|
||||
</div>
|
||||
{noticePostTableComponent}
|
||||
</div>
|
||||
{/* 신규 게시글 */}
|
||||
<div className="card card-border !p-0">
|
||||
<div className="card-header !m-0 !border-none">
|
||||
<h3 className="text-2xl font-medium">신규 게시글</h3>
|
||||
</div>
|
||||
{recentPostTableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
|
||||
@@ -39,7 +39,8 @@ function FilePage() {
|
||||
mutationFn: (atflId: string) =>
|
||||
AttachmentApi.deleteAttachmentByAtflId(atflId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", query.refetch);
|
||||
query.refetch();
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
DialogFooter,
|
||||
Input,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserApi,
|
||||
@@ -42,8 +41,7 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userName, setUserName] = useState("");
|
||||
const [searchParam, setSearchParam] = useState<UserSearchParams>({
|
||||
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||
userName: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
@@ -55,10 +53,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParam, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination],
|
||||
queryFn: () =>
|
||||
UserApi.findPageUser({
|
||||
...searchParam,
|
||||
...userSearchQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
@@ -120,9 +118,6 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
onRowSelectionChange: setUserRowSelection,
|
||||
getRowId: (row) => row.userUnqId,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddUsers = async () => {
|
||||
@@ -149,30 +144,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParam({ userName });
|
||||
const handleSearch = () => {
|
||||
setPagination({ pageIndex: 0, pageSize: 10 });
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
setPagination({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
setUserName("");
|
||||
setSearchParam({ userName: "" });
|
||||
setUserRowSelection({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) resetSearch();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -182,31 +157,20 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
||||
<DialogHeader>
|
||||
<DialogTitle>사용자 추가</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSearch} className="mb-4 flex items-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="userName" className="mr-2 text-label">
|
||||
사용자 이름
|
||||
</Label>
|
||||
<Input
|
||||
id="userName"
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
placeholder="사용자 이름으로 검색"
|
||||
/>
|
||||
</div>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={resetSearch}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mb-4 flex items-end gap-2">
|
||||
<Input
|
||||
value={userSearchQuery.userName}
|
||||
onChange={(e) =>
|
||||
setUserSearchQuery({
|
||||
...userSearchQuery,
|
||||
userName: e.target.value,
|
||||
})
|
||||
}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사용자 이름으로 검색"
|
||||
/>
|
||||
<Button onClick={handleSearch}>검색</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
<DialogFooter>
|
||||
<Button color="white" onClick={() => setOpen(false)}>
|
||||
|
||||
@@ -35,14 +35,9 @@ export const columns: ColumnDef<Post>[] = [
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
|
||||
if (pstgPrdSeCd === "99") {
|
||||
return pstgPrdSeNm;
|
||||
}
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -16,13 +16,14 @@ import {
|
||||
SelectValue,
|
||||
LoadingWithImage,
|
||||
} from "@common/components";
|
||||
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
|
||||
import { LoginlogApi } from "@/entities/loginlog";
|
||||
import { type LoginlogParams } from "@/entities/loginlog/model/loginlog-interface";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -32,12 +33,13 @@ function LoginlogPage() {
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
||||
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
||||
@@ -47,7 +49,6 @@ function LoginlogPage() {
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
sort: "lgnDt,desc",
|
||||
});
|
||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
|
||||
@@ -88,12 +89,8 @@ function LoginlogPage() {
|
||||
...searchParams,
|
||||
userId,
|
||||
errOcrnYn,
|
||||
searchDateFrom: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
searchDateTo: dateRange.to
|
||||
? dayjs(dateRange.to).format("YYYY-MM-DD")
|
||||
: "",
|
||||
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||
});
|
||||
query.refetch().finally(() => {
|
||||
setIsManualLoading(false);
|
||||
@@ -137,7 +134,6 @@ function LoginlogPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
@@ -146,31 +142,23 @@ function LoginlogPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
TreeUserOrganization,
|
||||
TreeUser,
|
||||
} from "@/entities/user-organization";
|
||||
import type { TreeMenu, TreeNode } from "@/entities/tree-menu";
|
||||
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
|
||||
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
|
||||
-1
@@ -19,7 +19,6 @@ export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
|
||||
qrcd?: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
menuSubKornNm?: string;
|
||||
requiredUrlPath?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
@@ -21,34 +20,22 @@ function EditableNode({
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
aria-label="저장"
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>저장</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
aria-label="취소"
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>취소</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,10 @@ import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button, cn } from "@common/components";
|
||||
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
|
||||
import { useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||
import type { TreeMenu } from "../model/tree-menu-interface";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuNodeProps = {
|
||||
|
||||
@@ -21,10 +21,11 @@ import {
|
||||
type MenuFormData,
|
||||
} from "@/entities/menu";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
|
||||
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";
|
||||
|
||||
@@ -110,7 +111,6 @@ function MenuPage() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
});
|
||||
form.reset();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,11 +12,8 @@ import { Button } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useMutationWrapper } from "@/shared/lib";
|
||||
import type { MenuFormData } from "@/entities/menu";
|
||||
import {
|
||||
TreeMenuApi,
|
||||
type TreeMenu,
|
||||
type TreeNode,
|
||||
} from "@/entities/tree-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";
|
||||
|
||||
@@ -72,7 +72,7 @@ function AddUserDialog({ onAddUsers, ...props }: AddUserDialogProps) {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>사용자 조회</DialogTitle>
|
||||
<DialogTitle>콘텐츠 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
|
||||
@@ -242,16 +242,6 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
|
||||
</div>
|
||||
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
|
||||
<Table>
|
||||
<colgroup>
|
||||
<col width="5%" />
|
||||
<col width="5%" />
|
||||
<col width="auto" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="15%" />
|
||||
<col width="15%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky top-0">
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
@@ -310,9 +300,6 @@ const UserCells = memo(
|
||||
jobPositionCodeData,
|
||||
jobGradeCodeData,
|
||||
}: UserCellsProps) => {
|
||||
const [jbpsSeCd, setJbpsSeCd] = useState(user.jbpsSeCd);
|
||||
const [jbgdSeCd, setJbgdSeCd] = useState(user.jbgdSeCd);
|
||||
|
||||
const updateMode = (index: number) => {
|
||||
const mode = form.watch(`userOrganization.${index}.mode`);
|
||||
if (mode === "R") {
|
||||
@@ -335,33 +322,17 @@ const UserCells = memo(
|
||||
<CodeSelect
|
||||
codeData={jobPositionCodeData}
|
||||
defaultValue={user.jbpsSeCd}
|
||||
onValueChange={(value) => {
|
||||
setJbpsSeCd(value);
|
||||
updateFormValue(index, "jbpsSeCd")(value);
|
||||
}}
|
||||
onValueChange={updateFormValue(index, "jbpsSeCd")}
|
||||
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
|
||||
/>
|
||||
{!jbpsSeCd && (
|
||||
<p className="pt-1 text-base text-destructive">
|
||||
직위를 선택해주세요.
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CodeSelect
|
||||
codeData={jobGradeCodeData}
|
||||
defaultValue={user.jbgdSeCd}
|
||||
onValueChange={(value) => {
|
||||
setJbgdSeCd(value);
|
||||
updateFormValue(index, "jbgdSeCd")(value);
|
||||
}}
|
||||
onValueChange={updateFormValue(index, "jbgdSeCd")}
|
||||
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
|
||||
/>
|
||||
{!jbgdSeCd && (
|
||||
<p className="pt-1 text-base text-destructive">
|
||||
직급을 선택해주세요.
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
|
||||
@@ -33,14 +33,9 @@ export const columns: ColumnDef<Post>[] = [
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
|
||||
if (pstgPrdSeCd === "99") {
|
||||
return pstgPrdSeNm;
|
||||
}
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,16 +15,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import {
|
||||
PrivacyLogPrivacy,
|
||||
type PrivacyLogParams,
|
||||
} from "@/entities/privacylog";
|
||||
import { PrivacyLogPrivacy } from "@/entities/privacylog";
|
||||
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;
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -38,7 +36,6 @@ function PrivacyLogPage() {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
|
||||
@@ -86,10 +83,8 @@ function PrivacyLogPage() {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
inqInfoKeyword,
|
||||
inqDtStart: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -128,7 +123,6 @@ function PrivacyLogPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
@@ -137,37 +131,30 @@ function PrivacyLogPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Label className="ml-4 mr-2 text-label">조회정보</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={inqInfoKeyword}
|
||||
onChange={(e) => setInqInfoKeyword(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -37,7 +37,7 @@ import { columns } from "../model/table-columns";
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -51,21 +51,22 @@ function RecentPage() {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
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 [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
|
||||
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: "",
|
||||
@@ -192,7 +193,6 @@ function RecentPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
@@ -250,31 +250,21 @@ function RecentPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">날짜</Label>
|
||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange?.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange?.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||
<Select
|
||||
value={bbsId?.toString() || ""}
|
||||
@@ -291,6 +281,7 @@ function RecentPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
@@ -301,6 +292,7 @@ function RecentPage() {
|
||||
<SelectItem value="Y">삭제</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={keywordType}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import type {
|
||||
SiteAuthorityCreatePayload,
|
||||
SiteAuthorityList,
|
||||
SiteAuthoritySearchParams,
|
||||
SiteAuthorityService,
|
||||
} from "../model/site-authority-interface";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export class SiteAuthorityApiService implements SiteAuthorityService {
|
||||
public async getSiteAuthorityList(
|
||||
params: SiteAuthoritySearchParams,
|
||||
): Promise<SiteAuthorityList> {
|
||||
const response = await client.get<SiteAuthorityList>(
|
||||
ENDPOINT.CMS_SERVICE.SITE_AUTHORITY,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createSiteAuthority(
|
||||
data: SiteAuthorityCreatePayload[],
|
||||
): Promise<void> {
|
||||
await client.post(ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const SiteAuthorityApi = new SiteAuthorityApiService();
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface SiteAuthoritySearchParams extends PageableParams {
|
||||
siteId?: string;
|
||||
authrtId?: string;
|
||||
}
|
||||
|
||||
export interface SiteAuthority {
|
||||
siteId: string;
|
||||
authrtId: string;
|
||||
siteNm: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export type SiteAuthorityCreatePayload = Omit<SiteAuthority, "siteNm">;
|
||||
|
||||
export type SiteAuthorityList = PageableList<SiteAuthority>;
|
||||
|
||||
export interface SiteAuthorityService {
|
||||
getSiteAuthorityList: (
|
||||
params: SiteAuthoritySearchParams,
|
||||
) => Promise<SiteAuthorityList>;
|
||||
createSiteAuthority: (data: SiteAuthorityCreatePayload[]) => Promise<void>;
|
||||
}
|
||||
@@ -3,13 +3,8 @@ import { useState } from "react";
|
||||
import type { Role } from "@/entities/role";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
export const createColumns = ({
|
||||
deleteRole,
|
||||
}: {
|
||||
deleteRole: (roleId: string) => void;
|
||||
}): ColumnDef<Role>[] => [
|
||||
export const columns: ColumnDef<Role>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
@@ -44,18 +39,7 @@ export const createColumns = ({
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MappingRoleButton roleId={row.original.roleId} />
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={() => deleteRole(row.original.roleId)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => <MappingRoleButton roleId={row.original.roleId} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
DialogTitle,
|
||||
Button,
|
||||
Switch,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import {
|
||||
RoleAuthorizationsApi,
|
||||
@@ -33,18 +31,12 @@ function MappingRoleDialog({
|
||||
}: MappingRoleDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
keyword: "",
|
||||
keywordType: "authorizationName",
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, searchParams],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||
queryFn: () =>
|
||||
RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({
|
||||
roleId,
|
||||
...searchParams,
|
||||
}),
|
||||
enabled: open,
|
||||
});
|
||||
@@ -54,7 +46,7 @@ function MappingRoleDialog({
|
||||
RoleAuthorizationsApi.createRoleAuthorizations(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||
});
|
||||
alert("권한 맵핑이 완료되었습니다.");
|
||||
},
|
||||
@@ -74,26 +66,6 @@ function MappingRoleDialog({
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const keyword = formData.get("keyword") as string;
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
@@ -107,31 +79,21 @@ function MappingRoleDialog({
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">권한맵핑</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>모듈 목록</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name="keyword"
|
||||
className="w-[32rem]"
|
||||
/>
|
||||
<Button type="submit">검색</Button>
|
||||
<Button type="button" color="white" onClick={handleReset}>
|
||||
초기화
|
||||
<div className="max-h-[40rem] overflow-y-auto">
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
<Button size="sm" onClick={() => handleApplyAll("Y")}>
|
||||
일괄적용
|
||||
</Button>
|
||||
</form>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button onClick={() => handleApplyAll("Y")}>일괄적용</Button>
|
||||
<Button color="gray" onClick={() => handleApplyAll("N")}>
|
||||
<Button size="sm" color="gray" onClick={() => handleApplyAll("N")}>
|
||||
일괄해제
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</div>
|
||||
<div className="max-h-[60rem] overflow-y-auto">{tableComponent}</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
@@ -18,9 +16,6 @@ import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { RoleApi, type RoleFormData } from "@/entities/role";
|
||||
import { FormField, FormItem, FormControl } from "@/shared/ui/form";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { SiteAuthorityApi } from "../api/site-authority-service";
|
||||
import type { SiteAuthorityCreatePayload } from "../model/site-authority-interface";
|
||||
|
||||
type RoleFormProps = {
|
||||
roleId?: string;
|
||||
@@ -29,24 +24,12 @@ type RoleFormProps = {
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
function RoleForm({ roleId, form, ...props }: RoleFormProps) {
|
||||
const isModifyMode = !!roleId;
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isValidRoleId, setIsValidRoleId] = useState(false);
|
||||
const [checkedSiteAuthority, setCheckedSiteAuthority] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const { data: siteAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, roleId],
|
||||
queryFn: () =>
|
||||
SiteAuthorityApi.getSiteAuthorityList({
|
||||
authrtId: roleId,
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
});
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [isValidRoleId, setIsValidRoleId] = useState(false);
|
||||
|
||||
const { mutateAsync: checkDuplicateRoleId } = useMutationWrapper({
|
||||
mutationFn: (roleId: string) => RoleApi.checkDuplicateRoleId(roleId),
|
||||
@@ -60,49 +43,8 @@ function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSiteAuthorityChecked = (checked: boolean, siteId: string) => {
|
||||
if (checked) {
|
||||
setCheckedSiteAuthority((prev) => [...prev, siteId]);
|
||||
} else {
|
||||
setCheckedSiteAuthority((prev) => prev.filter((id) => id !== siteId));
|
||||
}
|
||||
};
|
||||
|
||||
const saveInterceptor = async (data: RoleFormData) => {
|
||||
const siteAuthorityPayload: SiteAuthorityCreatePayload[] =
|
||||
checkedSiteAuthority.map((siteId) => ({
|
||||
siteId,
|
||||
authrtId: data.roleId,
|
||||
mode: "C",
|
||||
}));
|
||||
|
||||
try {
|
||||
await handleSave(data);
|
||||
if (data.mngrYn === "N") return;
|
||||
await SiteAuthorityApi.createSiteAuthority(siteAuthorityPayload);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!siteAuthorityList?.content.length) return;
|
||||
const checkedSiteAuthority = siteAuthorityList.content.filter(
|
||||
(item) => item.mode === "R",
|
||||
);
|
||||
setCheckedSiteAuthority(checkedSiteAuthority.map((item) => item.siteId));
|
||||
}, [siteAuthorityList]);
|
||||
|
||||
return (
|
||||
<FormWrapper
|
||||
form={form}
|
||||
isModifyMode={isModifyMode}
|
||||
handleSave={saveInterceptor}
|
||||
{...props}
|
||||
>
|
||||
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
@@ -223,29 +165,6 @@ function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{form.watch("mngrYn") === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead data-required>관리사이트</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{siteAuthorityList?.content.map((item) => (
|
||||
<div
|
||||
key={item.siteId}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={item.mode === "R"}
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
handleSiteAuthorityChecked(checked, item.siteId);
|
||||
}}
|
||||
/>
|
||||
<span className="whitespace-nowrap">{item.siteNm}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
|
||||
@@ -7,12 +7,11 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { createColumns } from "../model/table-columns";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function RolePage() {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<RoleParams>({
|
||||
@@ -34,13 +33,6 @@ function RolePage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useMutationWrapper({
|
||||
mutationFn: (roleId: string) => RoleApi.deleteRole(roleId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", query.refetch);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRowClick = (row: Role) => {
|
||||
router.push(`/role/register/${row.roleId}`);
|
||||
};
|
||||
@@ -48,9 +40,7 @@ function RolePage() {
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns({
|
||||
deleteRole,
|
||||
}),
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { TreeMenu } from "@/entities/tree-menu";
|
||||
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
): NodeModel<TreeMenu>[] => {
|
||||
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
|
||||
const node: NodeModel<TreeMenu> = {
|
||||
id: menu.menuCd ?? 0,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuSubKornNm ?? "",
|
||||
droppable: true,
|
||||
data: menu,
|
||||
};
|
||||
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateMenuTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
@@ -1,32 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import { memo, useState } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import type { Site } from "@/entities/site";
|
||||
import { TreeMenuApi } from "@/entities/tree-menu";
|
||||
import {
|
||||
MenuApi,
|
||||
siteMapFormSchema,
|
||||
type SiteMapFormData,
|
||||
} from "@/entities/menu";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { MenuTree } from "../ui/menu-tree";
|
||||
import { generateMenuTreeData } from "../lib/utils";
|
||||
|
||||
export const columns: ColumnDef<Site>[] = [
|
||||
{
|
||||
@@ -71,81 +45,4 @@ export const columns: ColumnDef<Site>[] = [
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ActionCell data={row.original} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const ActionCell = memo(({ data }: { data: Site }) => {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: menuTreeData, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, data.siteId],
|
||||
queryFn: () => TreeMenuApi.getTreeMenuList(data.siteId),
|
||||
select: (data) => generateMenuTreeData(data),
|
||||
enabled: !!data.siteId && open,
|
||||
});
|
||||
|
||||
const { mutate: publishSiteMap } = useMutationWrapper({
|
||||
mutationFn: (data: SiteMapFormData["siteMap"]) =>
|
||||
MenuApi.publishSiteMap(data),
|
||||
onSuccess: () => {
|
||||
alert("사이트맵이 저장되었습니다.", () => {
|
||||
setOpen(false);
|
||||
refetch();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<SiteMapFormData>({
|
||||
resolver: zodResolver(siteMapFormSchema),
|
||||
});
|
||||
const fieldArray = useFieldArray({
|
||||
control: form.control,
|
||||
name: "siteMap",
|
||||
});
|
||||
|
||||
const handlePublishSiteMap = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
publishSiteMap(form.getValues("siteMap"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">사이트맵</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{data.siteNm} - 사이트맵 관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<div className="flex items-center justify-end gap-2 p-2">
|
||||
<span className="text-xl">표시여부</span>
|
||||
<Checkbox checked />
|
||||
</div>
|
||||
<MenuTree tree={menuTreeData ?? []} fieldArray={fieldArray} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handlePublishSiteMap}>저장</Button>
|
||||
<DialogClose asChild>
|
||||
<Button color="white">취소</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
ActionCell.displayName = "ActionCell";
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
} & ComponentProps<typeof Input>;
|
||||
|
||||
function EditableNode({
|
||||
depth,
|
||||
onSave,
|
||||
onCancel,
|
||||
...props
|
||||
}: EditableNodeProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-sm bg-accent p-2"
|
||||
style={{ marginInlineStart: !depth ? 0 : depth * 5 + 5 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
aria-label="저장"
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>저장</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
aria-label="취소"
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>취소</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { EditableNode };
|
||||
@@ -1,128 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type { UseFieldArrayReturn } from "react-hook-form";
|
||||
import { Button, Checkbox, cn } from "@common/components";
|
||||
import { type TreeMenu } from "@/entities/tree-menu";
|
||||
import type { SiteMapFormData, SiteMapPayloadData } from "@/entities/menu";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuNodeProps = {
|
||||
node: NodeModel<TreeMenu>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
};
|
||||
|
||||
function MenuNode({
|
||||
node,
|
||||
depth,
|
||||
isOpen,
|
||||
fieldArray,
|
||||
onToggle,
|
||||
}: MenuNodeProps) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(text);
|
||||
const [otptYn, setOtptYn] = useState(data?.otptYn ?? "N");
|
||||
|
||||
const updateField = (id: number, newField: SiteMapPayloadData) => {
|
||||
const existingFieldIndex = fieldArray.fields.findIndex(
|
||||
(field) => field.menuCd === id,
|
||||
);
|
||||
if (existingFieldIndex !== -1) {
|
||||
fieldArray.update(existingFieldIndex, newField);
|
||||
} else {
|
||||
fieldArray.append(newField);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
|
||||
const handleSave = async () => {
|
||||
const newField = {
|
||||
menuCd: id as number,
|
||||
menuSubKornNm: nodeName,
|
||||
otptYn,
|
||||
};
|
||||
updateField(id as number, newField);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditMode(false);
|
||||
setNodeName(text);
|
||||
};
|
||||
|
||||
const handleOtptYnChange = (checked: boolean) => {
|
||||
const newField = {
|
||||
menuCd: id as number,
|
||||
menuSubKornNm: nodeName,
|
||||
otptYn: checked ? "Y" : "N",
|
||||
};
|
||||
updateField(id as number, newField);
|
||||
setOtptYn(checked ? "Y" : "N");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className={cn(
|
||||
"group flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
editMode && "p-0 pr-2 hover:bg-transparent",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
{editMode ? (
|
||||
<EditableNode
|
||||
value={nodeName}
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">
|
||||
{nodeName}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="invisible !bg-transparent group-hover:visible"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditMode(true);
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Checkbox
|
||||
className="ml-auto"
|
||||
checked={otptYn === "Y"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onCheckedChange={handleOtptYnChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuNode };
|
||||
@@ -1,70 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import type { UseFieldArrayReturn } from "react-hook-form";
|
||||
import {
|
||||
Tree,
|
||||
type NodeModel,
|
||||
type TreeMethods,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components";
|
||||
import type { SiteMapFormData } from "@/entities/menu";
|
||||
import { type TreeMenu } from "@/entities/tree-menu";
|
||||
import { MenuNode } from "./menu-node";
|
||||
|
||||
type MenuTreeProps = {
|
||||
tree: NodeModel<TreeMenu>[];
|
||||
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
|
||||
};
|
||||
|
||||
function MenuTree({ tree, fieldArray }: MenuTreeProps) {
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-h-[calc(100vh-33.5rem)] min-h-[40vh] overflow-auto">
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
ref={treeMethods}
|
||||
render={(node: NodeModel<TreeMenu>, props) => {
|
||||
return <MenuNode node={node} fieldArray={fieldArray} {...props} />;
|
||||
}}
|
||||
onDrop={() => {}}
|
||||
canDrag={() => false}
|
||||
canDrop={() => false}
|
||||
classes={{
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.openAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-expand" />
|
||||
전체펼침
|
||||
</Button>
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.closeAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-contract" />
|
||||
전체접기
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuTree };
|
||||
@@ -36,14 +36,9 @@ export const columns: ColumnDef<Post>[] = [
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
|
||||
if (pstgPrdSeCd === "99") {
|
||||
return pstgPrdSeNm;
|
||||
}
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
USER_STATE_CODE,
|
||||
type UserListResponse,
|
||||
type UserStateCode,
|
||||
} from "@/entities/user";
|
||||
import { Badge } from "@common/components";
|
||||
import type { UserListResponse } from "@/entities/user";
|
||||
|
||||
export const columns: ColumnDef<UserListResponse>[] = [
|
||||
{
|
||||
@@ -47,11 +42,8 @@ export const columns: ColumnDef<UserListResponse>[] = [
|
||||
header: "기관/부서",
|
||||
},
|
||||
{
|
||||
accessorKey: "userStateCode",
|
||||
accessorKey: "userStateCodeName",
|
||||
header: "회원상태",
|
||||
cell: ({ row }) => {
|
||||
return getUserStateBadge(row.original.userStateCode);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "aprvYmd",
|
||||
@@ -71,18 +63,3 @@ export const columns: ColumnDef<UserListResponse>[] = [
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getUserStateBadge = (userStateCode: UserStateCode) => {
|
||||
switch (userStateCode) {
|
||||
case USER_STATE_CODE.대기:
|
||||
return <Badge color="gray">대기</Badge>;
|
||||
case USER_STATE_CODE.정상:
|
||||
return <Badge color="green">정상</Badge>;
|
||||
case USER_STATE_CODE.정지:
|
||||
return <Badge color="yellow">정지</Badge>;
|
||||
case USER_STATE_CODE.탈퇴:
|
||||
return <Badge color="purple">탈퇴</Badge>;
|
||||
case USER_STATE_CODE.삭제:
|
||||
return <Badge color="red">삭제</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,11 +16,20 @@ function CreatePage() {
|
||||
defaultValues: {
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
expryYmd: "",
|
||||
instSeCd: "",
|
||||
enoCd: "",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -28,7 +28,7 @@ import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@ function UsersPage() {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
@@ -168,7 +167,6 @@ function UsersPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
@@ -279,34 +277,23 @@ function UsersPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="mr-2 text-label">기간</Label>
|
||||
<Select
|
||||
value={presetDateRange}
|
||||
onValueChange={handlePresetDateRange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
<SelectItem value="6M">6개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange?.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange?.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder="기간 선택"
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
<SelectItem value="6M">6개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -23,7 +23,6 @@ export const ENDPOINT = {
|
||||
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
DASHBOARD: "/cms-service/api/v1/dashboard",
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||
@@ -54,7 +53,6 @@ export const ENDPOINT = {
|
||||
GROUP_USERS: "/cms-service/api/v1/group-users",
|
||||
MANAGER_IP: "/cms-service/api/v1/mgrip",
|
||||
APILOG: "/cms-service/api/v1/api-logs",
|
||||
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
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.66:8883/sewerage-service/api # 박이정
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
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.66:8883/sewerage-service/api # 박이정
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
|
||||
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://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "./store/common";
|
||||
import { useDmStore } from "./store";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Base({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const { getMenuAuth } = useDmStore((state) => state);
|
||||
|
||||
const router = useRouter();
|
||||
const handleClick = (id:MenuItem) =>{
|
||||
getMenuAuth(id.menuCd);
|
||||
router.push(id.urlPath);
|
||||
}
|
||||
|
||||
return <BaseLayout handleMenuClick={handleClick}>{children}</BaseLayout>;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
type CommonProps = {
|
||||
value: string | number | '';
|
||||
format: string;
|
||||
}
|
||||
|
||||
export default function TransValueFormat({value='', format=''}: CommonProps) {
|
||||
return (
|
||||
format === 'yyyyMMdd' ?
|
||||
<>{value.toString().substring(0,4)+'-'+value.toString().substring(4,6)+'-'+value.toString().substring(6,8)}</>
|
||||
: format === 'number' ?
|
||||
<>{String(value ?? '').includes('.')
|
||||
? String(value).split('.').map((part, index) =>
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part
|
||||
).join('.')
|
||||
: String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</>
|
||||
: format === 'float1' ?
|
||||
<>{String(value ?? '').includes('.')
|
||||
? String(value).split('.').map((part, index) =>
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part
|
||||
).join('.')
|
||||
: String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</>
|
||||
: <>{value}</>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,70 +0,0 @@
|
||||
@import "../../../packages/common/components/src/base.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client"
|
||||
import Base from "./base";
|
||||
import "./globals.css";
|
||||
import { StoreProvider } from "./store";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<StoreProvider>
|
||||
<Base>{children}</Base>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@common/components/ui';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrAbout">물재이용이란</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrExample">물재이용 사례</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrStatus">물재이용 현황</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrOverview">물재이용 사업추진절차</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmInsert">수요처 등록</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmInfoInsert">수요처 정보 등록</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
"use client"
|
||||
import { Table, TableBody, TableCell, TableRow } from '@common/components/ui/table';
|
||||
|
||||
import { useDmStore } from '@/app/store';
|
||||
import { ContentsDiv, Title, UiModal, UiTable } from '@common/business_components/ui';
|
||||
import ListDiv from '@common/business_components/ui/list_div';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import SdmLogin from '../sdmLogin/page';
|
||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||
import TransValueFormat from '@/app/common/transValueFormat';
|
||||
|
||||
const TABLE_STYLES = {
|
||||
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||
cell: "text-right whitespace-pre-line",
|
||||
fullWidth: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||
table: "fixed w-full"
|
||||
} as const;
|
||||
|
||||
type SdmApplyProps = {
|
||||
sdmNo : string;
|
||||
}
|
||||
|
||||
export default function SdmApplyList({}: SdmApplyProps) {
|
||||
const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state);
|
||||
const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm;
|
||||
const {selectSidoData, selectSigunData} = Common;
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false);
|
||||
const [sdmApplyDetail, setSdmApplyDetail] = useState<DemandCompType>({} as DemandCompType);
|
||||
const [transformedList, setTransformedList] = useState<DemandCompType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('sessionStorage : ',sessionStorage.getItem('user'));
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
setIsLoginStorageInfo(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (successLogin || isLoginStorageInfo) {
|
||||
setIsLogin(true);
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
getSdmInfoStatusList(userData.dmdInfoId);
|
||||
} catch (error) {
|
||||
console.error('Error parsing user data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [successLogin, getSdmInfoStatusList, isLoginStorageInfo]);
|
||||
|
||||
// 데이터 변환을 위한 별도의 useEffect
|
||||
useEffect(() => {
|
||||
if (sdmInfoStatusList.length > 0) {
|
||||
const newTransformedList = sdmInfoStatusList.map((item: DemandCompType) => ({
|
||||
...item,
|
||||
ctpvCd: selectSidoData.find(sido => sido.val === item.ctpvCd?.trim())?.text || '',
|
||||
sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || ''
|
||||
}));
|
||||
setTransformedList(newTransformedList);
|
||||
}
|
||||
}, [sdmInfoStatusList, selectSidoData, selectSigunData]);
|
||||
|
||||
const onClickSdmStatus = (index: number | undefined) => {
|
||||
if (index !== undefined) {
|
||||
const selectedStatus = transformedList[index];
|
||||
if (selectedStatus) {
|
||||
setSdmApplyDetail(selectedStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title="수요신청현황" />
|
||||
<ContentsDiv>
|
||||
<ListDiv>
|
||||
<div className="overflow-y-scroll h-[250px]">
|
||||
<div className="h-[110%] w-[99%]">
|
||||
<UiTable
|
||||
headName='수요정보 신청현황'
|
||||
tableData={transformedList}
|
||||
headlist={upHeadSdmInfoStatus}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={(index) => onClickSdmStatus(index)}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ListDiv>
|
||||
<ListDiv>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">수요신청 상세현황</h3>
|
||||
</div>
|
||||
<div className={`card !m-[1.2rem] !p-0r`}>
|
||||
<Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>요구시기</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.dmndTmng?.toString()}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>요구수량(m³/일)</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.dmndQty} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>사용용도</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.useUsgCd}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>희망단가(원/m³)</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.wntUntprc} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>요구수량</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총대장균군수(개/100ml)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>생물학적 산소요구량(BOD)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총질소(T-N)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총인(T-P)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>수소이온농도(pH)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.ph} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>염화물(mgCL/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>비고</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.rmrk}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>공급처 후보지</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지1</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</ListDiv>
|
||||
<ListDiv>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">수요신청 진행상태</h3>
|
||||
</div>
|
||||
<div className={`card !m-[1.2rem] !p-0r`}>
|
||||
<Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>진행상태</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.status?.toString()}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>공급처매칭결과</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</ListDiv>
|
||||
</ContentsDiv>
|
||||
{!isLogin?
|
||||
<UiModal
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
:null}
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useDmStore } from '@/app/store';
|
||||
import type { MoneyDetailType, SimilarCaseType } from '@/app/store/sdmMng';
|
||||
import { SelectBox, Title, UiTable } from '@common/business_components/ui';
|
||||
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
|
||||
import { Table, TableBody, TableCell, TableHeader, TableRow } from '@common/components/ui/table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type modalType = {
|
||||
sply: number | 0;
|
||||
usageCd: string | '';
|
||||
money: number | 0;
|
||||
};
|
||||
|
||||
const searchUseTypeList = [
|
||||
{ text: '공급량', val: 'S' },
|
||||
{ text: '사용용도', val: 'U' },
|
||||
];
|
||||
|
||||
export default function ModalSimilarPage({ sply = 0, usageCd = '', money = 0 }: modalType) {
|
||||
const { Sdm, getSimilarCaseList } = useDmStore((state) => state);
|
||||
const [searchUseType, setSearchUseType] = useState<{text: string, value: string}>({text: '공급량', value: 'S'});
|
||||
const [moneyDetailList, setMoneyDetailList] = useState<MoneyDetailType[]>([]);
|
||||
const {
|
||||
upHeadSimilarCaseList,
|
||||
similarCaseList,
|
||||
upHeadMoneyDetailList,
|
||||
} = Sdm;
|
||||
|
||||
const rowSpans = useMemo(() => {
|
||||
const spans: { [key: number]: number } = {};
|
||||
let currentValue = '';
|
||||
let currentSpan = 0;
|
||||
let startIndex = 0;
|
||||
|
||||
similarCaseList.forEach((item: SimilarCaseType, index: number) => {
|
||||
if (item.ruseBizNm === currentValue
|
||||
|| item.bizBgngDate === currentValue
|
||||
|| item.bizEndDate === currentValue
|
||||
|| item.reqBizReuseFcltCst === currentValue) {
|
||||
currentSpan++;
|
||||
spans[startIndex] = currentSpan;
|
||||
} else {
|
||||
currentValue = item.ruseBizNm;
|
||||
currentSpan = 1;
|
||||
startIndex = index;
|
||||
spans[index] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
return spans;
|
||||
}, [similarCaseList]);
|
||||
|
||||
const handleCellClick = (index: number) => {
|
||||
setMoneyDetailList([{
|
||||
reqBizGrssCst: similarCaseList[index]?.reqBizGrssCst ?? '',
|
||||
reqBizReuseFcltCst: similarCaseList[index]?.reqBizReuseFcltCst ?? '',
|
||||
reqBizPplCst: similarCaseList[index]?.reqBizPplCst ?? '',
|
||||
reqBizArchCst: similarCaseList[index]?.reqBizArchCst ?? '',
|
||||
reqBizMchnCst: similarCaseList[index]?.reqBizMchnCst ?? '',
|
||||
reqBizMtrngCst: similarCaseList[index]?.reqBizMtrngCst ?? '',
|
||||
reqBizGisCst: similarCaseList[index]?.reqBizGisCst ?? '',
|
||||
reqBizCctvCst: similarCaseList[index]?.reqBizCctvCst ?? '',
|
||||
reqBizCmcnCst: similarCaseList[index]?.reqBizCmcnCst ?? '',
|
||||
reqBizWstCst: similarCaseList[index]?.reqBizWstCst ?? '',
|
||||
reqBizDesignCst: similarCaseList[index]?.reqBizDesignCst ?? '',
|
||||
reqBizSpvsCst: similarCaseList[index]?.reqBizSpvsCst ?? '',
|
||||
reqBizSbsdCst: similarCaseList[index]?.reqBizSbsdCst ?? '',
|
||||
reqBizEtc: similarCaseList[index]?.reqBizEtc ?? '',
|
||||
prcmPlanNlep: similarCaseList[index]?.prcmPlanNlep ?? '',
|
||||
prcmPlanLcep: similarCaseList[index]?.prcmPlanLcep ?? '',
|
||||
prcmPlanBrdnCst: similarCaseList[index]?.prcmPlanBrdnCst ?? '',
|
||||
prcmPlanEcqrRt: similarCaseList[index]?.prcmPlanEcqrRt ?? '',
|
||||
prcmPlanGrssCst: similarCaseList[index]?.prcmPlanGrssCst ?? '',
|
||||
clclnNlep: similarCaseList[index]?.clclnNlep ?? '',
|
||||
clclnLcep: similarCaseList[index]?.clclnLcep ?? '',
|
||||
clclnBrdnCst: similarCaseList[index]?.clclnBrdnCst ?? '',
|
||||
clclnEcqrRt: similarCaseList[index]?.clclnEcqrRt ?? '',
|
||||
clclnGrssCst: similarCaseList[index]?.clclnGrssCst ?? ''
|
||||
}]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const type = searchUseType.value;
|
||||
getSimilarCaseList(type, type === 'S' ? sply : type === 'U' ? usageCd : money);
|
||||
}, [getSimilarCaseList, money, searchUseType, sply, usageCd]);
|
||||
|
||||
return (
|
||||
<div className="align-top w-[1360px] overflow-x-auto">
|
||||
<Title title="유사사례 조회" />
|
||||
<div className="overflow-x-auto h-[450px] w-full mt-8" tabIndex={0} aria-label="유사사례 테이블 영역">
|
||||
<div className="w-[150%]">
|
||||
<div className="card-header justify-self-start">
|
||||
<h3 className="text-2xl font-medium text-label">유사사례</h3>
|
||||
<SelectBox
|
||||
tabIndex={0}
|
||||
aria-label="검색 기준 선택"
|
||||
selectArray={searchUseTypeList}
|
||||
value={searchUseType.value}
|
||||
onSelectValue={(val) => {
|
||||
setSearchUseType({text: searchUseTypeList.find(item => item.val === val)?.text ?? '', value: val.toString()});
|
||||
}}
|
||||
className="w-[140px] ml-20"
|
||||
label=""
|
||||
selectClass="w-[140px]"
|
||||
/>
|
||||
</div>
|
||||
<div className={`card !m-[1.2rem] !p-0r`}>
|
||||
<Table variant="secondary">
|
||||
<TableHeader className="font-bold">
|
||||
<HeadMakeColSpan upHeadList={upHeadSimilarCaseList} />
|
||||
</TableHeader>
|
||||
<TableBody className='text-center'>
|
||||
{similarCaseList.length > 0 ? similarCaseList?.map((el: SimilarCaseType, index) => (
|
||||
<TableRow key={`similar-case-row-${index}-${el.ruseBizNm}-${el.bizBgngDate}`}>
|
||||
{upHeadSimilarCaseList.map((head) => (
|
||||
head.id && head.id.length > 1 && (
|
||||
<TableCell
|
||||
tabIndex={head.id === 'someClickableId' ? 0 : undefined}
|
||||
role="button"
|
||||
aria-label={`유사사례 ${head.id} 클릭 시 상세 정보 표시`}
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id as keyof SimilarCaseType] === 'number' ? 'text-right' : 'text-center'} cursor-pointer`}
|
||||
key={`similar-case-cell-${head.id}-${index}-${el.ruseBizNm}`}
|
||||
rowSpan={(head.id === 'businessName' || head.id ==='businessDuration' || head.id === 'money') ? rowSpans[index] : undefined}
|
||||
style={(head.id === 'businessName' || head.id === 'businessDuration' || head.id === 'money') && index > 0 && similarCaseList[index - 1]?.ruseBizNm === el.ruseBizNm ? { display: 'none' } : undefined}
|
||||
onClick={() => handleCellClick(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleCellClick(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{typeof el[head.id as keyof SimilarCaseType] !== 'number'
|
||||
? String(el[head.id as keyof SimilarCaseType] ?? '')
|
||||
: String(el[head.id as keyof SimilarCaseType]).includes('.')
|
||||
? String(el[head.id as keyof SimilarCaseType]).split('.').map((part, index) =>
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part
|
||||
).join('.')
|
||||
: String(el[head.id as keyof SimilarCaseType] ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
</TableCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
)) : <></>}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto h-[270px] w-full mt-8">
|
||||
<div className="w-[130%]">
|
||||
<UiTable
|
||||
headName={"사업비 상세정보"}
|
||||
tableData={moneyDetailList}
|
||||
headlist={upHeadMoneyDetailList}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component";
|
||||
import { UiModal } from "@common/business_components/ui/dialog_ui";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import * as z from "zod";
|
||||
import SdmLogin from "../sdmLogin/page";
|
||||
|
||||
const createFormSchema = () => z.object({
|
||||
sdmNm: z.string()
|
||||
.min(1, "수요처를 선택해주세요"),
|
||||
demandYmd: z.date().optional(),
|
||||
demandQnty: z.string()
|
||||
.min(1, "요구수량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"),
|
||||
usePurpose: z.string().optional(),
|
||||
money: z.string()
|
||||
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
|
||||
/**요구수질 */
|
||||
reqWaterQlty : z.string().optional(),
|
||||
grssClfg: z.string()
|
||||
.min(1, "총대장균군수를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
|
||||
tn : z.string()
|
||||
.min(1, "총질소를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
|
||||
combResdch : z.string()
|
||||
.min(1, "결합잔류염소를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
|
||||
tph : z.string()
|
||||
.min(1, "총인산물을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
|
||||
trbt : z.string()
|
||||
.min(1, "탁도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
|
||||
ph : z.string()
|
||||
.min(1, "수소이온농도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
|
||||
bod : z.string()
|
||||
.min(1, "생물학적 산소요구량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt : z.string()
|
||||
.min(1, "염화물을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
|
||||
smll : z.string()
|
||||
.min(1, "냄새를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
|
||||
cdtnv : z.string()
|
||||
.min(1, "전기전도도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
|
||||
crmty : z.string()
|
||||
.min(1, "색도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
|
||||
rmrk : z.string().optional(),
|
||||
/* telno: z.string()
|
||||
.min(8, "전화번호는 8자 이상 입력해주세요")
|
||||
.max(12, "전화번호는 12자 이내로 입력해주세요")
|
||||
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
|
||||
candidateTitle: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
|
||||
function SdmInfoInsertContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const {insertSdmInfoData, Sdm} = useDmStore((state) => state);
|
||||
const {successLogin} = Sdm;
|
||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||
const formSchema = createFormSchema();
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [facilityName, setFacilityName] = useState<string>('');
|
||||
const [defaultValues, setDefaultValues] = useState<FormData>({
|
||||
sdmNm: '',
|
||||
demandYmd: new Date(),
|
||||
demandQnty: '',
|
||||
usePurpose: '',
|
||||
money: '',
|
||||
/**요구수질 */
|
||||
reqWaterQlty: '',
|
||||
grssClfg: '',
|
||||
tn: '',
|
||||
combResdch: '',
|
||||
tph: '',
|
||||
trbt: '',
|
||||
ph: '',
|
||||
bod: '',
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt: '',
|
||||
smll: '',
|
||||
cdtnv: '',
|
||||
crmty: '',
|
||||
rmrk: '',
|
||||
/** 후보지 제목 */
|
||||
candidateTitle: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = sessionStorage.getItem('user');
|
||||
setDefaultValues(prev => ({
|
||||
...prev,
|
||||
sdmNm: searchParams.get('dmdCoNm') || '',
|
||||
}));
|
||||
sessionStorage.setItem('user', searchParams.get('dmdInfoId') || '');
|
||||
setIsLogin(user || searchParams.get('dmdInfoId') ? true : false);
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const local = localStorage.getItem('user');
|
||||
if(local) {
|
||||
setIsLogin(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
// 로그인 성공 시 모달 닫기 및 폼 업데이트
|
||||
useEffect(() => {
|
||||
if (successLogin) {
|
||||
setIsLogin(true);
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
if (userData.dmdCoNm) {
|
||||
setFacilityName(userData.dmdCoNm);
|
||||
form.setValue('sdmNm', userData.dmdCoNm);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing user data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [successLogin, form]);
|
||||
|
||||
// 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화
|
||||
useEffect(() => {
|
||||
if (ismoneyDisabled) {
|
||||
form.clearErrors('money');
|
||||
form.setValue('money', '');
|
||||
}
|
||||
}, [ismoneyDisabled, form]);
|
||||
|
||||
const onSubmit: SubmitHandler<FormData> = async (data) => {
|
||||
try {
|
||||
// Transform form data to match API schema
|
||||
const apiData : DemandCompType = {
|
||||
dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
|
||||
dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
|
||||
dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm,
|
||||
ctpvCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.ctpvCd?.toString() || searchParams.get('ctpvCd') || '',
|
||||
sggCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.sggCd?.toString() || searchParams.get('sggCd') || '',
|
||||
daddr: JSON.parse(sessionStorage.getItem('user') || '{}')?.daddr?.toString() || searchParams.get('daddr') || '',
|
||||
telno: JSON.parse(sessionStorage.getItem('user') || '{}')?.telno?.toString() || searchParams.get('telno') || '',
|
||||
rgtr: JSON.parse(sessionStorage.getItem('user') || '{}')?.rgtr?.toString() || searchParams.get('rgtr') || '',
|
||||
useUsgCd: data.usePurpose || '',
|
||||
wntUntprc: Number(data.money) || '',
|
||||
dmndTmng: data.demandYmd || new Date(),
|
||||
dmndQty: Number(data.demandQnty) || '',
|
||||
grssClfg: Number(data.grssClfg) || '',
|
||||
tn: Number(data.tn) || '',
|
||||
combResdch: Number(data.combResdch) || '',
|
||||
tph: Number(data.tph) || '',
|
||||
trbt: Number(data.trbt) || '',
|
||||
ph: Number(data.ph) || '',
|
||||
blgcOxygDmndQnt: Number(data.bod) || '',
|
||||
clIonQnt: Number(data.clIonQnt) || '',
|
||||
smll: Number(data.smll) || '',
|
||||
cdtnv: Number(data.cdtnv) || '',
|
||||
crmty: Number(data.crmty) || '',
|
||||
rmrk: data.rmrk || '',
|
||||
bzmnNo: "",
|
||||
dmdAplyPswd: "",
|
||||
index: 0,
|
||||
reqDt: "",
|
||||
dmdInfoSn: 0,
|
||||
status : '',
|
||||
};
|
||||
|
||||
await insertSdmInfoData(apiData);
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요정보 등록
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<SdmInfoRegForm
|
||||
form={form}
|
||||
onCheckboxChange={setIsmoneyDisabled}
|
||||
facilityName={facilityName}
|
||||
/>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
수요정보 등록
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLogin?
|
||||
<UiModal
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
:null}
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyForm() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SdmInfoInsertContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import SdmInsertForm from '@common/business_components/ui/sdmInsertForm';
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
export default function MyForm() {
|
||||
const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state);
|
||||
const { selectSidoData, selectSigunData } = Common;
|
||||
const { demandCompList, successNextPage } = Sdm;
|
||||
const router = useRouter();
|
||||
const formDataRef = useRef<DemandCompType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (successNextPage && formDataRef.current) {
|
||||
const data = formDataRef.current;
|
||||
const queryParams = new URLSearchParams({
|
||||
dmdCoNm: data.dmdCoNm || '',
|
||||
ctpvCd: data.ctpvCd || '',
|
||||
sggCd: data.sggCd || '',
|
||||
dmdInfoId: data.dmdInfoId || '',
|
||||
daddr: data.daddr || '',
|
||||
telno: data.telno || '',
|
||||
});
|
||||
router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
|
||||
}
|
||||
}, [successNextPage, router]);
|
||||
|
||||
const callback = useCallback((searchText: string, pageCnt: number) => {
|
||||
searchText = searchText.trim();
|
||||
if(searchText.length > 0) {
|
||||
getDemandCompList(searchText, pageCnt);
|
||||
}
|
||||
}, [getDemandCompList]);
|
||||
|
||||
const handleInsertSdmData = useCallback((data: import('@common/business_components/ui/sdmInsertForm/form').DemandCompType) => {
|
||||
const convertedData: DemandCompType = {
|
||||
...data,
|
||||
dmdInfoSn: '',
|
||||
dmdInfoFk: '',
|
||||
status: '',
|
||||
};
|
||||
formDataRef.current = convertedData;
|
||||
insertSdmData(convertedData).catch(error => {
|
||||
console.error('Error inserting SDM data:', error);
|
||||
});
|
||||
}, [insertSdmData]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SdmInsertForm
|
||||
selectSidoData={selectSidoData}
|
||||
selectSigunData={selectSigunData}
|
||||
dataList={demandCompList}
|
||||
rtnValue={callback}
|
||||
rtnInputData={handleInsertSdmData}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { SdmLoginForm } from '@/components/ui/sdmLoginForm/component';
|
||||
|
||||
export default function SdmLogin() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요처 로그인
|
||||
</h1>
|
||||
</div>
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<SdmLoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const NAVS = [
|
||||
{ id: 'card1', label: '물재이용이란' },
|
||||
{ id: 'card2', label: '물재이용 필요성' },
|
||||
{ id: 'card3', label: '빗물 재이용' },
|
||||
{ id: 'card4', label: '중수 재이용' },
|
||||
{ id: 'card5', label: '하수 재이용' },
|
||||
{ id: 'card6', label: '물재이용 목표' },
|
||||
];
|
||||
|
||||
export default function WrAboutPage() {
|
||||
const [activeIdx, setActiveIdx] = useState(0);
|
||||
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
// 클릭 시 이동
|
||||
const handleNavClick = (idx: number) => {
|
||||
setActiveIdx(idx);
|
||||
const targetCard = cardRefs.current[idx];
|
||||
if (targetCard) {
|
||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
// 키보드 이벤트 처리
|
||||
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleNavClick(idx);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
|
||||
if (idx !== -1) {
|
||||
setActiveIdx(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// 각 카드 요소를 관찰
|
||||
cardRefs.current.forEach((card) => {
|
||||
if (card) {
|
||||
observer.observe(card);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cardRefs.current.forEach((card) => {
|
||||
if (card) {
|
||||
observer.unobserve(card);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
|
||||
{/* 사이드바 */}
|
||||
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
|
||||
<ul role="list" className="relative space-y-6">
|
||||
{NAVS.map((nav, idx) => (
|
||||
<li
|
||||
key={nav.id}
|
||||
ref={(el) => { navRefs.current[idx] = el; }}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
aria-current={idx === activeIdx ? 'true' : 'false'}
|
||||
aria-label={`현재 위치: ${nav.label}`}
|
||||
onClick={() => handleNavClick(idx)}
|
||||
onKeyDown={(e) => handleKeyDown(e, idx)}
|
||||
className={`nav-item relative pl-6 text-2xl font-bold text-gray-700 cursor-pointer transition-all duration-300
|
||||
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5]
|
||||
${idx === activeIdx ? 'text-4xl font-bold text-gray-900' : ''}`}
|
||||
>
|
||||
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
|
||||
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
|
||||
{nav.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
|
||||
<h2 id="card1-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용이란
|
||||
</h2>
|
||||
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card1-desc" className="text-[#269DDA] text-4xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
|
||||
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
|
||||
하천 유지 등의 용도로 이용하는 것을 말합니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_01_1.png"
|
||||
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8 rounded"
|
||||
/>
|
||||
<p className="text-center">
|
||||
환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br />
|
||||
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드2 */}
|
||||
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
|
||||
<h2 id="card2-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 필요성
|
||||
</h2>
|
||||
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card2-desc" className="text-[#EE7B62] text-4xl font-bold mb-8">물 재이용, 왜 필요한가요?</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
|
||||
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_1.png"
|
||||
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8 w-[80%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
|
||||
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_2.png"
|
||||
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8"
|
||||
/>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
|
||||
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_3.png"
|
||||
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[93%] mx-auto"
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드3 */}
|
||||
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
|
||||
<h2 id="card3-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
빗물 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card3-desc" className="text-[#269DDA] text-4xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_03_1.png"
|
||||
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_03_2.png"
|
||||
alt="빗물이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold leading-relaxed">
|
||||
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
|
||||
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
|
||||
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드4 */}
|
||||
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
|
||||
<h2 id="card4-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
중수 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card4-desc" className="text-[#9771B0] text-4xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_04_1.png"
|
||||
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_04_2.png"
|
||||
alt="중수재이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold leading-relaxed">
|
||||
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
|
||||
- 의무 재이용량: 물 사용량의 10% 이상<br />
|
||||
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드5 */}
|
||||
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
|
||||
<h2 id="card5-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
하수 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card5-desc" className="text-[#00A5B1] text-4xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_05_1.png"
|
||||
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_05_2.png"
|
||||
alt="하수처리 재이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold leading-relaxed">
|
||||
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
|
||||
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
|
||||
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드6 */}
|
||||
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
|
||||
<h2 id="card6-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 목표
|
||||
</h2>
|
||||
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card6-desc" className="text-[#269DDA] text-4xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
|
||||
<p className="text-2xl font-bold mb-6 leading-relaxed">
|
||||
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_06_1.png"
|
||||
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[80%] mx-auto mb-6"
|
||||
/>
|
||||
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
|
||||
<div className="flex-1">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_01.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="text-center font-semibold text-nowrap mb-2">도시 물 순환이용<br />체계로의 전환</h3>
|
||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
||||
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
|
||||
<li>- 물 재이용시설 관리체계 강화</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_02.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="text-center font-semibold text-nowrap mb-2">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
|
||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
||||
<li>- 유역기반 통합물관리 연계 강화</li>
|
||||
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
|
||||
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_03.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="text-center font-semibold text-nowrap mb-2">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
|
||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
||||
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
|
||||
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/img/wrAbout/img_06_2.png"
|
||||
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto"
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function WrExamplePage() {
|
||||
return (
|
||||
<div className="bg-[#F1F6F9]">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span>
|
||||
물 재이용사례
|
||||
</h2>
|
||||
</div>
|
||||
<div className="bg-[#F1F6F9] text-gray-800">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
{/* 카드 1 */}
|
||||
<div className="card-wrap mb-24" id="card1">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 1
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기권역 반도체 사업장 물 재이용 사업<br />
|
||||
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 배경 및 목적 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br />
|
||||
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
|
||||
</p>
|
||||
</section>
|
||||
{/* 단계별 시각 정보 */}
|
||||
<section className="bg-white px-4 mt-8">
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_01_1.png"
|
||||
alt="사업단계 설명"
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[100%] mx-auto mb-8"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드2 */}
|
||||
<div className="card-wrap mb-24" id="card2">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 2
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기도 파주시 하수재이용<br />
|
||||
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br />
|
||||
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 방식 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||
공급량 : 4만m³/일<br />
|
||||
관로 : 13.6km<br />
|
||||
사업비 : 651억원
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-0 items-start">
|
||||
{/* 좌측 이미지 */}
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_02_1.png"
|
||||
alt="적용 성공사례 지도"
|
||||
width={1000}
|
||||
height={1200}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 우측 표 */}
|
||||
<div className="space-y-3">
|
||||
{/* 하수처리수 공급 */}
|
||||
<div>
|
||||
<table className="min-w-full h-[500px]text-center border border-black">
|
||||
<colgroup>
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "40%" }} />
|
||||
<col style={{ width: "40%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={3} className="bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
|
||||
</tr>
|
||||
<tr className="text-xl h-[30px]">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">위치</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 첫 화살표 (자연유하 / 관로이송) */}
|
||||
<div className="flex h-[30px] justify-between items-center my-3 px-4">
|
||||
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6">
|
||||
<span>자연유하: 161m</span>
|
||||
<Image
|
||||
src="/img/wrExample/img_arrow_down_1.png"
|
||||
alt="오른쪽 화살표"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold">
|
||||
<Image
|
||||
src="/img/wrExample/img_arrow_down_1.png"
|
||||
alt="왼쪽 화살표"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
<span>관로이송: 3.2km</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 물재이용 처리시설 */}
|
||||
<div>
|
||||
<table className="min-w-full text-center border border-black">
|
||||
<colgroup>
|
||||
<col style={{ width: "50%" }} />
|
||||
<col style={{ width: "50%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={2} className="bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 두 번째 화살표 (공급관로) */}
|
||||
<div className="flex flex-col items-center my-3">
|
||||
<div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold">
|
||||
<span>공급관로 L=10.4km</span>
|
||||
<Image
|
||||
src="/img/wrExample/img_arrow_down_2.png"
|
||||
alt="아래 화살표"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수요처 공급 */}
|
||||
<div>
|
||||
<table className="min-w-full text-center border border-black">
|
||||
<colgroup>
|
||||
<col style={{ width: "50%" }} />
|
||||
<col style={{ width: "50%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={2} className="bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드3 */}
|
||||
<div className="card-wrap mb-24" id="card3">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 3
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
충남 보령 하수재이용<br />
|
||||
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 방식 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||
공급량 : 1만m3/일<br />
|
||||
관로 : 8.7km<br />
|
||||
사업비 : 315억원(국비 216억원, 재원협의(안))
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕
|
||||
으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
|
||||
</p>
|
||||
<div className="p-2">
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_03_1.png"
|
||||
alt="국가전력 기반시설 설명"
|
||||
width={700}
|
||||
height={350}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드4 */}
|
||||
<div className="card-wrap mb-24" id="card4">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 4
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기도 수원시 빗물이용과 중수도
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br />
|
||||
※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br />
|
||||
다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br />
|
||||
- (중수도) 수원시청, 장안구청, 월드컵경기장에 빗물 연계 중수도를 화장실·조경용수 등으로 사용하여 수도요금 절감(연 7백~30백만원)
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-start p-2">
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_1.png"
|
||||
alt="월드컵경기장 인근도로 노면 살수 이미지"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_2.png"
|
||||
alt="주유기 모양의 빗물 공급기 이미지"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_3.png"
|
||||
alt="빗물주유기에서 살수용수 공급 이미지"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_4.png"
|
||||
alt="빗물 공급기 및 빗물분사장치 위치"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function WrOverviewPage() {
|
||||
return (
|
||||
<div className="bg-[#F1F6F9]">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
{/* 카드 1 - 물재이용 사업 추진 절차 */}
|
||||
<div className="card-wrap mb-24" id="card1">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 사업 추진 절차
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
<table className="table-fixed w-full text-center text-sm">
|
||||
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
|
||||
<colgroup>
|
||||
<col style={{ width: "28%" }} />
|
||||
<col style={{ width: "2%" }} />
|
||||
<col style={{ width: "35%" }} />
|
||||
<col style={{ width: "5%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{/* 1단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">타당성 조사</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 2단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">매칭시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획(안) 수립</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 3단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">하수도정보시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 신청</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 4단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 확정 통보</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 5단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획 수립(실시설계)</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 6단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">재원협의<br />(지방환경관서장)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 7단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">설치승인<br />(시도지사)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 8단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">공사시행</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 9단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">준공(국고정산)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드 2 - 물재이용 매칭 시스템 */}
|
||||
<div className="card-wrap mb-24" id="card2">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 매칭 시스템이란?
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 개요 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
|
||||
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
|
||||
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 구성 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
|
||||
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
|
||||
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
|
||||
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
|
||||
<li>운영주체: 한국환경공단</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 절차 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
|
||||
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 기대효과 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
|
||||
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
|
||||
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
|
||||
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
|
||||
</ul>
|
||||
<div className="text-center my-6">
|
||||
<Image
|
||||
src="/img/wrOverview/img_ov_01_1.png"
|
||||
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
|
||||
width={650}
|
||||
height={500}
|
||||
className="h-[500px] mx-auto mb-4 rounded"
|
||||
/>
|
||||
<p className="text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface RegionData {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
values: number[];
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
region: string;
|
||||
sewage: number;
|
||||
rainwater: number;
|
||||
reclaimed: number;
|
||||
thermal: number;
|
||||
}
|
||||
|
||||
|
||||
const WrStatusPage: React.FC = () => {
|
||||
const [selectedRegion, setSelectedRegion] = useState<string | null>(null);
|
||||
const [hoveredRegion, setHoveredRegion] = useState<string | null>(null);
|
||||
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
const svgContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [chartData] = useState<any[]>([
|
||||
{
|
||||
label : '서울',
|
||||
하수 : 4,
|
||||
빗물 : 816,
|
||||
중수도 : 371,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '부산',
|
||||
하수 : 13,
|
||||
빗물 : 68,
|
||||
중수도 : 20,
|
||||
온배수 : 1
|
||||
},
|
||||
{
|
||||
label : '대구',
|
||||
하수 : 9,
|
||||
빗물 : 294,
|
||||
중수도 : 14,
|
||||
온배수 : 2
|
||||
},
|
||||
{
|
||||
label : '인천',
|
||||
하수 : 17,
|
||||
빗물 : 165,
|
||||
중수도 : 35,
|
||||
온배수 : 15
|
||||
},
|
||||
{
|
||||
label : '광주',
|
||||
하수 : 3,
|
||||
빗물 : 74,
|
||||
중수도 : 15,
|
||||
온배수 : 15
|
||||
},
|
||||
{
|
||||
label : '대전',
|
||||
하수 : 2,
|
||||
빗물 : 52,
|
||||
중수도 : 11,
|
||||
온배수 : 20
|
||||
},
|
||||
{
|
||||
label : '울산',
|
||||
하수 : 8,
|
||||
빗물 : 111,
|
||||
중수도 : 19,
|
||||
온배수 : 30
|
||||
},
|
||||
{
|
||||
label : '세종',
|
||||
하수 : 9,
|
||||
빗물 : 65,
|
||||
중수도 : 0,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '경기',
|
||||
하수 : 176,
|
||||
빗물 : 1009,
|
||||
중수도 : 252,
|
||||
온배수 : 40
|
||||
},
|
||||
{
|
||||
label : '강원',
|
||||
하수 : 69,
|
||||
빗물 : 69,
|
||||
중수도 : 22,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '충북',
|
||||
하수 : 38,
|
||||
빗물 : 17,
|
||||
중수도 : 91,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '충남',
|
||||
하수 : 67,
|
||||
빗물 : 54,
|
||||
중수도 : 188,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '전북',
|
||||
하수 : 55,
|
||||
빗물 : 33,
|
||||
중수도 : 220,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '전남',
|
||||
하수 : 83,
|
||||
빗물 : 16,
|
||||
중수도 : 75,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '경북',
|
||||
하수 : 81,
|
||||
빗물 : 37,
|
||||
중수도 : 70,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '경남',
|
||||
하수 : 75,
|
||||
빗물 : 27,
|
||||
중수도 : 140,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '제주',
|
||||
하수 : 8,
|
||||
빗물 : 47,
|
||||
중수도 : 64,
|
||||
온배수 : 0
|
||||
}
|
||||
]);
|
||||
|
||||
const chartConfig = chartData.reduce((config, element) => {
|
||||
config[element.label] = {
|
||||
label: element.label,
|
||||
color : "hsl(var(--chart-1))"
|
||||
};
|
||||
return config;
|
||||
}, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig;
|
||||
|
||||
const chartOption = {
|
||||
chartForm : CHART_FORM.BAR,
|
||||
chartConfig : chartConfig,
|
||||
chartData : chartData,
|
||||
chartTitle : '',
|
||||
chartClassName : "h-[300px] w-full",
|
||||
graphSize : 40,
|
||||
yFieldText : '개소수(건)',
|
||||
showLegend : true
|
||||
}
|
||||
|
||||
const colorLegend = [
|
||||
{ label: '하수', color: '#FF6B6B', key: 'sewage' },
|
||||
{ label: '빗물', color: '#4ECDC4', key: 'rainwater' },
|
||||
{ label: '중수도', color: '#45B7D1', key: 'reclaimed' },
|
||||
{ label: '온배수', color: '#FED16A', key: 'thermal' },
|
||||
];
|
||||
|
||||
const regionIdMap: Record<string, string> = {
|
||||
서울: '서울특별시',
|
||||
부산: '부산광역시',
|
||||
대구: '대구광역시',
|
||||
인천: '인천광역시',
|
||||
광주: '광주광역시',
|
||||
대전: '대전광역시',
|
||||
울산: '울산광역시',
|
||||
세종: '세종특별자치시',
|
||||
경기: '경기도',
|
||||
강원: '강원특별자치도',
|
||||
충북: '충청북도',
|
||||
충남: '충청남도',
|
||||
전북: '전라북도',
|
||||
전남: '전라남도',
|
||||
경북: '경상북도',
|
||||
경남: '경상남도',
|
||||
제주: '제주특별자치도'
|
||||
};
|
||||
|
||||
const tableData: TableData[] = [
|
||||
{ region: '서울특별시', sewage: 4, rainwater: 816, reclaimed: 371, thermal: 0 },
|
||||
{ region: '부산광역시', sewage: 13, rainwater: 68, reclaimed: 20, thermal: 1 },
|
||||
{ region: '대구광역시', sewage: 9, rainwater: 294, reclaimed: 14, thermal: 2 },
|
||||
{ region: '인천광역시', sewage: 17, rainwater: 165, reclaimed: 35, thermal: 15 },
|
||||
{ region: '광주광역시', sewage: 3, rainwater: 74, reclaimed: 15, thermal: 15 },
|
||||
{ region: '대전광역시', sewage: 2, rainwater: 52, reclaimed: 11, thermal: 20 },
|
||||
{ region: '울산광역시', sewage: 8, rainwater: 111, reclaimed: 19, thermal: 30 },
|
||||
{ region: '세종특별자치시', sewage: 9, rainwater: 65, reclaimed: 0, thermal: 0 },
|
||||
{ region: '경기도', sewage: 176, rainwater: 1009, reclaimed: 252, thermal: 40 },
|
||||
{ region: '강원특별자치도', sewage: 69, rainwater: 69, reclaimed: 22, thermal: 0 },
|
||||
{ region: '충청북도', sewage: 38, rainwater: 17, reclaimed: 91, thermal: 0 },
|
||||
{ region: '충청남도', sewage: 67, rainwater: 54, reclaimed: 188, thermal: 0 },
|
||||
{ region: '전라북도', sewage: 55, rainwater: 33, reclaimed: 220, thermal: 0 },
|
||||
{ region: '전라남도', sewage: 83, rainwater: 16, reclaimed: 75, thermal: 0 },
|
||||
{ region: '경상북도', sewage: 81, rainwater: 37, reclaimed: 70, thermal: 0 },
|
||||
{ region: '경상남도', sewage: 75, rainwater: 27, reclaimed: 140, thermal: 0 },
|
||||
{ region: '제주특별자치도', sewage: 8, rainwater: 47, reclaimed: 64, thermal: 0 },
|
||||
|
||||
];
|
||||
|
||||
const regionData: RegionData[] = [
|
||||
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '강원', x: 330, y: 120, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '충북', x: 260, y: 280, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '충남', x: 100, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '세종', x: 180, y: 305, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '대전', x: 195, y: 360, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '전북', x: 170, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '전남', x: 160, y: 600, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '광주', x: 120, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경북', x: 400, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경남', x: 310, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '대구', x: 350, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
];
|
||||
|
||||
const handleMarkerClick = (regionId: string) => {
|
||||
if (selectedRegion === regionId) {
|
||||
setSelectedRegion(null);
|
||||
} else {
|
||||
setSelectedRegion(regionId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
|
||||
const radius = 26;
|
||||
const innerRadius = 18;
|
||||
// const regionId = regionIdMap[name];
|
||||
const regionId = regionIdMap[name];
|
||||
if (!regionId) {
|
||||
console.warn(`Invalid region name: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
g.setAttribute('transform', `translate(${x}, ${y})`);
|
||||
g.setAttribute('id', `marker-${name}`);
|
||||
g.style.cursor = 'pointer';
|
||||
g.setAttribute('role', 'button');
|
||||
g.setAttribute('tabindex', '0');
|
||||
g.setAttribute('aria-label', `${name} 지역 선택`);
|
||||
|
||||
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
bg.setAttribute('r', radius.toString());
|
||||
bg.setAttribute('fill', 'white');
|
||||
bg.setAttribute('pointer-events', 'all');
|
||||
g.appendChild(bg);
|
||||
|
||||
let startAngle = 0;
|
||||
values.forEach((value, idx) => {
|
||||
const endAngle = startAngle + value * 2 * Math.PI;
|
||||
const x1 = radius * Math.sin(startAngle);
|
||||
const y1 = -radius * Math.cos(startAngle);
|
||||
const x2 = radius * Math.sin(endAngle);
|
||||
const y2 = -radius * Math.cos(endAngle);
|
||||
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
|
||||
const d = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} L 0 0 Z`;
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', d);
|
||||
path.setAttribute('fill', colors[idx % colors.length] as string);
|
||||
g.appendChild(path);
|
||||
|
||||
startAngle = endAngle;
|
||||
});
|
||||
|
||||
const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
center.setAttribute('r', innerRadius.toString());
|
||||
center.setAttribute('fill', 'white');
|
||||
g.appendChild(center);
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', '0');
|
||||
text.setAttribute('y', '2');
|
||||
text.setAttribute('text-anchor', 'middle');
|
||||
text.setAttribute('font-size', '13');
|
||||
text.setAttribute('font-weight', 'bold');
|
||||
text.setAttribute('dominant-baseline', 'middle');
|
||||
text.setAttribute('fill', 'black');
|
||||
text.textContent = name || '';
|
||||
g.appendChild(text);
|
||||
|
||||
g.addEventListener('mouseenter', (e) => {
|
||||
setHoveredRegion(regionId);
|
||||
setTooltipPosition({ x: e.clientX, y: e.clientY });
|
||||
});
|
||||
|
||||
g.addEventListener('mouseleave', () => {
|
||||
setHoveredRegion(null);
|
||||
setTooltipPosition(null);
|
||||
});
|
||||
|
||||
g.addEventListener('click', () => {
|
||||
handleMarkerClick(regionId);
|
||||
});
|
||||
|
||||
g.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
|
||||
const matrix = g.getScreenCTM();
|
||||
if (!matrix) return; // 또는 console.warn('matrix is null'); return;
|
||||
|
||||
const point = svg.createSVGPoint();
|
||||
point.x = 0;
|
||||
point.y = 0;
|
||||
const transformed = point.matrixTransform(matrix);
|
||||
|
||||
setTooltipPosition({ x: transformed.x, y: transformed.y });
|
||||
setHoveredRegion(regionId);
|
||||
handleMarkerClick(regionId);
|
||||
}
|
||||
});
|
||||
|
||||
svg.appendChild(g);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadSVG = async () => {
|
||||
try {
|
||||
const response = await fetch('/img/wrStatus/southKoreaMap.svg');
|
||||
const svgText = await response.text();
|
||||
|
||||
if (svgContainerRef.current) {
|
||||
svgContainerRef.current.innerHTML = svgText;
|
||||
|
||||
const svg = svgContainerRef.current.querySelector('svg') as SVGSVGElement;
|
||||
if (svg) {
|
||||
svg.style.width = '58%';
|
||||
svg.style.height = 'auto';
|
||||
svg.style.objectFit = 'contain';
|
||||
|
||||
// Set default fills
|
||||
const paths = svg.querySelectorAll('path');
|
||||
paths.forEach(path => {
|
||||
if (!path.getAttribute('fill') || path.getAttribute('fill') === 'none') {
|
||||
path.setAttribute('fill', '#ffffff');
|
||||
}
|
||||
});
|
||||
|
||||
// Create markers
|
||||
regionData.forEach(region => {
|
||||
createDonutMarker(svg, region.x, region.y, region.name, region.values);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SVG 로딩 실패:', error);
|
||||
if (svgContainerRef.current) {
|
||||
svgContainerRef.current.innerHTML = "<p className='text-red-500'>SVG 로딩 실패</p>";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSVG();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const svg = svgContainerRef.current?.querySelector('svg');
|
||||
if (svg) {
|
||||
const paths = svg.querySelectorAll('path');
|
||||
paths.forEach(path => {
|
||||
const pathId = path.getAttribute('id');
|
||||
if (pathId) {
|
||||
if (selectedRegion === pathId) {
|
||||
path.setAttribute('fill', '#2563eb');
|
||||
} else {
|
||||
const original = '#ffffff';
|
||||
path.setAttribute('fill', original);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [selectedRegion]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#F4F7FB] text-gray-800">
|
||||
{/* 우측 콘텐츠 영역 */}
|
||||
<main className="flex-1 p-6 overflow-auto">
|
||||
{/* 상단 제목 */}
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">물재이용 현황</h2>
|
||||
<div className="text-sm text-gray-500">관리자 ▼</div>
|
||||
</div>
|
||||
|
||||
{/* 콘텐츠 본문 */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* 지도 카드 */}
|
||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]">
|
||||
{/* 타이틀 영역 */}
|
||||
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
||||
<h3 className="text-[12px] font-semibold">재이용시설개소수(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 개소수 조회</p>
|
||||
</div>
|
||||
|
||||
{/* 본문 콘텐츠 */}
|
||||
<div className="p-4">
|
||||
<div className="bg-[#E2EDF3] p-4 rounded shadow relative">
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
ref={svgContainerRef}
|
||||
className="w-full max-w-5xl max-h-[680px] overflow-y-auto block mx-auto"
|
||||
aria-label="대한민국 지역별 재이용시설 분포도"
|
||||
role="region"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{typeof window !== 'undefined' && hoveredRegion && tooltipPosition && (
|
||||
<div
|
||||
className="fixed bg-white text-[12px] text-gray-700 border border-gray-300 shadow-lg rounded px-3 py-2 z-50"
|
||||
style={{
|
||||
top: tooltipPosition.y + 10,
|
||||
left: tooltipPosition.x + 10,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div className="font-semibold mb-1">{hoveredRegion}</div>
|
||||
<div className="space-y-1">
|
||||
{colorLegend.map(({ label, color, key }) => (
|
||||
<div key={key} className="flex items-center space-x-2">
|
||||
<span className="text-[12px] inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: color }}></span>
|
||||
<span className="text-[12px]">
|
||||
{label}:{' '}
|
||||
{tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 범례 */}
|
||||
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span>
|
||||
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717건)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span>
|
||||
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730건)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span>
|
||||
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074건)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span>
|
||||
<span className="text-[12px] text-gray-900">온배수시설 (총 201건)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측 콘텐츠 (표 + 차트) */}
|
||||
<div className="col-span-1 flex flex-col space-y-4">
|
||||
{/* 재이용시설현황 표 */}
|
||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
|
||||
{/* 타이틀 + 버튼 영역 */}
|
||||
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
|
||||
<div>
|
||||
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 비교</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="rounded border border-[#DBE2E6] shadow overflow-auto max-h-[292px] p-4">
|
||||
<table className="min-w-full text-sm text-center">
|
||||
<caption className="sr-only text-[12px]">행정구역별 하수, 빗물, 중수도, 온배수 재이용시설 수</caption>
|
||||
<thead className="bg-gray-50 text-xs text-gray-600 uppercase border-b">
|
||||
<tr>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">하수</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">빗물</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">중수도</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">온배수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{tableData.map((row, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50">
|
||||
<td className="text-[12px] px-4 py-2 text-center">
|
||||
{index+1}
|
||||
</td>
|
||||
<td className="text-[12px]">{row.region}</td>
|
||||
<td className="text-[12px]">{row.sewage}</td>
|
||||
<td className="text-[12px]">{row.rainwater}</td>
|
||||
<td className="text-[12px]">{row.reclaimed}</td>
|
||||
<td className="text-[12px]">{row.thermal}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 재이용통계 차트 */}
|
||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
|
||||
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
||||
<h3 className="text-[12px] font-semibold">재이용통계(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">항목별 통계 보기</p>
|
||||
</div>
|
||||
<div className="p-4 bg-[#f9fbfd]">
|
||||
<div className="h-[305px] text-gray-500">
|
||||
<MainChart
|
||||
chartForm={chartOption.chartForm}
|
||||
chartConfig={chartOption.chartConfig}
|
||||
chartData={chartOption.chartData}
|
||||
chartTitle={chartOption.chartTitle}
|
||||
chartClassName={chartOption.chartClassName}
|
||||
graphSize={chartOption.graphSize}
|
||||
yFieldText={chartOption.yFieldText}
|
||||
showLegend={chartOption.showLegend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrStatusPage;
|
||||
@@ -1,419 +0,0 @@
|
||||
/**====================================
|
||||
* 보편적으로 쓰는 store 공간
|
||||
====================================**/
|
||||
|
||||
import { fetchType, getFetch } from '@/components/ui/api';
|
||||
|
||||
/**
|
||||
* STORE - TYPE
|
||||
*/
|
||||
|
||||
// selectBox
|
||||
export type SelectDataType = {
|
||||
text: string;
|
||||
val: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
// grid header
|
||||
export type TableUpperProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
upName?: string;
|
||||
upSequnce?: number;
|
||||
align? : string;
|
||||
row? : number;
|
||||
rowSpan?: number;
|
||||
colSpan?: number;
|
||||
}
|
||||
|
||||
// 시도 데이터
|
||||
export type sidoDataType = {
|
||||
adstDstrCd: number,
|
||||
sido: string,
|
||||
}
|
||||
|
||||
// 시군구 데이터
|
||||
export type sigunDataType = {
|
||||
adstDstrCd: number,
|
||||
gugun: string
|
||||
}
|
||||
|
||||
// 권역 데이터
|
||||
export type regionDataType = {
|
||||
rvbsnCd: string,
|
||||
rvbsn: string,
|
||||
}
|
||||
|
||||
// 단위유역 데이터
|
||||
export type unitWaterShedDataType = {
|
||||
rvbsnCd: string,
|
||||
lgbsn: string,
|
||||
}
|
||||
|
||||
// 중권역 데이터
|
||||
export type middleRegionDataType = {
|
||||
rvbsnCd: string,
|
||||
mdmbsn: string,
|
||||
}
|
||||
|
||||
// 환경청 데이터
|
||||
export type envPartDataType = {
|
||||
lclgvrnmntCd: string,
|
||||
lclgvrnmntNm: string,
|
||||
adstDliStrCd: string,
|
||||
adstDstrCd: string,
|
||||
}
|
||||
|
||||
export type envPartSelectDataType = SelectDataType & {
|
||||
adstDliStrCd: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* STORE - DATA
|
||||
*/
|
||||
|
||||
// 시도 데이터
|
||||
let selectSidoData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 시군구 데이터
|
||||
let selectSigunData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 권역 데이터
|
||||
const selectRegionData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 단위유역 데이터
|
||||
const selectUnitWaterShedData: SelectDataType[] = [
|
||||
];
|
||||
|
||||
// 환경청 데이터
|
||||
let selectEnvPartData: envPartSelectDataType[] = [
|
||||
{ text: "전체", val: '0', adstDliStrCd: '' }
|
||||
];
|
||||
|
||||
// 환경청 내 시도 데이터
|
||||
const selectEnvPartSidoData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 환경청 내 시군구 데이터
|
||||
const selectEnvPartSigunData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 구분 데이터
|
||||
// const selectPartData:SelectDataType[] = [
|
||||
// { text: "행정별", val: '0' },
|
||||
// { text: "유역별", val: '01' },
|
||||
// { text: "환경청별", val: '02' },
|
||||
// ];
|
||||
|
||||
// 상향 하향 데이터
|
||||
const selectUpdownData:SelectDataType[] = [
|
||||
{ text: "전체", val: '0' },
|
||||
{ text: "500이하", val: '499' },
|
||||
{ text: "500이상", val: '500' },
|
||||
];
|
||||
|
||||
// 운영 상태 데이터
|
||||
const selectOperationData:SelectDataType[] = [
|
||||
{ text: "전체", val: '0' },
|
||||
{ text: "가동중", val: '1' },
|
||||
{ text: "재가동", val: '2' },
|
||||
{ text: "가동중지", val: '3' },
|
||||
{ text: "시설폐쇄", val: '4' },
|
||||
];
|
||||
|
||||
// 시설 구분 데이터
|
||||
const selectFacilityPartData:SelectDataType[] = [
|
||||
{ text: "전체", val: '0' },
|
||||
{ text: "공공하수처리시설", val: 'SW' },
|
||||
{ text: "오수처리장", val: 'SS' },
|
||||
{ text: "개인하수처리시설", val: 'PB' },
|
||||
{ text: "분뇨처리장", val: 'NS' },
|
||||
{ text: "폐수처리장", val: 'LW' },
|
||||
{ text: "침출수처리장", val: 'LC' },
|
||||
{ text: "기타기초시설", val: 'ET' },
|
||||
];
|
||||
|
||||
// 년도 데이터
|
||||
const getYearData = () => {
|
||||
const year = new Date().getFullYear();
|
||||
const year1 = year - 1;
|
||||
const year2 = year - 2;
|
||||
const year3 = year - 3;
|
||||
const year4 = year - 4;
|
||||
const year5 = year - 5;
|
||||
return [
|
||||
{ text: year1 + "년", val: year1.toString() },
|
||||
{ text: year2 + "년", val: year2.toString() },
|
||||
{ text: year3 + "년", val: year3.toString() },
|
||||
{ text: year4 + "년", val: year4.toString() },
|
||||
{ text: year5 + "년", val: year5.toString() },
|
||||
]
|
||||
}
|
||||
|
||||
const selectSearchYear:SelectDataType[] = getYearData();
|
||||
|
||||
// 유량 기간 구분
|
||||
const selectFlowTerm : SelectDataType[] = [
|
||||
{ text: "연간", val: '0' },
|
||||
{ text: "하절기", val: '01' },
|
||||
{ text: "동절기", val: '02' },
|
||||
{ text: "청천시", val: '03' },
|
||||
{ text: "강우시", val: '04' },
|
||||
];
|
||||
|
||||
// 유량 기간 구분
|
||||
const selectFlowPanel : SelectDataType[] = [
|
||||
{ text: "전체", val: '0'},
|
||||
{ text: "3mm미만", val: '1'},
|
||||
{ text: "3~10mm미만", val: '2'},
|
||||
{ text: "10~20mm미만", val: '3'},
|
||||
{ text: "20~30mm미만", val: '4'},
|
||||
{ text: "30~40mm미만", val: '5'},
|
||||
{ text: "40~50mm미만", val: '6'},
|
||||
{ text: "50~60mm미만", val: '7'},
|
||||
{ text: "60mm이상", val: '8'},
|
||||
];
|
||||
|
||||
// 중권역 데이터
|
||||
const selectMiddleRegionData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
/**
|
||||
* STORE - API
|
||||
*/
|
||||
|
||||
// 시도 API 호출 함수 추가
|
||||
const fetchSidoData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/city');
|
||||
const data = res.data;
|
||||
selectSidoData = [
|
||||
{ text: "전체", val: '0'},
|
||||
...data.map((item: sidoDataType) => ({
|
||||
text: item.sido || '',
|
||||
val: String(item.adstDstrCd) || ''
|
||||
}))
|
||||
];
|
||||
return selectSidoData;
|
||||
} catch (error) {
|
||||
console.error('시도 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectSidoData;
|
||||
}
|
||||
};
|
||||
|
||||
// 시군구 API 호출 함수 추가
|
||||
const fetchSigunData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/sgg');
|
||||
const data = res.data;
|
||||
selectSigunData = [
|
||||
{ text: "전체", val: '0' },
|
||||
...data
|
||||
.map((item: sigunDataType) => ({
|
||||
text: item.gugun || '',
|
||||
val: String(item.adstDstrCd) || ''
|
||||
}))
|
||||
];
|
||||
return selectSigunData;
|
||||
} catch (error) {
|
||||
console.error('시군구 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectSigunData;
|
||||
}
|
||||
};
|
||||
|
||||
// 권역 API 호출 함수 추가
|
||||
const fetchRegionData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/sarea');
|
||||
const data = res.data;
|
||||
return [
|
||||
{ text: "전체", val: '0'},
|
||||
...data.map((item: regionDataType) => ({
|
||||
text: item.rvbsn || '',
|
||||
val: String(item.rvbsnCd) || ''
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('권역 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectRegionData;
|
||||
}
|
||||
};
|
||||
|
||||
// 중권역 API 호출 함수 추가
|
||||
const fetchMiddleRegionData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/midArea');
|
||||
const data = res.data;
|
||||
return [
|
||||
{ text: "전체", val: '0' },
|
||||
...data
|
||||
.map((item: middleRegionDataType) => ({
|
||||
text: item.mdmbsn || '',
|
||||
val: String(item.rvbsnCd) || ''
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('중권역 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectMiddleRegionData;
|
||||
}
|
||||
};
|
||||
|
||||
// 단위유역 API 호출 함수 추가
|
||||
const fetchUnitWaterShedData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/largeArea');
|
||||
const data = res.data;
|
||||
return [
|
||||
{ text: "전체", val: '0' },
|
||||
...data
|
||||
.map((item: unitWaterShedDataType) => ({
|
||||
text: item.lgbsn || '',
|
||||
val: String(item.rvbsnCd) || ''
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('단위유역 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectUnitWaterShedData;
|
||||
}
|
||||
};
|
||||
|
||||
// 환경청 데이터
|
||||
const fetchEnvPartData = async (): Promise<envPartSelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/govArea');
|
||||
const data = res.data;
|
||||
selectEnvPartData = [
|
||||
{ text: "전체", val: '0', adstDliStrCd: '' },
|
||||
...data
|
||||
.map((item: envPartDataType) => ({
|
||||
text: item.lclgvrnmntNm || '',
|
||||
val: String(item.lclgvrnmntCd) || '',
|
||||
adstDliStrCd: item.adstDliStrCd || ''
|
||||
}))
|
||||
];
|
||||
return selectEnvPartData;
|
||||
} catch (error) {
|
||||
console.error('환경청 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectEnvPartData;
|
||||
}
|
||||
};
|
||||
|
||||
/**====================================
|
||||
* Store - expaort
|
||||
====================================*/
|
||||
export type CommonAction = {
|
||||
initializeCommonData: () => Promise<void>;
|
||||
getMenuAuth: (menuCd:number) => Promise<void>;
|
||||
}
|
||||
|
||||
export type CommonType = {
|
||||
Common: {
|
||||
isInit: boolean;
|
||||
selectSidoData: SelectDataType[];
|
||||
selectSigunData: SelectDataType[];
|
||||
selectUpdownData: SelectDataType[];
|
||||
selectOperationData: SelectDataType[];
|
||||
selectFacilityPartData: SelectDataType[];
|
||||
selectSearchYear: SelectDataType[];
|
||||
selectUnitWaterShedData: SelectDataType[];
|
||||
selectRegionData: SelectDataType[];
|
||||
selectMiddleRegionData: SelectDataType[];
|
||||
selectEnvPartSidoData: SelectDataType[];
|
||||
selectEnvPartSigunData: SelectDataType[];
|
||||
selectFlowTerm: SelectDataType[];
|
||||
selectFlowPanel: SelectDataType[];
|
||||
selectEnvPartData: envPartSelectDataType[];
|
||||
btnClass: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const CommonState:CommonType = {
|
||||
Common: {
|
||||
isInit: false,
|
||||
selectSidoData: selectSidoData,
|
||||
selectSigunData: selectSigunData,
|
||||
selectUpdownData: selectUpdownData,
|
||||
selectOperationData: selectOperationData,
|
||||
selectFacilityPartData: selectFacilityPartData,
|
||||
selectSearchYear: selectSearchYear,
|
||||
selectRegionData: selectRegionData,
|
||||
selectUnitWaterShedData: selectUnitWaterShedData,
|
||||
selectMiddleRegionData: selectMiddleRegionData,
|
||||
selectEnvPartSidoData : selectEnvPartSidoData,
|
||||
selectEnvPartSigunData : selectEnvPartSigunData,
|
||||
selectFlowTerm: selectFlowTerm,
|
||||
selectFlowPanel: selectFlowPanel,
|
||||
selectEnvPartData : selectEnvPartData,
|
||||
btnClass: "p-4 m-2 text-white rounded-sm text-lg font-medium"
|
||||
}
|
||||
}
|
||||
|
||||
export const CommonReducer:(set:any)=>CommonAction=(set: any) => {
|
||||
return {
|
||||
getMenuAuth : async (menuCd : number) =>{
|
||||
const response = await getFetch(
|
||||
"local",
|
||||
`/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}`
|
||||
);
|
||||
set((state: CommonType) => {
|
||||
return {
|
||||
...state,
|
||||
auth: {
|
||||
mngAuthrtYn: response[0].mngAuthrtYn,
|
||||
inqAuthrtYn: response[0].mngAuthrtYn,
|
||||
regAuthrtYn: response[0].mngAuthrtYn,
|
||||
mdfcnAuthrtYn: response[0].mngAuthrtYn,
|
||||
delAuthrtYn: response[0].mngAuthrtYn,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
// 시도 변경 이벤트
|
||||
initializeCommonData: async () => {
|
||||
const promiseSet = await Promise.all([fetchSidoData(), fetchSigunData(), fetchRegionData(), fetchUnitWaterShedData(), fetchMiddleRegionData(), fetchEnvPartData()]);
|
||||
const [sidoData, sggData, regionData, unitData, midData, envData] = promiseSet;
|
||||
set((state: CommonType) => ({
|
||||
Common: {
|
||||
...state.Common,
|
||||
selectSidoData: sidoData,
|
||||
selectSigunData: sggData,
|
||||
selectRegionData: regionData,
|
||||
selectMiddleRegionData: midData,
|
||||
selectUnitWaterShedData: unitData,
|
||||
selectEnvPartData: envData
|
||||
}
|
||||
})
|
||||
)},
|
||||
}
|
||||
}
|
||||
|
||||
export type CommonStore = CommonType & CommonAction;
|
||||
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, createContext, useContext, useRef } from 'react'
|
||||
import { useStore } from 'zustand'
|
||||
import { type DmStore, StoreZus } from './store'
|
||||
|
||||
|
||||
export type StoreApi = ReturnType<typeof StoreZus>
|
||||
|
||||
export const StoreContext = createContext<StoreApi | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
export interface StoreProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const StoreProvider = ({
|
||||
children,
|
||||
}: StoreProviderProps) => {
|
||||
const storeRef = useRef<StoreApi | null>(null)
|
||||
if (!storeRef.current) {
|
||||
// Create store instance if not initialized
|
||||
storeRef.current = StoreZus() as StoreApi
|
||||
}
|
||||
return (
|
||||
<StoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</StoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDmStore = <T,>(
|
||||
selector: (store: DmStore) => T,
|
||||
): T => {
|
||||
const storeContext = useContext(StoreContext)
|
||||
if (!storeContext) {
|
||||
throw new Error(`useStore must be used within StoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(storeContext, selector)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
# Zustand store
|
||||
## Zustand 등록방법
|
||||
**-- index.tsx --**
|
||||
### context 등록
|
||||
기본적으로 createContext를 통해 react에 context를 생성해서 react에서 사용할 수 있도록 해야함.
|
||||
``` js
|
||||
//StoreZus는 내부적으로 사용하기 위한 store파일에서 따로 생성함.
|
||||
export type StoreApi = ReturnType<typeof StoreZus>
|
||||
//react에서 사용하기 위한 context 생성.
|
||||
export const StoreContext = createContext<StoreApi | undefined>(
|
||||
undefined,
|
||||
)
|
||||
```
|
||||
|
||||
### component로 작성
|
||||
등록된 context를 component화 시키기 위한 작업
|
||||
```js
|
||||
//외부에서 호출시 <StoreProvider> 형태로 호출.
|
||||
//app이나 home에서 전체적인 사용이 되도록 작성해야함.
|
||||
export const StoreProvider = ({
|
||||
children,
|
||||
}: StoreProviderProps) => {
|
||||
const storeRef = useRef<StoreApi>(null)
|
||||
if (!storeRef.current) {
|
||||
//StoreZus는 store.tsx를 봐야함.
|
||||
storeRef.current = StoreZus()
|
||||
}
|
||||
return (
|
||||
<StoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</StoreContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### store 등록
|
||||
react에서 사용하는 store에 현재 context 등록
|
||||
```js
|
||||
export const useWrStore = <T,>(
|
||||
selector: (store: WrStore) => T,
|
||||
): T => {
|
||||
const storeContext = useContext(StoreContext)
|
||||
if (!storeContext) {
|
||||
throw new Error(`useStore must be used within StoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(storeContext, selector)
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand 사용방법
|
||||
**-- store.tsx, 폴더별 index.tsx --**
|
||||
### 기본 store 생성
|
||||
index.tsx의 context에 사용되는 StoreZus를 추가하여 사용됨.
|
||||
|
||||
외부에서 생성된 state, actions를 사용 가능한 구조
|
||||
```js
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
import { tableActionsExport, tableStateExport, type tableState, type tableType } from './publicReuseFaciltyInsert/index';//외부 파일 위치.
|
||||
export type WrStore = tableType;//typescript이므로 사용되는 스크립트를 type1 & type2 형식으로 넣어야함.
|
||||
//index.tsx에서 사용하는 store 생성.
|
||||
export const StoreZus = (initState: tableState = tableStateExport) => {
|
||||
return createStore<WrStore>()((set) => {
|
||||
return {
|
||||
//아래에서 사용하는 state & actions은 외부 폴더에서 가져다가 사용.
|
||||
...initState,//외부에 있는 기본 초기값 state
|
||||
...tableActionsExport(set)//외부에 있는 actions
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
### 외부 스토어 작성 방법
|
||||
외부 스토어는 export를 어떻게 할건지만 정하면됨.
|
||||
|
||||
사용하는 store.tsx에서 import를 받기만 하면 사용가능.
|
||||
#### 타입을 외부로 빼는 방법
|
||||
```js
|
||||
export type tableState = {
|
||||
isInit: boolean;
|
||||
upHeadList: TableUpperProps[];
|
||||
publicReuseFacilityUper: PublicReuseFacility[];
|
||||
}
|
||||
type tableActions = {
|
||||
decrementList: () => void
|
||||
}
|
||||
|
||||
export type tableType = tableState & tableActions;
|
||||
```
|
||||
#### 초기값 state를 외부로 빼는 방법
|
||||
```js
|
||||
export const tableStateExport:tableState= {
|
||||
isInit:false,
|
||||
upHeadList: upHeadList,
|
||||
publicReuseFacilityUper: publicReuseFacilityUper,
|
||||
}
|
||||
```
|
||||
#### actions를 외부로 빼는 방법
|
||||
```js
|
||||
//tableActionsExport를 import 받아서 실행하여 return 된 값을 사용
|
||||
export const tableActionsExport:(set:any)=>tableActions=
|
||||
//set을 통해 react store에 등록
|
||||
(set: any) => {
|
||||
return {
|
||||
//decrementList Object 형태로 전달
|
||||
decrementList:
|
||||
//set 함수를 실행해서 react store에 등록
|
||||
()=>set(
|
||||
// store에 등록되는 함수 구조를 가지고 있음.
|
||||
(state:tableState) => {
|
||||
//actions 실질적인 로직 작성하는 곳.
|
||||
return (
|
||||
{
|
||||
upHeadList: state.upHeadList,
|
||||
publicReuseFacilityUper:
|
||||
state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1)
|
||||
})}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,526 +0,0 @@
|
||||
import { fetchType, getFetch, postFetch } from "@/components/ui/api";
|
||||
import type { SelectDataType } from "@common/business_components/ui";
|
||||
import type { TableUpperProps } from "../common";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
/**====================================
|
||||
* Store - 타입
|
||||
=====================================*/
|
||||
// 수요처 정보
|
||||
export type DemandCompType = {
|
||||
index : number | '';
|
||||
dmdInfoId : string | '';
|
||||
dmdInfoFk : string | '';
|
||||
bzmnNo : string | '';
|
||||
dmdCoNm : string | '';
|
||||
daddr : string | '';
|
||||
telno : string | '';
|
||||
rgtr : string | '';
|
||||
useUsgCd : string | '';
|
||||
ctpvCd : string | '';
|
||||
sggCd : string | '';
|
||||
dmdAplyPswd : string | '';
|
||||
wntUntprc : number | '';
|
||||
dmndTmng : Date | '';
|
||||
dmndQty : number | '';
|
||||
grssClfg : number | '';
|
||||
tn : number | '';
|
||||
combResdch : number | '';
|
||||
tph : number | '';
|
||||
trbt : number | '';
|
||||
ph : number | '';
|
||||
blgcOxygDmndQnt : number | '';
|
||||
clIonQnt : number | '';
|
||||
smll : number | '';
|
||||
cdtnv : number | '';
|
||||
crmty : number | '';
|
||||
rmrk : string | '';
|
||||
reqDt : string | '';
|
||||
dmdInfoSn : number | '';
|
||||
status : string | '';
|
||||
}
|
||||
|
||||
export type DemandMatchings = {
|
||||
splyFcltFk : string | '';
|
||||
splyFcltFlg : string | '';
|
||||
pbnstRuseFcltBizFk : number | '';
|
||||
splyBgngDay : string | '';
|
||||
splyEndDay : string | '';
|
||||
splyCpct : number | '';
|
||||
splyFmYn : string | '';
|
||||
useUsgCd : string | '';
|
||||
dmdInfoSn : number | '';
|
||||
splyCndtList : string | '';
|
||||
mtchnCmptnYmd : string | '';
|
||||
rvwCmptnYmd : string | '';
|
||||
regDt : string | '';
|
||||
rgtr : string | '';
|
||||
mdfcnDt : string | '';
|
||||
mdfr : string | '';
|
||||
}
|
||||
|
||||
// 유사사례
|
||||
export type SimilarCaseType = {
|
||||
ruseBizNm : string | '';
|
||||
bizBgngDate : string | '';
|
||||
bizEndDate : string | '';
|
||||
splyFcltKnd : string | '';
|
||||
splyFcltNm : string | '';
|
||||
splyFcltCpct : string | '';
|
||||
dmdFcltNm : string | '';
|
||||
dmdFcltAddr : string | '';
|
||||
ruseFcltCpct : string | '';
|
||||
ruseFcltPplLen : string | '';
|
||||
ruseFcltUsg : string | '';
|
||||
ruseFcltSplyCpct : string | '';
|
||||
ruseFcltPrcsMsrc : string | '';
|
||||
grnteWqUsg : string | '';
|
||||
grnteWqGrssClfg : string | '';
|
||||
grnteWqCombResdch : string | '';
|
||||
grnteWqTrbt : string | '';
|
||||
grnteWqBod : string | '';
|
||||
grnteWqSmll : string | '';
|
||||
grnteWqCrmty : string | '';
|
||||
grnteWqTn : string | '';
|
||||
grnteWqTp : string | '';
|
||||
grnteWqPh : string | '';
|
||||
grnteWqClIonQnt : string | '';
|
||||
grnteWqCndctStat : string | '';
|
||||
grnteWqRmrk : string | '';
|
||||
reqBizReuseFcltCst : string | '';
|
||||
reqBizPplCst : string | '';
|
||||
reqBizArchCst : string | '';
|
||||
reqBizMchnCst : string | '';
|
||||
reqBizMtrngCst : string | '';
|
||||
reqBizGisCst : string | '';
|
||||
reqBizCctvCst : string | '';
|
||||
reqBizCmcnCst : string | '';
|
||||
reqBizWstCst : string | '';
|
||||
reqBizDesignCst : string | '';
|
||||
reqBizSpvsCst : string | '';
|
||||
reqBizSbsdCst : string | '';
|
||||
reqBizEtc : string | '';
|
||||
reqBizGrssCst : string | '';
|
||||
prcmPlanNlep : string | '';
|
||||
prcmPlanLcep : string | '';
|
||||
prcmPlanBrdnCst : string | '';
|
||||
prcmPlanEcqrRt : string | '';
|
||||
prcmPlanGrssCst : string | '';
|
||||
clclnNlep : string | '';
|
||||
clclnLcep : string | '';
|
||||
clclnBrdnCst : string | '';
|
||||
clclnEcqrRt : string | '';
|
||||
clclnGrssCst : string | '';
|
||||
}
|
||||
|
||||
// 사업비 상세정보
|
||||
export type MoneyDetailType = {
|
||||
reqBizGrssCst : string | '';
|
||||
reqBizReuseFcltCst : string | '';
|
||||
reqBizPplCst : string | '';
|
||||
reqBizArchCst : string | '';
|
||||
reqBizMchnCst : string | '';
|
||||
reqBizMtrngCst : string | '';
|
||||
reqBizGisCst : string | '';
|
||||
reqBizCctvCst : string | '';
|
||||
reqBizCmcnCst : string | '';
|
||||
reqBizWstCst : string | '';
|
||||
reqBizDesignCst : string | '';
|
||||
reqBizSpvsCst : string | '';
|
||||
reqBizSbsdCst : string | '';
|
||||
reqBizEtc : string | '';
|
||||
|
||||
/** 재원조달계획(백만원원) */
|
||||
prcmPlanGrssCst : string | '';
|
||||
prcmPlanNlep : string | '';
|
||||
prcmPlanLcep : string | '';
|
||||
prcmPlanBrdnCst : string | '';
|
||||
prcmPlanEcqrRt : string | '';
|
||||
/** 사업비 정산(백만원) */
|
||||
clclnGrssCst : string | '';
|
||||
clclnNlep : string | '';
|
||||
clclnLcep : string | '';
|
||||
clclnBrdnCst : string | '';
|
||||
clclnEcqrRt : string | '';
|
||||
}
|
||||
|
||||
/**====================================
|
||||
* Store - 데이터
|
||||
====================================*/
|
||||
// 수요처 정보
|
||||
const demandCompList : DemandCompType[] = [
|
||||
]
|
||||
|
||||
// 사용용도
|
||||
const useTypesData : SelectDataType[] = [
|
||||
{ text: '청소', val: "청소" },
|
||||
{ text: "화장실용수", val: "화장실용수" },
|
||||
{ text: "세척", val: "세척" },
|
||||
{ text: "살수", val: "살수" },
|
||||
{ text: "조경", val: "조경" },
|
||||
{ text: "농업", val: "농업" },
|
||||
{ text: "하천", val: "하천" },
|
||||
{ text: "지하수", val: "지하수" },
|
||||
{ text: "공업", val: "공업" },
|
||||
];
|
||||
|
||||
// 유사사례 모달 헤더
|
||||
export const similarCaseModalHeader: TableUpperProps[] = [
|
||||
{ id: "ruseBizNm", title: "사업명", row: 0, rowSpan: 2 },
|
||||
{ id: "bizBgngDate", title: "사업시작일", row: 0, rowSpan: 2 },
|
||||
{ id: "bizEndDate", title: "사업종료일", row: 0, rowSpan: 2 },
|
||||
{ id: "A", title: "공급처", row: 0, colSpan: 3 },
|
||||
{ id: "splyFcltKnd", title: "시설종류", row: 1 },
|
||||
{ id: "splyFcltNm", title: "공급처명", row: 1 },
|
||||
{ id: "splyFcltCpct", title: "시설용량\n(㎥/일)", row: 1 },
|
||||
{ id: "B", title: "수요처", row: 0, colSpan: 2 },
|
||||
{ id: "dmdFcltNm", title: "수요처명", row: 1 },
|
||||
{ id: "dmdFcltAddr", title: "주소", row: 1 },
|
||||
{ id: "C", title: "재이용시설", row: 0, colSpan: 5 },
|
||||
{ id: "ruseFcltCpct", title: "시설용량\n(㎥/일)", row: 1 },
|
||||
{ id: "ruseFcltPplLen", title: "관로\n(km)", row: 1 },
|
||||
{ id: "ruseFcltUsg", title: "용도", row: 1 },
|
||||
{ id: "ruseFcltSplyCpct", title: "공급량\n(㎥/일)", row: 1 },
|
||||
{ id: "ruseFcltPrcsMsrc", title: "처리공법", row: 1 },
|
||||
{ id: "D", title: "보증수질", row: 0, colSpan: 13 },
|
||||
{ id: "grnteWqUsg", title: "용도별", row: 1 },
|
||||
{ id: "grnteWqGrssClfg", title: "총대장균군수\n(개/100ml)", row: 1 },
|
||||
{ id: "grnteWqCombResdch", title: "결합잔류염소\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqTrbt", title: "탁도\n(NTU)", row: 1 },
|
||||
{ id: "grnteWqBod", title: "생물학적산소요구량\n(BOD)\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqSmll", title: "냄새\n(개/100ml)", row: 1 },
|
||||
{ id: "grnteWqCrmty", title: "색도\n(도)", row: 1 },
|
||||
{ id: "grnteWqTn", title: "총질소\n(T-N)\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqTp", title: "총인\n(T-P)\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqPh", title: "수소이온농도\n(pH)", row: 1 },
|
||||
{ id: "grnteWqClIonQnt", title: "염화물\n(mgCL/L)", row: 1 },
|
||||
{ id: "grnteWqCndctStat", title: "전기전도도\n(μS/cm)", row: 1 },
|
||||
{ id: "grnteWqRmrk", title: "비고", row: 1 },
|
||||
{ id: "reqBizGrssCst", title: "소요사업비\n(백만원)", row: 0, rowSpan: 2 },
|
||||
]
|
||||
|
||||
// 유사사례 데이터
|
||||
const similarCaseList : SimilarCaseType[] = [
|
||||
|
||||
]
|
||||
|
||||
// 사업비 상세정보 모달 헤더
|
||||
export const moneyDetailModalHeader: TableUpperProps[] = [
|
||||
{ id: "A", title: "소요사업비(백만원)", row: 0, colSpan: 14 },
|
||||
{ id: "reqBizGrssCst", title: "총", row: 1, rowSpan: 2 },
|
||||
{ id: "B", title: "토목", row: 1, colSpan: 2 },
|
||||
{ id: "reqBizReuseFcltCst", title: "재이용시설", row: 2 },
|
||||
{ id: "reqBizPplCst", title: "관로", row: 2 },
|
||||
{ id: "reqBizArchCst", title: "건축", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizMchnCst", title: "기계", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizMtrngCst", title: "전기,계측", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizGisCst", title: "GIS/DB", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizCctvCst", title: "CCTV", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizCmcnCst", title: "시공 및 준공", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizWstCst", title: "폐기물", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizDesignCst", title: "설계비", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizSpvsCst", title: "감리비", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizSbsdCst", title: "시설 부대비", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizEtc", title: "기타", row: 1, rowSpan: 2 },
|
||||
{ id: "C", title: "재원조달계획(백만원)", row: 0, colSpan: 5 },
|
||||
{ id: "prcmPlanGrssCst", title: "총", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanNlep", title: "국비", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanLcep", title: "지방비", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanEcqrRt", title: "국고비율", row: 1, rowSpan: 2 },
|
||||
{ id: "D", title: "사업비 정산(백만원)", row: 0, colSpan: 5 },
|
||||
{ id: "clclnGrssCst", title: "총", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnNlep", title: "국비", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnLcep", title: "지방비", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnEcqrRt", title: "국고비율", row: 1, rowSpan: 2 },
|
||||
]
|
||||
|
||||
// 사업비 상세정보
|
||||
const moneyDetailList : MoneyDetailType[] = [
|
||||
]
|
||||
|
||||
// 수요정보 신청현황 헤더
|
||||
export const upHeadSdmInfoStatus: TableUpperProps[] = [
|
||||
{ id: "index", title: "No."},
|
||||
{ id: "ctpvCd", title: "시도", row: 0, rowSpan: 2 },
|
||||
{ id: "sggCd", title: "시군구", row: 0, rowSpan: 2 },
|
||||
{ id: "dmdCoNm", title: "회사명", row: 0, rowSpan: 2 },
|
||||
{ id: "daddr", title: "공장주소", row: 0, rowSpan: 2 },
|
||||
{ id: "telno", title: "전화번호", row: 0, rowSpan: 2 },
|
||||
{ id: "dmndTmng", title: "신청일자", row: 0, rowSpan: 2 },
|
||||
{ id: "status", title: "진행상태", row: 0, rowSpan: 2 },
|
||||
]
|
||||
|
||||
// 수요정보 신청현황 데이터
|
||||
|
||||
/**====================================
|
||||
* Store - Api
|
||||
=====================================*/
|
||||
// 수요처 등록
|
||||
const postSdmData = async (data: DemandCompType) => {
|
||||
try {
|
||||
await postFetch("/reuseFc/request", data);
|
||||
toast.success("수요처 등록에 성공하였습니다.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("수요처 등록에 실패하였습니다.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 수요정보 등록
|
||||
const postSdmInfoData = async (data: DemandCompType) => {
|
||||
try {
|
||||
await postFetch("/reuseFc/request/demandInfo", data);
|
||||
toast.success('수요 정보 등록에 성공하였습니다.');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error('수요 정보 등록에 실패하였습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
const postSdmLoginData = async (data: DemandCompType) => {
|
||||
try {
|
||||
const res = await postFetch("/reuseFc/request/login", data);
|
||||
const jsonData = await res.json();
|
||||
|
||||
const status = jsonData.status;
|
||||
const loginData = jsonData.data;
|
||||
let loginSuccess = false;
|
||||
if(status === 200 && loginData != null) {
|
||||
alert("수요처 로그인에 성공하였습니다.");
|
||||
sessionStorage.setItem("user", JSON.stringify(loginData));
|
||||
loginSuccess = true;
|
||||
} else if(status === 401) {
|
||||
alert("패스워드가 틀렸습니다.");
|
||||
loginSuccess = false;
|
||||
} else {
|
||||
alert("등록된 수요처가 없습니다.");
|
||||
loginSuccess = false;
|
||||
}
|
||||
|
||||
return loginSuccess;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert("수요처 로그인에 실패하였습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
const getDemandCompList = async (searchText: string, pageCnt: number) => {
|
||||
try {
|
||||
const url = '/reuseFc/request/' + (searchText ? searchText : '') + '/' + pageCnt;
|
||||
const res = await getFetch(fetchType.WR, url);
|
||||
return {
|
||||
data: res?.data || [],
|
||||
count: res?.count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error+' 수요처 정보를 불러오지 못했습니다.');
|
||||
return { data: [], count: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
const getSdmInfoStatusList = async (dmdInfoId : string) => {
|
||||
try {
|
||||
const url = '/reuseFc/request/' + dmdInfoId;
|
||||
const res = await getFetch(fetchType.WR, url);
|
||||
return res?.data || [];
|
||||
} catch (error) {
|
||||
console.error(error+' 수요정보 신청현황을 불러오지 못했습니다.');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 유사사례 조회
|
||||
const fetchGetSimilarCaseList = async (part: string, value: number|string) => {
|
||||
try {
|
||||
const url = '/reuseFc/pubFcRuseCase/dm/' + part + '/' + (value ? value : 0);
|
||||
const res = await getFetch(fetchType.WR, url);
|
||||
return res?.data || [];
|
||||
} catch (error) {
|
||||
console.error(error+' 유사사례 조회에 실패하였습니다.');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**====================================
|
||||
* Store - expaort
|
||||
====================================*/
|
||||
export type SdmActions = {
|
||||
initializeSdmData: () => void;
|
||||
insertSdmData: (data: DemandCompType) => Promise<void>;
|
||||
insertSdmInfoData: (data: DemandCompType) => Promise<void>;
|
||||
getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>;
|
||||
actionLogin: (data: DemandCompType) => Promise<void>;
|
||||
getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>;
|
||||
getSimilarCaseList: (part: string, value: number|string) => Promise<void>;
|
||||
}
|
||||
|
||||
export type SdmType ={
|
||||
Sdm :{
|
||||
isInit: boolean;
|
||||
useTypesData: SelectDataType[];
|
||||
demandCompList: DemandCompType[];
|
||||
upHeadSimilarCaseList: TableUpperProps[];
|
||||
similarCaseList: SimilarCaseType[];
|
||||
upHeadMoneyDetailList : TableUpperProps[];
|
||||
moneyDetailList: MoneyDetailType[];
|
||||
|
||||
// 수요정보 신청현황
|
||||
sdmInfoStatusList: DemandCompType[];
|
||||
upHeadSdmInfoStatus: TableUpperProps[];
|
||||
|
||||
// 로그인관련
|
||||
successNextPage : boolean;
|
||||
successLogin: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const SdmState: SdmType = {
|
||||
Sdm:{
|
||||
isInit:false,
|
||||
useTypesData: useTypesData,
|
||||
demandCompList: demandCompList,
|
||||
upHeadSimilarCaseList: similarCaseModalHeader,
|
||||
similarCaseList: similarCaseList,
|
||||
upHeadMoneyDetailList : moneyDetailModalHeader,
|
||||
moneyDetailList: moneyDetailList,
|
||||
|
||||
// 수요정보 신청현황
|
||||
sdmInfoStatusList: [],
|
||||
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
|
||||
|
||||
// 로그인관련
|
||||
successNextPage : false,
|
||||
successLogin: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const SdmReducer = (set: any): SdmActions => {
|
||||
return {
|
||||
initializeSdmData: async () => {
|
||||
},
|
||||
|
||||
insertSdmData: async (data: DemandCompType) => {
|
||||
const preRes = await getSdmInfoStatusList(data.dmdInfoId);
|
||||
if(preRes?.dmdInfoId) {
|
||||
toast.error('이미 등록된 수요처입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await postSdmData(data);
|
||||
if(res) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
successNextPage: true,
|
||||
}
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
insertSdmInfoData: async (data: DemandCompType) => {
|
||||
postSdmInfoData(data);
|
||||
},
|
||||
|
||||
// 수요처 정보
|
||||
getDemandCompList: async (searchText: string, pageCnt: number) => {
|
||||
const res = await getDemandCompList(searchText, pageCnt);
|
||||
if(res.data.length === 0) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
demandCompList: [],
|
||||
}
|
||||
}))
|
||||
return;
|
||||
} else {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
demandCompList: res.data,
|
||||
}
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
// 수요정보 신청현황
|
||||
getSdmInfoStatusList: async (dmdInfoId: string) => {
|
||||
const res = await getSdmInfoStatusList(dmdInfoId);
|
||||
if(res.length === 0) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
sdmInfoStatusList: [],
|
||||
}
|
||||
}))
|
||||
return;
|
||||
}
|
||||
const sdmInfoStatusList : DemandCompType[] = [];
|
||||
|
||||
res.demandInfo.map((el: DemandCompType, index: number) => {
|
||||
const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn);
|
||||
const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청';
|
||||
|
||||
const tmpData = {
|
||||
...el,
|
||||
index: ++index,
|
||||
bzmnNo: res.bzmnNo,
|
||||
dmdCoNm: res.dmdCoNm,
|
||||
dmdInfoId: res.dmdInfoId,
|
||||
daddr: res.daddr,
|
||||
telno: res.telno.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'),
|
||||
rgtr: res.rgtr,
|
||||
ctpvCd: res.ctpvCd,
|
||||
sggCd: res.sggCd,
|
||||
useUsgCd: el.useUsgCd,
|
||||
dmndTmng: el.dmndTmng,
|
||||
dmdInfoSn : el.dmdInfoSn,
|
||||
status : status
|
||||
}
|
||||
sdmInfoStatusList.push(tmpData);
|
||||
})
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
sdmInfoStatusList: sdmInfoStatusList,
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
actionLogin: async (data: DemandCompType) => {
|
||||
const loginSuccess = await postSdmLoginData(data);
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
successLogin: loginSuccess,
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
// 유사사례 조회
|
||||
getSimilarCaseList: async (part: string, value: number|string) => {
|
||||
const res = await fetchGetSimilarCaseList(part, value);
|
||||
if(res && res.length !== 0) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
similarCaseList: res,
|
||||
}
|
||||
}))
|
||||
return;
|
||||
}else{
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
similarCaseList: [],
|
||||
}
|
||||
}))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export type SdmStore = SdmType & SdmActions;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common';
|
||||
import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng';
|
||||
|
||||
export type DmStore = CommonStore & SdmStore;
|
||||
export type DmState = CommonType & SdmType;
|
||||
|
||||
export const StoreZus = (initState: DmState= {
|
||||
...CommonState,
|
||||
...SdmState,
|
||||
}) => {
|
||||
return createStore<DmStore>()((set) => {
|
||||
return {
|
||||
...initState,
|
||||
...CommonReducer(set),
|
||||
...SdmReducer(set)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.formInsideTitle{
|
||||
background-color: gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.smallTitle{
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
import { Button } from "@common/components/ui";
|
||||
import Link from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export function ToastFormUi(body:string,title:string,url:string,buttonName:string){
|
||||
|
||||
//const router = useRouter();
|
||||
|
||||
return(
|
||||
|
||||
toast.custom((t) => (
|
||||
|
||||
<div
|
||||
className={`${
|
||||
t.visible ? 'animate-enter' : 'animate-leave'
|
||||
} w-4/12 h-40 bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5`}
|
||||
>
|
||||
<div className="w-full flex-1 py-10">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 pt-0.5">
|
||||
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<p className="text-xl font-medium underline">
|
||||
{title}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-20">
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
>
|
||||
<Link href={url}> {buttonName}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex pl-4 pr-4 py-20">
|
||||
<Button
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
size='sm'
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//8880
|
||||
import { DEV_URL_FO, DEV_URL_WR } from "../config";
|
||||
|
||||
export const fetchType = {
|
||||
COMMON: "common",
|
||||
WR: "wr"
|
||||
};
|
||||
|
||||
interface RequestParams {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const getFetch = async (type: string, url: string) => {
|
||||
const baseUrl = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR;
|
||||
const response = await fetch(baseUrl + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
const res = await response.json();
|
||||
return res;
|
||||
};
|
||||
|
||||
export const postFetch = async (url: string, params: RequestParams) => {
|
||||
const response = await fetch(DEV_URL_WR + url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
export const putFetch = async (url: string, params: RequestParams) => {
|
||||
const response = await fetch(DEV_URL_WR + url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
export const deleteFetch = async (url: string, params: RequestParams) => {
|
||||
const response = await fetch(DEV_URL_WR + url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { useDmStore } from '@/app/store';
|
||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
type thisProps = {
|
||||
label: string | undefined;
|
||||
initValue: string | undefined;
|
||||
setValue : (dmdCoNm?: string) => void;
|
||||
setRtnData : (rtnData: DemandCompType[]) => void;
|
||||
}
|
||||
|
||||
export default function SearchInputText({label=undefined, initValue=undefined, setValue=()=>{}, setRtnData=()=>{}} :thisProps) {
|
||||
|
||||
const { getDemandCompList, Sdm } = useDmStore((state) => state);
|
||||
|
||||
const {
|
||||
demandCompList
|
||||
} = Sdm;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [textValue, setTextValue] = useState(initValue || '');
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setTextValue(value);
|
||||
setValue(value);
|
||||
if (value) {
|
||||
setIsOpen(true);
|
||||
getDemandCompList(value, 20);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [getDemandCompList, setValue]);
|
||||
|
||||
const handleSuggestionClick = useCallback((dmdCoNm: string) => {
|
||||
setTextValue(dmdCoNm);
|
||||
setValue(dmdCoNm);
|
||||
setIsOpen(false);
|
||||
setRtnData(demandCompList);
|
||||
}, [demandCompList, setRtnData, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<input
|
||||
type="text"
|
||||
value={textValue}
|
||||
onChange={handleInputChange}
|
||||
/* onFocus={() => setIsOpen(true)} */
|
||||
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={`${label}을(를) 입력하세요`}
|
||||
/>
|
||||
{isOpen && (
|
||||
<div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
|
||||
<div className="max-h-72">
|
||||
{demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"
|
||||
onClick={() => handleSuggestionClick(item.dmdCoNm)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleSuggestionClick(item.dmdCoNm);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.dmdInfoId.substring(10,14) !== '0000' ? (
|
||||
<>
|
||||
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}
|
||||
<span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoId.substring(10,14)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}
|
||||
</>
|
||||
)}
|
||||
<span className="float-right">{item.dmdCoNm}</span>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PROXY_HOST, SERVER_API_FO_URL, SERVER_API_WR_URL } from "@/src/shared/config/env";
|
||||
|
||||
export const LOCAL_URL = PROXY_HOST;
|
||||
export const DEV_URL_FO = SERVER_API_FO_URL;
|
||||
export const DEV_URL_WR = SERVER_API_WR_URL;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { FormUi } from "@common/business_components/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ControllerRenderProps, FieldError, FieldValues, Path } from "react-hook-form";
|
||||
|
||||
|
||||
interface AccessibleFormFieldProps<T extends FieldValues> {
|
||||
field: ControllerRenderProps<T, Path<T>>;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
error?: FieldError;
|
||||
inputComponent: ReactNode;
|
||||
}
|
||||
|
||||
export function AccessibleFormField<T extends FieldValues>({
|
||||
label,
|
||||
required = false,
|
||||
error,
|
||||
inputComponent,
|
||||
}: AccessibleFormFieldProps<T>) {
|
||||
const labelText = required ? `${label} *` : label;
|
||||
|
||||
return (
|
||||
<FormUi label={labelText}>
|
||||
<div>
|
||||
{inputComponent}
|
||||
{error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
|
||||
<p key={idx} className="text-red-500 text-sm mt-1" role="alert">
|
||||
{msg}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</FormUi>
|
||||
);
|
||||
}
|
||||
@@ -1,572 +0,0 @@
|
||||
import ModalSimilarPage from "@/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
|
||||
import type { FormData } from "@/app/pages/sdmMng/sdmInfoInsert/page";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import { CANDIDATE, DatePicker, FormField, FormLabel, FormUi, SelectBox, UiModal } from "@common/business_components";
|
||||
import { Input } from "@common/components";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AccessibleFormField } from "./AccessibleFormField";
|
||||
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
|
||||
import SearchInputText from "../common/searhDemandInput/component";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
|
||||
export interface SdmInfoRegFormProps {
|
||||
form: UseFormReturn<FormData>;
|
||||
onCheckboxChange?: (checked: boolean) => void;
|
||||
facilityName?: string;
|
||||
}
|
||||
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
|
||||
export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfoRegFormProps) {
|
||||
const { initializeCommonData, Sdm } = useDmStore((state) => state);
|
||||
const { useTypesData } = Sdm;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [sdmNmValue, setSdmNmValue] = useState<string>(searchParams.get('dmdCoNm') || '');
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const [isSimilarCase, setIsSimilarCase] = useState(false);
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
//모달 조회에 필요한 데이터
|
||||
const [sply, setSply] = useState<number>(0);
|
||||
const [usageCd, setUsageCd] = useState<string>('청소');
|
||||
const [money, setMoney] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (useTypesData && useTypesData.length > 0) {
|
||||
setTimeout(() => {
|
||||
form.setValue('usePurpose', useTypesData[0]?.val?.toString() || '');
|
||||
}, 500);
|
||||
}
|
||||
}, [useTypesData, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (facilityName) {
|
||||
setSdmNmValue(facilityName);
|
||||
form.setValue('sdmNm', facilityName);
|
||||
setKey(prev => prev + 1);
|
||||
}
|
||||
}, [facilityName, form]);
|
||||
|
||||
const searchCandidate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const searchSimilarCase = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSimilarCase(true);
|
||||
}
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
setIsBznmTotNoDisabled(checked);
|
||||
onCheckboxChange?.(checked);
|
||||
};
|
||||
|
||||
// 시설 선택시 이벤트
|
||||
const selectComp = useCallback((rtnData: DemandCompType[]) => {
|
||||
const rtnCompData = rtnData?.[0];
|
||||
form.setValue('sdmNm', rtnCompData?.dmdCoNm || '');
|
||||
}, [form]);
|
||||
|
||||
const formFields: Array<{
|
||||
name: keyof FormData;
|
||||
label: string;
|
||||
component: FieldComponent | undefined;
|
||||
formIndex: number;
|
||||
required?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "sdmNm",
|
||||
label: "수요처선택",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<div key={key}>
|
||||
<SearchInputText
|
||||
label="수요처명"
|
||||
setValue={(val) => {
|
||||
setSdmNmValue(val || '');
|
||||
field.onChange(val || '');
|
||||
}}
|
||||
setRtnData={selectComp}
|
||||
initValue={facilityName || sdmNmValue}
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "demandYmd",
|
||||
label: "요구시기",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<div className="w-full">
|
||||
<DatePicker
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date as Date);
|
||||
}}
|
||||
className="w-full"
|
||||
defaultMonth={field.value as Date}
|
||||
field={{
|
||||
value: field.value as Date,
|
||||
onChange: (date: Date | undefined) => {
|
||||
field.onChange(date as Date);
|
||||
}
|
||||
}}
|
||||
selectClass={undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "demandQnty",
|
||||
label: "요구수량(㎥/d)",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="요구수량(㎥/d)을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
setSply(Number(e.target.value));
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "usePurpose",
|
||||
label: "사용용도",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<SelectBox
|
||||
selectClass={undefined}
|
||||
label=""
|
||||
selectArray={useTypesData}
|
||||
onSelectValue={(val) => {
|
||||
field.onChange(val.toString());
|
||||
setUsageCd(val.toString());
|
||||
}}
|
||||
className=""
|
||||
value={field.value as string}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "money",
|
||||
label: "희망단가(원/m3)",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label="희망단가(원/㎥)"
|
||||
required={true}
|
||||
error={form.formState.errors.money}
|
||||
inputComponent={
|
||||
<FormUi
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isDisabled={isBznmTotNoDisabled}
|
||||
>
|
||||
<Input
|
||||
placeholder="희망단가(원/㎥)을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
setMoney(Number(e.target.value));
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormUi>
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "reqWaterQlty",
|
||||
label: "요구수질",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: undefined
|
||||
},
|
||||
{
|
||||
name: "grssClfg",
|
||||
label: "총대장균군수(개/100ml)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총대장균군수를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "tn",
|
||||
label: "총질소(T-N)(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총질소를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "combResdch",
|
||||
label: "결합잔류염소(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="결합잔류염소를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "tph",
|
||||
label: "총인(T-P)(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총인을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "trbt",
|
||||
label: "탁도(NTU)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="탁도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "ph",
|
||||
label: "수소이온농도(pH)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="수소이온농도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "bod",
|
||||
label: "생물학적 산소요구량(BOD)(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="생물학적 산소요구량을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "clIonQnt",
|
||||
label: "염화물(mgCL/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="염화물을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "smll",
|
||||
label: "냄새",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="냄새를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "cdtnv",
|
||||
label: "전기전도도(㎲/cm)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="전기전도도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "crmty",
|
||||
label: "색도(도)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="색도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "rmrk",
|
||||
label: "비고",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="비고를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "candidateTitle",
|
||||
label: CANDIDATE || "공급처 후보지 선택(필수아님)",
|
||||
formIndex: 2,
|
||||
component: (field) => (
|
||||
<div>
|
||||
<div className="-mt-14 float-right flex gap-10">
|
||||
<Button className={'bg-gray-200'} onClick={searchCandidate}>
|
||||
후보지 조회
|
||||
</Button>
|
||||
<Button className={'bg-gray-200'} onClick={searchSimilarCase}>
|
||||
유사사례
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-center gap-10 mt-5">
|
||||
<Input
|
||||
placeholder="후보지1"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Input
|
||||
placeholder="후보지2"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Input
|
||||
placeholder="후보지3"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const normalFields = formFields.filter(f => f.formIndex === 0 && f.component);
|
||||
const groupFields = formFields.filter(f => f.formIndex === 1 && f.component);
|
||||
const candidateFields = formFields.filter(f => f.formIndex === 2 && f.component);
|
||||
|
||||
return (
|
||||
<>
|
||||
{normalFields.map(({ name, label, component, required }) => (
|
||||
component ? (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
name === 'money' ? component(field) : (
|
||||
// <FormUi label={label}>
|
||||
// {component(field)}
|
||||
// </FormUi>
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label={label}
|
||||
required={required}
|
||||
error={form.formState.errors[name]}
|
||||
inputComponent={component(field)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
|
||||
{groupFields.length > 0 && (
|
||||
<div className="mt-10">
|
||||
<FormLabel className="text-xl">요구수질</FormLabel>
|
||||
<div className="flex flex-wrap justify-start gap-10 mt-3 mb-3">
|
||||
{groupFields.map(({ name, label, component, required }) => (
|
||||
<div key={name} className="min-w-[180px]">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
// <FormUi label={label}>
|
||||
// {component?.(field)}
|
||||
// </FormUi>
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label={label}
|
||||
required={required}
|
||||
error={form.formState.errors[name]}
|
||||
inputComponent={component?.(field)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{candidateFields.map(({ name, label, component, required }, index) => (
|
||||
component ? (
|
||||
<FormField
|
||||
key={name + index}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
// <FormUi label={label}>
|
||||
// {component(field)}
|
||||
// </FormUi>
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label={label}
|
||||
required={required}
|
||||
error={form.formState.errors[name]}
|
||||
inputComponent={component?.(field)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
|
||||
{isSimilarCase && (
|
||||
<UiModal
|
||||
body={<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />}
|
||||
bodyClass="sm:max-w-[100vw] w-[1400px] h-[900px]"
|
||||
open={isSimilarCase}
|
||||
onOpenChange={(open) => !open && setIsSimilarCase(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
export function SdmLoginForm() {
|
||||
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
const createFormSchema = () => z.object({
|
||||
dmdInfoId: z.string()
|
||||
.min(1, "사업자등록번호를 입력해주세요")
|
||||
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"),
|
||||
bznmTotNo: isBznmTotNoDisabled
|
||||
? z.string().optional()
|
||||
: z.string().length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"),
|
||||
password: z.string()
|
||||
.min(1, "비밀번호를 입력해주세요")
|
||||
.min(8, "비밀번호는 8자 이상 입력해주세요")
|
||||
.max(16, "비밀번호는 16자 이내로 입력해주세요"),
|
||||
rgtr: z.string().optional(),
|
||||
dmdAplyPswd: z.string().optional(),
|
||||
});
|
||||
|
||||
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const formSchema = createFormSchema();
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
const defaultValues: FormData = {
|
||||
dmdInfoId: '',
|
||||
bznmTotNo: '',
|
||||
password: '',
|
||||
rgtr: '',
|
||||
dmdAplyPswd: '',
|
||||
};
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
setIsBznmTotNoDisabled(checked);
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
const data = form.getValues();
|
||||
const passwordStr = sha512.hex(data.password)
|
||||
data.dmdAplyPswd = passwordStr;
|
||||
const param = {
|
||||
dmdInfoId: data.dmdInfoId + (isBznmTotNoDisabled ? '0000' : data.bznmTotNo),
|
||||
dmdAplyPswd: data.dmdAplyPswd,
|
||||
}
|
||||
actionLogin(param as unknown as DemandCompType);
|
||||
};
|
||||
|
||||
const formFields: Array<{
|
||||
name: keyof FormData;
|
||||
label: string;
|
||||
component: FieldComponent | undefined;
|
||||
}> = [
|
||||
{
|
||||
name: "dmdInfoId",
|
||||
label: "사업자등록번호",
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="사업자등록번호를 입력하세요"
|
||||
type="text"
|
||||
className="!mt-5 !mb-5 !text-xl text-gray-600"
|
||||
{...field}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "bznmTotNo",
|
||||
label: BZNM_TOT_NO || "종사업자등록번호",
|
||||
component: (field) => (
|
||||
<FormUi
|
||||
label="종사업자등록번호"
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isDisabled={isBznmTotNoDisabled}
|
||||
>
|
||||
<Input
|
||||
placeholder="여러사업장이 있을 경우 입력해주세요"
|
||||
type="text"
|
||||
className="!mt-5 !text-xl text-gray-600"
|
||||
disabled={isBznmTotNoDisabled}
|
||||
{...field}
|
||||
/>
|
||||
</FormUi>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
label: "비밀번호",
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
type="password"
|
||||
className="!mt-5 !mb-5 !text-xl text-gray-600"
|
||||
{...field}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
{formFields.map(({ name, label, component }) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
label==='종사업자등록번호' ?
|
||||
<>
|
||||
{component && component(field)}
|
||||
</>
|
||||
: <FormUi label={label}>
|
||||
{component && component(field)}
|
||||
</FormUi>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</FormProvider>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
로그인
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{rules:{
|
||||
// other rules..
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
}},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,128 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||
|
||||
// const CLAIM_NAME = "Authorization";
|
||||
// const AUTH_USER_ID = "token-id";
|
||||
const serverApiUrl = process.env.NEXT_PUBLIC_CS_SERVER_API_URL;
|
||||
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
|
||||
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
|
||||
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
console.log(`[Middleware] Request URL: ${req.url}`);
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
// if (pathname.startsWith("/server")) {
|
||||
// const queryParams = searchParams.toString();
|
||||
// const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
// queryParams ? `?${queryParams}` : ""
|
||||
// }`;
|
||||
// console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`);
|
||||
// return NextResponse.rewrite(new URL(newUrl));
|
||||
// }
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const decodedParams = Array.from(searchParams.entries())
|
||||
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
|
||||
console.log(' sesesesesesevereveraserwereqwrewbrtkehjwrtg server')
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
decodedParams ? `?${decodedParams}` : ""
|
||||
}`;
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
// const token =
|
||||
// "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzUyMTM1OTk3fQ.itGY6M6mC2iwKvEpXw2KS6354T_B_OFz4hD26ResIgwJhpwiDJCMuSXD-rZ3AarmAIKd0udpCuJR7UlvFMN2ww";
|
||||
// const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0";
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
|
||||
//wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad
|
||||
//http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
console.log(
|
||||
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`
|
||||
);
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/user-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
|
||||
console.log(
|
||||
`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`
|
||||
);
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/geoserver")) {
|
||||
let queryParams = searchParams.toString();
|
||||
const typename = searchParams.get("TYPENAME");
|
||||
console.log("typename ", typename);
|
||||
|
||||
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
|
||||
if (
|
||||
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|
||||
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|
||||
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|
||||
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
|
||||
) {
|
||||
newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
else if (
|
||||
typename == "hasudo:tb_ms_swage_ppl" //관로
|
||||
|| typename == "hasudo:tb_ms_swage_mhl" //맨홀
|
||||
|| typename == "hasudo:tb_ms_swage_pmstn" //펌프장
|
||||
|| typename == "hasudo:tb_ms_stofcbr" //우수토실
|
||||
) {
|
||||
newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
else if (
|
||||
typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실
|
||||
|| typename == "hasudo:om_wqlt_station" //관측소
|
||||
|| typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설
|
||||
|| typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설
|
||||
|| typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설
|
||||
|| typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설
|
||||
|| typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처
|
||||
|| typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처
|
||||
){
|
||||
const url = req.nextUrl.clone();
|
||||
url.searchParams.set('TYPENAME', typename.replace("hasudo:",""));
|
||||
queryParams = url.searchParams.toString();
|
||||
newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
|
||||
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/sewerage-service")) {
|
||||
const queryParams = searchParams.toString();
|
||||
const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"],
|
||||
};
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:3000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/pst(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/pst/:path*",
|
||||
destination: `${subDomain}/pst/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "dm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/business_components": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-sha512": "^0.9.0",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.1.7",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user