Merge branch 'main' into iipbs/sn/ii
This commit is contained in:
@@ -37,4 +37,5 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env*
|
||||
.env*
|
||||
__ENV__.js
|
||||
@@ -0,0 +1,44 @@
|
||||
import { findUp } from "find-up";
|
||||
import { config } from "dotenv";
|
||||
import fs from "fs";
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
async function parseDotenv(nodeEnv) {
|
||||
const envFilePath = await findUp(`.env.${nodeEnv}`);
|
||||
|
||||
const parsedEnv = config({ path: envFilePath }).parsed || {};
|
||||
|
||||
return parsedEnv;
|
||||
}
|
||||
|
||||
function writeEnv(parsedEnv) {
|
||||
const scriptFilePath = `${fs.realpathSync(process.cwd())}/public/__ENV__.js`;
|
||||
|
||||
fs.writeFileSync(
|
||||
scriptFilePath,
|
||||
`window.__ENV__ = ${JSON.stringify(parsedEnv)}`,
|
||||
);
|
||||
}
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.command(
|
||||
"next-env",
|
||||
"Create Next.js runtime environment js",
|
||||
function builder(y) {
|
||||
return y.option("env", {
|
||||
alias: "e",
|
||||
type: "string",
|
||||
description: "Environment name(ex: development, production)",
|
||||
});
|
||||
},
|
||||
async function handler(args) {
|
||||
const nodeEnv = args.e || args.env || "development";
|
||||
|
||||
const parsedEnv = await parseDotenv(nodeEnv); // dotenv 파싱
|
||||
writeEnv(parsedEnv); // 환경 변수 스크립트 파일 생성
|
||||
|
||||
return parsedEnv;
|
||||
},
|
||||
)
|
||||
.parse();
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "features",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 13000",
|
||||
"dev": "node cli.js next-env -e ${NODE_ENV:-development} && next dev -p 13000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 13000",
|
||||
"start": "node cli.js next-env -e ${NODE_ENV:-production} && next start -p 13000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -27,6 +28,8 @@
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"find-up": "^7.0.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "15.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -44,6 +47,7 @@
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"postcss": "^8",
|
||||
|
||||
@@ -4,12 +4,16 @@ import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
|
||||
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<head>
|
||||
<Script src="/__ENV__.js" strategy="beforeInteractive" />
|
||||
</head>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { PROXY_HOST } from "./env";
|
||||
"use client";
|
||||
|
||||
export const BASE_URL = `${PROXY_HOST}/server`;
|
||||
import { getCookie, getEnv } from "../lib";
|
||||
|
||||
const SITE_ID = getCookie("site-id-key") ?? "";
|
||||
const PROXY_HOST_KEY = `NEXT_PUBLIC_PROXY_HOST_${SITE_ID}`;
|
||||
export const BASE_URL = `${getEnv(PROXY_HOST_KEY)}/server`;
|
||||
|
||||
export const ENDPOINT = {
|
||||
ATTACH_SERVICE: {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
__ENV__?: Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEnvConfig(): Record<string, string> {
|
||||
if (typeof window === "undefined") return {};
|
||||
return window.__ENV__ ?? {};
|
||||
}
|
||||
|
||||
export function getEnv(key: string): string | undefined {
|
||||
return getEnvConfig()[key];
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export { useAlertStore } from "./use-alert-store";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export { usePrompt } from "./use-prompt";
|
||||
export { getEnv } from "./env";
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Authorizations {
|
||||
httpMethodCode: string;
|
||||
sortSeq: number;
|
||||
useYn: string;
|
||||
siteId: string;
|
||||
mdfr: string;
|
||||
mdfcnDt: string;
|
||||
}
|
||||
@@ -27,6 +28,7 @@ export interface RoleAuthorizations {
|
||||
urlPatternValue: string;
|
||||
httpMethodCode: string;
|
||||
sortSeq: number;
|
||||
siteId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ export const authorizationFormSchema = z.object({
|
||||
httpMethodCode: z.string(),
|
||||
sortSeq: z.number(),
|
||||
useYn: z.string(),
|
||||
siteId: z.string(),
|
||||
});
|
||||
|
||||
export type AuthorizationsList = PageableList<Authorizations>;
|
||||
|
||||
@@ -161,12 +161,7 @@ export const LinkTableColumnsSchema = z.object({
|
||||
}, {
|
||||
message: '최대 14자리까지 입력 가능합니다.',
|
||||
}),
|
||||
col_seq: z.coerce.number().nullable().optional().refine((val) => {
|
||||
if (val == null) return true;
|
||||
return val > 0;
|
||||
}, {
|
||||
message: "컬럼 매칭 순서는 0 이상이어야 합니다."
|
||||
}),
|
||||
col_seq: z.coerce.number().nullable().optional(),
|
||||
mdfcn_dt: z.string().nullable().optional(),
|
||||
mdfr_id: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface WithRoleIdRoleAuthorizationsParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
roleId?: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
export interface RoleAuthorizations {
|
||||
|
||||
@@ -134,7 +134,7 @@ function DashboardPage() {
|
||||
{/* 전체 게시물 카드 */}
|
||||
<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">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter text-foreground">
|
||||
<h3 className="text-2xl font-medium">전체 게시물</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.pstTotCnt ? data.pstTotCnt.toLocaleString() : "-"}
|
||||
@@ -157,7 +157,7 @@ function DashboardPage() {
|
||||
{/* 오늘 접속자 수 카드 */}
|
||||
<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">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter text-foreground">
|
||||
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.accessTodayCnt
|
||||
@@ -184,7 +184,7 @@ function DashboardPage() {
|
||||
{/* 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">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter text-foreground">
|
||||
<h3 className="text-2xl font-medium">API 호출 건수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.apiTodayCnt ? data.apiTodayCnt.toLocaleString() : "-"}
|
||||
@@ -206,13 +206,13 @@ function DashboardPage() {
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 승인 대기 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="card-header text-foreground">
|
||||
<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 flex-1 flex-col items-start rounded-md bg-accent p-6 text-foreground">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
|
||||
<svg
|
||||
@@ -240,13 +240,13 @@ function DashboardPage() {
|
||||
: "0"}
|
||||
건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
<div className="mt-2 text-2xl opacity-50">
|
||||
신규 가입 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 매뉴 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6 text-foreground">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
||||
<svg
|
||||
@@ -273,13 +273,13 @@ function DashboardPage() {
|
||||
<div className="text-5xl font-bold">
|
||||
{(3).toLocaleString()}건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
<div className="mt-2 text-2xl opacity-50">
|
||||
매뉴 수정 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 컨텐츠 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6 text-foreground">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
|
||||
<svg
|
||||
@@ -307,7 +307,7 @@ function DashboardPage() {
|
||||
: "0"}
|
||||
건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
<div className="mt-2 text-2xl opacity-50">
|
||||
컨텐츠 수정 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
@@ -318,11 +318,11 @@ function DashboardPage() {
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 실시간 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 text-foreground">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">신규 사용자 가입</span>
|
||||
<span>{(1).toLocaleString()}분 전</span>
|
||||
@@ -344,11 +344,11 @@ function DashboardPage() {
|
||||
</div>
|
||||
{/* 일정 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">일정 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 text-foreground">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">
|
||||
{(3).toLocaleString()}월 정기 시스템 점검
|
||||
@@ -377,7 +377,7 @@ function DashboardPage() {
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 접속자 통계 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">접속자 통계</h3>
|
||||
</div>
|
||||
<div className="h-80 p-4">
|
||||
@@ -392,7 +392,7 @@ function DashboardPage() {
|
||||
</div>
|
||||
{/* 메뉴별 접속 통계 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
|
||||
</div>
|
||||
<div className="h-80 p-4">
|
||||
@@ -412,14 +412,14 @@ function DashboardPage() {
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 공지사항 */}
|
||||
<div className="card card-border !p-0">
|
||||
<div className="card-header !m-0 !border-none">
|
||||
<div className="card-header !m-0 !border-none text-foreground">
|
||||
<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">
|
||||
<div className="card-header !m-0 !border-none text-foreground">
|
||||
<h3 className="text-2xl font-medium">신규 게시글</h3>
|
||||
</div>
|
||||
{recentPostTableComponent}
|
||||
|
||||
@@ -13,21 +13,22 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import type {
|
||||
AuthorizationFormData,
|
||||
WithRoleAuthorizations,
|
||||
} from "@/entities/authorization";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { SiteSelect } from "@/entities/site";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
|
||||
type BannerFormProps = {
|
||||
type ModuleFormProps = {
|
||||
form: UseFormReturn<AuthorizationFormData>;
|
||||
authorizationNo?: number;
|
||||
handleSave: (data: AuthorizationFormData) => Promise<WithRoleAuthorizations>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
|
||||
function ModuleForm({ form, authorizationNo, ...props }: ModuleFormProps) {
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!authorizationNo} {...props}>
|
||||
<div className="card">
|
||||
@@ -99,6 +100,26 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사이트</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<SiteSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
className="w-[20rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
|
||||
@@ -10,12 +10,18 @@ import {
|
||||
Button,
|
||||
Switch,
|
||||
Input,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@common/components";
|
||||
import {
|
||||
RoleAuthorizationsApi,
|
||||
type RoleAuthorizationsData,
|
||||
type RoleAuthorizationsByRoleId,
|
||||
} from "@/entities/role-authorization";
|
||||
import { SiteSelect } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper, useDataTable } from "@/shared/lib";
|
||||
|
||||
@@ -34,9 +40,12 @@ function MappingRoleDialog({
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [keywordType, setKeywordType] = useState("");
|
||||
const [siteId, setSiteId] = useState("");
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
keyword: "",
|
||||
keywordType: "authorizationName",
|
||||
keywordType: "",
|
||||
siteId: "",
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
@@ -79,18 +88,20 @@ function MappingRoleDialog({
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const keyword = formData.get("keyword") as string;
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
keywordType,
|
||||
siteId: siteId === "ALL" ? "" : siteId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
setKeywordType("");
|
||||
setSiteId("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
keywordType: "",
|
||||
siteId: "",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,17 +118,35 @@ function MappingRoleDialog({
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">권한맵핑</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogContent className="max-w-[96rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>모듈 목록</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
||||
<SiteSelect
|
||||
value={siteId}
|
||||
onValueChange={(value) => setSiteId(value)}
|
||||
enabledAllSite
|
||||
/>
|
||||
<Select
|
||||
value={keywordType}
|
||||
onValueChange={(value) => setKeywordType(value)}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="authorizationName">API명</SelectItem>
|
||||
<SelectItem value="urlPatternValue">URL주소</SelectItem>
|
||||
<SelectItem value="httpMethodCode">전송방식</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name="keyword"
|
||||
className="w-[32rem]"
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
<Button type="submit">검색</Button>
|
||||
<Button type="button" color="white" onClick={handleReset}>
|
||||
@@ -145,7 +174,7 @@ const createColumns = (
|
||||
accessorKey: "authorizationName",
|
||||
header: "API명",
|
||||
meta: {
|
||||
width: 130,
|
||||
width: 320,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, roleId],
|
||||
queryFn: () =>
|
||||
SiteAuthorityApi.getSiteAuthorityList({
|
||||
authrtId: roleId,
|
||||
authrtId: roleId ?? "",
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
|
||||
@@ -11,6 +11,7 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id
|
||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
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 # 박이정
|
||||
|
||||
@@ -14,6 +14,7 @@ NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
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 # 박이정
|
||||
|
||||
@@ -12,6 +12,7 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id
|
||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ 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_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ 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_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client"
|
||||
import Base from "./base";
|
||||
import "./globals.css";
|
||||
import { StoreProvider } from "./store";
|
||||
|
||||
@@ -12,7 +11,7 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<body>
|
||||
<StoreProvider>
|
||||
<Base>{children}</Base>
|
||||
{children}
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div>{children}</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { SearchBar } from "./searchBar/searchBar";
|
||||
import { SearchBar } from "../../map/searchBar/searchBar";
|
||||
|
||||
interface FeatureData {
|
||||
fid?: string;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import { useFoStore } from "@/app/store";
|
||||
import { useEffect, useState } from "react";
|
||||
import SearchInput from "../../components/common/SearchInput";
|
||||
import SearchInput from "../../../components/common/SearchInput";
|
||||
|
||||
export default function DashBoardFclty({ onFacilityChange, sidoSigun, facilityNm, }: { onFacilityChange?: (facility: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | '' }) => void, sidoSigun?: string, facilityNm?: string }) {
|
||||
// 입력값 상태 관리
|
||||
+2
-2
@@ -4,8 +4,8 @@ import type { FacilityWaterQltyProps, modalExcelType } from "@/app/store/facilit
|
||||
import { downloadExcel, UITab, UiTable } from "@common/business_components/ui";
|
||||
import { Button, TabsContent } from "@common/components/ui";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { ReturnType } from "../../../components/common/FacilityTermModalHeader";
|
||||
import FacilityTermModalHeader from "../../../components/common/FacilityTermModalHeader";
|
||||
import type { ReturnType } from "../../../../components/common/FacilityTermModalHeader";
|
||||
import FacilityTermModalHeader from "../../../../components/common/FacilityTermModalHeader";
|
||||
import { HeadMakeColSpan } from "../HeadMakeColSpan";
|
||||
|
||||
type ModalProps ={
|
||||
+1
-1
@@ -5,7 +5,7 @@ import type { FlowRateModalInterface, modalExcelType } from "@/app/store/flowRat
|
||||
import { downloadExcel, UITab, UiTable } from "@common/business_components/ui";
|
||||
import { Button, TabsContent } from "@common/components/ui";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { ReturnType } from "../../../components/common/FacilityTermModalHeader";
|
||||
import type { ReturnType } from "../../../../components/common/FacilityTermModalHeader";
|
||||
|
||||
const tabList = [
|
||||
{ value: 'tab_term_year', label: '연간' },
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import Base from "@/app/base";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<Base>{children}</Base>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import type { TableUpperProps } from "@/app/store/common";
|
||||
import { TableHead, TableRow } from "@common/components/ui";
|
||||
import type { TableUpperProps } from "../../store/dashboard";
|
||||
|
||||
const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProps[], layoutNum: number }) => {
|
||||
return (
|
||||
@@ -4,10 +4,11 @@ NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302
|
||||
NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com #자료관 데이터
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
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_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api
|
||||
+6
-5
@@ -5,12 +5,13 @@ NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com #자료관 데이터
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
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 # 최용성 부장님 API
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api # 박이정 차장님 API
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 내 API
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api
|
||||
@@ -9,5 +9,6 @@ NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082
|
||||
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_DC_URL = https://apigateway.apps.rsonesoft.com #자료관 데이터
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service/api
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { NoFooterLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import useStore from '../store/useStore';
|
||||
@@ -33,12 +33,12 @@ export default function MapLayout({
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
<NoFooterLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
{children}
|
||||
</BaseLayout>
|
||||
</NoFooterLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from 'zod'
|
||||
import BaseLayout from '../BaseLayout'
|
||||
import { handleDelete } from '@/components/lib/utils'
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import useStore from '../../../store/useStore';
|
||||
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -80,6 +81,8 @@ const Result = () => {
|
||||
const [sggCd, setSggCd] = useState<number>(0); // 시군구 코드 상태
|
||||
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
|
||||
|
||||
const {ctpv, sgg} = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
// setCurPage(1);
|
||||
handleSearchButtonClick();
|
||||
@@ -137,12 +140,15 @@ const Result = () => {
|
||||
const dataWithRowNumber = (data.content || []).map((item: any, index: number) => {
|
||||
// 🎯 먼저 각 아이템을 포맷팅
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
const ctpvOne = ctpv.find(code => code.id === item.ctpvCd);
|
||||
const sggOne = sgg.find(code => code.id === item.sggCd);
|
||||
// 추가 처리 및 일련번호 추가
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
|
||||
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경
|
||||
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn, // scsYn이 null이면 'N'으로 변경
|
||||
ctpvNm: ctpvOne ? ctpvOne.name : item.ctpvCd,
|
||||
sggNm: sggOne ? sggOne.name : item.sggCd,
|
||||
};
|
||||
})
|
||||
.filter((item: any) => item.scsYn === 'Y');
|
||||
|
||||
@@ -8,7 +8,7 @@ import { z } from 'zod'
|
||||
import BaseLayout from '../BaseLayout'
|
||||
import { handleDelete } from '@/components/lib/utils'
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
|
||||
import useStore from '../../../store/useStore';
|
||||
|
||||
const formSchema = z.object({
|
||||
swtrmtFcltCd: z.string().default('0'), // 하수처리시설코드
|
||||
@@ -77,6 +77,7 @@ const Result = () => {
|
||||
const [ctpvCd, setCtpvCd] = useState<number>(0); // 시도 코드 상태
|
||||
const [sggCd, setSggCd] = useState<number>(0); // 시군구 코드 상태
|
||||
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
|
||||
const {ctpv, sgg} = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
// setCurPage(1);
|
||||
@@ -134,12 +135,15 @@ const Result = () => {
|
||||
const dataWithRowNumber = (data.content || []).map((item: any, index: number) => {
|
||||
// 🎯 먼저 각 아이템을 포맷팅
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
const ctpvOne = ctpv.find(code => code.id === item.ctpvCd);
|
||||
const sggOne = sgg.find(code => code.id === item.sggCd);
|
||||
// 추가 처리 및 일련번호 추가
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
|
||||
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경
|
||||
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn, // scsYn이 null이면 'N'으로 변경
|
||||
ctpvNm: ctpvOne ? ctpvOne.name : item.ctpvCd,
|
||||
sggNm: sggOne ? sggOne.name : item.sggCd,
|
||||
};
|
||||
})
|
||||
.filter((item: any) => item.scsYn === 'Y');
|
||||
|
||||
@@ -24,5 +24,5 @@ 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_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
|
||||
|
||||
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||
|
||||
@@ -11,7 +11,7 @@ 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_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
import type { MenuItem } from "@common/components/api";
|
||||
import { NoFooterLayout } from "@common/components/ui";
|
||||
import { usePathname,useRouter } from "next/navigation";
|
||||
import { useWrStore } from "../store";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const { getMenuAuthrts } = useWrStore((state) => state);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const changeMenu = (path: MenuItem) => {
|
||||
getMenuAuthrts(path.menuCd);
|
||||
router.push(path.urlPath);
|
||||
};
|
||||
return (
|
||||
<NoFooterLayout currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
|
||||
{children}
|
||||
</NoFooterLayout>
|
||||
);
|
||||
}
|
||||
+73
-33
@@ -2,11 +2,12 @@
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { mapService } from "../service";
|
||||
import { mapService } from "../../service";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { SearchBar } from "@common/business_components/ui/map/searchBar/searchBar";
|
||||
import { useWrStore } from "@/app/store";
|
||||
import { getFeatureCd } from "@/components/lib/func/\mapUtil";
|
||||
import { getFeatureCd } from "@/components/lib/func/mapUtil";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
|
||||
const Map = () => {
|
||||
const { comMap } = useWrStore((state) => state);
|
||||
@@ -23,7 +24,7 @@ const Map = () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await mapService.getLyrStngs();
|
||||
const response = await mapService.getLyrStngs('WR_NETWORK');
|
||||
setTreeData(response); // API 응답 저장
|
||||
} catch (error) {
|
||||
console.error("데이터 불러오기 실패:", error);
|
||||
@@ -173,7 +174,6 @@ const Map = () => {
|
||||
setIsSearchModalOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const getSelectedLayers = (isFeature: any) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) {
|
||||
@@ -206,33 +206,52 @@ const Map = () => {
|
||||
|
||||
const renderDetailView = useCallback(
|
||||
(props: any) => {
|
||||
const isFeature = props.featureData.getProperties();
|
||||
if (
|
||||
Object.keys(isFeature).length === 0 &&
|
||||
isFeature.constructor === Object
|
||||
) {
|
||||
const url = `/mapDetilePopUp?layerNm=${encodeURIComponent(props.seletectedLayerNm)}`;
|
||||
const width = window.screen.availWidth;
|
||||
const height = window.screen.availHeight;
|
||||
const left = 0;
|
||||
const top = 0;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
|
||||
} else {
|
||||
const layerNm = getSelectedLayers(featureData);
|
||||
const safeProps = featureData.getProperties();
|
||||
const featureCd = getFeatureCd(safeProps, String(layerNm));
|
||||
const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
|
||||
const width = Math.floor(window.innerWidth * 0.4); // 화면 폭의 40%
|
||||
const height = Math.floor(window.innerHeight - 450); // 150px 여백 확보; // calc(100% - 70px)
|
||||
const left = (window.screen.availWidth - width) / 2;
|
||||
const top = (window.screen.availHeight - height) / 2;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||
popupDetailRef.current = window.open(
|
||||
url,
|
||||
"지도 시설 상세정보 팝업",
|
||||
popupOptions
|
||||
);
|
||||
const map = gisMaps.current;
|
||||
if (!map) {
|
||||
console.warn("지도가 초기화되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isFeature = props?.featureData;
|
||||
|
||||
console.log('isFeature 1', isFeature)
|
||||
if( typeof isFeature.getProperties === "function" ){
|
||||
|
||||
if (
|
||||
Object.keys(isFeature?.getProperties()).length === 0 &&
|
||||
isFeature.constructor === Object
|
||||
) {
|
||||
const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(props.seletectedLayerNm)}`;
|
||||
const width = window.screen.availWidth;
|
||||
const height = window.screen.availHeight;
|
||||
const left = 0;
|
||||
const top = 0;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;
|
||||
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
|
||||
} else {
|
||||
const layerNm = getSelectedLayers(isFeature);
|
||||
const safeProps = isFeature.getProperties();
|
||||
const featureCd = getFeatureCd(safeProps, String(layerNm));
|
||||
const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
|
||||
const width =
|
||||
String(layerNm) === "하수재이용시설"
|
||||
? Math.floor(window.innerWidth * 0.8)
|
||||
: Math.floor(window.innerWidth * 0.4); // 화면 폭의 40%
|
||||
const height =
|
||||
String(layerNm) === "하수재이용시설"
|
||||
? Math.floor(window.innerHeight - 150)
|
||||
: Math.floor(window.innerHeight - 450); // 150px 여백 확보; // calc(100% - 70px)
|
||||
const left = (window.screen.availWidth - width) / 2;
|
||||
const top = (window.screen.availHeight - height) / 2;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||
popupDetailRef.current = window.open(
|
||||
url,
|
||||
"지도 시설 상세정보 팝업",
|
||||
popupOptions
|
||||
);
|
||||
}
|
||||
}else{
|
||||
console.log(' data확인 필요')
|
||||
}
|
||||
},
|
||||
[checkedLayers, checkedLayersData, featureData] // 필요 시 의존성 넣기
|
||||
@@ -246,6 +265,23 @@ const Map = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenPopup = (data:any) => {
|
||||
if(data){
|
||||
|
||||
const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(data)}`;
|
||||
const width = window.screen.availWidth;
|
||||
const height = window.screen.availHeight;
|
||||
const left = 0;
|
||||
const top = 0;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;
|
||||
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
|
||||
}else{
|
||||
toast.error('시설 및 수요처가 선택되지 않았습니다.')
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-container">
|
||||
<Gis
|
||||
@@ -255,7 +291,7 @@ const Map = () => {
|
||||
isMiniMap={false}
|
||||
searchView={undefined}
|
||||
detailView={renderDetailView}
|
||||
onFeatureSelect={onFeatureFocus}
|
||||
onFeatureSelect={onFeatureSelect}
|
||||
setIsOpen={setIsOpen}
|
||||
setFeatureData={setFeatureData}
|
||||
featureData={featureData}
|
||||
@@ -280,8 +316,12 @@ const Map = () => {
|
||||
/>
|
||||
}
|
||||
waterDepth={undefined}
|
||||
onOpenPopup={(data:any) =>{
|
||||
onOpenPopup(data)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -30,8 +30,8 @@ export type LyrStngProps = {
|
||||
|
||||
export const mapService = {
|
||||
//레이어 설정 조회
|
||||
getLyrStngs: async () => {
|
||||
const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, {
|
||||
getLyrStngs: async (lyrGrp : string) => {
|
||||
const response = await fetch(`${API_URL}/lyrStngs/${lyrGrp}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
+8
-8
@@ -16,7 +16,7 @@ export default function MapDetailPopUp() {
|
||||
const renderConditionalContent = () => {
|
||||
const layerId = searchParams.get("layerNm");
|
||||
switch (layerId) {
|
||||
case "하수재이용시설":
|
||||
case "tb_wr_bdst_ruse_fclt":
|
||||
return (
|
||||
<RuseFacility
|
||||
rtnFeature={(feature) => {
|
||||
@@ -24,7 +24,7 @@ export default function MapDetailPopUp() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "하수처리시설":
|
||||
case "tb_il_swage_prcs_fclt":
|
||||
return (
|
||||
<HasuFacility
|
||||
rtnFeature={(feature) => {
|
||||
@@ -32,7 +32,7 @@ export default function MapDetailPopUp() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "빗물이용시설":
|
||||
case "tb_wr_rnwtr_utztn_fclt":
|
||||
return (
|
||||
<RainFacility
|
||||
rtnFeature={(feature) => {
|
||||
@@ -40,7 +40,7 @@ export default function MapDetailPopUp() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "중수도시설":
|
||||
case "tb_wr_rswtr_fclt":
|
||||
return (
|
||||
<RswtFacility
|
||||
rtnFeature={(feature) => {
|
||||
@@ -48,7 +48,7 @@ export default function MapDetailPopUp() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "온배수시설":
|
||||
case "tb_wr_htdfl_fclt":
|
||||
return (
|
||||
<HtdflFacility
|
||||
rtnFeature={(feature) => {
|
||||
@@ -56,7 +56,7 @@ export default function MapDetailPopUp() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "수요처":
|
||||
case "tb_wr_ruse_fclt_dmd_co":
|
||||
return (
|
||||
<CoSttnMap
|
||||
rtnFeature={(feature) => {
|
||||
@@ -64,7 +64,7 @@ export default function MapDetailPopUp() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "잠재수요처":
|
||||
case "tb_wr_tmpr_dmd_co":
|
||||
return (
|
||||
<LdrmntCoMap
|
||||
rtnFeature={(feature) => {
|
||||
@@ -78,7 +78,7 @@ export default function MapDetailPopUp() {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div>{renderConditionalContent()}</div>
|
||||
<div className="h-full overflow-auto">{renderConditionalContent()}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect} from "react";
|
||||
import { Label } from "@common/components/ui/label";
|
||||
|
||||
export const DRGT_LAYER_TYPES: string[] = [
|
||||
// 수정 필요
|
||||
'drought1',
|
||||
'drought2',
|
||||
'drought3',
|
||||
'drought4',
|
||||
'drought5',
|
||||
'drought6',
|
||||
];
|
||||
// 지정된 순서에 따른 lyr_sn 배열
|
||||
export const EXPRS_ORDERED_LAYERS: string[] = [
|
||||
// 수정 필요
|
||||
'106', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도
|
||||
'107', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도
|
||||
'108', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도
|
||||
'109', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도
|
||||
'110', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도
|
||||
'111', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도
|
||||
];
|
||||
|
||||
export function DrgtLegend() {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-lg shadow-lg border border-gray-200 w-[160px] px-4 py-3">
|
||||
<div className="flex items-center justify-between mx-2 my-4">
|
||||
<Label className="text-3xl text-gray-900 font-bold">가뭄정보 범례</Label>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="w-full bg-gray-200 rounded-full h-1">
|
||||
<div className="bg-blue-900 h-1 rounded-full transition-all duration-300 ease-in-out" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[140px] mx-auto flex flex-col items-center">
|
||||
<div className="bg-blue-900 text-white text-2xl text-center py-1 mb-2 rounded w-full">가 뭄 정 보</div>
|
||||
<div className="space-y-1 w-full">
|
||||
<Label className="text-xl text-gray-900 font-bold">관심</Label>
|
||||
<LegendItem color="rgba(222,235,247, 1)" label="1회 미만" /> {/* 관심 - 1회 미만 */}
|
||||
<LegendItem color="rgba(46, 117, 182, 1)" label="1회 이상" /> {/* 관심 - 1회 이상 */}
|
||||
<Label className="text-xl text-gray-900 font-bold">주의</Label>
|
||||
<LegendItem color="rgba(255, 242, 204, 1)" label="1회 미만" /> {/* 주의 - 1회 미만 */}
|
||||
<LegendItem color="rgba(255, 217, 102, 1)" label="1회 이상" /> {/* 주의 - 1회 이상 */}
|
||||
<Label className="text-xl text-gray-900 font-bold">관심 + 주의</Label>
|
||||
<LegendItem color="rgba(251, 229, 214, 1)" label="1회 미만" /> {/* 관심+주의 - 1회 미만 */}
|
||||
<LegendItem color="rgba(237, 125, 49, 1)" label="1회 이상" /> {/* 관심+주의 - 1회 이상 */}
|
||||
</div>
|
||||
<div className="border-t-2 border-blue-900 mt-1 mx-auto w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LegendItem({ color, label }: { color: string; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<span
|
||||
className="inline-block w-16 h-6"
|
||||
style={{ backgroundColor: color, border: "1px solid #ccc" }}
|
||||
/>
|
||||
<span className="text-xl text-gray-900 font-bold">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getCenter } from "ol/extent";
|
||||
import { boundingExtent } from "ol/extent";
|
||||
import Feature from "ol/Feature";
|
||||
import Geometry from "ol/geom/Geometry";
|
||||
|
||||
export function medianCoordinate2D(feature: Feature<Geometry>) {
|
||||
// Feature를 입력 받아 중심 좌표를 계산하는 함수
|
||||
if (!feature) {
|
||||
console.warn("유효한 피처가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(feature)
|
||||
|
||||
const geometry = feature.getGeometry();
|
||||
if (!geometry) {
|
||||
console.warn("피처에 geometry가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const extent = geometry.getExtent();
|
||||
const center = getCenter(extent);
|
||||
const centerExtent = boundingExtent([center]);
|
||||
return centerExtent;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { mapService } from "../../(with-layout)/map/service";
|
||||
import { DrgtLegend, DRGT_LAYER_TYPES, EXPRS_ORDERED_LAYERS } from "./drgtLegend";
|
||||
import { SearchBar } from "./searchBar";
|
||||
|
||||
|
||||
export default function MapDrgtPopUp() {
|
||||
const searchParams = useSearchParams();
|
||||
const ctpvCd = searchParams.get('ctpvCd') || '';
|
||||
const sggCd = searchParams.get('sggCd') || '';
|
||||
const fromDate = searchParams.get('fromDate') || '';
|
||||
const toDate = searchParams.get('toDate') || '';
|
||||
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const [checkedLayers, setCheckedLayers] = useState<string[]>([]);
|
||||
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
|
||||
const [showDrgtLegend, setShowDrgtLegend] = useState(false); // DrgtLegend 표시 여부
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
// 컴포넌트 마운트 시 1회 실행
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
function fetchData() {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await mapService.getLyrStngs('WR_DROUGHT');
|
||||
setTreeData(response); // API 응답 저장
|
||||
console.log("Drought layers fetched:", response);
|
||||
} catch (error) {
|
||||
console.error("데이터 불러오기 실패:", error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// WaterDepth 표시 여부를 결정하는 함수
|
||||
const handleDrgtLegndVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
const drgtLegndLayers = checkedNodesData.filter(node =>
|
||||
DRGT_LAYER_TYPES.includes(node.data?.lyrTyNm)
|
||||
);
|
||||
|
||||
const hasDrgtLegndLayer = drgtLegndLayers.length > 0;
|
||||
setShowDrgtLegend(hasDrgtLegndLayer);
|
||||
}, []);
|
||||
|
||||
// 체크된 레이어 변경 처리 함수
|
||||
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
handleDrgtLegndVisibility(checkedIds, checkedNodesData);
|
||||
}, [handleDrgtLegndVisibility]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-container">
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={false}
|
||||
gisMaps={gisMaps}
|
||||
isMiniMap={false}
|
||||
searchView={undefined}
|
||||
detailView={undefined}
|
||||
onFeatureSelect={undefined}
|
||||
setIsOpen={setIsOpen}
|
||||
setFeatureData={undefined}
|
||||
featureData={undefined}
|
||||
setIsSearchModalOpen={undefined}
|
||||
isOpen={isOpen}
|
||||
isSearchModalOpen={undefined}
|
||||
onCheckedLayersChange={handleCheckedLayersChange}
|
||||
imgVisble={true}
|
||||
searchBar={<SearchBar gisMaps={gisMaps} ctpvCode={ctpvCd} sggCode={sggCd} fromDate={fromDate} toDate={toDate} />}
|
||||
waterDepth={showDrgtLegend ? DrgtLegend : undefined}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying as any}
|
||||
targetLayerTypes={DRGT_LAYER_TYPES as any}
|
||||
orderedLayers={EXPRS_ORDERED_LAYERS as any}
|
||||
checkInterval={2500}
|
||||
height={`h-[100vh]`}
|
||||
droughtDates={{ startYmd: fromDate, endYmd: toDate }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { SelectBox } from '@common/business_components/ui'
|
||||
import { Label, Button } from '@common/components';
|
||||
import { type SelectDataType } from "@common/business_components/ui";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import GeoJSON from "ol/format/GeoJSON";
|
||||
import Feature from "ol/Feature";
|
||||
import Geometry from "ol/geom/Geometry";
|
||||
import { medianCoordinate2D } from "./median";
|
||||
|
||||
interface CtpvSggPrcsFcltSelectBoxProps {
|
||||
gisMaps: React.RefObject<any>;
|
||||
ctpvCode: string;
|
||||
sggCode: string;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTION: SelectDataType = { val: '0', text: '전체' };
|
||||
|
||||
export function SearchBar({ gisMaps, ctpvCode, sggCode }: CtpvSggPrcsFcltSelectBoxProps) {
|
||||
const [ctpvCd, setCtpvCd] = useState<string>(ctpvCode);
|
||||
const [sggCd, setSggCd] = useState<string>(sggCode);
|
||||
const [ctpv, setCtpv] = useState<SelectDataType[]>([]);
|
||||
const [sgg, setSgg] = useState<SelectDataType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await fetchSido();
|
||||
await fetchSigungu();
|
||||
setCtpvCd(ctpvCode);
|
||||
setSggCd(sggCode);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
function onViewMapClick() {
|
||||
if (sggCd && sggCd !== '0') {
|
||||
fetchSggData();
|
||||
return;
|
||||
}
|
||||
if (ctpvCd && ctpvCd !== '0') fetchCtpvData();
|
||||
}
|
||||
|
||||
async function fetchCtpvData() {
|
||||
try {
|
||||
const ctpvData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_ctpv&outputFormat=application%2Fjson&cql_filter=mctpv_cd=%27${ctpvCd.trim()}%27`)
|
||||
.then(res => res.json());
|
||||
console.log('시도 데이터', ctpvData.features[0]);
|
||||
|
||||
const format = new GeoJSON();
|
||||
const feature: Feature<Geometry> = format.readFeature(ctpvData.features[0], {
|
||||
dataProjection: "EPSG:3857", // GeoJSON 좌표계
|
||||
featureProjection: GisApp.TO_PROJ // 지도 좌표계
|
||||
});
|
||||
|
||||
const extent = medianCoordinate2D(feature);
|
||||
|
||||
handleFeatureFocus(extent);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("시도 data fetching Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSggData() {
|
||||
try {
|
||||
const sggData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_sgg&outputFormat=application%2Fjson&cql_filter=sgg_cd=%27${sggCd}%27`)
|
||||
.then(res => res.json());
|
||||
console.log('시군구 데이터', sggData.features[0]);
|
||||
|
||||
|
||||
const format = new GeoJSON();
|
||||
const feature: Feature<Geometry> = format.readFeature(sggData.features[0], {
|
||||
dataProjection: "EPSG:3857", // GeoJSON 좌표계
|
||||
featureProjection: GisApp.TO_PROJ // 지도 좌표계
|
||||
});
|
||||
|
||||
const extent = medianCoordinate2D(feature);
|
||||
|
||||
handleFeatureFocus(extent);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("시군구 data fetching Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFeatureFocus(extent: any) {
|
||||
console.log('ctpv', ctpv);
|
||||
console.log('sgg', sgg);
|
||||
const map = gisMaps.current;
|
||||
if (!map) {
|
||||
console.warn("지도가 초기화되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 11
|
||||
});
|
||||
};
|
||||
|
||||
const fetchSido = async () => {
|
||||
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`;
|
||||
const res = await fetch(url).then(res => res.json());
|
||||
|
||||
const list = res.features.map((f: any) => ({
|
||||
text: f.properties.mctpv_korn_nm,
|
||||
val: f.properties.mctpv_cd,
|
||||
}));
|
||||
|
||||
console.log('시도 데이터', list);
|
||||
setCtpv(list);
|
||||
};
|
||||
|
||||
const fetchSigungu = async () => {
|
||||
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`;
|
||||
const res = await fetch(url).then(res => res.json());
|
||||
|
||||
const list = res.features.map((f: any) => ({
|
||||
text: f.properties.sgg_korn_nm,
|
||||
val: f.properties.sgg_cd,
|
||||
}));
|
||||
|
||||
console.log('시군구 데이터', list);
|
||||
setSgg(list);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 bg-white min-h-[48px] min-w-[430px] px-5 py-3 rounded-lg shadow">
|
||||
<div className={`flex items-center justify-center mr-8`}>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">시도</Label>
|
||||
<SelectBox
|
||||
className="w-60 mr-8"
|
||||
selectClass="rounded-lg bg-white"
|
||||
label=""
|
||||
selectArray={[DEFAULT_OPTION, ...ctpv]}
|
||||
value={ctpvCd}
|
||||
onSelectValue={val => {
|
||||
setCtpvCd(String(val));
|
||||
setSggCd(String(0));
|
||||
}}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">시군구</Label>
|
||||
<SelectBox
|
||||
className="w-48"
|
||||
selectClass="w-48 rounded-lg bg-white truncate"
|
||||
label=""
|
||||
selectArray={
|
||||
[DEFAULT_OPTION,
|
||||
...sgg.filter(opt => String(opt.val).slice(0, 2) === String(ctpvCd).trim())
|
||||
]
|
||||
}
|
||||
value={sggCd}
|
||||
onSelectValue={val => { setSggCd(String(val)) }}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-1 justify-end '>
|
||||
<Button size="sm" onClick={() => onViewMapClick()}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useWrStore } from "@/app/store";
|
||||
import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal";
|
||||
import { RuseDetailPopup } from "@/components/ui/map/detailPopup/ruseDetailPopup";
|
||||
import CoDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/coDatailModal";
|
||||
import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal";
|
||||
import { Title, UiTable } from "@common/business_components/ui";
|
||||
@@ -11,7 +11,6 @@ export default function MapFeaturePopup() {
|
||||
const [isFeatureCd, setIsFeatureCd] = useState<any>(null);
|
||||
const {
|
||||
pbnMap,
|
||||
ruseMapDetail,
|
||||
rainMapDetile,
|
||||
rswtMapDetile,
|
||||
htdfMapDetile,
|
||||
@@ -46,10 +45,8 @@ export default function MapFeaturePopup() {
|
||||
|
||||
useEffect(() => {
|
||||
if (isFeatureCd && layerNm) {
|
||||
let detail = {};
|
||||
if (layerNm === "하수재이용시설") {
|
||||
detail = ruseMapDetail(isFeatureCd);
|
||||
setIsDetail(detail);
|
||||
setIsDetail(isFeatureCd);
|
||||
} else if (layerNm === "하수처리시설") {
|
||||
pbnMapDetaile(isFeatureCd, isFeatureCd).then((res) => {
|
||||
setIsDetail(res);
|
||||
@@ -78,9 +75,9 @@ export default function MapFeaturePopup() {
|
||||
return (
|
||||
<>
|
||||
{isDetail ? (
|
||||
<div className="fixed inset-0 w-screen h-screen m-0 p-0 overflow-auto bg-white">
|
||||
<div className="fixed inset-0 w-screen h-screen m-0 p-0 bg-white">
|
||||
{layerNm === "하수재이용시설" ? (
|
||||
<PbnstSttnModal data={isDetail} />
|
||||
<RuseDetailPopup bscFctCd={isDetail} />
|
||||
) : layerNm === "하수처리시설" ? (
|
||||
<div style={{ overflowX: "hidden" }}>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@@ -125,6 +122,7 @@ export default function MapFeaturePopup() {
|
||||
: "HTB"
|
||||
}
|
||||
formObj={isDetail}
|
||||
isMap={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,269 +9,282 @@ import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/b
|
||||
import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui";
|
||||
import ListDiv from "@common/business_components/ui/list_div";
|
||||
import { Button, Label, ScrollArea } from "@common/components/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { toast, Toaster } from "react-hot-toast";
|
||||
|
||||
export default function DrgtSttn() {
|
||||
const { code, drgt, drgtSearch, drgtModalSearch, drgtInit } = useWrStore((state) => state);
|
||||
const { drgtRes, drgtMoRes } = drgt;
|
||||
const [fromDate, setFromDate] = useState<string>('202506');
|
||||
const [toDate, setToDate] = useState<string>('202506');
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[]>([{ val: '0', text: '전체' }]);
|
||||
const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" });
|
||||
const [tableData, setTableData] = useState<customDrgt[]>();
|
||||
const [selectedSgg, setSelectedSgg] = useState<string>('');
|
||||
const [excelData, setExcelData] = useState<any[]>();
|
||||
const [drgtModal, setDrgtModal] = useState<{
|
||||
isOpen: boolean;
|
||||
data: customDrgt[] | undefined;
|
||||
}>({ data: undefined, isOpen: false });
|
||||
const { code, drgt, drgtSearch, drgtModalSearch, drgtInit } = useWrStore((state) => state);
|
||||
const { drgtRes, drgtMoRes } = drgt;
|
||||
const [fromDate, setFromDate] = useState<string>('202506');
|
||||
const [toDate, setToDate] = useState<string>('202506');
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[]>([{ val: '0', text: '전체' }]);
|
||||
const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" });
|
||||
const [tableData, setTableData] = useState<customDrgt[]>();
|
||||
const [selectedSgg, setSelectedSgg] = useState<string>('');
|
||||
const [excelData, setExcelData] = useState<any[]>();
|
||||
const [drgtModal, setDrgtModal] = useState<{
|
||||
isOpen: boolean;
|
||||
data: customDrgt[] | undefined;
|
||||
}>({ data: undefined, isOpen: false });
|
||||
const popupRef = useRef<Window | null>(null);
|
||||
|
||||
const cellClick = (index: number) => {
|
||||
if (fromDate && toDate && drgtRes[index]) {
|
||||
drgtModalSearch(fromDate, toDate, drgtRes[index].sggNm ?? '');
|
||||
} else {
|
||||
toast.error("조회기간을 선택해주세요.");
|
||||
}
|
||||
const cellClick = (index: number) => {
|
||||
if (fromDate && toDate && drgtRes[index]) {
|
||||
drgtModalSearch(fromDate, toDate, drgtRes[index].sggNm ?? '');
|
||||
} else {
|
||||
toast.error("조회기간을 선택해주세요.");
|
||||
}
|
||||
}
|
||||
|
||||
const excelClick = () => {
|
||||
downloadExcel({
|
||||
data: excelData ?? [],
|
||||
fileName: "우리동네가뭄 현황",
|
||||
sheetNameArray: ["Sheet1"],
|
||||
headerRange: "A1:G1"
|
||||
})
|
||||
const excelClick = () => {
|
||||
downloadExcel({
|
||||
data: excelData ?? [],
|
||||
fileName: "우리동네가뭄 현황",
|
||||
sheetNameArray: ["Sheet1"],
|
||||
headerRange: "A1:G1"
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!drgt.isInit) {
|
||||
drgtInit();
|
||||
}
|
||||
}, [drgt.isInit, drgtInit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drgt.isInit) {
|
||||
drgtInit();
|
||||
useEffect(() => {
|
||||
let resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes];
|
||||
resData = resData.map((item) => {
|
||||
if (item.sggNm === '세종시') {
|
||||
return {
|
||||
...item,
|
||||
ctpvCd: '세종특별자치시',
|
||||
sggNm: ''
|
||||
}
|
||||
}, [drgt.isInit, drgtInit]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes];
|
||||
resData = resData.map((item) => {
|
||||
if (item.sggNm === '세종시') {
|
||||
return {
|
||||
...item,
|
||||
ctpvCd: '세종특별자치시',
|
||||
sggNm: ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
ctpvCd: code.cityCd.find(ct => (ct.text == item.sggNm || (code.sggCd.find(sgg => sgg.text == item.sggNm)?.val.substring(0, 2) == ct.val)))?.text,
|
||||
sggNm: code.sggCd.find(sgg => sgg.text == item.sggNm)?.text
|
||||
}
|
||||
})
|
||||
const data = Array.isArray(resData)
|
||||
? resData.map((item: customDrgt) => {
|
||||
return {
|
||||
"시도": item.ctpvCd,
|
||||
"시군구": item.sggNm,
|
||||
"정상(횟수)": item.normal,
|
||||
"관심(횟수)": item.attention,
|
||||
"주의(횟수)": item.caution,
|
||||
"관심+주의(횟수)": item.warning,
|
||||
"가뭄순위": item.ranking,
|
||||
}
|
||||
}) : [];
|
||||
setExcelData(data)
|
||||
setTableData(resData)
|
||||
}, [drgtRes])
|
||||
|
||||
useEffect(() => {
|
||||
if (drgtMoRes && drgtMoRes.length > 0) {
|
||||
setDrgtModal({ isOpen: true, data: drgtMoRes });
|
||||
return {
|
||||
...item,
|
||||
ctpvCd: code.cityCd.find(ct => (ct.text == item.sggNm || (code.sggCd.find(sgg => sgg.text == item.sggNm)?.val.substring(0, 2) == ct.val)))?.text,
|
||||
sggNm: code.sggCd.find(sgg => sgg.text == item.sggNm)?.text
|
||||
}
|
||||
})
|
||||
const data = Array.isArray(resData)
|
||||
? resData.map((item: customDrgt) => {
|
||||
return {
|
||||
"시도": item.ctpvCd,
|
||||
"시군구": item.sggNm,
|
||||
"정상(횟수)": item.normal,
|
||||
"관심(횟수)": item.attention,
|
||||
"주의(횟수)": item.caution,
|
||||
"관심+주의(횟수)": item.warning,
|
||||
"가뭄순위": item.ranking,
|
||||
}
|
||||
}, [drgtMoRes]);
|
||||
}) : [];
|
||||
setExcelData(data)
|
||||
setTableData(resData)
|
||||
}, [drgtRes])
|
||||
|
||||
const getSelCity = (val: string | number) => {
|
||||
const cityValue = String(val);
|
||||
useEffect(() => {
|
||||
if (drgtMoRes && drgtMoRes.length > 0) {
|
||||
setDrgtModal({ isOpen: true, data: drgtMoRes });
|
||||
}
|
||||
}, [drgtMoRes]);
|
||||
|
||||
if (cityValue === "0") {
|
||||
setSelectSgg([{ val: '0', text: '전체' }]);
|
||||
setSelectedSgg('0');
|
||||
setObjSearch({ city: cityValue, sgg: '0' });
|
||||
} else {
|
||||
const data = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(data);
|
||||
const getSelCity = (val: string | number) => {
|
||||
const cityValue = String(val);
|
||||
|
||||
const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? "");
|
||||
setSelectedSgg(firstSggVal);
|
||||
setObjSearch({ city: cityValue, sgg: firstSggVal });
|
||||
}
|
||||
};
|
||||
if (cityValue === "0") {
|
||||
setSelectSgg([{ val: '0', text: '전체' }]);
|
||||
setSelectedSgg('0');
|
||||
setObjSearch({ city: cityValue, sgg: '0' });
|
||||
} else {
|
||||
const data = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(data);
|
||||
|
||||
const getSgg = (val: string | number) => {
|
||||
setSelectedSgg(String(val));
|
||||
setObjSearch({ ...objSearch, sgg: String(val) });
|
||||
};
|
||||
const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? "");
|
||||
setSelectedSgg(firstSggVal);
|
||||
setObjSearch({ city: cityValue, sgg: firstSggVal });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (fromDate === undefined && toDate === undefined) {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const formatted = `${year}${month}`;
|
||||
setFromDate(formatted);
|
||||
setToDate(formatted);
|
||||
}
|
||||
}, [fromDate, toDate])
|
||||
return (
|
||||
<>
|
||||
<ContentsDiv>
|
||||
<div className="grid grid-cols-4 gap-4 ">
|
||||
<div className="col-span-4">
|
||||
<Title title={"우리동네가뭄"} />
|
||||
<SearchDiv>
|
||||
<SearchFormLeft>
|
||||
<div className="col-span-10 flex flex-row">
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl"
|
||||
label="시도"
|
||||
selectClass={""}
|
||||
selectArray={code.cityCd || []}
|
||||
onSelectValue={getSelCity}
|
||||
value={objSearch?.city ||code.cityCd[0]?.val ||""}
|
||||
/>
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-8"
|
||||
label="시군구"
|
||||
selectClass={""}
|
||||
selectArray={selectSgg || [{ val: '0', text: '전체' }]}
|
||||
onSelectValue={getSgg}
|
||||
value={selectedSgg ||'0'}
|
||||
/>
|
||||
<div className="col-span-5 w-auto">
|
||||
<div className="flex flex-row items-center">
|
||||
<Label className="text-xl text-gray-600 ml-8">조회기간</Label>
|
||||
<div className="flex flex-row w-1/4">
|
||||
<DateMonthPicker
|
||||
field={{
|
||||
value: fromDate ?? "",
|
||||
onChange: (date: string | undefined) => {
|
||||
const year = date ? date.slice(0, 4) : '';
|
||||
let month = date?.slice(4);
|
||||
if (month&&month?.length < 2) {
|
||||
month = month?.padStart(2, "0");
|
||||
}
|
||||
const formatted = `${year}${month}`;
|
||||
setFromDate(formatted);
|
||||
}
|
||||
}}
|
||||
selectClass="ml-4 text-2xl"
|
||||
/>
|
||||
<span className="ml-2 text-center">~</span>
|
||||
<DateMonthPicker
|
||||
field={{
|
||||
value: toDate ?? "",
|
||||
onChange: (date: string | undefined) => {
|
||||
const year = date ? date.slice(0, 4) : '';
|
||||
let month = date?.slice(4);
|
||||
if (month&&month.length < 2) {
|
||||
month = month?.padStart(2, "0");
|
||||
}
|
||||
const formatted = `${year}${month}`;
|
||||
setToDate(formatted);
|
||||
}
|
||||
}}
|
||||
selectClass="ml-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchFormLeft>
|
||||
<SearchFormRight
|
||||
className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 ">
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Button
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
let sidoText: string = '';
|
||||
let sggText: string | string[] = '';
|
||||
const getSgg = (val: string | number) => {
|
||||
setSelectedSgg(String(val));
|
||||
setObjSearch({ ...objSearch, sgg: String(val) });
|
||||
};
|
||||
|
||||
if (objSearch?.city.toString().length > 0) {
|
||||
sidoText = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? '';
|
||||
}
|
||||
if (objSearch?.sgg.toString().length > 1) {
|
||||
sggText = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? '';
|
||||
} else {
|
||||
sggText = code.sggCd.filter((item) => item.val.substring(0, 2) === objSearch?.city).map((item) => item.text);
|
||||
useEffect(() => {
|
||||
if (fromDate === undefined && toDate === undefined) {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const formatted = `${year}${month}`;
|
||||
setFromDate(formatted);
|
||||
setToDate(formatted);
|
||||
}
|
||||
}, [fromDate, toDate])
|
||||
|
||||
}
|
||||
if (sidoText === '전체') { sidoText = '' }
|
||||
function onViewMapClick() {
|
||||
const params = new URLSearchParams({
|
||||
ctpvCd: String(objSearch.city),
|
||||
sggCd: String(objSearch.sgg),
|
||||
fromDate: fromDate || '',
|
||||
toDate: toDate || ''
|
||||
});
|
||||
const popupOptions = `width=${window.screen.availWidth},height=${window.screen.availHeight},left=0,top=0,resizable=yes,scrollbars=yes`;
|
||||
popupRef.current = window.open(`/mapDrgtPopUp?${params.toString()}`, "가뭄 지도 보기 팝업", popupOptions);
|
||||
}
|
||||
|
||||
if (sidoText === '세종특별자치시') {
|
||||
sidoText = '세종시';
|
||||
sggText = sidoText;
|
||||
}
|
||||
|
||||
const searchArr:string[] = [];
|
||||
if (sidoText) searchArr.push(sidoText);
|
||||
if (Array.isArray(sggText)) {
|
||||
searchArr.push(...sggText);
|
||||
} else if (sggText) {
|
||||
searchArr.push(sggText);
|
||||
}
|
||||
|
||||
drgtSearch(fromDate ?? "", toDate ?? "", searchArr)
|
||||
}}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className={`${btnClass} green`}
|
||||
onClick={excelClick}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className={btnClass}
|
||||
onClick={() => { }}
|
||||
>
|
||||
지도보기
|
||||
</Button>
|
||||
</div>
|
||||
</SearchFormRight>
|
||||
</SearchDiv>
|
||||
return (
|
||||
<>
|
||||
<ContentsDiv>
|
||||
<div className="grid grid-cols-4 gap-4 ">
|
||||
<div className="col-span-4">
|
||||
<Title title={"우리동네가뭄"} />
|
||||
<SearchDiv>
|
||||
<SearchFormLeft>
|
||||
<div className="col-span-10 flex flex-row">
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl"
|
||||
label="시도"
|
||||
selectClass={""}
|
||||
selectArray={code.cityCd || []}
|
||||
onSelectValue={getSelCity}
|
||||
value={objSearch?.city || code.cityCd[0]?.val || ""}
|
||||
/>
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-8"
|
||||
label="시군구"
|
||||
selectClass={""}
|
||||
selectArray={selectSgg || [{ val: '0', text: '전체' }]}
|
||||
onSelectValue={getSgg}
|
||||
value={selectedSgg || '0'}
|
||||
/>
|
||||
<div className="col-span-5 w-auto">
|
||||
<div className="flex flex-row items-center">
|
||||
<Label className="text-xl text-gray-600 ml-8">조회기간</Label>
|
||||
<div className="flex flex-row w-1/4">
|
||||
<DateMonthPicker
|
||||
field={{
|
||||
value: fromDate ?? "",
|
||||
onChange: (date: string | undefined) => {
|
||||
const year = date ? date.slice(0, 4) : '';
|
||||
let month = date?.slice(4);
|
||||
if (month && month?.length < 2) {
|
||||
month = month?.padStart(2, "0");
|
||||
}
|
||||
const formatted = `${year}${month}`;
|
||||
setFromDate(formatted);
|
||||
}
|
||||
}}
|
||||
selectClass="ml-4 text-2xl"
|
||||
/>
|
||||
<span className="ml-2 text-center">~</span>
|
||||
<DateMonthPicker
|
||||
field={{
|
||||
value: toDate ?? "",
|
||||
onChange: (date: string | undefined) => {
|
||||
const year = date ? date.slice(0, 4) : '';
|
||||
let month = date?.slice(4);
|
||||
if (month && month.length < 2) {
|
||||
month = month?.padStart(2, "0");
|
||||
}
|
||||
const formatted = `${year}${month}`;
|
||||
setToDate(formatted);
|
||||
}
|
||||
}}
|
||||
selectClass="ml-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4 ">
|
||||
<div className="col-span-6">
|
||||
<ListDiv>
|
||||
<ScrollArea className="h-[66vh]">
|
||||
<UiTable
|
||||
headName={undefined}
|
||||
tableData={tableData || []}
|
||||
headlist={drgtHeadListExport}
|
||||
pageSize={14}
|
||||
total={0}
|
||||
cellClick={(index: number | undefined) => {
|
||||
cellClick(index ?? -1);
|
||||
}}
|
||||
onPageChange={() => { }}
|
||||
></UiTable>
|
||||
</ScrollArea>
|
||||
</ListDiv>
|
||||
</div>
|
||||
</div>
|
||||
<UiModal
|
||||
open={drgtModal.isOpen}
|
||||
onOpenChange={() => {
|
||||
setDrgtModal({ ...drgtModal, isOpen: !drgtModal.isOpen });
|
||||
</SearchFormLeft>
|
||||
<SearchFormRight
|
||||
className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 ">
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Button
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
let sidoText: string = '';
|
||||
let sggText: string | string[] = '';
|
||||
|
||||
if (objSearch?.city.toString().length > 0) {
|
||||
sidoText = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? '';
|
||||
}
|
||||
if (objSearch?.sgg.toString().length > 1) {
|
||||
sggText = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? '';
|
||||
} else {
|
||||
sggText = code.sggCd.filter((item) => item.val.substring(0, 2) === objSearch?.city).map((item) => item.text);
|
||||
|
||||
}
|
||||
if (sidoText === '전체') { sidoText = '' }
|
||||
|
||||
if (sidoText === '세종특별자치시') {
|
||||
sidoText = '세종시';
|
||||
sggText = sidoText;
|
||||
}
|
||||
|
||||
const searchArr: string[] = [];
|
||||
if (sidoText) searchArr.push(sidoText);
|
||||
if (Array.isArray(sggText)) {
|
||||
searchArr.push(...sggText);
|
||||
} else if (sggText) {
|
||||
searchArr.push(sggText);
|
||||
}
|
||||
|
||||
drgtSearch(fromDate ?? "", toDate ?? "", searchArr)
|
||||
}}
|
||||
bodyClass={"w-[50vw] max-w-none h-[80vh]"}
|
||||
body={<DrgtDetailModal data={drgtModal.data as customDrgt[]} />}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</ContentsDiv>
|
||||
</>
|
||||
)
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className={`${btnClass} green`}
|
||||
onClick={excelClick}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className={btnClass}
|
||||
onClick={onViewMapClick}
|
||||
>
|
||||
지도보기
|
||||
</Button>
|
||||
</div>
|
||||
</SearchFormRight>
|
||||
</SearchDiv>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4 ">
|
||||
<div className="col-span-6">
|
||||
<ListDiv>
|
||||
<ScrollArea className="h-[66vh]">
|
||||
<UiTable
|
||||
headName={undefined}
|
||||
tableData={tableData || []}
|
||||
headlist={drgtHeadListExport}
|
||||
pageSize={14}
|
||||
total={0}
|
||||
cellClick={(index: number | undefined) => {
|
||||
cellClick(index ?? -1);
|
||||
}}
|
||||
onPageChange={() => { }}
|
||||
></UiTable>
|
||||
</ScrollArea>
|
||||
</ListDiv>
|
||||
</div>
|
||||
</div>
|
||||
<UiModal
|
||||
open={drgtModal.isOpen}
|
||||
onOpenChange={() => {
|
||||
setDrgtModal({ ...drgtModal, isOpen: !drgtModal.isOpen });
|
||||
}}
|
||||
bodyClass={"w-[50vw] max-w-none h-[80vh]"}
|
||||
body={<DrgtDetailModal data={drgtModal.data as customDrgt[]} />}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</ContentsDiv>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { cmsPostFectch } from "@/components/lib/api";
|
||||
|
||||
export const isLogin = async () => {
|
||||
const headers = {
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": "WR",
|
||||
"Access-Control-Expose-Headers" : ["Authorization","refresh-token","token-id"]
|
||||
};
|
||||
const param = { userId: "tuser10003", password: "test1234!" };
|
||||
await cmsPostFectch("/user-service/login",param,headers);
|
||||
};
|
||||
|
||||
// export type LoginState = {
|
||||
// login: {} as any;
|
||||
// };
|
||||
|
||||
// export type loginType = LoginState & LoginActions;
|
||||
|
||||
// export const loginState: LoginState = {
|
||||
// login: {},
|
||||
// };
|
||||
|
||||
// export type LoginActions = {
|
||||
// isLogin: () => void;
|
||||
// };
|
||||
|
||||
// export const loginActionExport: (set: any) => LoginActions = (set: any) => {
|
||||
// return {
|
||||
// isLogin: async () => {},
|
||||
// };
|
||||
// };
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchType, getFetch } from "@/components/lib/api";
|
||||
import { fetchType, getFetch, postFectch } from "@/components/lib/api";
|
||||
import type { CommonTableProps } from "../../common/type/header";
|
||||
import type { WaterReuseFacility } from "../../ruseFcltMng/fcltSttn/pbnstSttn/type";
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ export type ruseMapState = {
|
||||
ruseMapCount: number;
|
||||
isInit: boolean;
|
||||
fclts: string[];
|
||||
ruseDetailList: customPbnst[];
|
||||
ruseMapHeadList: CommonTableProps[];
|
||||
ruseMapNodeList: CommonTableProps[];
|
||||
};
|
||||
@@ -31,14 +32,17 @@ type ruseMapActions = {
|
||||
fclts: string[]
|
||||
) => void;
|
||||
ruseMapDetail: (ruseFcltId: string) => Promise<customPbnst | undefined>;
|
||||
getRuseMaplList: (bscFctCd: string) => void;
|
||||
};
|
||||
|
||||
|
||||
export type ruseMapType = ruseMapState & ruseMapActions;
|
||||
|
||||
export const ruseMapStateExport: ruseMapState = {
|
||||
ruseMap: {
|
||||
isInit: false,
|
||||
ruseMapRes: [],
|
||||
ruseDetailList: [],
|
||||
fclts: [],
|
||||
ruseMapCount: 1,
|
||||
ruseMapHeadList: pbnSttHeadListExport,
|
||||
@@ -107,5 +111,42 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
|
||||
throw errorHasu; // 호출부에서 catch 가능하도록
|
||||
}
|
||||
},
|
||||
getRuseMaplList: async (bscFctCd: string) => {
|
||||
try {
|
||||
const ruseDetailList = await getFetch(
|
||||
fetchType.WR,
|
||||
`/reuseFc/pubFc/${bscFctCd}`
|
||||
);
|
||||
const year = new Date().getFullYear();
|
||||
const data = [bscFctCd];
|
||||
const rusePrfList = await postFectch(
|
||||
`/reuseFc/pubDetailList/${year}/0/1/10`,
|
||||
data,
|
||||
fetchType.WR
|
||||
);
|
||||
const dataList = ruseDetailList.map((ruse) => {
|
||||
const matchRuse = rusePrfList?.data.find(
|
||||
(prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId
|
||||
);
|
||||
return {
|
||||
...ruse,
|
||||
prfList: matchRuse.prfList,
|
||||
};
|
||||
});
|
||||
console.log("/dataList ", dataList);
|
||||
set((state: ruseMapState) => {
|
||||
return {
|
||||
...state,
|
||||
ruseMap: {
|
||||
...state.ruseMap,
|
||||
ruseDetailList: dataList,
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (errorHasu) {
|
||||
console.log(errorHasu);
|
||||
throw errorHasu; // 호출부에서 catch 가능하도록
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -231,6 +231,30 @@ export const fcBizSchema = z.object({
|
||||
mdfr: z.string().nullish(),
|
||||
});
|
||||
|
||||
const PrfSchema = z.object({
|
||||
pbnstRuseFcltId: z.string(),
|
||||
ym: z.string(), // 예: "202310"
|
||||
wshwtr: z.number(),
|
||||
coolUswtr: z.number(),
|
||||
cleanUswtr: z.number(),
|
||||
drwtUswtr: z.number(),
|
||||
dltnUswtr: z.number(),
|
||||
ithlRswtr: z.number(),
|
||||
ithlEtcUswtr: z.number(),
|
||||
cleanToilUswtr: z.number(),
|
||||
resn: z.number(),
|
||||
lndwtr: z.number(),
|
||||
hydUswtr: z.number(),
|
||||
rvrKeepUswtr: z.number(),
|
||||
farmUswtr: z.number(),
|
||||
ugwtChr: z.number(),
|
||||
indstUswtr: z.number(),
|
||||
regDt: z.date().default(new Date()).nullish(),
|
||||
rgtr: z.string().nullish(),
|
||||
mdfcnDt: z.date().default(new Date()).nullish(),
|
||||
mdfr: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const pbnstRegZod = z.object({
|
||||
pbnstRuseFcltId: z.string().default(""),
|
||||
swtrmtFcltId: z.string().default(""),
|
||||
@@ -258,11 +282,14 @@ export const pbnstRegZod = z.object({
|
||||
fcBizs: z.array(fcBizSchema).default([]),
|
||||
fcOpers://z.array(fcOperSchema).default([]),
|
||||
z .array(fcOperSchema)
|
||||
.min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" })
|
||||
.min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" }),
|
||||
prfList: z.array(PrfSchema).nullish(),
|
||||
});
|
||||
|
||||
export const pbnstRegArrayZod = z.array(pbnstRegZod);
|
||||
|
||||
|
||||
|
||||
export const pbnstRegArrayZod = z.array(pbnstRegZod);
|
||||
export type PbnstRegArrrayType = z.infer<typeof pbnstRegArrayZod>;
|
||||
|
||||
export type fcBizType = z.infer<typeof fcBizSchema>;
|
||||
|
||||
@@ -161,7 +161,6 @@ import {
|
||||
type rswtStatState,
|
||||
type rswtStatType,
|
||||
} from "./stats/rswtStat";
|
||||
//import { isLogin } from "./common/login";
|
||||
import { LoginApi } from "@/src/entities/auth";
|
||||
import {
|
||||
authActionsExport,
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
//8880
|
||||
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/";
|
||||
import { getCookie } from "@common/components/lib/cookie";
|
||||
|
||||
export const fetchType = { COMMON: "common", WR: "wr", LOCAL: "LOCAL" , CS: "CS" };
|
||||
export const fetchType = {
|
||||
COMMON: "common",
|
||||
WR: "wr",
|
||||
LOCAL: "LOCAL",
|
||||
CS: "CS",
|
||||
};
|
||||
|
||||
export const getFetch = async (type: string, url: string) => {
|
||||
const div =
|
||||
type === fetchType.COMMON
|
||||
? DEV_URL_FO
|
||||
: type === fetchType.LOCAL
|
||||
? LOCAL_URL
|
||||
: type === fetchType.CS
|
||||
? ''
|
||||
: DEV_URL_WR;
|
||||
type === fetchType.COMMON
|
||||
? DEV_URL_FO
|
||||
: type === fetchType.LOCAL
|
||||
? LOCAL_URL
|
||||
: type === fetchType.CS
|
||||
? ""
|
||||
: DEV_URL_WR;
|
||||
|
||||
const tokenId = getCookie(AUTH_USER_ID);
|
||||
const Authorization = getCookie(CLAIM_NAME);
|
||||
const response = await fetch(div + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
AUTH_USER_ID: tokenId ?? "",
|
||||
CLAIM_NAME: Authorization ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`오류 발생: ${response.status}`);
|
||||
console.log(`오류 발생: ${response.status}`);
|
||||
}
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
|
||||
@@ -30,7 +41,10 @@ export const getFetch = async (type: string, url: string) => {
|
||||
// JSON 응답 처리
|
||||
const res = await response.json();
|
||||
return res.data;
|
||||
} else {
|
||||
} else if(contentType.includes('application/octet-stream')) {
|
||||
// 파일 응답 처리 (ex. PDF, Excel 등)
|
||||
|
||||
console.log('contentType ' ,contentType)
|
||||
// 파일 응답 처리 (ex. PDF, Excel 등)
|
||||
const blob = await response.blob();
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
@@ -41,30 +55,36 @@ export const getFetch = async (type: string, url: string) => {
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
|
||||
|
||||
return null; // 다운로드이므로 반환값 없음
|
||||
}
|
||||
}else{
|
||||
throw Error('오류 발생')
|
||||
}
|
||||
};
|
||||
|
||||
export const getFetchWithCount = async (type: string, url: string) => {
|
||||
const div =
|
||||
type === fetchType.COMMON
|
||||
? DEV_URL_FO
|
||||
: type === fetchType.LOCAL
|
||||
? LOCAL_URL
|
||||
: type === fetchType.CS
|
||||
? ''
|
||||
: DEV_URL_WR;
|
||||
type === fetchType.COMMON
|
||||
? DEV_URL_FO
|
||||
: type === fetchType.LOCAL
|
||||
? LOCAL_URL
|
||||
: type === fetchType.CS
|
||||
? ""
|
||||
: DEV_URL_WR;
|
||||
const tokenId = getCookie(AUTH_USER_ID);
|
||||
const Authorization = getCookie(CLAIM_NAME);
|
||||
|
||||
const response = await fetch(div + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
AUTH_USER_ID: tokenId ?? "",
|
||||
CLAIM_NAME: Authorization ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`오류 발생: ${response.status}`);
|
||||
console.log(`오류 발생: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
@@ -73,10 +93,11 @@ export const getFetchWithCount = async (type: string, url: string) => {
|
||||
// JSON 응답 처리
|
||||
const res = await response.json();
|
||||
|
||||
|
||||
return res;
|
||||
} else {
|
||||
} else if(contentType.includes('application/octet-stream')) {
|
||||
// 파일 응답 처리 (ex. PDF, Excel 등)
|
||||
|
||||
console.log('contentType ' ,contentType)
|
||||
const blob = await response.blob();
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
@@ -86,19 +107,25 @@ export const getFetchWithCount = async (type: string, url: string) => {
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
|
||||
|
||||
return null; // 다운로드이므로 반환값 없음
|
||||
}
|
||||
}else{
|
||||
throw Error('오류 발생')
|
||||
}
|
||||
};
|
||||
|
||||
export const postFectch = async (url: string, params: any, type?: string) => {
|
||||
const div = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR;
|
||||
|
||||
const tokenId = getCookie(AUTH_USER_ID);
|
||||
const Authorization = getCookie(CLAIM_NAME);
|
||||
const response = await fetch(div + url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
AUTH_USER_ID: tokenId ?? "",
|
||||
CLAIM_NAME: Authorization ?? "",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
@@ -108,39 +135,25 @@ export const postFectch = async (url: string, params: any, type?: string) => {
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const cmsPostFectch = async (url: string, params: any,headers:any) => {
|
||||
const response = fetch(url, {
|
||||
method: "POST",
|
||||
headers:headers ,
|
||||
body: JSON.stringify(params),
|
||||
}).then((res) =>
|
||||
{
|
||||
console.log(' res ' , res)
|
||||
console.log(' access-token ' , res.headers.get('access-token'))
|
||||
console.log(' refresh-token ' , res.headers.get('refresh-token'))
|
||||
console.log(' token-id ' , res.headers.get('token-id'))
|
||||
|
||||
})
|
||||
|
||||
|
||||
return await response;
|
||||
};
|
||||
|
||||
export const putFectch = async (url: string, params: any,type?:string) => {
|
||||
export const putFectch = async (url: string, params: any, type?: string) => {
|
||||
const div =
|
||||
type === fetchType.COMMON
|
||||
? DEV_URL_FO
|
||||
: type === fetchType.LOCAL
|
||||
? LOCAL_URL
|
||||
: type === fetchType.CS
|
||||
? ''
|
||||
: DEV_URL_WR;
|
||||
? ""
|
||||
: DEV_URL_WR;
|
||||
|
||||
const tokenId = getCookie(AUTH_USER_ID);
|
||||
const Authorization = getCookie(CLAIM_NAME);
|
||||
|
||||
const response = await fetch(div + url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
AUTH_USER_ID: tokenId ?? "",
|
||||
CLAIM_NAME: Authorization ?? "",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
@@ -150,25 +163,33 @@ export const putFectch = async (url: string, params: any,type?:string) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const deleteFectch = async (url: string, params?: any, type?: string) => {
|
||||
export const deleteFectch = async (
|
||||
url: string,
|
||||
params?: any,
|
||||
type?: string
|
||||
) => {
|
||||
const div =
|
||||
type === fetchType.COMMON
|
||||
? DEV_URL_FO
|
||||
: type === fetchType.LOCAL
|
||||
? LOCAL_URL
|
||||
: type === fetchType.CS
|
||||
? ''
|
||||
: DEV_URL_WR;
|
||||
? ""
|
||||
: DEV_URL_WR;
|
||||
const tokenId = getCookie(AUTH_USER_ID);
|
||||
const Authorization = getCookie(CLAIM_NAME);
|
||||
|
||||
const response = await fetch(div + url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
AUTH_USER_ID: tokenId ?? "",
|
||||
CLAIM_NAME: Authorization ?? "",
|
||||
},
|
||||
body: params?JSON.stringify(params):undefined,
|
||||
body: params ? JSON.stringify(params) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return await response.text();
|
||||
};
|
||||
return await response.text();
|
||||
};
|
||||
|
||||
@@ -50,6 +50,10 @@ export default function ConCostForm({
|
||||
const ref = useRef<boolean>(false);
|
||||
|
||||
const [checkFinish, setCheckFinish] = useState<boolean>(false);
|
||||
const [lastSub, setLastSub] = useState<{
|
||||
ruseUtztnFcltNm: string;
|
||||
ruseUtztnFcltCd: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
@@ -80,7 +84,7 @@ export default function ConCostForm({
|
||||
const dmndQtyWatch = form.watch("dmndQty");
|
||||
const totalConstCost = form.watch("totalConstCost");
|
||||
const cstrnCst = form.watch("demandCst.cstrnCst");
|
||||
|
||||
const splyList = form.watch("splyCndtList");
|
||||
const calculateConstCost = useCallback(() => {
|
||||
const usgPrcsProcs = usgPrcsProcsWatch;
|
||||
if (!dmndQtyWatch) {
|
||||
@@ -157,8 +161,11 @@ export default function ConCostForm({
|
||||
return;
|
||||
}
|
||||
|
||||
const splycd = form.getValues("splyCndtList")
|
||||
const pk = splycd&&splycd[0]?.ruseUtztnFcltCd?splycd[0]?.ruseUtztnFcltCd:'11000SW001R1'
|
||||
const splycd = form.getValues("splyCndtList");
|
||||
const pk =
|
||||
splycd && splycd[0]?.ruseUtztnFcltCd
|
||||
? splycd[0]?.ruseUtztnFcltCd
|
||||
: "11000SW001R1";
|
||||
|
||||
if (type === "mat") {
|
||||
if (value.demandCst) {
|
||||
@@ -183,11 +190,7 @@ export default function ConCostForm({
|
||||
onClick={() => {
|
||||
toast.dismiss(t.id);
|
||||
|
||||
coMatRvwUpdate(
|
||||
pk,
|
||||
form.getValues("dmdInfoId"),
|
||||
data
|
||||
);
|
||||
coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data);
|
||||
}}
|
||||
>
|
||||
확인
|
||||
@@ -199,7 +202,6 @@ export default function ConCostForm({
|
||||
</span>
|
||||
));
|
||||
} else {
|
||||
|
||||
coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data);
|
||||
}
|
||||
}
|
||||
@@ -216,6 +218,10 @@ export default function ConCostForm({
|
||||
console.log(" form ", form.formState.errors);
|
||||
}, [form.formState]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("splyCndtList ", splyList);
|
||||
}, [splyList]);
|
||||
|
||||
useEffect(() => {
|
||||
form.register("dmdInfoSn");
|
||||
form.register("dmdInfoId");
|
||||
@@ -369,8 +375,8 @@ export default function ConCostForm({
|
||||
console.log("dmndQty ", e);
|
||||
//field.onChange(Number(e.target.value) * 365 );
|
||||
}}
|
||||
// value={Number(field.value) * 365 || 0}
|
||||
value={Number(field.value) || 0}
|
||||
// value={Number(field.value) * 365 || 0}
|
||||
value={Number(field.value) || 0}
|
||||
readOnly
|
||||
/>
|
||||
</FormUi>
|
||||
@@ -501,30 +507,32 @@ export default function ConCostForm({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="col-span-3 mt-4">
|
||||
<div className="col-span-3 mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>공급처 선정</Label>
|
||||
<Button size={"sm"} onClick={(e) => {}} className="bg-none">
|
||||
<Button
|
||||
size={"sm"}
|
||||
onClick={(e) => {
|
||||
console.log(e);
|
||||
setLastSub(null) // 빌드오류때문에 넣은거 삭제필요
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}
|
||||
className="bg-none"
|
||||
>
|
||||
후보지 조회
|
||||
</Button>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name=""
|
||||
render={({ field }) => (
|
||||
<FormUi label="">
|
||||
<Input
|
||||
placeholder=""
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormUi>
|
||||
)}
|
||||
/>
|
||||
</div> */}
|
||||
<FormUi label="">
|
||||
<Input
|
||||
placeholder=""
|
||||
type="text"
|
||||
value={lastSub?.ruseUtztnFcltNm || ""}
|
||||
readOnly
|
||||
/>
|
||||
</FormUi>
|
||||
</div>
|
||||
<div className="col-span-3 mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -536,9 +544,8 @@ export default function ConCostForm({
|
||||
type="number"
|
||||
step="0.01"
|
||||
onChange={(e) => {
|
||||
if (readonly) return;
|
||||
|
||||
if(readonly) return;
|
||||
|
||||
field.onChange(Number(e.target.value));
|
||||
}}
|
||||
value={field.value ?? 0}
|
||||
|
||||
@@ -34,11 +34,13 @@ export default function HtdflRegForm({
|
||||
isModal,
|
||||
readonly = false,
|
||||
setIsModal,
|
||||
isMap
|
||||
}: {
|
||||
htdflForm: customHtdflBdst;
|
||||
isModal: boolean;
|
||||
readonly?: boolean;
|
||||
setIsModal?: (modal: boolean) => void;
|
||||
isMap?: boolean;
|
||||
}) {
|
||||
const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state);
|
||||
const [frontPHdata, setFrontPHdata] = useState("");
|
||||
@@ -140,14 +142,16 @@ export default function HtdflRegForm({
|
||||
|
||||
const [divStyle, setDivStyle] = useState("h-[53vh]");
|
||||
useEffect(() => {
|
||||
if (isModal === true) {
|
||||
setDivStyle("h-[53vh] max-h-[37vh]");
|
||||
if(isMap){
|
||||
setDivStyle("h-[100vh]")
|
||||
}else if (isModal === true) {
|
||||
setDivStyle("h-[53vh] max-h-[37vh]")
|
||||
} else if (readonly === true) {
|
||||
setDivStyle("h-[62vh]");
|
||||
setDivStyle("h-[62vh]")
|
||||
} else {
|
||||
setDivStyle("h-[57vh]")
|
||||
}
|
||||
}, [isModal, readonly]);
|
||||
}, [isModal, readonly ,isMap]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("form errors:", form.formState.errors);
|
||||
|
||||
@@ -30,11 +30,13 @@ export default function RainRegForm({
|
||||
isModal,
|
||||
readonly = false,
|
||||
setIsModal,
|
||||
isMap
|
||||
}: {
|
||||
rainForm: customRainBdst;
|
||||
isModal: boolean;
|
||||
setIsModal?: (modal: boolean) => void;
|
||||
readonly?: boolean;
|
||||
isMap?: boolean;
|
||||
}) {
|
||||
const { code, rainBdstReg, rainBdstUpdate } = useWrStore(
|
||||
(state) => state
|
||||
@@ -54,14 +56,16 @@ export default function RainRegForm({
|
||||
|
||||
const [divStyle, setDivStyle] = useState("h-[53vh]");
|
||||
useEffect(() => {
|
||||
if (isModal === true) {
|
||||
if(isMap=== true){
|
||||
setDivStyle("h-[100vh]")
|
||||
}else if (isModal === true) {
|
||||
setDivStyle("h-[53vh] max-h-[37vh]")
|
||||
} else if (readonly === true) {
|
||||
setDivStyle("h-[62vh]")
|
||||
} else {
|
||||
setDivStyle("h-[57vh]")
|
||||
}
|
||||
}, [isModal, readonly]);
|
||||
}, [isModal, readonly ,isMap]);
|
||||
|
||||
const cleanRainData = (raw: any): z.infer<typeof rainZod> => {
|
||||
const buildings =
|
||||
|
||||
@@ -34,25 +34,30 @@ export default function RswtrRegForm({
|
||||
isModal,
|
||||
setIsModal,
|
||||
readonly = false,
|
||||
isMap
|
||||
}: {
|
||||
rswtForm: customRswtBdst;
|
||||
isModal: boolean;
|
||||
setIsModal?: (isModal: boolean) => void;
|
||||
readonly?: boolean;
|
||||
isMap?: boolean;
|
||||
|
||||
}) {
|
||||
const { code, rswtBdstUpdate, rswtBdstReg } = useWrStore(
|
||||
(state) => state
|
||||
);
|
||||
const [divStyle, setDivStyle] = useState("h-[53vh]");
|
||||
useEffect(() => {
|
||||
if (isModal === true) {
|
||||
if(isMap){
|
||||
setDivStyle("h-[100vh]")
|
||||
}else if (isModal === true) {
|
||||
setDivStyle("h-[53vh] max-h-[37vh]")
|
||||
} else if (readonly === true) {
|
||||
setDivStyle("h-[62vh]")
|
||||
} else {
|
||||
setDivStyle("h-[57vh]")
|
||||
}
|
||||
}, [isModal, readonly]);
|
||||
}, [isModal, readonly ,isMap]);
|
||||
// const defaultValues: z.infer<typeof rswtZod> = rswtZod.parse({});
|
||||
|
||||
const form = useForm<z.infer<typeof rswtZod>>({
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function coSttnDetailPopup(){
|
||||
|
||||
|
||||
return(
|
||||
<>
|
||||
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useWrStore } from "@/app/store";
|
||||
import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type";
|
||||
import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@common/components/ui";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function RuseDetailPopup({ bscFctCd }: { bscFctCd: string }) {
|
||||
const { ruseMap, getRuseMaplList } = useWrStore((state) => state);
|
||||
const { ruseDetailList } = ruseMap;
|
||||
const [fcltTab, setFcltTab] = useState<string>("");
|
||||
const [isDetail, setIsDetail] = useState<customPbnst>({} as customPbnst);
|
||||
const initRef = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initRef.current) {
|
||||
getRuseMaplList(bscFctCd);
|
||||
initRef.current = true;
|
||||
}
|
||||
}, [bscFctCd]);
|
||||
|
||||
useEffect(() => {
|
||||
const detail = ruseDetailList.find(
|
||||
(list) => list.pbnstRuseFcltId === fcltTab
|
||||
);
|
||||
if (detail) setIsDetail(detail);
|
||||
}, [fcltTab, ruseDetailList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ruseDetailList && ruseDetailList[0]) {
|
||||
setFcltTab(ruseDetailList[0].pbnstRuseFcltId);
|
||||
}
|
||||
}, [ruseDetailList]);
|
||||
|
||||
if (!(ruseDetailList && ruseDetailList[0])) return;
|
||||
return (
|
||||
<div>
|
||||
<Tabs value={fcltTab} onValueChange={setFcltTab}>
|
||||
<TabsList className="mt-4">
|
||||
{ruseDetailList.map((item, index) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={item.pbnstRuseFcltId + index}
|
||||
className="!text-xl"
|
||||
value={item.pbnstRuseFcltId}
|
||||
>
|
||||
{item.ruseUtztnFcltNm}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent className="flex-1 px-4" value={fcltTab}>
|
||||
<PbnstSttnModal data={isDetail} isMap={true} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titleVisible?: boolean }) {
|
||||
export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; titleVisible?: boolean ,isMap?:boolean}) {
|
||||
const { pbnstSttn, prfmnc, pbnstPrfmncSttn } = useWrStore((state) => state);
|
||||
const [fcltTab, setFcltTab] = useState<string>("fclt");
|
||||
const [ruseTab, setRuseTab] = useState<string>("totalPrf");
|
||||
@@ -223,17 +223,24 @@ export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titl
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (pbnstSttn.pbnstSttnRes && data) {
|
||||
const prfList = pbnstSttn.pbnstSttnRes.find(
|
||||
(item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId
|
||||
)?.prfList;
|
||||
if(isMap){
|
||||
if(data.prfList &&data.prfList.length>0){
|
||||
getTablePrfData(data.prfList);
|
||||
}
|
||||
}else{
|
||||
|
||||
if (prfList) {
|
||||
|
||||
getTablePrfData(prfList);
|
||||
if (pbnstSttn.pbnstSttnRes && data) {
|
||||
const prfList = pbnstSttn.pbnstSttnRes.find(
|
||||
(item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId
|
||||
)?.prfList;
|
||||
|
||||
if (prfList) {
|
||||
|
||||
getTablePrfData(prfList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [pbnstSttn.pbnstSttnRes, data]);
|
||||
}, [pbnstSttn.pbnstSttnRes, data,isMap]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -252,7 +259,7 @@ export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titl
|
||||
<Title title={"하수재이용 시설 상세정보"} />
|
||||
</>
|
||||
)}
|
||||
<ScrollArea className={"h-[70vh]"}>
|
||||
<ScrollArea className={ isMap==true? "h-[100vh]":"h-[70vh]" }>
|
||||
<PbnstMngForm
|
||||
form={form}
|
||||
formType={"sttn"}
|
||||
|
||||
@@ -15,25 +15,26 @@ import RswtrRegForm from "../../../form/fcltReg/rswtrReg/rswtrRegForm";
|
||||
export default function SttnDetailModal({
|
||||
type,
|
||||
formObj,
|
||||
isMap ,
|
||||
}: {
|
||||
type: string;
|
||||
formObj: any;
|
||||
isMap?: boolean;
|
||||
}) {
|
||||
const { rainSttn, rswtSttn, rainSttnChkSearch } = useWrStore(
|
||||
const { rainSttn, rswtSttn, rainSttnChkSearch } = useWrStore(
|
||||
(state) => state
|
||||
);
|
||||
const [detailData, setDetailData] = useState({
|
||||
head: [] as CommonTableProps[],
|
||||
node:[] as CommonTableProps[],
|
||||
node: [] as CommonTableProps[],
|
||||
data: {} as any,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "RNB" && formObj?.reuseBuilding[0]) {
|
||||
rainSttnChkSearch(formObj.reuseBuilding[0].bdstRuseFcltSn);
|
||||
|
||||
} else if (type === "RST") {
|
||||
// rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn)
|
||||
// rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn)
|
||||
}
|
||||
}, [type, formObj]);
|
||||
|
||||
@@ -60,61 +61,89 @@ export default function SttnDetailModal({
|
||||
});
|
||||
setDetailData({
|
||||
head: rainSttn.rainSttnDetailHeadList,
|
||||
node:rainSttn.rainSttnDetailNodeList,
|
||||
node: rainSttn.rainSttnDetailNodeList,
|
||||
data: data,
|
||||
});
|
||||
} else if (type === "RST" && rswtSttn.rswtSttnRes) {
|
||||
// const tableTotalData = rswtChkTableData(rswtSttn.rswtSttnRes as unknown as rswtChkZodType[]);
|
||||
|
||||
// setDetailData({
|
||||
// head: rswtSttn.rswtSttHeadList,
|
||||
// node:rswtSttn.rswtSttNodeList,
|
||||
// // data: tableTotalData,
|
||||
// });
|
||||
|
||||
|
||||
// const tableTotalData = rswtChkTableData(rswtSttn.rswtSttnRes as unknown as rswtChkZodType[]);
|
||||
// setDetailData({
|
||||
// head: rswtSttn.rswtSttHeadList,
|
||||
// node:rswtSttn.rswtSttNodeList,
|
||||
// // data: tableTotalData,
|
||||
// });
|
||||
}
|
||||
}, [rainSttn.rainChkRes]);
|
||||
|
||||
useEffect(() =>{
|
||||
setDetailData({data:[] , head:[], node:[]})
|
||||
},[])
|
||||
|
||||
useEffect(() => {
|
||||
setDetailData({ data: [], head: [], node: [] });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
<Title title={ type === "RNB" ? "빗물이용시설 상세정보"
|
||||
: type === "RST" ? "중수도시설 상세정보"
|
||||
: "온배수시설 상세정보"}></Title>
|
||||
<ScrollArea className={detailData.data.length>0?"h-[80vh]":"h-[70vh]"} >
|
||||
<Title
|
||||
title={
|
||||
type === "RNB"
|
||||
? "빗물이용시설 상세정보"
|
||||
: type === "RST"
|
||||
? "중수도시설 상세정보"
|
||||
: "온배수시설 상세정보"
|
||||
}
|
||||
></Title>
|
||||
<ScrollArea
|
||||
className={isMap ?"h-[100vh]":
|
||||
detailData.data.length > 0
|
||||
? "h-[80vh]"
|
||||
: "h-[70vh]"
|
||||
}
|
||||
>
|
||||
{type === "RNB" ? (
|
||||
<RainRegForm rainForm={formObj as customRainBdst} isModal={false} readonly={true}/>
|
||||
<RainRegForm
|
||||
rainForm={formObj as customRainBdst}
|
||||
isModal={false}
|
||||
readonly={true}
|
||||
isMap={isMap}
|
||||
/>
|
||||
) : type === "RST" ? (
|
||||
<RswtrRegForm rswtForm={formObj as customRswtBdst} isModal={false} readonly={true}/>
|
||||
) : (<HtdflRegForm htdflForm={formObj as customHtdflBdst} isModal={false} readonly={true}/>)}
|
||||
<RswtrRegForm
|
||||
rswtForm={formObj as customRswtBdst}
|
||||
isModal={false}
|
||||
readonly={true}
|
||||
isMap={isMap}
|
||||
/>
|
||||
) : (
|
||||
<HtdflRegForm
|
||||
htdflForm={formObj as customHtdflBdst}
|
||||
isModal={false}
|
||||
readonly={true}
|
||||
isMap={isMap}
|
||||
/>
|
||||
)}
|
||||
|
||||
{detailData.data.length>0?
|
||||
<UiTable
|
||||
headName={
|
||||
type === "RNB" ? "빗물이용시설 상세정보"
|
||||
: type === "RST" ? "중수도시설 상세정보"
|
||||
: "온배수시설 상세정보"
|
||||
}
|
||||
tableData={detailData.data || []}
|
||||
headlist={detailData.head || []}
|
||||
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={function (page: number): void {
|
||||
console.log('page ', page)
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
>
|
||||
<HeadMakeColSpan upHeadList={detailData.node} />
|
||||
</UiTable>:<></>}
|
||||
{detailData.data.length > 0 ? (
|
||||
<UiTable
|
||||
headName={
|
||||
type === "RNB"
|
||||
? "빗물이용시설 상세정보"
|
||||
: type === "RST"
|
||||
? "중수도시설 상세정보"
|
||||
: "온배수시설 상세정보"
|
||||
}
|
||||
tableData={detailData.data || []}
|
||||
headlist={detailData.head || []}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={function (page: number): void {
|
||||
console.log("page ", page);
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
>
|
||||
<HeadMakeColSpan upHeadList={detailData.node} />
|
||||
</UiTable>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ export function middleware(req: NextRequest) {
|
||||
let queryParams = searchParams.toString();
|
||||
const typename = searchParams.get("TYPENAME");
|
||||
console.log("typename ", typename);
|
||||
const startYmd = searchParams.get("START_YMD") ?? null;
|
||||
const endYmd = searchParams.get("END_YMD") ?? null;
|
||||
|
||||
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
|
||||
@@ -116,10 +118,13 @@ export function middleware(req: NextRequest) {
|
||||
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" //잠재수요처
|
||||
typename == "hasudo:tb_wr_tmpr_dmd_co" || //잠재수요처
|
||||
typename?.startsWith("hasudo:drought") //가뭄 지도
|
||||
) {
|
||||
const url = req.nextUrl.clone();
|
||||
url.searchParams.set("TYPENAME", typename.replace("hasudo:", ""));
|
||||
if (startYmd) url.searchParams.set("startYmd", startYmd);
|
||||
if (endYmd) url.searchParams.set("endYmd", endYmd);
|
||||
queryParams = url.searchParams.toString();
|
||||
newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
# CS 프로젝트 구조 문서
|
||||
|
||||
## 개요
|
||||
|
||||
CS 프로젝트는 Next.js App Router를 기반으로 하는 웹 애플리케이션입니다. 이 문서는 프로젝트의 구조와 작동 방식에 대해 설명합니다.
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
CS 프로젝트는 다음과 같은 주요 디렉토리 구조를 가지고 있습니다:
|
||||
|
||||
```
|
||||
/apps/cs/
|
||||
├── app/ # Next.js App Router 디렉토리
|
||||
│ ├── (with-base-layout)/ # 기본 레이아웃을 사용하는 페이지들
|
||||
│ ├── (without-base-layout)/ # 기본 레이아웃을 사용하지 않는 페이지들
|
||||
│ ├── api/ # API 라우트
|
||||
│ ├── auth/ # 인증 관련(로그인) 페이지
|
||||
│ └── layout.tsx # 루트 레이아웃
|
||||
├── src/ # 소스 코드
|
||||
│ ├── app/ # 앱 설정 및 프로바이더
|
||||
│ ├── entities/ # 비즈니스 엔티티 (pages에서 두 번 이상 사용하는 비즈니스 코드)
|
||||
│ ├── pages/ # 페이지 컴포넌트
|
||||
│ ├── shared/ # 공유 유틸리티 및 컴포넌트
|
||||
│ └── widgets/ # 위젯 컴포넌트 (shared 컴포넌트의 조합)
|
||||
├── public/ # 정적 파일
|
||||
└── middleware.ts # Next.js 미들웨어
|
||||
```
|
||||
|
||||
## App과 Pages의 1:1 매칭 구조
|
||||
|
||||
CS 프로젝트에서는 `app/` 디렉토리와 `src/pages/` 디렉토리가 1:1 매칭 구조를 가지고 있습니다.
|
||||
|
||||
### 작동 방식
|
||||
|
||||
1. `app/` 디렉토리는 Next.js의 App Router 기능을 사용하여 라우팅을 처리합니다.
|
||||
2. `app/` 디렉토리의 각 페이지는 `src/pages/` 디렉토리에 있는 해당 페이지 컴포넌트를 임포트하여 사용합니다.
|
||||
|
||||
예를 들어, `app/(with-base-layout)/dashboard/page.tsx`는 `src/pages/dashboard/ui/dashboard-page.tsx`를 임포트합니다.
|
||||
|
||||
### 예시 코드
|
||||
|
||||
```tsx
|
||||
// app/(with-base-layout)/dashboard/page.tsx
|
||||
import { DashboardPage } from "@/pages/dashboard";
|
||||
export default DashboardPage;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/pages/dashboard/index.ts
|
||||
export { DashboardPage } from "./ui/dashboard-page";
|
||||
```
|
||||
|
||||
## 레이아웃 구조
|
||||
|
||||
CS 프로젝트는 다음과 같은 레이아웃 구조를 제공합니다:
|
||||
|
||||
1. **루트 레이아웃**: `app/layout.tsx`에서 정의되며, 모든 페이지에 적용됩니다.
|
||||
2. **기본 레이아웃**: `app/(with-base-layout)/layout.tsx`에서 정의되며, `@common/components`의 헤더, 사이드바, 푸터를 포함합니다.
|
||||
3. **레이아웃 없음**: `app/(without-base-layout)/`에 있는 페이지들은 기본 레이아웃 없이 렌더링됩니다.
|
||||
|
||||
## Common 프로젝트와의 연동
|
||||
|
||||
CS 프로젝트는 `@common/` 패키지와 연동되어 있습니다. 이 패키지들은 여러 프로젝트에서 공유되는 컴포넌트와 유틸리티를 제공합니다.
|
||||
|
||||
### Common 패키지 구조
|
||||
|
||||
```
|
||||
/packages/common/
|
||||
├── components/ # 공통 UI 컴포넌트
|
||||
├── assets/ # 공통 에셋 (이미지, 아이콘 등)
|
||||
├── config/ # 공통 설정 (환경 변수)
|
||||
└── network/ # 네트워크 관련 유틸리티
|
||||
```
|
||||
|
||||
### Config 패키지 상세 설명
|
||||
|
||||
`@common/config` 패키지는 애플리케이션의 공통 환경 변수를 관리합니다. 주요 파일인 `initialize-config.ts`는 다음과 같은 기능을 제공합니다:
|
||||
|
||||
#### 1. 환경 변수 인터페이스 정의
|
||||
|
||||
```typescript
|
||||
export interface EnvConfig {
|
||||
PORT?: string;
|
||||
PROXY_HOST?: string;
|
||||
SERVER_API_URL?: string;
|
||||
SITE_ID?: string;
|
||||
CLAIM_NAME?: string;
|
||||
ACCESS_TOKEN?: string;
|
||||
REFRESH_TOKEN?: string;
|
||||
AUTH_USER_ID?: string;
|
||||
}
|
||||
```
|
||||
|
||||
이 인터페이스는 애플리케이션에서 사용하는 공통 환경 변수의 타입을 정의합니다.
|
||||
|
||||
#### 2. 기본 설정값
|
||||
|
||||
```typescript
|
||||
const defaultConfig: EnvConfig = {
|
||||
PORT: "3000",
|
||||
PROXY_HOST: "",
|
||||
SERVER_API_URL: "",
|
||||
SITE_ID: "",
|
||||
CLAIM_NAME: "Authorization",
|
||||
ACCESS_TOKEN: "access-token",
|
||||
REFRESH_TOKEN: "refresh-token",
|
||||
AUTH_USER_ID: "token-id",
|
||||
};
|
||||
```
|
||||
|
||||
환경 변수가 제공되지 않을 경우 사용할 기본값을 정의합니다.
|
||||
|
||||
#### 3. 환경 설정 생성 함수
|
||||
|
||||
```typescript
|
||||
export function generateEnvConfig(values: Partial<EnvConfig>): EnvConfig {
|
||||
// 유효한 값만 필터링
|
||||
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
acc[key as keyof EnvConfig] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Partial<EnvConfig>);
|
||||
|
||||
// PROXY_HOST가 없으면 자동 생성
|
||||
if (!filteredValues.PROXY_HOST) {
|
||||
filteredValues.PROXY_HOST = `http://localhost:${filteredValues.PORT || defaultConfig.PORT}`;
|
||||
}
|
||||
|
||||
return { ...defaultConfig, ...filteredValues };
|
||||
}
|
||||
```
|
||||
|
||||
이 함수는 입력받은 환경 변수 중 유효한 값만 필터링하고, 기본값과 병합하여 최종 설정을 반환합니다.
|
||||
|
||||
#### 4. 환경별 설정 가져오기
|
||||
|
||||
```typescript
|
||||
// Next.js 환경 변수
|
||||
export function getNextConfig(): EnvConfig {
|
||||
const env: Partial<EnvConfig> = {
|
||||
PORT: process.env.NEXT_PUBLIC_PORT,
|
||||
PROXY_HOST: process.env.NEXT_PUBLIC_PROXY_HOST,
|
||||
SERVER_API_URL: process.env.NEXT_PUBLIC_SERVER_API_URL,
|
||||
// 기타 환경 변수들...
|
||||
};
|
||||
return generateEnvConfig(env);
|
||||
}
|
||||
|
||||
export function getConfig(): EnvConfig {
|
||||
return getNextConfig();
|
||||
}
|
||||
```
|
||||
|
||||
Next.js에서 환경 변수를 가져오는 함수를 제공하며, `getConfig()` 함수를 통해 현재 실행 중인 애플리케이션의 환경 변수를 `@common/` 패키지에서 가져올 수 있습니다.
|
||||
|
||||
#### 5. 사용 방법
|
||||
|
||||
```typescript
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
const config = getConfig();
|
||||
const serverUrl = config.SERVER_API_URL;
|
||||
const authHeader = config.CLAIM_NAME;
|
||||
|
||||
// API 요청 등에 사용
|
||||
```
|
||||
|
||||
## 애플리케이션 시작점 및 작동 방식
|
||||
|
||||
### 1. 애플리케이션 시작점
|
||||
|
||||
애플리케이션의 시작점은 `app/layout.tsx` 파일입니다:
|
||||
|
||||
```tsx
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "유역하수도 통합 정보플랫폼",
|
||||
};
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### 2. 앱 구성 요소
|
||||
|
||||
`src/app/index.tsx`에서 기본 앱 구성 요소를 정의합니다:
|
||||
|
||||
```tsx
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { AppProvider } from "./providers/app-provider";
|
||||
|
||||
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export { App };
|
||||
```
|
||||
|
||||
### 3. 프로바이더 설정
|
||||
|
||||
`src/app/providers/app-provider.tsx`에서 애플리케이션에 필요한 모든 프로바이더를 설정합니다:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { Toaster } from "@/shared/ui/sonner";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
<SelectPromptDialog />
|
||||
<Toaster position="top-center" />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AppProvider };
|
||||
```
|
||||
|
||||
### 4. 기본 레이아웃 구성
|
||||
|
||||
`app/(with-base-layout)/layout.tsx`에서 기본 레이아웃을 구성합니다:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 미들웨어 처리
|
||||
|
||||
`middleware.ts`에서 API 요청 리다이렉션 및 인증 처리를 담당합니다:
|
||||
|
||||
```tsx
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
CLAIM_NAME,
|
||||
AUTH_USER_ID,
|
||||
PORT,
|
||||
PROXY_HOST,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "./src/shared/config";
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
|
||||
// 서버 API 요청 처리
|
||||
if (pathname.startsWith("/server")) {
|
||||
// API 요청을 실제 서버로 리다이렉션
|
||||
}
|
||||
|
||||
// 첨부 파일 서비스 처리
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
// 인증 토큰을 헤더에 추가하여 요청 처리
|
||||
}
|
||||
|
||||
// 루트 경로를 대시보드로 리다이렉션
|
||||
if (pathname === "/") {
|
||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 페이지 렌더링 흐름
|
||||
|
||||
1. 사용자가 URL 접근 시 미들웨어에서 요청 처리
|
||||
2. App Router가 해당 경로에 맞는 페이지 컴포넌트 로드
|
||||
3. 루트 레이아웃 → 기본 레이아웃 → 페이지 컴포넌트 순으로 렌더링
|
||||
4. 페이지 컴포넌트는 `src/pages/` 디렉토리에서 가져와 렌더링
|
||||
@@ -364,7 +364,9 @@ function Gis({
|
||||
const dummyFeature = {
|
||||
getProperties: () => ({}), // 최소 구조
|
||||
};
|
||||
onFeatureSelect(dummyFeature);
|
||||
console.log(layerNm)
|
||||
onOpenPopup(layerNm)
|
||||
// onFeatureSelect(dummyFeature);
|
||||
}}
|
||||
imageVisible={imgVisble}
|
||||
isPlaying={isPlaying}
|
||||
|
||||
@@ -453,8 +453,8 @@ import {
|
||||
src="/images/map/layericon/Layer_Select_BTN.png"
|
||||
onClick={() =>
|
||||
handleLayerImgClick(
|
||||
node.text,
|
||||
node.id
|
||||
node.data.lyrTyNm,
|
||||
node.id
|
||||
)
|
||||
}
|
||||
alt={node.text}
|
||||
|
||||
@@ -16,11 +16,8 @@ import { Point } from 'ol/geom';
|
||||
import { getTopLeft } from 'ol/extent';
|
||||
import TileWMTS from 'ol/source/WMTS';
|
||||
import WMTSTileGrid from 'ol/tilegrid/WMTS';
|
||||
// import { get as getProjection } from 'ol/proj';
|
||||
import proj4 from 'proj4';
|
||||
import { register } from 'ol/proj/proj4';
|
||||
import olSourceImageWMS from 'ol/source/ImageWMS';
|
||||
import { GeoJSON } from 'ol/format';
|
||||
import GisOlEditLayer from './gis.edit';
|
||||
import GisOlLayer from './gis.layer';
|
||||
|
||||
@@ -34,7 +31,6 @@ GisApp.layerInfos = [];
|
||||
GisApp.DROUGHT_DATES = { startYmd: '', endYmd: '' };
|
||||
//기본정보
|
||||
GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows';
|
||||
GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry';
|
||||
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
|
||||
|
||||
//맵 정보
|
||||
@@ -44,14 +40,8 @@ GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z
|
||||
GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
|
||||
|
||||
// 인터넷망: NGII 정보
|
||||
GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키
|
||||
GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`;
|
||||
|
||||
//행정망: NGII 정보
|
||||
//GisApp.NGII_API_KEY = 'BDE2BEF39DAFEC293AB6730150F222AC'; // NGII에서 발급받은 API 키
|
||||
//GisApp.NGII_WMTS_URL = `http://10.98.25.94:18080/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`;
|
||||
|
||||
// NGII 정보(배경지도)
|
||||
GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL;
|
||||
|
||||
GisApp.EMAP_URL = '/emap/';
|
||||
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ENDPOINT } from "../config";
|
||||
import { getApiClient } from "./client";
|
||||
|
||||
interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
}
|
||||
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
interface PageableConfig {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
}
|
||||
|
||||
interface PageableList<T> {
|
||||
content: T[];
|
||||
pageable: PageableConfig;
|
||||
sort: SortConfig;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
numberOfElements: number;
|
||||
size: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
interface NoticeUserListParams extends PageableParams {
|
||||
recvUnqId?: string;
|
||||
senderUnqId?: string;
|
||||
notiType?: string;
|
||||
notiTtlKeyword?: string;
|
||||
notiCnKeyword?: string;
|
||||
notReadYn?: string;
|
||||
delYn?: string;
|
||||
}
|
||||
|
||||
interface NoticeUser {
|
||||
notiId: number;
|
||||
recvUnqId: string;
|
||||
senderUnqId: string;
|
||||
notiType: string;
|
||||
notiTtl: string;
|
||||
notReadYn: string;
|
||||
regDt: string;
|
||||
}
|
||||
|
||||
const client = getApiClient();
|
||||
|
||||
const getNoticeUserList = async (
|
||||
recvUnqId: string,
|
||||
params?: NoticeUserListParams,
|
||||
) => {
|
||||
const response = await client.get<PageableList<NoticeUser>>(
|
||||
`${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/list`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getUnreadNoticeCount = async (recvUnqId: string) => {
|
||||
const response = await client.get<number>(
|
||||
`${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/unread/count`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const readNotice = async (notiId: number) => {
|
||||
const response = await client.put(
|
||||
`${ENDPOINT.CMS_SERVICE.NOTICE}/${notiId}/read`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export { getNoticeUserList, getUnreadNoticeCount, readNotice };
|
||||
@@ -7,6 +7,8 @@ export const ENDPOINT = {
|
||||
POPUPS: "/cms-service/api/v1/popups",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
|
||||
NOTICE: "/cms-service/api/v1/noti",
|
||||
NOTICE_USER: "/cms-service/api/v1/noti/user",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
USERS: "/user-service/api/v1/users",
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Footer() {
|
||||
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
|
||||
|
||||
return (
|
||||
<footer className="flex w-full shrink-0 items-start border-t border-solid border-border bg-background px-6 py-8">
|
||||
<footer className="flex w-full shrink-0 items-start border-t border-solid border-border bg-background px-6 py-8 text-foreground">
|
||||
<div className="w-1/2">
|
||||
<h6 className="mb-4 text-2xl font-bold">{siteInfo?.siteNm ?? ""}</h6>
|
||||
<p className="my-1 text-xl opacity-60">{siteInfo?.siteAddr ?? ""}</p>
|
||||
@@ -32,10 +32,6 @@ export function Footer() {
|
||||
전화: {siteInfo?.telNo ?? ""} / 팩스: {siteInfo?.faxNumber ?? ""}
|
||||
</p>
|
||||
<p className="my-1 text-xl opacity-60">{siteInfo?.lwndCn ?? ""}</p>
|
||||
<p className="my-1 text-xl opacity-60">
|
||||
본 시스템은 Chrome, Firefox, Safari, Edge 브라우저에 최적화되어
|
||||
있습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-1/2 flex-col">
|
||||
<div className="flex justify-end gap-4">
|
||||
|
||||
@@ -13,16 +13,12 @@ import {
|
||||
getUserInfo,
|
||||
type Popup,
|
||||
} from "../../api";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "../dropdown-menu";
|
||||
import { Button } from "../button";
|
||||
import { SidebarTrigger } from "../sidebar";
|
||||
import { Skeleton } from "../skeleton";
|
||||
import { SidebarTrigger } from "../sidebar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
import { SearchForm } from "./search-form";
|
||||
import { NoticePopover } from "./notice-popover";
|
||||
|
||||
const appConfig = getConfig();
|
||||
const siteId = appConfig.SITE_ID ?? "";
|
||||
@@ -140,30 +136,28 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
<SearchForm />
|
||||
{mounted ? (
|
||||
isLoggedIn && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
|
||||
data-trigger="dropdown"
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label">
|
||||
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
|
||||
{userInfo?.userName}
|
||||
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
|
||||
<div className="flex flex-col items-start gap-0 px-4 py-3">
|
||||
<p className="text-base text-label">최종접속일</p>
|
||||
<p>{userInfo?.lastLoginDate}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-0 px-4 py-3">
|
||||
<p className="text-base text-label">접속 IP</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-48 border-border p-1">
|
||||
<ul>
|
||||
<li className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent">
|
||||
<NoticePopover />
|
||||
</li>
|
||||
<li
|
||||
onClick={logout}
|
||||
className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent"
|
||||
>
|
||||
로그아웃
|
||||
</li>
|
||||
</ul>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
) : (
|
||||
<Skeleton className="mx-4 h-[3rem] w-[15rem]" />
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getConfig } from "@common/config";
|
||||
import { ENDPOINT } from "../../config";
|
||||
import { cn, getCookie } from "../../lib";
|
||||
import {
|
||||
getNoticeUserList,
|
||||
getUnreadNoticeCount,
|
||||
readNotice,
|
||||
} from "../../api/notice-user";
|
||||
import { Label } from "../label";
|
||||
import { Badge } from "../badge";
|
||||
import { Button } from "../button";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import "dayjs/locale/ko";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.locale("ko");
|
||||
|
||||
const appConfig = getConfig();
|
||||
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
|
||||
export function NoticePopover() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedNoticeIds, setSelectedNoticeIds] = useState<number[]>([]);
|
||||
|
||||
const { data: noticeUserList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER],
|
||||
queryFn: () => getNoticeUserList(userId!),
|
||||
enabled: !!userId,
|
||||
});
|
||||
|
||||
const { data: unreadNoticeCount } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.NOTICE],
|
||||
queryFn: () => getUnreadNoticeCount(userId!),
|
||||
enabled: !!userId,
|
||||
});
|
||||
|
||||
const handleSelectNotice = (notiId: number) => {
|
||||
setSelectedNoticeIds((prevIds) => {
|
||||
if (prevIds.includes(notiId)) {
|
||||
return prevIds.filter((id) => id !== notiId);
|
||||
} else {
|
||||
return [...prevIds, notiId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (noticeUserList?.content) {
|
||||
if (selectedNoticeIds.length === noticeUserList.content.length) {
|
||||
setSelectedNoticeIds([]);
|
||||
} else {
|
||||
const allIds = noticeUserList.content.map((item) => item.notiId);
|
||||
setSelectedNoticeIds(allIds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReadNotice = async () => {
|
||||
if (selectedNoticeIds.length === 0) return;
|
||||
try {
|
||||
await Promise.all(selectedNoticeIds.map((notiId) => readNotice(notiId)));
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER],
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.NOTICE],
|
||||
});
|
||||
setSelectedNoticeIds([]);
|
||||
} catch (error) {
|
||||
console.error("알림 읽음 처리 중 오류 발생:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const isAllSelected = noticeUserList?.content
|
||||
? selectedNoticeIds.length === noticeUserList.content.length &&
|
||||
noticeUserList.content.length > 0
|
||||
: false;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setSelectedNoticeIds([]);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex w-full items-center gap-2 text-left">
|
||||
알림 <Badge>{unreadNoticeCount ?? 0}</Badge>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[32rem] border-border px-0">
|
||||
<div className="flex items-center gap-4 p-4 pt-0">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-[1.4rem]">
|
||||
전체 선택
|
||||
</Label>
|
||||
</div>
|
||||
<ul>
|
||||
{noticeUserList?.content?.map((item) => (
|
||||
<li
|
||||
key={item.notiId}
|
||||
className={cn(
|
||||
"flex items-center gap-4 border-t border-border p-4",
|
||||
item.notReadYn === "N" && "bg-primary-transparent",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedNoticeIds.includes(item.notiId)}
|
||||
onCheckedChange={() => handleSelectNotice(item.notiId)}
|
||||
/>
|
||||
<div>
|
||||
<span className="text-[1.2rem]">{item.notiType}</span>
|
||||
<p className="text-[1.4rem]">{item.notiTtl}</p>
|
||||
<span className="text-[1.2rem] opacity-50">
|
||||
{dayjs(item.regDt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div
|
||||
className="border-t border-border pt-4 text-center"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Button onClick={handleReadNotice}>확인</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { BaseLayout } from "./base";
|
||||
export { VBaseLayout } from "./vbase";
|
||||
export { NoFooterLayout } from "./nofooter";
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import { ClientIpGuard } from "../../lib/client-ip-guard";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
import { Header } from "../header";
|
||||
import "./common.css";
|
||||
|
||||
export function NoFooterLayout({
|
||||
children,
|
||||
queryClient,
|
||||
currentPath,
|
||||
handleMenuClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
currentPath?: string;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<ClientIpGuard currentPath={currentPath}>
|
||||
<PopupProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<div className="flex">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
/>
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</PopupProvider>
|
||||
</ClientIpGuard>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user