Merge branch 'iipbs/sn/dev2' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev2
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_TINY_BASE_URL=
|
||||
+17
-8
@@ -1,21 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
CLAIM_NAME,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
PORT,
|
||||
PROXY_HOST,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "./src/shared/config";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000";
|
||||
|
||||
const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
const linkBackendApiHost =
|
||||
process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;");
|
||||
response.headers.set(
|
||||
"Content-Security-Policy",
|
||||
"img-src 'self' data: https: blob:;",
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -66,7 +70,7 @@ export function middleware(req: NextRequest) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
|
||||
// /api/link 경로를 제거하고 백엔드 API 경로로 변환
|
||||
const apiPath = pathname.replace("/api/link", "");
|
||||
const queryParams = searchParams.toString();
|
||||
@@ -94,11 +98,16 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
// console.log(logMessages.join("\n"));
|
||||
|
||||
|
||||
const response = NextResponse.next();
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"],
|
||||
matcher: [
|
||||
"/server/:path*",
|
||||
"/attach-service/:path*",
|
||||
"/api/link/:path*",
|
||||
"/",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SERVER_API_URL } from "@/shared/config";
|
||||
import { TINY_BASE_URL } from "@/shared/config";
|
||||
|
||||
export const editorBaseConfig = {
|
||||
// 폐쇄망 환경에서는 API 키 없이 자체 호스팅 모드 사용
|
||||
@@ -7,7 +7,7 @@ export const editorBaseConfig = {
|
||||
licenseKey: "gpl",
|
||||
init: {
|
||||
language: "ko_KR",
|
||||
language_url: '/tinymce/langs/ko_KR.js', // 직접 호스팅한 경우 경로 지정
|
||||
language_url: "/tinymce/langs/ko_KR.js", // 직접 호스팅한 경우 경로 지정
|
||||
height: 500,
|
||||
// 폐쇄망 환경을 위한 설정
|
||||
base_url: "/tinymce",
|
||||
@@ -38,7 +38,7 @@ export const editorBaseConfig = {
|
||||
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
|
||||
"link image",
|
||||
],
|
||||
images_upload_base_path: SERVER_API_URL,
|
||||
images_upload_base_path: TINY_BASE_URL,
|
||||
file_picker_types: "image",
|
||||
// 폐쇄망 환경을 위한 추가 설정
|
||||
content_css: "/tinymce/skins/content/default/content.min.css",
|
||||
|
||||
@@ -43,7 +43,7 @@ const GroupsDialog = memo(
|
||||
const { data: group } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
||||
queryFn: () => GroupApi.findGroupById(groupId!),
|
||||
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
||||
enabled: !isRegistration,
|
||||
});
|
||||
|
||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||
@@ -92,8 +92,9 @@ const GroupsDialog = memo(
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) form.reset();
|
||||
}, [open]);
|
||||
// 그룹 등록 및 취소 시 폼 초기화
|
||||
if (isRegistration && !open) form.reset();
|
||||
}, [groupId, open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||
|
||||
@@ -16,7 +16,13 @@ type RoleSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
|
||||
function RoleSelect({ mngrYn = "N", ...props }: RoleSelectProps) {
|
||||
const { data: roles } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES],
|
||||
queryFn: () => RoleApi.getRoles(),
|
||||
queryFn: () =>
|
||||
RoleApi.getRoles({
|
||||
keywordType: "useYn",
|
||||
keyword: "Y",
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
});
|
||||
|
||||
const options = roles?.content.filter((role) => role.mngrYn === mngrYn);
|
||||
|
||||
@@ -3,13 +3,8 @@ import { useState } from "react";
|
||||
import type { Role } from "@/entities/role";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
export const createColumns = ({
|
||||
deleteRole,
|
||||
}: {
|
||||
deleteRole: (roleId: string) => void;
|
||||
}): ColumnDef<Role>[] => [
|
||||
export const createColumns = (): ColumnDef<Role>[] => [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
@@ -44,18 +39,7 @@ export const createColumns = ({
|
||||
{
|
||||
id: "action",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MappingRoleButton roleId={row.original.roleId} />
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={() => deleteRole(row.original.roleId)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => <MappingRoleButton roleId={row.original.roleId} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -24,13 +24,6 @@ function ModifyPage({ roleId }: { roleId: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useMutationWrapper({
|
||||
mutationFn: () => RoleApi.deleteRole(roleId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", () => router.push("/role"));
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<RoleFormData>({
|
||||
resolver: zodResolver(roleFormSchema),
|
||||
values: {
|
||||
@@ -47,12 +40,7 @@ function ModifyPage({ roleId }: { roleId: string }) {
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">권한 수정</h3>
|
||||
</div>
|
||||
<RoleForm
|
||||
roleId={roleId}
|
||||
form={form}
|
||||
handleSave={updateRole}
|
||||
handleDelete={deleteRole}
|
||||
/>
|
||||
<RoleForm roleId={roleId} form={form} handleSave={updateRole} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ type RoleFormProps = {
|
||||
roleId?: string;
|
||||
form: UseFormReturn<RoleFormData>;
|
||||
handleSave: (data: RoleFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
function RolePage() {
|
||||
@@ -16,6 +16,7 @@ function RolePage() {
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<RoleParams>({
|
||||
keywordType: "roleName",
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
@@ -34,13 +35,6 @@ function RolePage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useMutationWrapper({
|
||||
mutationFn: (roleId: string) => RoleApi.deleteRole(roleId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", query.refetch);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRowClick = (row: Role) => {
|
||||
router.push(`/role/register/${row.roleId}`);
|
||||
};
|
||||
@@ -48,9 +42,7 @@ function RolePage() {
|
||||
const { tableComponent } = useDataTable({
|
||||
query,
|
||||
tableOptions: {
|
||||
columns: createColumns({
|
||||
deleteRole,
|
||||
}),
|
||||
columns: createColumns(),
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const SERVER_API_URL = process.env.SERVER_API_URL;
|
||||
export const AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
|
||||
@@ -11,4 +11,5 @@ export const ACCESS_TOKEN =
|
||||
export const REFRESH_TOKEN =
|
||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
||||
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
export const TINY_BASE_URL = process.env.NEXT_PUBLIC_TINY_BASE_URL;
|
||||
export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginPage } from "@/src/features/auth/login";
|
||||
export default LoginPage;
|
||||
@@ -1,4 +1,28 @@
|
||||
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env";
|
||||
import { CLAIM_NAME, SITE_ID } from "@/src/shared/config/env";
|
||||
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
|
||||
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env";
|
||||
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const handleUnauthorizedResponse = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
|
||||
const isExpired = remainingMs === null || remainingMs <= 0;
|
||||
|
||||
if (!isExpired) return;
|
||||
|
||||
tokenManager.clearAuthToken();
|
||||
|
||||
const isLoginPage =
|
||||
window.location.pathname === "/auth/login" ||
|
||||
window.location.pathname.startsWith("/auth/login/");
|
||||
|
||||
if (!isLoginPage) {
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
};
|
||||
|
||||
export const getFetch = async (part : string, url: string) => {
|
||||
let reqUrl = "";
|
||||
@@ -7,8 +31,12 @@ export const getFetch = async (part : string, url: string) => {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
}
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -21,8 +49,12 @@ export const getFetchAreaList = async (url: string) => {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
}
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -37,9 +69,13 @@ export const postFetch = async (part : string, url: string, params: unknown) =>
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -53,9 +89,13 @@ export const putFetch = async (part : string, url: string, params: unknown) => {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -69,11 +109,15 @@ export const deleteFetch = async (part : string, url: string, params: unknown) =
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SITE_ID } from "@/src/shared/config/env";
|
||||
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";
|
||||
@@ -59,7 +60,10 @@ const Map = () => {
|
||||
const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`;
|
||||
const response = await fetch(reqUrl, {
|
||||
method: "GET",
|
||||
headers: { accept: "application/json" },
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP 오류! 상태: ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { SITE_ID } from "@/src/shared/config/env";
|
||||
import React from "react";
|
||||
|
||||
export default function TestPage() {
|
||||
@@ -6,6 +7,9 @@ export default function TestPage() {
|
||||
const handleFindUser = () => {
|
||||
fetch('/operation-service/api/v1/fclt/flow/1/10/2024', {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
}).then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
@@ -19,4 +23,4 @@ export default function TestPage() {
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -1,6 +1,13 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
|
||||
|
||||
type JwtPayload = {
|
||||
sub?: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
@@ -18,6 +25,49 @@ export class TokenManager {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
private parseJwt(token: string): JwtPayload {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("유효한 JWT 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
const base64Url = parts[1] ?? "";
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(padded)
|
||||
.split("")
|
||||
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload) as JwtPayload;
|
||||
}
|
||||
|
||||
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
|
||||
const token = this.getToken(key);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.parseJwt(token);
|
||||
if (typeof payload.exp !== "number") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.exp * 1000 - Date.now();
|
||||
} catch (error) {
|
||||
console.error("JWT 파싱 오류:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public isExpiredToken(key: string = CLAIM_NAME): boolean {
|
||||
const remainingMs = this.getRemainingMsFromToken(key);
|
||||
return remainingMs === null || remainingMs <= 0;
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
setCookie(key, value, {
|
||||
secure: location.protocol === "https:",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/src/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/pages/dashboard";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* OLYM Cascading Style Sheets
|
||||
* File Name : login.css
|
||||
* Description : 로그인 스타일시트
|
||||
* Author : Web business Team / Choi Hyun Mi
|
||||
* Email : desingtj@olym.co.kr
|
||||
* Date : 2023.07.20
|
||||
* Update : 2023.07.26
|
||||
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.fx {
|
||||
display: flex;
|
||||
-ms-display: flex;
|
||||
-webkit-display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.login .left_box {
|
||||
width: 100%;
|
||||
background: url(/images/bg_login.jpg) no-repeat center/cover;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 7% 5% 5% 5%;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 53px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .left_box .text h1 span {
|
||||
font-weight: 800;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 40px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .login_box {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 4% 12% 4% 6%;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2640a5;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st {
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box > ul > li .input_st .diveicon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 28px;
|
||||
transform: translateY(-50%);
|
||||
font-family: "Xeicon";
|
||||
font-size: 25px;
|
||||
color: #003288;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
width: 100%;
|
||||
padding: 20px 20px 20px 70px;
|
||||
background: #f7f7f7;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
height: auto;
|
||||
}
|
||||
.login .login_box > ul > li input:focus {
|
||||
outline: 1px solid #3c5fe5;
|
||||
}
|
||||
.login .login_box > ul > li input::placeholder {
|
||||
color: #afabab;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 50px 0 45px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .checks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #636363;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message i {
|
||||
color: #327af5;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
padding: 24px 15px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-family: "Montserrat";
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 22px;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.login .login_box .login_btn:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.login .login_box .login_btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.login .footer .copy {
|
||||
margin-top: 36px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 3%;
|
||||
}
|
||||
.login .footer .copy p {
|
||||
font-size: 13px;
|
||||
color: #d4d4d4;
|
||||
width: 44%;
|
||||
text-align: center;
|
||||
}
|
||||
.login .footer .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
content: "";
|
||||
width: 20%;
|
||||
height: 1px;
|
||||
background: #d4d4d4;
|
||||
}
|
||||
.login .footer .fot_btn {
|
||||
gap: 20px;
|
||||
margin: 30px 0 10px 0;
|
||||
}
|
||||
.login .footer .fot_btn a {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login .footer .fot_btn a:hover {
|
||||
color: #003288;
|
||||
}
|
||||
.login .footer .addr {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
gap: 3px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 비밀번호 변경 팝업창 */
|
||||
.login .modal .modal_container {
|
||||
width: 750px;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 10px 10px 30px 10px;
|
||||
color: #666;
|
||||
}
|
||||
.login .pw_desc p {
|
||||
font-size: 1.25rem;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.login .pw_desc span {
|
||||
color: #f60;
|
||||
font-weight: 700;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20px 50px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.login {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
.login .left_box {
|
||||
padding: 30px;
|
||||
flex-direction: column-reverse;
|
||||
gap: 20px;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 30px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .left_box .img img {
|
||||
height: 40px;
|
||||
}
|
||||
.login .login_box {
|
||||
padding: 30px;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
gap: 20px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st::after {
|
||||
left: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
padding: 15px 10px 15px 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .id_save label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
padding: 15px;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 18px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.login .login_box .copy p {
|
||||
width: 100%;
|
||||
}
|
||||
.login .login_box .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
display: none;
|
||||
}
|
||||
.login .footer .addr {
|
||||
font-size: 11px;
|
||||
gap: 3px 10px;
|
||||
}
|
||||
.login .modal .modal_container {
|
||||
width: 90%;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
/* // login */
|
||||
+3
-2
@@ -12,7 +12,8 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://localhost:8883
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost:8883
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
|
||||
|
||||
@@ -13,4 +13,4 @@ NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
@@ -15,4 +15,3 @@ NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
|
||||
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
|
||||
@@ -34,6 +34,14 @@ export default function MapLayout({
|
||||
|
||||
const { fetchCtpv, fetchSgg, fetchBIZ_SE_CD, fetchBIZ_TY_CD } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add("sn-map-page-lock");
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("sn-map-page-lock");
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCtpv();
|
||||
fetchSgg();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
type LegacyAgedPipePopupPageProps = {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
};
|
||||
|
||||
export default async function LegacyAgedPipePopupPage({
|
||||
searchParams,
|
||||
}: LegacyAgedPipePopupPageProps) {
|
||||
const resolvedSearchParams = await searchParams;
|
||||
const nextSearchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(resolvedSearchParams).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => nextSearchParams.append(key, item));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
nextSearchParams.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const queryString = nextSearchParams.toString();
|
||||
redirect(
|
||||
queryString
|
||||
? `/popup/agedPipePopup?${queryString}`
|
||||
: "/popup/agedPipePopup",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AgedPipeFilters } from "./agedPipeUtils";
|
||||
|
||||
type Props = {
|
||||
filters: AgedPipeFilters;
|
||||
onFiltersChange: (filters: AgedPipeFilters) => void;
|
||||
onApply: (filters?: AgedPipeFilters) => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeLayerSettingPanel({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onApply,
|
||||
}: Props) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [elapsedYearsText, setElapsedYearsText] = useState(String(filters.elapsedYears ?? ""));
|
||||
|
||||
useEffect(() => {
|
||||
setElapsedYearsText(String(filters.elapsedYears ?? ""));
|
||||
}, [filters.elapsedYears]);
|
||||
|
||||
const commitElapsedYears = () => {
|
||||
const nextFilters = {
|
||||
...filters,
|
||||
elapsedYears: elapsedYearsText === "" ? ("" as unknown as number) : Number(elapsedYearsText),
|
||||
};
|
||||
onFiltersChange(nextFilters);
|
||||
onApply(nextFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-sm border border-red-100 bg-red-50/35">
|
||||
<div className="relative flex min-h-[30px] items-center justify-center border-b border-red-100 bg-white px-8">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-[#555]"
|
||||
onClick={() => setIsCollapsed((value) => !value)}
|
||||
title={isCollapsed ? "펼치기" : "접기"}
|
||||
>
|
||||
{isCollapsed ? "›" : "⌄"}
|
||||
</button>
|
||||
<span className="text-[1.2rem] font-semibold text-red-700">노후관로 설정</span>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="px-2 py-2">
|
||||
<div className="flex min-h-[30px] items-center gap-1 whitespace-nowrap text-[1.2rem] text-[#333]">
|
||||
<label htmlFor="aged-pipe-elapsed-years">경과년도(년)</label>
|
||||
<input
|
||||
id="aged-pipe-elapsed-years"
|
||||
className="h-[22px] w-14 border border-[#ccd6df] px-1 text-center text-[1.2rem]"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="off"
|
||||
value={elapsedYearsText}
|
||||
onFocus={(event) => event.currentTarget.select()}
|
||||
onChange={(event) => {
|
||||
const digits = event.target.value.replace(/[^0-9]/g, "");
|
||||
setElapsedYearsText(digits);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") commitElapsedYears();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-[22px] w-[22px] place-items-center rounded-full bg-[#31a83b] text-[1.4rem] font-bold leading-none text-white"
|
||||
onClick={commitElapsedYears}
|
||||
title="적용"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_LIST_HEADLIST,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
buildAgedPipeCqlFilters,
|
||||
normalizeAgedPipeFilters,
|
||||
toDisplayPipeRow,
|
||||
validateElapsedYears,
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
} from "./agedPipeUtils";
|
||||
|
||||
const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
|
||||
export default function AgedPipePopupPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [filters, setFilters] = useState<AgedPipeFilters>(() =>
|
||||
normalizeAgedPipeFilters({
|
||||
elapsedYears: Number(searchParams.get("elapsedYears") ?? AGED_PIPE_DEFAULT_FILTERS.elapsedYears),
|
||||
ctpvCd: searchParams.get("ctpvCd") ?? AGED_PIPE_DEFAULT_FILTERS.ctpvCd,
|
||||
sggCd: searchParams.get("sggCd") ?? AGED_PIPE_DEFAULT_FILTERS.sggCd,
|
||||
}),
|
||||
);
|
||||
const [rows, setRows] = useState<AgedPipeRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))),
|
||||
[filters.ctpvCd, sgg],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (ctpv.length === 0) fetchCtpv();
|
||||
if (sgg.length === 0) fetchSgg();
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||
|
||||
const fetchRows = useCallback(
|
||||
async (page: number, nextFilters: AgedPipeFilters, size: number) => {
|
||||
const normalized = normalizeAgedPipeFilters(nextFilters);
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
sort: "string",
|
||||
});
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
|
||||
const response = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
const responseRows = Array.isArray(response?.data) ? response.data : [];
|
||||
|
||||
return {
|
||||
rows: responseRows.map((item: Record<string, unknown>, index: number) =>
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg),
|
||||
),
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadRows = useCallback(
|
||||
async (page = 1, nextFilters = filters) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetchRows(page, nextFilters, AGED_PIPE_PAGE_SIZE);
|
||||
setRows(response.rows);
|
||||
setTotal(response.total);
|
||||
setCurrentPage(page);
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 조회 중 오류:", error);
|
||||
setRows([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchRows, filters],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadRows(1, filters);
|
||||
// 최초 로딩만 수행한다. 이후 조회는 버튼/페이지 변경으로 처리한다.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFilterSyncMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== POPUP_FILTER_SYNC_MESSAGE) return;
|
||||
if (!event.data?.filters) return;
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters);
|
||||
setFilters(normalized);
|
||||
setSelectedIndex(-1);
|
||||
loadRows(1, normalized);
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleFilterSyncMessage);
|
||||
return () => window.removeEventListener("message", handleFilterSyncMessage);
|
||||
}, [loadRows]);
|
||||
|
||||
const handleSearch = (page = 1) => {
|
||||
if (!validateElapsedYears(Number(filters.elapsedYears))) {
|
||||
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
setFilters(normalized);
|
||||
loadRows(page, normalized);
|
||||
};
|
||||
|
||||
const handleRowClick = (index?: number) => {
|
||||
if (typeof index !== "number") return;
|
||||
const row = rows[index];
|
||||
if (!row) return;
|
||||
|
||||
setSelectedIndex(index);
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: POPUP_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (total === 0) {
|
||||
alert("다운로드할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchRows(1, filters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 현황"],
|
||||
headerRange: "A1:J1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: POPUP_MINIMIZE_MESSAGE,
|
||||
filters: normalizeAgedPipeFilters(filters),
|
||||
total,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
window.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex h-screen flex-col overflow-hidden bg-white">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<header className="flex min-h-[44px] items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
|
||||
<h1 className="text-[1.45rem] font-semibold text-white">노후관로 현황</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
||||
onClick={handleMinimize}
|
||||
>
|
||||
최소화
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 border-b border-slate-200 bg-slate-50 pb-4 md:grid-cols-[140px_minmax(0,1fr)_140px_minmax(0,1fr)_auto_auto]">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년도</div>
|
||||
<Input
|
||||
className="h-10 !text-[1.1rem]"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="off"
|
||||
value={String(filters.elapsedYears ?? "")}
|
||||
onChange={(event) => {
|
||||
const digits = event.target.value.replace(/[^0-9]/g, "");
|
||||
setFilters({
|
||||
...filters,
|
||||
elapsedYears: digits === "" ? ("" as unknown as number) : Number(digits),
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") handleSearch(1);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) => setFilters({ ...filters, sggCd: event.target.value })}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="h-10 px-5 !text-[1.1rem]"
|
||||
onClick={() => handleSearch(1)}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="green"
|
||||
className="h-10 px-5 !text-[1.1rem]"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiTable<AgedPipeRow>
|
||||
headName={<>노후관로 조회 결과 총 {total.toLocaleString()}건</>}
|
||||
tableData={rows}
|
||||
headlist={AGED_PIPE_LIST_HEADLIST}
|
||||
pageSize={10}
|
||||
total={total}
|
||||
cellClick={(index) => handleRowClick(index)}
|
||||
onPageChange={(page) => handleSearch(page)}
|
||||
curPage={currentPage}
|
||||
index={selectedIndex}
|
||||
widthFixed={true}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import {
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
} from "../common/table";
|
||||
|
||||
export const AGED_PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
|
||||
export const AGED_PIPE_DEFAULT_ELAPSED_YEARS = 30;
|
||||
export const AGED_PIPE_PAGE_SIZE = 10;
|
||||
export const AGED_PIPE_MIN_ZOOM = 14;
|
||||
export const AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES = ["19000101", "19990101"];
|
||||
|
||||
export type AgedPipeFilters = {
|
||||
elapsedYears: number;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
};
|
||||
|
||||
export type AgedPipeRow = {
|
||||
fid: string;
|
||||
lyrTypeNm: string;
|
||||
rowNumber: number;
|
||||
fcltMngNo: string;
|
||||
instlYmd: string;
|
||||
pdmtVl: string | number;
|
||||
prlgVl: string | number;
|
||||
usgCd: string;
|
||||
mqltCd: string;
|
||||
elapsedYears: number | string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type PipeSource = Record<string, unknown>;
|
||||
|
||||
const useMap = new Map(MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text]));
|
||||
const materialMap = new Map(MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]));
|
||||
|
||||
export const AGED_PIPE_DEFAULT_FILTERS: AgedPipeFilters = {
|
||||
elapsedYears: AGED_PIPE_DEFAULT_ELAPSED_YEARS,
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
};
|
||||
|
||||
export const AGED_PIPE_LIST_HEADLIST = [
|
||||
{ id: "rowNumber", title: "NO", widthPercent: 6 },
|
||||
{ id: "fcltMngNo", title: "시설관리번호", widthPercent: 18 },
|
||||
{ id: "instlYmd", title: "설치일자", widthPercent: 12 },
|
||||
{ id: "pdmtVl", title: "관경(mm)", widthPercent: 10 },
|
||||
{ id: "prlgVl", title: "연장(m)", widthPercent: 10 },
|
||||
{ id: "usgCd", title: "용도", widthPercent: 14 },
|
||||
{ id: "mqltCd", title: "재질", widthPercent: 18 },
|
||||
{ id: "elapsedYears", title: "경과년도", widthPercent: 12, align: "right" },
|
||||
];
|
||||
|
||||
export const AGED_PIPE_EXCEL_HEADER = {
|
||||
rowNumber: "NO",
|
||||
fcltMngNo: "시설관리번호",
|
||||
instlYmd: "설치일자",
|
||||
pdmtVl: "관경(mm)",
|
||||
prlgVl: "연장(m)",
|
||||
usgCd: "용도",
|
||||
mqltCd: "재질",
|
||||
elapsedYears: "경과년도",
|
||||
ctpvNm: "시도",
|
||||
sggNm: "시군구",
|
||||
};
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const normalizeYmd = (value: unknown) =>
|
||||
String(value ?? "").replace(/[^0-9]/g, "").slice(0, 8);
|
||||
|
||||
const getToday = () => new Date();
|
||||
|
||||
export const normalizeAgedPipeFilters = (filters: AgedPipeFilters): AgedPipeFilters => {
|
||||
const elapsedYears = Math.trunc(Number(filters.elapsedYears));
|
||||
|
||||
return {
|
||||
...filters,
|
||||
elapsedYears:
|
||||
Number.isFinite(elapsedYears) && elapsedYears >= 1 && elapsedYears <= 100
|
||||
? elapsedYears
|
||||
: AGED_PIPE_DEFAULT_ELAPSED_YEARS,
|
||||
};
|
||||
};
|
||||
|
||||
export const validateElapsedYears = (value: number) =>
|
||||
Number.isInteger(value) && value >= 1 && value <= 100;
|
||||
|
||||
export const getCutoffYmd = (elapsedYears: number) => {
|
||||
const today = getToday();
|
||||
const cutoff = new Date(today.getFullYear() - elapsedYears, today.getMonth(), today.getDate());
|
||||
return `${cutoff.getFullYear()}${pad2(cutoff.getMonth() + 1)}${pad2(cutoff.getDate())}`;
|
||||
};
|
||||
|
||||
export const getElapsedYears = (value: unknown) => {
|
||||
const normalized = normalizeYmd(value);
|
||||
if (normalized.length < 4 || isInvalidInstallYmd(value)) return null;
|
||||
|
||||
const year = Number(normalized.slice(0, 4));
|
||||
const month = Number(normalized.slice(4, 6) || "1");
|
||||
const day = Number(normalized.slice(6, 8) || "1");
|
||||
if (!year) return null;
|
||||
|
||||
const today = getToday();
|
||||
let elapsed = today.getFullYear() - year;
|
||||
if (
|
||||
today.getMonth() + 1 < month ||
|
||||
(today.getMonth() + 1 === month && today.getDate() < day)
|
||||
) {
|
||||
elapsed -= 1;
|
||||
}
|
||||
|
||||
return Math.max(elapsed, 0);
|
||||
};
|
||||
|
||||
export const isInvalidInstallYmd = (value: unknown) => {
|
||||
const normalized = normalizeYmd(value);
|
||||
return (
|
||||
normalized.length < 8 ||
|
||||
AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
||||
);
|
||||
};
|
||||
|
||||
export const getElapsedYearsDisplayText = (value: unknown) => {
|
||||
const elapsedYears = getElapsedYears(value);
|
||||
return elapsedYears == null ? "확인불가" : `${elapsedYears}년`;
|
||||
};
|
||||
|
||||
export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const cqlFilters = [
|
||||
"instl_ymd IS NOT NULL",
|
||||
...AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.map((prefix) => `instl_ymd NOT LIKE '${prefix}%'`),
|
||||
`instl_ymd <= '${getCutoffYmd(normalized.elapsedYears)}'`,
|
||||
];
|
||||
|
||||
if (normalized.sggCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.sggCd}%'`);
|
||||
} else if (normalized.ctpvCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.ctpvCd}%'`);
|
||||
}
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const getRegionCode = (source: PipeSource) =>
|
||||
String(source.pbadmsEmdCd ?? source.pbadms_emd_cd ?? "").trim();
|
||||
|
||||
export const matchesAgedPipeFilters = (source: PipeSource, filters: AgedPipeFilters) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const instlYmd = normalizeYmd(source.instl_ymd ?? source.instlYmd);
|
||||
if (!instlYmd || isInvalidInstallYmd(instlYmd) || instlYmd > getCutoffYmd(normalized.elapsedYears)) return false;
|
||||
|
||||
const regionCode = getRegionCode(source);
|
||||
if (normalized.sggCd !== "0") return regionCode.startsWith(normalized.sggCd);
|
||||
if (normalized.ctpvCd !== "0") return regionCode.startsWith(normalized.ctpvCd);
|
||||
return true;
|
||||
};
|
||||
|
||||
const resolveRegionName = (
|
||||
code: string,
|
||||
directName: unknown,
|
||||
options: SelectDataType[],
|
||||
prefixLength: number,
|
||||
) => {
|
||||
const direct = String(directName ?? "").trim();
|
||||
if (direct) return direct;
|
||||
|
||||
const prefix = code.slice(0, prefixLength);
|
||||
return options.find((item) => String(item.val).trim() === prefix)?.text ?? "";
|
||||
};
|
||||
|
||||
export const toDisplayPipeRow = (
|
||||
source: PipeSource,
|
||||
index: number,
|
||||
page: number,
|
||||
ctpvOptions: SelectDataType[],
|
||||
sggOptions: SelectDataType[],
|
||||
): AgedPipeRow => {
|
||||
const formatted = formatDisplayData(source) as PipeSource;
|
||||
const codePrefix = getRegionCode(source);
|
||||
const instlYmd = formatted.instlYmd ?? source.instlYmd ?? formatted.instl_ymd ?? source.instl_ymd;
|
||||
const invalidInstallYmd = isInvalidInstallYmd(source.instlYmd ?? source.instl_ymd ?? instlYmd);
|
||||
const trimmedUse = String(source.usgCd ?? source.usg_cd ?? "").trim();
|
||||
const trimmedMaterial = String(source.mqltCd ?? source.mqlt_cd ?? "").trim();
|
||||
const pdmtVl = formatted.pdmtVl ?? source.pdmtVl ?? source.pdmt_vl ?? "-";
|
||||
const prlgVl = formatted.prlgVl ?? source.prlgVl ?? source.prlg_vl ?? "-";
|
||||
const fcltMngNo = String(formatted.fcltMngNo ?? source.fcltMngNo ?? source.fclt_mng_no ?? "-");
|
||||
|
||||
return {
|
||||
...source,
|
||||
...formatted,
|
||||
fid: String(source.fid ?? ""),
|
||||
lyrTypeNm: String(source.lyrTypeNm ?? AGED_PIPE_LAYER_TYPE),
|
||||
rowNumber: (page - 1) * AGED_PIPE_PAGE_SIZE + index + 1,
|
||||
fcltMngNo,
|
||||
swtrmtFcltCd: source.swtrmtFcltCd ?? fcltMngNo,
|
||||
instlYmd: invalidInstallYmd ? "확인불가" : String(instlYmd ?? "-"),
|
||||
pdmtVl: typeof pdmtVl === "number" ? pdmtVl : String(pdmtVl),
|
||||
prlgVl: typeof prlgVl === "number" ? prlgVl : String(prlgVl),
|
||||
usgCd: useMap.get(trimmedUse) ?? String(formatted.usgCd ?? source.usgCd ?? source.usg_cd ?? "-"),
|
||||
mqltCd:
|
||||
materialMap.get(trimmedMaterial) ??
|
||||
String(formatted.mqltCd ?? source.mqltCd ?? source.mqlt_cd ?? "-"),
|
||||
elapsedYears: getElapsedYears(source.instlYmd ?? source.instl_ymd) ?? "확인불가",
|
||||
ctpvNm: resolveRegionName(
|
||||
codePrefix,
|
||||
formatted.ctpvNm ?? source.ctpvNm ?? source.ctpv_nm,
|
||||
ctpvOptions,
|
||||
2,
|
||||
),
|
||||
sggNm: resolveRegionName(
|
||||
codePrefix,
|
||||
formatted.sggNm ?? source.sggNm ?? source.sgg_nm,
|
||||
sggOptions,
|
||||
5,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const isAgedPipeLayerNode = (node: any) => {
|
||||
const data = node?.data ?? node;
|
||||
const layerName = String(data?.lyrNm ?? node?.text ?? "");
|
||||
const layerTypeName = String(data?.lyrTyNm ?? "");
|
||||
|
||||
return (
|
||||
layerTypeName === "tb_old_pipe" ||
|
||||
layerName.includes("노후관로")
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export const MsSwagePplCol = [
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
{ id: 'inst_mng_no', title: '기관관리번호' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'elapsedYears', title: '경과년도' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
{ id: 'pdmt_vl', title: '관경(mm)' },
|
||||
{ id: 'prlg_vl', title: '연장(m)' },
|
||||
|
||||
@@ -13,29 +13,30 @@
|
||||
* - 위경도로 강우시 미처리 하수 목록 조회: GET /api/v1/onsemiro/getSwgRainsByGeomety (sewerage-service)
|
||||
* - 강우시 월류 하수 관리 모니터링 조회: GET /api/v1/onsemiro/swgRain/{adstDstrCd}/{manageNo}/mntrngs (sewerage-service)
|
||||
*/
|
||||
import { UiModal, UiTable } from '@common/business_components/ui';
|
||||
'use client';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const inputColumn = [
|
||||
{ id: "prrtymngYn", title: "중점여부", type: "select", options: ["우수토실", "중점우수토실"] },
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
];
|
||||
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick, renderMode = 'inline' }: {
|
||||
featureData: any,
|
||||
isMainSsc?: boolean,
|
||||
chmbrXcnts?: string,
|
||||
chmbrYdnts?: string,
|
||||
onSaveClick?: () => void
|
||||
onSaveClick?: () => void,
|
||||
renderMode?: 'inline' | 'popup'
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDataLoaded, setIsDataLoaded] = useState(false);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [rainMeasureData, setRainMeasureData] = useState<any>({}); // 강우, 측정 정보
|
||||
const [mntrngsData, setMntrngsData] = useState<any[]>([]); // 수량/수질측정 정보
|
||||
@@ -65,22 +66,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
|
||||
|
||||
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
|
||||
const formatNoYearNoSec = (val: any) => {
|
||||
@@ -138,22 +124,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
|
||||
|
||||
// 각 아이템 전처리
|
||||
const processedDataArray = result.content.map((data: any) => {
|
||||
@@ -181,14 +152,15 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
}
|
||||
}, [chmbrXcnts, chmbrYdnts]);
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
const openDetailPopup = () => {
|
||||
const popupWindow = openWindowPopup("sscDetail", {
|
||||
chmbrXcnts,
|
||||
chmbrYdnts,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
if (!isDataLoaded) {
|
||||
setTimeout(() => setIsDataLoaded(true), 100);
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -322,37 +294,28 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isMainSsc && (
|
||||
{isMainSsc && renderMode !== 'popup' && (
|
||||
<div>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
|
||||
</div>
|
||||
<Button
|
||||
className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600'
|
||||
onClick={openModal}
|
||||
onClick={openDetailPopup}
|
||||
>
|
||||
상세보기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-none
|
||||
max-h-none
|
||||
overflow-y-auto
|
||||
z-[101]
|
||||
`}
|
||||
body={renderModalBody()}
|
||||
noHeaderYn='Y'
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
{isMainSsc && renderMode === 'popup' && (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">중점우수토실 운영정보</h1>
|
||||
{renderModalBody()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Ssc
|
||||
export default Ssc
|
||||
|
||||
@@ -9,28 +9,37 @@
|
||||
* - 조회 버튼을 통한 모달 표시 및 ReportTab 연동
|
||||
*/
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { UiModal } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import ReportTab from '../../searchModal/reportTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
// 하수처리시설
|
||||
const headList = [
|
||||
{ id: "title", title: "하수도정비 기본계획보고서" },
|
||||
];
|
||||
|
||||
const SwtrmtFclt = ({ gisObject }: { gisObject?: any }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
|
||||
const ctpvNm = gisObject?.ctpv_nm;
|
||||
const sggNm = gisObject?.sgg_nm;
|
||||
|
||||
const handleOpenReportPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VerticalTable
|
||||
children={{
|
||||
title: <Button className="gray" onClick={() => setIsModalOpen(true)}>조회</Button>
|
||||
title: <Button className="gray" onClick={handleOpenReportPopup}>조회</Button>
|
||||
}}
|
||||
headName='디지털 자료관 연계정보'
|
||||
tableData={[]}
|
||||
@@ -43,29 +52,6 @@ const SwtrmtFclt = ({ gisObject }: { gisObject?: any }) => {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
/>
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle="하수도정비 기본계획보고서"
|
||||
headerTitleClass="text-3xl font-semibold text-gray-700"
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-[60vw]
|
||||
max-h-[100vh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={
|
||||
<ReportTab
|
||||
open={isModalOpen}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm}
|
||||
sggNm={sggNm}
|
||||
/>
|
||||
}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TInftwtAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useInfilChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
@@ -28,7 +26,6 @@ type DataType = {
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore();
|
||||
|
||||
const [selectedAplcnYm, setSelectedAplcnYm] = useState('');
|
||||
const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]);
|
||||
@@ -66,12 +63,6 @@ const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
setInftwtQty('0');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState(); // 모달이 닫힐 때 리셋
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
@@ -276,21 +267,10 @@ const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1000px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">침입수 분석</h1>
|
||||
{renderBody()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TRdiiAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useRdiiChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
@@ -29,7 +27,6 @@ type LineBarChartData = {
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore();
|
||||
|
||||
const [aplcnYm, setAplcnYm] = useState('');
|
||||
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
||||
@@ -72,12 +69,6 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
setTotalVolume('0');// 총체적 m³/mm 초기화
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState();
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
@@ -336,21 +327,10 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1100px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
|
||||
{renderBody()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ModalStore {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useInfilChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
|
||||
export const useRdiiChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
@@ -34,13 +33,12 @@ import Stroke from "ol/style/Stroke.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import { getLayerContentComponent } from "../common";
|
||||
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
|
||||
import InfilChart from './chart/infilChart';
|
||||
import RdiiChart from './chart/rdiiChart';
|
||||
import { useInfilChartModalStore, useRdiiChartModalStore } from "./chartModalStore";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { codeMapper } from "../common/codeMapper";
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { searchUpperPpls } from "./smh";
|
||||
import { getElapsedYearsDisplayText, isInvalidInstallYmd } from "../agedPipe/agedPipeUtils";
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const linkedFacilityHighlightStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
@@ -63,7 +61,6 @@ const linkedFacilityHighlightStyle = new Style({
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
|
||||
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
|
||||
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
|
||||
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
|
||||
@@ -74,12 +71,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
const { setIsOpen: setIsModalInfil } = useInfilChartModalStore();
|
||||
const { setIsOpen: setIsModalRdii } = useRdiiChartModalStore();
|
||||
const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태
|
||||
const [isLoading, setIsLoading] = useState(false); // 로딩 상태
|
||||
const [chmbrXcnts, setChmbrXcnts] = useState<string>(''); // 로딩 상태
|
||||
@@ -88,7 +82,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const linkedFacilityOriginalStyleRef = useRef<any>(null);
|
||||
|
||||
// 🎯 관로관리번호 추출
|
||||
const pplMngNo = featureData?.values_?.fclt_mng_no || "";
|
||||
const pplMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || "";
|
||||
|
||||
const featureId = typeof featureData.getId === 'function'
|
||||
? featureData.getId()
|
||||
@@ -258,7 +252,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
case 'tb_ms_swage_ppl': {
|
||||
result = '하수관로';
|
||||
setHeadlist(MsSwagePplCol2)
|
||||
setLayerSpecificNm(featureData.swtrmtFcltCd);
|
||||
setLayerSpecificNm(featureData.fcltMngNo ?? featureData.swtrmtFcltCd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
@@ -341,6 +335,14 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 🎯 기본 데이터 포맷팅
|
||||
const formattedData = formatDisplayData(data);
|
||||
if (selectedLayerName === '하수관로') {
|
||||
const installYmd = data.instl_ymd ?? data.instlYmd;
|
||||
formattedData.elapsedYears = getElapsedYearsDisplayText(installYmd);
|
||||
if (isInvalidInstallYmd(installYmd)) {
|
||||
formattedData.instl_ymd = "확인불가";
|
||||
formattedData.instlYmd = "확인불가";
|
||||
}
|
||||
}
|
||||
|
||||
setFetchData(formattedData);
|
||||
setTableData(formattedData);
|
||||
@@ -407,8 +409,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 자체입력정보 API 엔드포인트 선택
|
||||
let apiEndpoint = '';
|
||||
const fcltMngNo = featureData?.values_?.fclt_mng_no || null;
|
||||
const mngInstCd = featureData?.values_?.mng_inst_cd || null;
|
||||
const fcltMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || null;
|
||||
const mngInstCd = featureData?.values_?.mng_inst_cd || featureData?.mngInstCd || null;
|
||||
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`;
|
||||
@@ -461,16 +463,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
const shouldSave = window.confirm('정보를 저장하시겠습니까?');
|
||||
if (!shouldSave) return;
|
||||
btnClickEvent();
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
@@ -557,14 +552,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
{/* 하수관로일 때만 Infil/RDII 버튼 표시 */}
|
||||
{layerNm === '하수관로' && (
|
||||
<>
|
||||
<Button onClick={() => setIsModalInfil(true)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const popupWindow = openWindowPopup("infilChart", { pplMngNo });
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}}
|
||||
className="py-1 text-xl text-blue-600 border border-blue-600 rounded hover:bg-gray-700"
|
||||
color="white"
|
||||
size="sm"
|
||||
>
|
||||
침입수분석
|
||||
</Button>
|
||||
<Button onClick={() => setIsModalRdii(true)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const popupWindow = openWindowPopup("rdiiChart", { pplMngNo });
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}}
|
||||
className="py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700 mr-4"
|
||||
color="white"
|
||||
size="sm"
|
||||
@@ -598,7 +605,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}>
|
||||
<VerticalTable
|
||||
headName='기본정보'
|
||||
headName={
|
||||
layerNm === '하수관로' && tableData?.elapsedYears
|
||||
? <>기본정보 <span className="ml-2 rounded-sm bg-red-50 px-2 py-0.5 text-[1.1rem] font-semibold text-red-700">경과년도 {tableData.elapsedYears}</span></>
|
||||
: '기본정보'
|
||||
}
|
||||
tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined}
|
||||
tableData={[tableData]}
|
||||
headlist={headlist}
|
||||
@@ -617,7 +628,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
@@ -631,9 +641,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<InfilChart pplMngNo={pplMngNo} />
|
||||
<RdiiChart pplMngNo={pplMngNo} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,40 +15,20 @@
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getData } from '@/components/lib/api';
|
||||
|
||||
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
|
||||
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
|
||||
const BASE_URL = "/sewerage-service/api";
|
||||
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get("content-length");
|
||||
if (contentLength === "0") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === "") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// JSON 파싱 시도
|
||||
const response = JSON.parse(text);
|
||||
data = response.data;
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data) {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
|
||||
)
|
||||
}
|
||||
|
||||
export default Smh;
|
||||
export default Smh;
|
||||
|
||||
@@ -18,52 +18,176 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { getData } from "../../components/lib/api";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getData, getRawJson } from "../../components/lib/api";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
|
||||
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
import { Button } from "@common/components";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useStore from "@/store/useStore";
|
||||
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import AgedPipeLayerSettingPanel from "./components/agedPipe/AgedPipeLayerSettingPanel";
|
||||
import {
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
AGED_PIPE_MIN_ZOOM,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
buildAgedPipeCqlFilters,
|
||||
isAgedPipeLayerNode,
|
||||
matchesAgedPipeFilters,
|
||||
normalizeAgedPipeFilters,
|
||||
toDisplayPipeRow,
|
||||
validateElapsedYears,
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
} from "./components/agedPipe/agedPipeUtils";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
|
||||
const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
|
||||
const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
|
||||
const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
|
||||
const AGED_PIPE_FOCUS_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||
const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [50, 50, 50, 50];
|
||||
|
||||
const appendAgedPipeControlNode = (data: any[]) => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
if (data.some((item) => String(item.lyrSn) === AGED_PIPE_CONTROL_NODE_ID || item.lyrNm === "노후관로")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return [
|
||||
...data,
|
||||
{
|
||||
lyrSn: AGED_PIPE_CONTROL_NODE_ID,
|
||||
id: AGED_PIPE_CONTROL_NODE_ID,
|
||||
lyrNm: "노후관로",
|
||||
upperLyrSn: null,
|
||||
upId: null,
|
||||
sortOrdr: "4",
|
||||
drawOrdr: null,
|
||||
flterSql: null,
|
||||
gobjTy: "",
|
||||
lyrTyNm: "tb_old_pipe",
|
||||
useYn: "Y",
|
||||
minViewSize: null,
|
||||
iconNm: null,
|
||||
iconSize: null,
|
||||
lblAtrbNm: null,
|
||||
fontNm: null,
|
||||
fontIntnColor: null,
|
||||
fontExtnColor: null,
|
||||
fontExtnWdth: null,
|
||||
lnWdth: null,
|
||||
lnColor: null,
|
||||
polgnColor: null,
|
||||
initEprssYn: "Y",
|
||||
clickPsblYn: "N",
|
||||
exprsYn: "Y",
|
||||
cmncNm: "all",
|
||||
srvcNm: "wfs",
|
||||
actlLyrYn: "N",
|
||||
clckZoom: null,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const Map = () => {
|
||||
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
||||
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
|
||||
const [layerLoadError, setLayerLoadError] = useState<string | null>(null);
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
|
||||
const [featureData, setFeatureData] = useState<any>(null); // 선택된 피쳐 데이터
|
||||
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false);
|
||||
const [draftAgedPipeFilters, setDraftAgedPipeFilters] =
|
||||
useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
|
||||
const [appliedAgedPipeFilters, setAppliedAgedPipeFilters] =
|
||||
useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
|
||||
const [isAgedPipeListOpen, setIsAgedPipeListOpen] = useState(false);
|
||||
const [isAgedPipeListMinimized, setIsAgedPipeListMinimized] = useState(false);
|
||||
const [agedPipeRows, setAgedPipeRows] = useState<AgedPipeRow[]>([]);
|
||||
const [agedPipeTotalCount, setAgedPipeTotalCount] = useState(0);
|
||||
const [agedPipeCurrentPage, setAgedPipeCurrentPage] = useState(1);
|
||||
const [isAgedPipeListLoading, setIsAgedPipeListLoading] = useState(false);
|
||||
const { ctpv, sgg } = useStore();
|
||||
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
|
||||
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
if (gisMaps.current) {
|
||||
setMapReady(true);
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handlePopupMessage = async (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
const data = event.data;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fid = data.payload?.fid;
|
||||
if (!layerNm || !fid || !core?.showSearchResultsFromGeoJSON) return;
|
||||
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fidsRaw = data.payload?.fids;
|
||||
if (!layerNm || !fidsRaw || !core?.showMultipleSearchResultsFromGeoJSON) return;
|
||||
|
||||
let fids: string[] = [];
|
||||
try {
|
||||
fids = JSON.parse(fidsRaw);
|
||||
} catch (error) {
|
||||
console.error("다중 시설 메시지 파싱 실패:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handlePopupMessage);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("message", handlePopupMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') data = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') data = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
setTreeData(data);
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(appendAgedPipeControlNode(data));
|
||||
setLayerLoadError(null);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
|
||||
throw new Error('Invalid JSON response from server');
|
||||
const message =
|
||||
error instanceof Error ? error.message : "레이어 설정 조회 중 알 수 없는 오류가 발생했습니다.";
|
||||
console.error("레이어 설정 조회 실패:", error);
|
||||
setTreeData([]);
|
||||
setLayerLoadError(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,13 +236,23 @@ const Map = () => {
|
||||
if (geometry) {
|
||||
let extent = geometry.getExtent();
|
||||
|
||||
const layerTypeName = typeof foundFeature.getId === "function"
|
||||
? String(foundFeature.getId()).split(".")[0]
|
||||
: data?.lyrTypeNm;
|
||||
const shouldUseAgedPipeFocus =
|
||||
shouldFocusAgedPipeOnNextSelectRef.current && layerTypeName === AGED_PIPE_LAYER_TYPE;
|
||||
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
padding: shouldUseAgedPipeFocus ? AGED_PIPE_FOCUS_PADDING : DEFAULT_FEATURE_FIT_PADDING,
|
||||
maxZoom: shouldUseAgedPipeFocus ? 18 : 16,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
|
||||
if (shouldUseAgedPipeFocus) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
}
|
||||
}
|
||||
select.selectFeature(foundFeature, true);
|
||||
} else {
|
||||
@@ -131,6 +265,176 @@ const Map = () => {
|
||||
};
|
||||
|
||||
const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }];
|
||||
|
||||
const fetchAgedPipeRows = useCallback(
|
||||
async (page: number, filters: AgedPipeFilters, size: number) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
sort: "string",
|
||||
});
|
||||
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
|
||||
const response = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
const rows = Array.isArray(response?.data) ? response.data : [];
|
||||
|
||||
return {
|
||||
rows: rows.map((item: Record<string, unknown>, index: number) =>
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg),
|
||||
),
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadAgedPipeList = useCallback(
|
||||
async (page = 1, filters = appliedAgedPipeFilters) => {
|
||||
setIsAgedPipeListLoading(true);
|
||||
try {
|
||||
const response = await fetchAgedPipeRows(page, filters, AGED_PIPE_PAGE_SIZE);
|
||||
setAgedPipeRows(response.rows);
|
||||
setAgedPipeTotalCount(response.total);
|
||||
setAgedPipeCurrentPage(page);
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 조회 중 오류:", error);
|
||||
setAgedPipeRows([]);
|
||||
setAgedPipeTotalCount(0);
|
||||
} finally {
|
||||
setIsAgedPipeListLoading(false);
|
||||
}
|
||||
},
|
||||
[appliedAgedPipeFilters, fetchAgedPipeRows],
|
||||
);
|
||||
|
||||
const syncAgedPipePopupFilters = useCallback((filters: AgedPipeFilters) => {
|
||||
const popup = agedPipePopupWindowRef.current;
|
||||
if (!popup || popup.closed) {
|
||||
agedPipePopupWindowRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
popup.postMessage(
|
||||
{
|
||||
type: AGED_PIPE_FILTER_SYNC_MESSAGE,
|
||||
filters: normalizeAgedPipeFilters(filters),
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleApplyAgedPipeFilters = useCallback((filters = draftAgedPipeFilters) => {
|
||||
if (!validateElapsedYears(Number(filters.elapsedYears))) {
|
||||
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
setDraftAgedPipeFilters(normalized);
|
||||
setAppliedAgedPipeFilters(normalized);
|
||||
syncAgedPipePopupFilters(normalized);
|
||||
|
||||
if (isAgedPipeListOpen) {
|
||||
loadAgedPipeList(1, normalized);
|
||||
}
|
||||
}, [draftAgedPipeFilters, isAgedPipeListOpen, loadAgedPipeList, syncAgedPipePopupFilters]);
|
||||
|
||||
const openAgedPipePopupWindow = useCallback(() => {
|
||||
const popup = openWindowPopup("agedPipePopup", {
|
||||
elapsedYears: String(appliedAgedPipeFilters.elapsedYears),
|
||||
ctpvCd: appliedAgedPipeFilters.ctpvCd,
|
||||
sggCd: appliedAgedPipeFilters.sggCd,
|
||||
});
|
||||
|
||||
if (!popup) {
|
||||
alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
|
||||
return null;
|
||||
}
|
||||
|
||||
agedPipePopupWindowRef.current = popup;
|
||||
setIsAgedPipeListOpen(true);
|
||||
setIsAgedPipeListMinimized(false);
|
||||
return popup;
|
||||
}, [appliedAgedPipeFilters]);
|
||||
|
||||
const openAgedPipeList = useCallback(() => {
|
||||
openAgedPipePopupWindow();
|
||||
}, [openAgedPipePopupWindow]);
|
||||
|
||||
const handleAgedPipeRowDoubleClick = useCallback(
|
||||
async (row: AgedPipeRow) => {
|
||||
try {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = true;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
|
||||
} catch (error) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
console.error("노후관로 위치 이동 중 오류:", error);
|
||||
}
|
||||
|
||||
setFeatureData(row as any);
|
||||
setIsOpen(true);
|
||||
setIsSearchModalOpen(false);
|
||||
},
|
||||
[setIsSearchModalOpen],
|
||||
);
|
||||
|
||||
const handleAgedPipeDownload = useCallback(async () => {
|
||||
if (agedPipeTotalCount === 0) {
|
||||
alert("다운로드할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsAgedPipeListLoading(true);
|
||||
const response = await fetchAgedPipeRows(1, appliedAgedPipeFilters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 현황"],
|
||||
headerRange: "A1:J1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsAgedPipeListLoading(false);
|
||||
}
|
||||
}, [agedPipeTotalCount, appliedAgedPipeFilters, fetchAgedPipeRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAgedPipePopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data?.type === AGED_PIPE_ROW_SELECT_MESSAGE) {
|
||||
if (!event.data?.row) return;
|
||||
handleAgedPipeRowDoubleClick(event.data.row as AgedPipeRow);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === AGED_PIPE_POPUP_MINIMIZE_MESSAGE) {
|
||||
agedPipePopupWindowRef.current = null;
|
||||
if (event.data?.filters) {
|
||||
const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters);
|
||||
setDraftAgedPipeFilters(normalized);
|
||||
setAppliedAgedPipeFilters(normalized);
|
||||
}
|
||||
setAgedPipeTotalCount(Number(event.data?.total ?? agedPipeTotalCount));
|
||||
setIsAgedPipeListMinimized(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleAgedPipePopupMessage);
|
||||
return () => window.removeEventListener("message", handleAgedPipePopupMessage);
|
||||
}, [agedPipeTotalCount, handleAgedPipeRowDoubleClick]);
|
||||
|
||||
// WaterDepth 표시 여부를 결정하는 함수
|
||||
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
const waterDepthLayers = checkedNodesData.filter(node =>
|
||||
@@ -144,12 +448,88 @@ const Map = () => {
|
||||
// 체크된 레이어 변경 처리 함수
|
||||
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
handleWaterDepthVisibility(checkedIds, checkedNodesData);
|
||||
setIsAgedPipeChecked(checkedNodesData.some((node) => isAgedPipeLayerNode(node)));
|
||||
}, [handleWaterDepthVisibility]);
|
||||
|
||||
const handleOpenLayerPopup = useCallback(
|
||||
(_layerNm: string, node?: any) => {
|
||||
if (isAgedPipeLayerNode(node)) {
|
||||
openAgedPipeList();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[openAgedPipeList],
|
||||
);
|
||||
|
||||
const inlineLayerSettingView = useCallback(
|
||||
(node: any) => {
|
||||
if (!isAgedPipeChecked || !isAgedPipeLayerNode(node)) return null;
|
||||
|
||||
return (
|
||||
<AgedPipeLayerSettingPanel
|
||||
filters={draftAgedPipeFilters}
|
||||
onFiltersChange={setDraftAgedPipeFilters}
|
||||
onApply={handleApplyAgedPipeFilters}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[draftAgedPipeFilters, handleApplyAgedPipeFilters, isAgedPipeChecked],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
|
||||
if (!isAgedPipeChecked) return;
|
||||
|
||||
const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === AGED_PIPE_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
) as any;
|
||||
|
||||
if (!baseLayerCode) return;
|
||||
|
||||
const layerCode = {
|
||||
...baseLayerCode,
|
||||
id: AGED_PIPE_HIGHLIGHT_LAYER_ID,
|
||||
lyrSn: AGED_PIPE_HIGHLIGHT_LAYER_ID,
|
||||
lyrNm: "노후관로",
|
||||
lyrTyNm: AGED_PIPE_LAYER_TYPE,
|
||||
gobjTy: "line",
|
||||
actlLyrYn: "Y",
|
||||
exprsYn: "Y",
|
||||
clickPsblYn: "Y",
|
||||
cmncNm: "bbox",
|
||||
srvcNm: "wfs",
|
||||
flterSql: buildAgedPipeCqlFilters(appliedAgedPipeFilters).join(" and "),
|
||||
lnWdth: "4",
|
||||
lnColor: "rgba(235, 40, 40, 0.82)",
|
||||
drawOrdr: "9999",
|
||||
minViewSize: String(AGED_PIPE_MIN_ZOOM),
|
||||
};
|
||||
|
||||
const layer = core?.addWfsLayer?.(layerCode);
|
||||
layer?.set?.("id", AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
layer?.set?.("lyrNm", "노후관로");
|
||||
layer?.set?.("lyrTyNm", AGED_PIPE_LAYER_TYPE);
|
||||
|
||||
return () => {
|
||||
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
};
|
||||
}, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container">
|
||||
<div className="bg-container relative h-[calc(100dvh-5rem)] overflow-hidden">
|
||||
{layerLoadError ? (
|
||||
<div className="absolute left-4 top-4 z-[60] max-w-[520px] rounded border border-red-200 bg-white px-4 py-3 text-sm text-red-700 shadow">
|
||||
레이어 설정 조회 실패: {layerLoadError}
|
||||
</div>
|
||||
) : null}
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={true}
|
||||
@@ -165,8 +545,11 @@ const Map = () => {
|
||||
isSearchModalOpen={isSearchModalOpen}
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={handleCheckedLayersChange}
|
||||
onOpenPopup={handleOpenLayerPopup}
|
||||
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
|
||||
waterDepth={showWaterDepth ? WaterDepth : undefined}
|
||||
inlineLayerSettingView={inlineLayerSettingView}
|
||||
layerIconVisible={isAgedPipeLayerNode}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying as any}
|
||||
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
|
||||
@@ -176,6 +559,37 @@ const Map = () => {
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
/>
|
||||
{isAgedPipeListMinimized && (
|
||||
<div className="fixed left-[480px] top-[68px] z-[90] flex min-w-[250px] items-center gap-3 rounded-sm border border-red-200 bg-white/95 px-3 py-2 shadow-[0_8px_22px_rgba(15,23,42,0.22)] backdrop-blur">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-red-50">
|
||||
<span className="block h-4 w-4 rounded-sm border-2 border-red-500 bg-red-500/20" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[1.25rem] font-semibold leading-tight text-red-700">노후관로 현황</div>
|
||||
<div className="text-[1rem] leading-tight text-slate-500">
|
||||
{appliedAgedPipeFilters.elapsedYears}년 이상 · {agedPipeTotalCount.toLocaleString()}건
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
size="xs"
|
||||
className="h-7 px-3"
|
||||
onClick={openAgedPipeList}
|
||||
>
|
||||
열기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="h-7 px-3"
|
||||
onClick={() => setIsAgedPipeListMinimized(false)}
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭 (셀 클릭)
|
||||
*
|
||||
* 📄 Report Files Modal
|
||||
* Next.js App Router (app/) 기반 보고서 파일 모달 컴포넌트
|
||||
*
|
||||
* - 하수도정비 기본계획 보고서 파일 목록 표시
|
||||
* - 파일명 및 등록일자 정보 테이블 출력
|
||||
* - 페이지네이션 및 로딩 오버레이 지원
|
||||
* - 파일 클릭 이벤트 처리 및 상세 조회
|
||||
* - Zustand를 통한 모달 상태 및 데이터 관리
|
||||
* - 모달 열림/닫힘 시 데이터 초기화 기능
|
||||
*/
|
||||
'use client'
|
||||
import React, { useEffect } from 'react';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import useReportTab from './useReportTab';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { UiModal } from "@common/business_components";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
|
||||
type ModalOpenProp = { clsfSystmSn: string } // 자료관 기본계획 일련번호
|
||||
const ReportFilesModal: React.FC<ModalOpenProp> = ({ clsfSystmSn }: ModalOpenProp) => {
|
||||
// 하수도정비 기본계획 보고서 파일목록
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
const {
|
||||
curPage, setCurPage,
|
||||
isLoading,
|
||||
isResetPage,
|
||||
isResetSelection,
|
||||
setIsResetPage,
|
||||
setIsResetSelection
|
||||
} = useSearchModalStore();
|
||||
const {
|
||||
isOpen: isModalOpen,
|
||||
files,
|
||||
dcFiles: filesOrigin,
|
||||
setIsOpen: setIsModalOpen,
|
||||
setFiles,
|
||||
fetchDcFiles
|
||||
} = useReportTabStore();
|
||||
const { getFiles, handleCellClick } = useReportTab();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchDcFiles(clsfSystmSn);
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen === true) {
|
||||
getFiles(); // 파일 목록 조회
|
||||
} else {
|
||||
setFiles([]); // 모달이 닫힐 때 파일 목록 초기화
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
}
|
||||
}, [filesOrigin, isModalOpen, curPage]);
|
||||
|
||||
const renderBody = () => (
|
||||
<>
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={(() => {
|
||||
return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건</>);
|
||||
})()
|
||||
}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={filesOrigin.length}
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
min-h-[50vh] max-h-[90dvh]
|
||||
max-w-[750px]
|
||||
h-auto w-auto
|
||||
overflow-auto
|
||||
z-[5000]
|
||||
flex items-start
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportFilesModal);
|
||||
@@ -22,7 +22,7 @@ import useStore from '@/store/useStore';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import ReportFilesModal from './reportFilesModal'; // 파일목록모달
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
@@ -56,7 +56,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
const [ctpvCd, setCtpvCd] = useState('0');
|
||||
const [sggCd, setSggCd] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState("0");
|
||||
const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호
|
||||
|
||||
const {
|
||||
tableData,
|
||||
@@ -72,8 +71,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
} = useSearchModalStore();
|
||||
const { ctpv, sgg } = useStore();
|
||||
const {
|
||||
isOpen: isReportFilesModalOpen,
|
||||
setIsOpen: setIsReportFilesModalOpen,
|
||||
dcTree: reportOriginData,
|
||||
fetchDcTree
|
||||
} = useReportTabStore(); // 모달 상태 관리 스토어
|
||||
@@ -139,8 +136,14 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
if (typeof index === "number" && tableData[index]) {
|
||||
const reportData = tableData[index];
|
||||
if (reportData.fileSn !== "") {
|
||||
setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정
|
||||
setIsReportFilesModalOpen(true); // 모달 열기
|
||||
const popupWindow = openWindowPopup("reportFiles", {
|
||||
clsfSystmSn: reportData.clsfSystmSn,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -263,10 +266,8 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 모달 컴포넌트 */}
|
||||
{isReportFilesModalOpen && <ReportFilesModal clsfSystmSn={clsfSystmSn} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportTab);
|
||||
export default React.memo(ReportTab);
|
||||
|
||||
@@ -11,21 +11,26 @@
|
||||
* - Zustand를 통한 검색 모달 상태 관리
|
||||
* - 로딩 오버레이 및 선택 초기화 지원
|
||||
*/
|
||||
'use client';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { UiModal, UITab, UiTable } from '@common/business_components/ui';
|
||||
import { UITab, UiTable } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import BasicTab, { BASIC_TYPES } from './basicTab';
|
||||
import OperationTab from './operationTab';
|
||||
import ReportTab from './reportTab';
|
||||
import useSearchModal from './useSearchModal';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
const SearchModal: React.FC = () => {
|
||||
type SearchModalProps = {
|
||||
renderMode?: "launcher" | "popup";
|
||||
};
|
||||
|
||||
const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher" }) => {
|
||||
const {
|
||||
showTable,
|
||||
isSearchModalOpen, setIsSearchModalOpen,
|
||||
@@ -59,6 +64,7 @@ const SearchModal: React.FC = () => {
|
||||
operationResultExcelDownload,
|
||||
handleOperationPageNation
|
||||
} = useOperationTab();
|
||||
const hasAutoOpenedReportRef = useRef(false);
|
||||
|
||||
|
||||
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
|
||||
@@ -87,6 +93,42 @@ const SearchModal: React.FC = () => {
|
||||
return [];
|
||||
}, [activeTab, workType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderMode !== "launcher" || !isSearchModalOpen) return;
|
||||
|
||||
const popupWindow = openWindowPopup("mapSearch", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
|
||||
setIsSearchModalOpen(false);
|
||||
}, [renderMode, isSearchModalOpen]);
|
||||
|
||||
const openReportTabPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('?앹뾽??李⑤떒?섏뿀?듬땲?? 釉뚮씪?곗? ?앹뾽 ?덉슜 ?ㅼ젙???뺤씤?댁<?몄슂.');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'report') {
|
||||
hasAutoOpenedReportRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAutoOpenedReportRef.current) return;
|
||||
|
||||
openReportTabPopup();
|
||||
hasAutoOpenedReportRef.current = true;
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters();
|
||||
}, [activeTab, basicType, workType]);
|
||||
@@ -119,7 +161,23 @@ const SearchModal: React.FC = () => {
|
||||
/>
|
||||
{activeTab === 'basic' && (<BasicTab />)} {/* 기본정보 탭 */}
|
||||
{activeTab === 'operation' && (<OperationTab />)} {/* 유지관리 정보 탭 */}
|
||||
{activeTab === 'report' && (<ReportTab open={isSearchModalOpen} />)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
{activeTab === 'report' && (
|
||||
<div className="mt-4 flex h-[79dvh] w-full items-center justify-center rounded-lg border bg-white p-6">
|
||||
<div className="flex max-w-[520px] flex-col items-center text-center">
|
||||
<h2 className="mb-3 text-2xl font-semibold text-gray-900">
|
||||
하수도정비 기본계획보고서
|
||||
</h2>
|
||||
<p className="mb-5 text-lg leading-8 text-gray-600">
|
||||
보고서 상세는 모달 내부 탭 대신
|
||||
<br />
|
||||
전용 윈도우 팝업으로 열리도록 전환되었습니다.
|
||||
</p>
|
||||
<Button className="px-6 py-2 text-white" onClick={openReportTabPopup}>
|
||||
전용 팝업 열기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
|
||||
|
||||
{/* 📋 결과 리스트 */}
|
||||
@@ -214,22 +272,16 @@ const SearchModal: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
|
||||
if (renderMode === "launcher") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='overflow-x-auto overflow-y-hidden'>
|
||||
<UiModal
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[1220px]
|
||||
max-h-[970px]
|
||||
overflow-y-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={renderBody()}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
<div className='h-dvh overflow-x-auto overflow-y-hidden bg-white p-4'>
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">관망도 검색</h1>
|
||||
<div className="h-[calc(100dvh-5.6rem)] w-full overflow-y-hidden rounded-lg border bg-white p-4">
|
||||
{renderBody()}
|
||||
</div>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,13 @@ import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
|
||||
import { swageMhlCqlFilters } from './swageMhl';
|
||||
import { swagePmstnCqlFilters } from './swagePmstn';
|
||||
import { stofcbrCqlFilters } from './stofcbr';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
|
||||
export default function useBasicTab() {
|
||||
@@ -115,9 +122,21 @@ export default function useBasicTab() {
|
||||
// 기본정보 테이블 cellClick 핸들러
|
||||
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
const row = tableData[index];
|
||||
|
||||
if (isPopupWindow()) {
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fid: String(row.fid),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
@@ -23,6 +23,13 @@ import { confirmToast } from '@/components/ui/toast'
|
||||
import { toast } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import useStore from '@/store/useStore';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
|
||||
export default function useOperationTab() {
|
||||
@@ -200,9 +207,21 @@ export default function useOperationTab() {
|
||||
// 유지관리 정보 디테일 테이블 cellClick 핸들러
|
||||
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
const row = detailTableData[index];
|
||||
|
||||
if (isPopupWindow()) {
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fid: String(row.fid),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
@@ -434,6 +453,20 @@ export default function useOperationTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const emitMultipleFeaturesToMap = () => {
|
||||
if (detailTableData.length === 0 || !isPopupWindow()) return false;
|
||||
|
||||
const row = detailTableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
WORK_TYPES,
|
||||
@@ -446,7 +479,8 @@ export default function useOperationTab() {
|
||||
handleOperationCellClick,
|
||||
handleOperationResultCellClick,
|
||||
handleOperationPageNation,
|
||||
emitMultipleFeaturesToMap,
|
||||
operationExcelDownload,
|
||||
operationResultExcelDownload
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,13 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import useSearchModalStore from '../../../store/useSearchModalStore';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessage';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
export default function useSearchModal() {
|
||||
const {
|
||||
@@ -132,6 +139,18 @@ export default function useSearchModal() {
|
||||
const viewMultipleFeature = async () => {
|
||||
if (activeTab === 'basic') {
|
||||
if (!(tableData.length === 0)) {
|
||||
if (isPopupWindow()) {
|
||||
const row = tableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(tableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[0];
|
||||
@@ -144,6 +163,18 @@ export default function useSearchModal() {
|
||||
}
|
||||
} else if (activeTab === 'operation') {
|
||||
if (!(detailTableData.length === 0)) {
|
||||
if (isPopupWindow()) {
|
||||
const row = detailTableData[0];
|
||||
postPopupMessageAndClose(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
||||
{
|
||||
layerNm: row.lyrTypeNm,
|
||||
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[0];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function PopupLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<main className="min-h-dvh bg-white text-foreground">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isWindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import { windowPopupRegistry } from "@/features/windowPopup/registry";
|
||||
|
||||
type PopupPageProps = {
|
||||
params: Promise<{
|
||||
popupKey: string;
|
||||
}>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
};
|
||||
|
||||
export default async function PopupPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PopupPageProps) {
|
||||
const { popupKey } = await params;
|
||||
const resolvedSearchParams = await searchParams;
|
||||
|
||||
if (!isWindowPopupKey(popupKey)) {
|
||||
return (
|
||||
<section className="flex min-h-dvh flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<h1 className="text-2xl font-semibold">등록되지 않은 팝업입니다.</h1>
|
||||
<p className="text-base text-gray-600">popupKey: {popupKey}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const PopupComponent = windowPopupRegistry[popupKey].component;
|
||||
|
||||
return <PopupComponent searchParams={resolvedSearchParams} />;
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
// FileUpload.js
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { SITE_ID } from '@/shared/config';
|
||||
const SEWERAGE_URL = "/sewerage-service/api";
|
||||
|
||||
function FileUpload() {
|
||||
@@ -49,6 +50,7 @@ function FileUpload() {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({authToken: auth})
|
||||
}).then((data) => {
|
||||
@@ -58,7 +60,11 @@ function FileUpload() {
|
||||
const handleFindUserOna = () => {
|
||||
const auth = getCookie("Authorization");
|
||||
if (auth) console.log("쿠키 auth : " + auth);
|
||||
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => {
|
||||
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b", {
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
}).then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
};
|
||||
@@ -86,7 +92,11 @@ function FileUpload() {
|
||||
formData.append('inpFile', file);
|
||||
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData);
|
||||
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData, {
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
});
|
||||
|
||||
alert('업로드 성공: ' + response.data.message);
|
||||
} catch (err) {
|
||||
|
||||
@@ -17,22 +17,20 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
|
||||
*/
|
||||
'use client'
|
||||
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
|
||||
import { deleteData, useCookieAuth } from '@/components/lib/api'
|
||||
import { searchData } from '@/components/lib/common/apiHelpers'
|
||||
import { formatDate, handleDownload } from '@/components/lib/utils'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
|
||||
import ListDiv from '@common/business_components/ui/list_div'
|
||||
import { Button, DatePicker, Input, Label } from '@common/components/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { tableHeadlist } from '..'
|
||||
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
|
||||
import { tokenManager } from '@/entities/auth/lib/token-manager';
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -51,8 +49,6 @@ const Page = () => {
|
||||
const [curPage, setCurPage] = useState(1) //현재 페이지
|
||||
const [totalData, setTotalData] = useState(0) //총 데이터 수
|
||||
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) // 등록 모달 상태
|
||||
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
|
||||
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
|
||||
@@ -67,6 +63,17 @@ const Page = () => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED) return;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [fromDate, toDate, pjNm, curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
@@ -140,27 +147,24 @@ const Page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 현황 등록
|
||||
const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => {
|
||||
const json = {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflId: id,
|
||||
atflCd: cd,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
sbmrgPrevntBizNm: fileName
|
||||
}
|
||||
const data = await postData('v1/sbmrgPrevnt', json)
|
||||
if (data) {
|
||||
alert('파일이 등록되었습니다.');
|
||||
setIsUploadModalOpen(false);
|
||||
handleSearch();
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
const popupWindow = openWindowPopup("floodPreUpload");
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
setIsUploadModalOpen(true);
|
||||
const handleTablePopupOpen = (fileName: string, id: string) => {
|
||||
const popupWindow = openWindowPopup("floodPreTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +250,7 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
handleTablePopupOpen(fileName, id);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
@@ -254,40 +258,8 @@ const Page = () => {
|
||||
resetPageNo={isResetPage}
|
||||
/>
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isUploadModalOpen}
|
||||
onOpenChange={setIsUploadModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
body={
|
||||
<OperFileUploadForm
|
||||
title='도시침수 예방사업 추진현황 등록'
|
||||
onSubmit={async (file, year, quater, cd, id) => {
|
||||
await registFile(file, year, quater, cd, id)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
flex flex-col gap-[10px]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useState, useEffect } from 'react'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { UITab, UiTable } from '@common/business_components/ui'
|
||||
import * as XLSX from 'xlsx';
|
||||
import { getBlobData } from '@/components/lib/api';
|
||||
// import { Table, TableBody, TableHeader } from "@common/components/ui";
|
||||
// import { HeadMakeFloodPre } from './headMakeFloodPre';
|
||||
// import { InputTableBody } from './inputTableBody';
|
||||
@@ -54,12 +55,13 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
|
||||
if (atflId === '' || atflId === undefined) {
|
||||
alert('첨부파일 ID가 없어 파일을 불러올 수 없습니다.');
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
@@ -114,6 +116,8 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
const filteredResultData = filterEmptyRows(resultData);
|
||||
setResultTableData(filteredResultData);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('FloodPre attachment fetch details:', { atflId, fileNm, message });
|
||||
console.error('파일 처리 중 오류 발생:', error);
|
||||
alert('파일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -267,4 +271,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
)
|
||||
};
|
||||
|
||||
export default TableModal;
|
||||
export default TableModal;
|
||||
|
||||
@@ -17,22 +17,20 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
|
||||
*/
|
||||
'use client'
|
||||
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
|
||||
import { deleteData, useCookieAuth } from '@/components/lib/api'
|
||||
import { searchData } from '@/components/lib/common/apiHelpers'
|
||||
import { formatDate, handleDownload } from '@/components/lib/utils'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
|
||||
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui'
|
||||
import ListDiv from '@common/business_components/ui/list_div'
|
||||
import { Button, DatePicker, Input, Label } from '@common/components/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { tableHeadlistOldPp } from '..'
|
||||
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
|
||||
import { tokenManager } from '@/entities/auth/lib/token-manager';
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -49,8 +47,6 @@ const Page = () => {
|
||||
const [curPage, setCurPage] = useState(1)
|
||||
const [totalData, setTotalData] = useState(0) //총 데이터 수
|
||||
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
||||
const [isModalOpen, setIsModalOpen] = useState(false) // 등록 모달 상태
|
||||
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
|
||||
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
|
||||
@@ -65,6 +61,17 @@ const Page = () => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== WINDOW_POPUP_MESSAGE_TYPE.OLD_PP_UPLOAD_COMPLETED) return;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [fromDate, toDate, pjNm, curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
@@ -137,27 +144,25 @@ const Page = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 등록
|
||||
const registFile = async (fileNm: string, year: string, quater: string, cd: string, id: string) => {
|
||||
const json = {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflCd: cd,
|
||||
atflId: id,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
dcrpPplMtncBizNm: fileNm
|
||||
}
|
||||
const data = await postData('v1/dcrpPpl', json);
|
||||
if (data) {
|
||||
alert('파일이 등록되었습니다.');
|
||||
setIsModalOpen(false);
|
||||
handleSearch();
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
const popupWindow = openWindowPopup("oldPpUpload");
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
// 등록 버튼 클릭 핸들러
|
||||
const handleRegisterClick = () => {
|
||||
setIsModalOpen(true);
|
||||
// 엑셀 데이터는 브라우저 윈도우 팝업에서 독립 조회
|
||||
const handleTablePopupOpen = (fileName: string, id: string) => {
|
||||
const popupWindow = openWindowPopup("oldPpTable", {
|
||||
fileNm: fileName,
|
||||
atflId: id,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,47 +247,15 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
handleTablePopupOpen(fileName, id);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
resetSelection={resetSelection}
|
||||
/>
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
body={
|
||||
<OperFileUploadForm
|
||||
title='노후 하수관로 정비사업 추진현황 등록'
|
||||
onSubmit={async (file, year, quater, cd, id) => {
|
||||
await registFile(file, year, quater, cd, id)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
flex flex-col gap-[10px]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
export default Page
|
||||
|
||||
@@ -19,6 +19,7 @@ import { UITab, UiTable } from '@common/business_components/ui'
|
||||
import * as XLSX from 'xlsx';
|
||||
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
|
||||
import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } from './headList';
|
||||
import { getBlobData } from '@/components/lib/api';
|
||||
|
||||
const TABS = [
|
||||
{ val: "1", text: "raw data" },
|
||||
@@ -63,12 +64,13 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
|
||||
if (atflId === '' || atflId === undefined) {
|
||||
alert('첨부파일 ID가 없어 파일을 불러올 수 없습니다.');
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
@@ -246,6 +248,8 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
}));
|
||||
setTableData6(filteredData6);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('OldPp attachment fetch details:', { atflId, fileNm, message });
|
||||
console.error('파일 처리 중 오류 발생:', error);
|
||||
alert('파일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -476,4 +480,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
)
|
||||
};
|
||||
|
||||
export default TableModal;
|
||||
export default TableModal;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginPage } from "@/features/auth/login";
|
||||
export default LoginPage;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const hostname = request.nextUrl.hostname;
|
||||
const redirectUrl = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "https://portal.hasuon.or.kr";
|
||||
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr") {
|
||||
tokenManager.clearAuthToken();
|
||||
return NextResponse.redirect(new URL("/", request.url)); // 로컬 및 개발 환경
|
||||
} else {
|
||||
return NextResponse.redirect(new URL(redirectUrl)); // 공단 환경
|
||||
@@ -11,13 +11,51 @@
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from 'axios';
|
||||
import { CLAIM_NAME, SITE_ID } from "@/shared/config";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
|
||||
const BASE_URL = '/sewerage-service/api';
|
||||
const DEFAULT_HEADERS = {
|
||||
"X-Site-Id": SITE_ID,
|
||||
};
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const withSiteIdHeaders = (headers: HeadersInit = {}) => ({
|
||||
...headers,
|
||||
...DEFAULT_HEADERS,
|
||||
});
|
||||
|
||||
const resolveApiUrl = (url: string) =>
|
||||
url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`;
|
||||
|
||||
const handleUnauthorizedResponse = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
|
||||
const isExpired = remainingMs === null || remainingMs <= 0;
|
||||
|
||||
if (!isExpired) return;
|
||||
|
||||
tokenManager.clearAuthToken();
|
||||
|
||||
const isLoginPage =
|
||||
window.location.pathname === "/auth/login" ||
|
||||
window.location.pathname.startsWith("/auth/login/");
|
||||
|
||||
if (!isLoginPage) {
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
};
|
||||
|
||||
export const getData = async (url: string): Promise<any> => {
|
||||
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
@@ -47,12 +85,15 @@ export const getData = async (url: string): Promise<any> => {
|
||||
export const postData = async (url: string, data: object): Promise<any> => {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
const res = await fetch(`${BASE_URL}/${url}`, options);
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
const resData = await res.json();
|
||||
console.log('postData response:', resData);
|
||||
@@ -62,6 +103,7 @@ export const postData = async (url: string, data: object): Promise<any> => {
|
||||
|
||||
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
|
||||
const res = await axios.post(`${BASE_URL}/${url}`, data, {
|
||||
headers: DEFAULT_HEADERS,
|
||||
timeout: 600000, // 10분
|
||||
});
|
||||
console.log('postMultiPart response:', res);
|
||||
@@ -71,12 +113,15 @@ export const postMutiPart = async (url: string, data: FormData): Promise<any> =>
|
||||
export const putData = async (url: string, data: object): Promise<any> => {
|
||||
const options: RequestInit = {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
const res = await fetch(`${BASE_URL}/${url}`, options);
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
const resData = await res.json();
|
||||
console.log('putData response:', resData);
|
||||
return resData.data;
|
||||
@@ -85,21 +130,81 @@ export const putData = async (url: string, data: object): Promise<any> => {
|
||||
export const deleteData = async (url: string, data?: object): Promise<any> => {
|
||||
const options: RequestInit = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
};
|
||||
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options);
|
||||
const res = await fetch(resolveApiUrl(url), options);
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.status === 204) return true
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getRawJson = async (url: string): Promise<any> => {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch data: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
throw new Error(`Invalid JSON response from server: ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getBlobData = async (url: string): Promise<Blob> => {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`,
|
||||
);
|
||||
}
|
||||
|
||||
return res.blob();
|
||||
};
|
||||
|
||||
// DC API 호출
|
||||
export const getDcData = async (url: string, resType: string = "json"): Promise<any> => {
|
||||
const res = await fetch('/dc-service' + url, {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
let data;
|
||||
if (resType === "json") data = await res.json();
|
||||
@@ -114,7 +219,11 @@ export const getDcData = async (url: string, resType: string = "json"): Promise<
|
||||
export const getCmsData = async (url: string, data?: object): Promise<any> => {
|
||||
const res = await fetch('/cms-service' + url, {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data;
|
||||
@@ -126,7 +235,11 @@ export const getCmsData = async (url: string, data?: object): Promise<any> => {
|
||||
export const getGeoData = async (url: string, data?: object): Promise<any> => {
|
||||
const res = await fetch('/geoserver' + url, {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data;
|
||||
@@ -157,4 +270,4 @@ export const useCookieAuth = () => {
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제(첨부파일코드로 삭제): DELETE /api/v1/atfl/attachments/atflCd/{atflCd} (attach-service)
|
||||
*/
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { ENDPOINT } from "../api/attachment/attachment-service";
|
||||
|
||||
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
@@ -43,6 +44,7 @@ export const handleDelete = async (
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
@@ -118,4 +120,4 @@ export const handleDelete = async (
|
||||
// options?.handleDeleteError?.(deleteError);
|
||||
// throw deleteError;
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
* - Fetch 기반 HTTP 요청 처리
|
||||
*/
|
||||
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
|
||||
export const handleDownload = async (api: string, fileNm: string) => {
|
||||
try {
|
||||
const response = await fetch(api, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error('파일 다운로드 실패');
|
||||
|
||||
@@ -34,4 +39,4 @@ export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터
|
||||
// 메모리 해제
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 📄 Modal Component
|
||||
* 모달 컴포넌트
|
||||
*
|
||||
* - 모달 열기/닫기 상태 관리
|
||||
* - 제목 및 내용 표시
|
||||
* - 저장 및 닫기 버튼 지원
|
||||
* - 반응형 크기 조정 (최대/최소 너비 및 높이)
|
||||
* - 오버레이 및 z-index를 통한 최상위 레이어 배치
|
||||
* - 접근성 지원 (aria-describedby)
|
||||
*/
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogOverlay } from '@common/components/ui'
|
||||
import React from 'react'
|
||||
|
||||
interface ModalProps {
|
||||
isModalOpen: boolean;
|
||||
setIsModalOpen: (open: boolean) => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
closeModal: () => void;
|
||||
saveModal?: () => void;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal, saveModal }) => {
|
||||
return (
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<DialogOverlay className='z-[200]' />
|
||||
<DialogContent className="w-auto max-w-[1200px] min-w-[400px] min-h-[150px] h-auto max-h-[450px] overflow-auto z-[1000]" aria-describedby={undefined}>
|
||||
{title ? <DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader> : null}
|
||||
{children}
|
||||
<DialogFooter>
|
||||
<div className='flex justify-end gap-2'>
|
||||
{saveModal ? <button className="px-4 py-2 bg-primary text-white rounded" onClick={saveModal}>저장</button> : null}
|
||||
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import {
|
||||
AGED_PIPE_DETAIL_FIELDS,
|
||||
getElapsedYears,
|
||||
@@ -16,39 +17,48 @@ export default function AgedPipeDetailPanel({ featureData, onClose }: Props) {
|
||||
const source = featureData?.values_ ?? featureData ?? {};
|
||||
const facilityNo = String(getPipeDisplayValue(source, "fclt_mng_no") ?? "-");
|
||||
const elapsedYears = getElapsedYears(source.instl_ymd ?? source.instlYmd);
|
||||
const detailFields = [
|
||||
...AGED_PIPE_DETAIL_FIELDS,
|
||||
{ id: "elapsedYears", title: "경과년도" },
|
||||
];
|
||||
const tableData = detailFields.reduce<Record<string, string>>((acc, field) => {
|
||||
acc[field.id] =
|
||||
field.id === "elapsedYears"
|
||||
? elapsedYears === null
|
||||
? "확인불가"
|
||||
: `${elapsedYears}년`
|
||||
: String(getPipeDisplayValue(source, field.id) ?? "-");
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[320px] flex-col overflow-hidden border-l border-slate-200 bg-white shadow-[-12px_0_24px_rgba(15,23,42,0.1)]">
|
||||
<div className="flex items-start justify-between border-b border-slate-200 bg-slate-900 px-4 py-3">
|
||||
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[350px] flex-col overflow-hidden border-l border-gray-300 bg-white">
|
||||
<div className="card-header relative flex items-center justify-between bg-primary p-4">
|
||||
<div>
|
||||
<div className="text-[1.45rem] font-semibold text-white">노후관로 상세</div>
|
||||
<div className="mt-1 text-[1.05rem] text-slate-300">시설관리번호 {facilityNo}</div>
|
||||
<div className="mt-1 text-[1.05rem] text-amber-300">
|
||||
경과년도 {elapsedYears ?? "-"}년
|
||||
<div className="text-2xl font-medium text-white text-label">
|
||||
하수관로 정보 <br />관리번호 : {facilityNo}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
||||
className="text-white hover:text-gray-300"
|
||||
onClick={onClose}
|
||||
>
|
||||
닫기
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto px-4 py-3">
|
||||
<div className="space-y-2">
|
||||
{AGED_PIPE_DETAIL_FIELDS.map((field) => (
|
||||
<div key={field.id} className="rounded-lg border border-slate-200 px-3 py-2.5">
|
||||
<div className="text-[1rem] font-medium text-slate-500">{field.title}</div>
|
||||
<div className="mt-1 break-words text-[1.15rem] leading-6 text-slate-900">
|
||||
{String(getPipeDisplayValue(source, field.id) ?? "-")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<VerticalTable
|
||||
headName="기본정보"
|
||||
tableData={[tableData]}
|
||||
headlist={detailFields}
|
||||
pageSize={20}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Input } from "@common/components";
|
||||
import useStore from "@/store/useStore";
|
||||
import { useState } from "react";
|
||||
import type { AgedPipeFilters } from "./agedPipeUtils";
|
||||
|
||||
type OverlayProps = {
|
||||
@@ -13,89 +12,62 @@ type OverlayProps = {
|
||||
};
|
||||
|
||||
type Props = OverlayProps & {
|
||||
title?: string;
|
||||
filters: AgedPipeFilters;
|
||||
onFiltersChange: (filters: AgedPipeFilters) => void;
|
||||
onApply: () => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeFilterPanel({
|
||||
title = "지반침하정보",
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onApply,
|
||||
}: Props) {
|
||||
const { ctpv, sgg } = useStore();
|
||||
const sggOptions = filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)));
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-[280px] rounded-xl border border-slate-200 bg-white/95 p-3 shadow-[0_14px_30px_rgba(15,23,42,0.16)] backdrop-blur">
|
||||
<div className="mb-2">
|
||||
<div className="text-[1.35rem] font-semibold text-slate-900">노후관로 기준</div>
|
||||
<div className="mt-1 text-[1.05rem] leading-5 text-slate-500">
|
||||
경과년수 이상 관로만 지도와 목록에 반영합니다.
|
||||
<div className="overflow-hidden rounded-sm border border-[#bdbdbd] bg-white">
|
||||
<div className="relative flex min-h-[34px] items-center justify-center border-b border-[#eeeeee] bg-white px-8">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-[#555]"
|
||||
onClick={() => setIsCollapsed((value) => !value)}
|
||||
title={isCollapsed ? "펼치기" : "접기"}
|
||||
>
|
||||
{isCollapsed ? "›" : "⌄"}
|
||||
</button>
|
||||
<span className="text-[1.3rem] font-semibold text-[#4b5563]">{title}</span>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex min-h-[30px] items-center gap-1 whitespace-nowrap text-[1.2rem] text-[#333]">
|
||||
<label htmlFor="aged-pipe-elapsed-years">경과년도(년)</label>
|
||||
<input
|
||||
id="aged-pipe-elapsed-years"
|
||||
className="h-[22px] w-10 border border-[#ccd6df] px-1 text-center text-[1.2rem]"
|
||||
inputMode="numeric"
|
||||
value={String(filters.elapsedYears)}
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
||||
onFiltersChange({ ...filters, elapsedYears: next });
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") onApply();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-[22px] w-[22px] place-items-center rounded-full bg-[#31a83b] text-[1.4rem] font-bold leading-none text-white"
|
||||
onClick={onApply}
|
||||
title="적용"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
||||
<Input
|
||||
className="h-10 !text-[1.15rem]"
|
||||
inputMode="numeric"
|
||||
value={String(filters.elapsedYears)}
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
||||
onFiltersChange({ ...filters, elapsedYears: next });
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") onApply();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) => onFiltersChange({ ...filters, sggCd: event.target.value })}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button className="mt-3 h-10 w-full !text-[1.15rem]" onClick={onApply}>
|
||||
GIS 반영
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,14 +64,11 @@ export default function AgedPipeListModal({
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-[70] flex items-center justify-center px-6 py-8">
|
||||
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden rounded-2xl border border-slate-300 bg-white shadow-[0_28px_80px_rgba(15,23,42,0.28)]">
|
||||
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden border border-[#dbe2e6] bg-white shadow-[0_0_12px_rgba(0,0,0,0.25)]">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-900 px-5 py-3">
|
||||
<div className="flex items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
|
||||
<div>
|
||||
<div className="text-[1.45rem] font-semibold text-white">노후관로 목록</div>
|
||||
<div className="mt-1 text-[1rem] text-slate-300">
|
||||
지도 위에 띄워지는 윈도우형 팝업 미리보기
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -85,7 +82,7 @@ export default function AgedPipeListModal({
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
|
||||
<div className="grid grid-cols-1 gap-3 bg-slate-50 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
||||
<Input
|
||||
@@ -141,7 +138,7 @@ export default function AgedPipeListModal({
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
|
||||
<Button type="button" size="sm" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
|
||||
<Button type="button" size="sm" color="primary" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -7,7 +7,7 @@ import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import useStore from "@/store/useStore";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData, getRawJson } from "@/components/lib/api";
|
||||
import AgedPipeFilterPanel from "./AgedPipeFilterPanel";
|
||||
import AgedPipeDetailPanel from "./AgedPipeDetailPanel";
|
||||
import AgedPipeListModal from "./AgedPipeListModal";
|
||||
@@ -102,19 +102,7 @@ const Map = () => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const response = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const contentLength = response.headers.get("content-length");
|
||||
if (contentLength === "0") data = null;
|
||||
|
||||
const text = await response.text();
|
||||
if (!text || text.trim() === "") data = null;
|
||||
else data = JSON.parse(text);
|
||||
}
|
||||
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error("노후관로 레이어 설정 조회 중 오류:", error);
|
||||
@@ -333,12 +321,12 @@ const Map = () => {
|
||||
};
|
||||
}, [appliedFilters, mapReady]);
|
||||
|
||||
const FilterOverlay = useMemo(
|
||||
const LayerSettingView = useMemo(
|
||||
() =>
|
||||
function FilterOverlayComponent(props: any) {
|
||||
function LayerSettingComponent() {
|
||||
return (
|
||||
<AgedPipeFilterPanel
|
||||
{...props}
|
||||
title="지반침하정보"
|
||||
filters={draftFilters}
|
||||
onFiltersChange={setDraftFilters}
|
||||
onApply={handleApplyFilters}
|
||||
@@ -368,7 +356,7 @@ const Map = () => {
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={() => undefined}
|
||||
searchBar={<></>}
|
||||
waterDepth={FilterOverlay}
|
||||
layerSettingView={LayerSettingView}
|
||||
showZoomLevel={true}
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
|
||||
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
|
||||
|
||||
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
|
||||
const formatNoYearNoSec = (val: any) => {
|
||||
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
|
||||
|
||||
// 각 아이템 전처리
|
||||
const processedDataArray = result.content.map((data: any) => {
|
||||
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
)
|
||||
}
|
||||
|
||||
export default Ssc
|
||||
export default Ssc
|
||||
|
||||
+4
-14
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
@@ -74,7 +73,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
@@ -459,18 +457,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
};
|
||||
}, [featureData, gisMap]);
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
// Use the browser confirm dialog for lightweight save confirmation.
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
const shouldSave = window.confirm('정보를 저장하시겠습니까?');
|
||||
if (!shouldSave) return;
|
||||
void btnClickEvent();
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
@@ -617,7 +608,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
|
||||
@@ -15,40 +15,20 @@
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getData } from '@/components/lib/api';
|
||||
|
||||
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
|
||||
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
|
||||
const BASE_URL = "/sewerage-service/api";
|
||||
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get("content-length");
|
||||
if (contentLength === "0") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === "") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// JSON 파싱 시도
|
||||
const response = JSON.parse(text);
|
||||
data = response.data;
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data) {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
|
||||
)
|
||||
}
|
||||
|
||||
export default Smh;
|
||||
export default Smh;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { getData } from "../../../components/lib/api";
|
||||
import { getRawJson } from "../../../components/lib/api";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
@@ -43,22 +43,7 @@ const Map = () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') data = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') data = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+18
-2
@@ -12,7 +12,11 @@
|
||||
* - 앱 전체의 공통 스타일 및 스크립트 적용
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
import { cookies } from "next/headers";
|
||||
import { AppProvider } from "./providers";
|
||||
import "../src/app/styles/base.css";
|
||||
import "../src/app/styles/components.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 하수관로",
|
||||
@@ -24,4 +28,16 @@ export const metadata: Metadata = {
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default async function RootLayout({
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { AppProvider } from "../src/app/providers";
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,7 +1,3 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
|
||||
@@ -10,3 +10,7 @@ body {
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
body.sn-map-page-lock {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
import type { LoginService, ILogin } from "../model/login-interface";
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
@@ -23,8 +24,13 @@ export class LoginApiService implements LoginService {
|
||||
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
tokenManager.clearAuthToken();
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
@@ -57,7 +63,12 @@ export class LoginApiService implements LoginService {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{ method: "PUT" },
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { tokenManager } from "./token-manager";
|
||||
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
@@ -38,6 +39,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
setUserInfo(null);
|
||||
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";;
|
||||
|
||||
type JwtPayload = {
|
||||
sub?: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
@@ -18,6 +25,49 @@ export class TokenManager {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
private parseJwt(token: string): JwtPayload {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("유효한 JWT 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
const base64Url = parts[1] ?? "";
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(padded)
|
||||
.split("")
|
||||
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload) as JwtPayload;
|
||||
}
|
||||
|
||||
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
|
||||
const token = this.getToken(key);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.parseJwt(token);
|
||||
if (typeof payload.exp !== "number") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.exp * 1000 - Date.now();
|
||||
} catch (error) {
|
||||
console.error("JWT 파싱 오류:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public isExpiredToken(key: string = CLAIM_NAME): boolean {
|
||||
const remainingMs = this.getRemainingMsFromToken(key);
|
||||
return remainingMs === null || remainingMs <= 0;
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
// 환경에 따른 도메인 설정
|
||||
//const domain = this.getCookieDomain();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/map";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* OLYM Cascading Style Sheets
|
||||
* File Name : login.css
|
||||
* Description : 로그인 스타일시트
|
||||
* Author : Web business Team / Choi Hyun Mi
|
||||
* Email : desingtj@olym.co.kr
|
||||
* Date : 2023.07.20
|
||||
* Update : 2023.07.26
|
||||
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.fx {
|
||||
display: flex;
|
||||
-ms-display: flex;
|
||||
-webkit-display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.login .left_box {
|
||||
width: 100%;
|
||||
background: url(/images/bg_login.jpg) no-repeat center/cover;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 7% 5% 5% 5%;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 53px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .left_box .text h1 span {
|
||||
font-weight: 800;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 40px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .login_box {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 4% 12% 4% 6%;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2640a5;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st {
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box > ul > li .input_st .diveicon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 28px;
|
||||
transform: translateY(-50%);
|
||||
font-family: "Xeicon";
|
||||
font-size: 25px;
|
||||
color: #003288;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
width: 100%;
|
||||
padding: 20px 20px 20px 70px;
|
||||
background: #f7f7f7;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
height: auto;
|
||||
}
|
||||
.login .login_box > ul > li input:focus {
|
||||
outline: 1px solid #3c5fe5;
|
||||
}
|
||||
.login .login_box > ul > li input::placeholder {
|
||||
color: #afabab;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 50px 0 45px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .checks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #636363;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message i {
|
||||
color: #327af5;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
padding: 24px 15px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-family: "Montserrat";
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 22px;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.login .login_box .login_btn:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.login .login_box .login_btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.login .footer .copy {
|
||||
margin-top: 36px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 3%;
|
||||
}
|
||||
.login .footer .copy p {
|
||||
font-size: 13px;
|
||||
color: #d4d4d4;
|
||||
width: 44%;
|
||||
text-align: center;
|
||||
}
|
||||
.login .footer .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
content: "";
|
||||
width: 20%;
|
||||
height: 1px;
|
||||
background: #d4d4d4;
|
||||
}
|
||||
.login .footer .fot_btn {
|
||||
gap: 20px;
|
||||
margin: 30px 0 10px 0;
|
||||
}
|
||||
.login .footer .fot_btn a {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login .footer .fot_btn a:hover {
|
||||
color: #003288;
|
||||
}
|
||||
.login .footer .addr {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
gap: 3px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 비밀번호 변경 팝업창 */
|
||||
.login .modal .modal_container {
|
||||
width: 750px;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 10px 10px 30px 10px;
|
||||
color: #666;
|
||||
}
|
||||
.login .pw_desc p {
|
||||
font-size: 1.25rem;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.login .pw_desc span {
|
||||
color: #f60;
|
||||
font-weight: 700;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20px 50px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.login {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
.login .left_box {
|
||||
padding: 30px;
|
||||
flex-direction: column-reverse;
|
||||
gap: 20px;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 30px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .left_box .img img {
|
||||
height: 40px;
|
||||
}
|
||||
.login .login_box {
|
||||
padding: 30px;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
gap: 20px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st::after {
|
||||
left: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
padding: 15px 10px 15px 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .id_save label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
padding: 15px;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 18px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.login .login_box .copy p {
|
||||
width: 100%;
|
||||
}
|
||||
.login .login_box .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
display: none;
|
||||
}
|
||||
.login .footer .addr {
|
||||
font-size: 11px;
|
||||
gap: 3px 10px;
|
||||
}
|
||||
.login .modal .modal_container {
|
||||
width: 90%;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
/* // login */
|
||||
@@ -0,0 +1,14 @@
|
||||
export { openWindowPopup } from "./lib/openWindowPopup";
|
||||
export {
|
||||
isWindowPopupKey,
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "./model/popupDefinitions";
|
||||
export { windowPopupRegistry } from "./registry";
|
||||
export type {
|
||||
OpenWindowPopupParams,
|
||||
WindowPopupContentProps,
|
||||
WindowPopupMessageType,
|
||||
WindowPopupSearchParams,
|
||||
} from "./model/types";
|
||||
export { WINDOW_POPUP_MESSAGE_TYPE } from "./model/types";
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
isWindowPopupKey,
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "../model/popupDefinitions";
|
||||
import type { OpenWindowPopupParams } from "../model/types";
|
||||
|
||||
const buildQueryString = (params: OpenWindowPopupParams = {}) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === null || value === undefined || value === "") return;
|
||||
searchParams.set(key, String(value));
|
||||
});
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
return queryString ? `?${queryString}` : "";
|
||||
};
|
||||
|
||||
const buildPopupFeatures = (width: number, height: number) => {
|
||||
const screenLeft = window.screenX ?? window.screenLeft ?? 0;
|
||||
const screenTop = window.screenY ?? window.screenTop ?? 0;
|
||||
const outerWidth = window.outerWidth || window.screen.width;
|
||||
const outerHeight = window.outerHeight || window.screen.height;
|
||||
const left = Math.max(0, screenLeft + Math.round((outerWidth - width) / 2));
|
||||
const top = Math.max(0, screenTop + Math.round((outerHeight - height) / 2));
|
||||
|
||||
return [
|
||||
`width=${width}`,
|
||||
`height=${height}`,
|
||||
`left=${left}`,
|
||||
`top=${top}`,
|
||||
"resizable=yes",
|
||||
"scrollbars=yes",
|
||||
"status=no",
|
||||
"location=no",
|
||||
"menubar=no",
|
||||
"toolbar=no",
|
||||
].join(",");
|
||||
};
|
||||
|
||||
export const openWindowPopup = (
|
||||
popupKey: WindowPopupKey,
|
||||
params?: OpenWindowPopupParams,
|
||||
) => {
|
||||
if (typeof window === "undefined") return null;
|
||||
if (!isWindowPopupKey(popupKey)) return null;
|
||||
|
||||
const definition = windowPopupDefinitions[popupKey];
|
||||
const popupUrl = `/popup/${popupKey}${buildQueryString(params)}`;
|
||||
const popupFeatures = buildPopupFeatures(definition.width, definition.height);
|
||||
const popupWindow = window.open(
|
||||
popupUrl,
|
||||
`sn-popup-${popupKey}`,
|
||||
popupFeatures,
|
||||
);
|
||||
|
||||
popupWindow?.focus();
|
||||
|
||||
return popupWindow;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { WindowPopupMessageType } from "../model/types";
|
||||
|
||||
export const postPopupMessage = (
|
||||
type: WindowPopupMessageType,
|
||||
payload?: Record<string, string>,
|
||||
) => {
|
||||
if (typeof window === "undefined" || !window.opener) return;
|
||||
|
||||
window.opener.postMessage(
|
||||
{
|
||||
type,
|
||||
payload,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
export const postPopupMessageAndClose = (
|
||||
type: WindowPopupMessageType,
|
||||
payload?: Record<string, string>,
|
||||
) => {
|
||||
postPopupMessage(type, payload);
|
||||
window.close();
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { WindowPopupDefinition } from "./types";
|
||||
|
||||
export const windowPopupDefinitions = {
|
||||
oldPpUpload: {
|
||||
key: "oldPpUpload",
|
||||
title: "노후 하수관로 정비사업 등록",
|
||||
width: 860,
|
||||
height: 360,
|
||||
},
|
||||
oldPpTable: {
|
||||
key: "oldPpTable",
|
||||
title: "노후 하수관로 정비사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
requiredParams: ["fileNm", "atflId"],
|
||||
},
|
||||
floodPreUpload: {
|
||||
key: "floodPreUpload",
|
||||
title: "도시침수 예방사업 등록",
|
||||
width: 860,
|
||||
height: 360,
|
||||
},
|
||||
floodPreTable: {
|
||||
key: "floodPreTable",
|
||||
title: "도시침수 예방사업 상세",
|
||||
width: 1400,
|
||||
height: 900,
|
||||
requiredParams: ["fileNm", "atflId"],
|
||||
},
|
||||
reportTab: {
|
||||
key: "reportTab",
|
||||
title: "하수도정비 기본계획보고서",
|
||||
width: 1280,
|
||||
height: 920,
|
||||
},
|
||||
reportFiles: {
|
||||
key: "reportFiles",
|
||||
title: "보고서 파일 목록",
|
||||
width: 860,
|
||||
height: 720,
|
||||
requiredParams: ["clsfSystmSn"],
|
||||
},
|
||||
mapSearch: {
|
||||
key: "mapSearch",
|
||||
title: "관망도 검색",
|
||||
width: 1280,
|
||||
height: 980,
|
||||
},
|
||||
infilChart: {
|
||||
key: "infilChart",
|
||||
title: "침입수 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
rdiiChart: {
|
||||
key: "rdiiChart",
|
||||
title: "RDII 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
sscDetail: {
|
||||
key: "sscDetail",
|
||||
title: "중점우수토실 운영정보",
|
||||
width: 1180,
|
||||
height: 980,
|
||||
requiredParams: ["chmbrXcnts", "chmbrYdnts"],
|
||||
},
|
||||
agedPipePopup: {
|
||||
key: "agedPipePopup",
|
||||
title: "노후 하수관로 관리 ",
|
||||
width: 1180,
|
||||
height: 760,
|
||||
},
|
||||
} as const satisfies Record<string, WindowPopupDefinition>;
|
||||
|
||||
export type WindowPopupKey = keyof typeof windowPopupDefinitions;
|
||||
|
||||
export const isWindowPopupKey = (key: string): key is WindowPopupKey => {
|
||||
return key in windowPopupDefinitions;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
export type WindowPopupSearchParams = Record<
|
||||
string,
|
||||
string | string[] | undefined
|
||||
>;
|
||||
|
||||
export type WindowPopupContentProps = {
|
||||
searchParams: WindowPopupSearchParams;
|
||||
};
|
||||
|
||||
export type WindowPopupDefinition = {
|
||||
key: string;
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
requiredParams?: string[];
|
||||
};
|
||||
|
||||
export type WindowPopupRegistryItem = WindowPopupDefinition & {
|
||||
component: ComponentType<WindowPopupContentProps>;
|
||||
};
|
||||
|
||||
export type OpenWindowPopupParams = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
|
||||
export const WINDOW_POPUP_MESSAGE_TYPE = {
|
||||
OLD_PP_UPLOAD_COMPLETED: "sn:popup:oldPpUploadCompleted",
|
||||
FLOOD_PRE_UPLOAD_COMPLETED: "sn:popup:floodPreUploadCompleted",
|
||||
MAP_SEARCH_SELECT_FEATURE: "sn:popup:mapSearchSelectFeature",
|
||||
MAP_SEARCH_SHOW_MULTIPLE: "sn:popup:mapSearchShowMultiple",
|
||||
} as const;
|
||||
|
||||
export type WindowPopupMessageType =
|
||||
(typeof WINDOW_POPUP_MESSAGE_TYPE)[keyof typeof WINDOW_POPUP_MESSAGE_TYPE];
|
||||
@@ -0,0 +1,60 @@
|
||||
import { windowPopupDefinitions, type WindowPopupKey } from "./model/popupDefinitions";
|
||||
import type { WindowPopupRegistryItem } from "./model/types";
|
||||
import AgedPipePopupContent from "./ui/AgedPipePopupContent";
|
||||
import FloodPreTablePopupContent from "./ui/FloodPreTablePopupContent";
|
||||
import FloodPreUploadPopupContent from "./ui/FloodPreUploadPopupContent";
|
||||
import InfilChartPopupContent from "./ui/InfilChartPopupContent";
|
||||
import OldPpUploadPopupContent from "./ui/OldPpUploadPopupContent";
|
||||
import OldPpTablePopupContent from "./ui/OldPpTablePopupContent";
|
||||
import RdiiChartPopupContent from "./ui/RdiiChartPopupContent";
|
||||
import ReportFilesPopupContent from "./ui/ReportFilesPopupContent";
|
||||
import ReportTabPopupContent from "./ui/ReportTabPopupContent";
|
||||
import SearchModalPopupContent from "./ui/SearchModalPopupContent";
|
||||
import SscDetailPopupContent from "./ui/SscDetailPopupContent";
|
||||
|
||||
export const windowPopupRegistry = {
|
||||
oldPpUpload: {
|
||||
...windowPopupDefinitions.oldPpUpload,
|
||||
component: OldPpUploadPopupContent,
|
||||
},
|
||||
oldPpTable: {
|
||||
...windowPopupDefinitions.oldPpTable,
|
||||
component: OldPpTablePopupContent,
|
||||
},
|
||||
floodPreUpload: {
|
||||
...windowPopupDefinitions.floodPreUpload,
|
||||
component: FloodPreUploadPopupContent,
|
||||
},
|
||||
floodPreTable: {
|
||||
...windowPopupDefinitions.floodPreTable,
|
||||
component: FloodPreTablePopupContent,
|
||||
},
|
||||
reportTab: {
|
||||
...windowPopupDefinitions.reportTab,
|
||||
component: ReportTabPopupContent,
|
||||
},
|
||||
reportFiles: {
|
||||
...windowPopupDefinitions.reportFiles,
|
||||
component: ReportFilesPopupContent,
|
||||
},
|
||||
mapSearch: {
|
||||
...windowPopupDefinitions.mapSearch,
|
||||
component: SearchModalPopupContent,
|
||||
},
|
||||
infilChart: {
|
||||
...windowPopupDefinitions.infilChart,
|
||||
component: InfilChartPopupContent,
|
||||
},
|
||||
rdiiChart: {
|
||||
...windowPopupDefinitions.rdiiChart,
|
||||
component: RdiiChartPopupContent,
|
||||
},
|
||||
sscDetail: {
|
||||
...windowPopupDefinitions.sscDetail,
|
||||
component: SscDetailPopupContent,
|
||||
},
|
||||
agedPipePopup: {
|
||||
...windowPopupDefinitions.agedPipePopup,
|
||||
component: AgedPipePopupContent,
|
||||
},
|
||||
} as const satisfies Record<WindowPopupKey, WindowPopupRegistryItem>;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Suspense } from "react";
|
||||
import AgedPipePopupPage from "@/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage";
|
||||
|
||||
export default function AgedPipePopupContent() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<AgedPipePopupPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import TableModal from "@/app/(with-base-layout)/oper/floodPre/tableModal";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function FloodPreTablePopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const fileNm = getPopupParamValue(searchParams.fileNm);
|
||||
const atflId = getPopupParamValue(searchParams.atflId);
|
||||
const missingParams = [
|
||||
!fileNm ? "fileNm" : "",
|
||||
!atflId ? "atflId" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.floodPreTable.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.floodPreTable.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<TableModal fileNm={fileNm} atflId={atflId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postData } from "@/components/lib/api";
|
||||
import OperFileUploadForm from "@/components/ui/operFileUploadForm";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { postPopupMessageAndClose } from "../lib/popupMessage";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../model/types";
|
||||
|
||||
export default function FloodPreUploadPopupContent() {
|
||||
const handleSubmit = async (
|
||||
fileNm: string,
|
||||
year: string,
|
||||
quater: string,
|
||||
cd: string,
|
||||
id: string,
|
||||
) => {
|
||||
const data = await postData("v1/sbmrgPrevnt", {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflId: id,
|
||||
atflCd: cd,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
sbmrgPrevntBizNm: fileNm,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
alert("파일이 등록되었습니다.");
|
||||
postPopupMessageAndClose(WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<OperFileUploadForm
|
||||
title={windowPopupDefinitions.floodPreUpload.title}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import InfilChart from "@/app/(map-layout)/map/components/popupComponent/chart/infilChart";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function InfilChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||
|
||||
if (!pplMngNo) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.infilChart.title}
|
||||
missingParams={["pplMngNo"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfilChart pplMngNo={pplMngNo} />;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import TableModal from "@/app/(with-base-layout)/oper/oldPp/tableModal";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function OldPpTablePopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const fileNm = getPopupParamValue(searchParams.fileNm);
|
||||
const atflId = getPopupParamValue(searchParams.atflId);
|
||||
const missingParams = [
|
||||
!fileNm ? "fileNm" : "",
|
||||
!atflId ? "atflId" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.oldPpTable.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.oldPpTable.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<TableModal fileNm={fileNm} atflId={atflId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postData } from "@/components/lib/api";
|
||||
import OperFileUploadForm from "@/components/ui/operFileUploadForm";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { postPopupMessageAndClose } from "../lib/popupMessage";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "../model/types";
|
||||
|
||||
export default function OldPpUploadPopupContent() {
|
||||
const handleSubmit = async (
|
||||
fileNm: string,
|
||||
year: string,
|
||||
quater: string,
|
||||
cd: string,
|
||||
id: string,
|
||||
) => {
|
||||
const data = await postData("v1/dcrpPpl", {
|
||||
mngYr: year,
|
||||
mngQu: quater,
|
||||
atflCd: cd,
|
||||
atflId: id,
|
||||
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
|
||||
dcrpPplMtncBizNm: fileNm,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
alert("파일이 등록되었습니다.");
|
||||
postPopupMessageAndClose(WINDOW_POPUP_MESSAGE_TYPE.OLD_PP_UPLOAD_COMPLETED);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<OperFileUploadForm
|
||||
title={windowPopupDefinitions.oldPpUpload.title}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function PopupMissingParams({
|
||||
title,
|
||||
missingParams,
|
||||
}: {
|
||||
title: string;
|
||||
missingParams: string[];
|
||||
}) {
|
||||
return (
|
||||
<section className="flex min-h-dvh flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<h1 className="text-2xl font-semibold">{title}</h1>
|
||||
<p className="text-base text-gray-600">
|
||||
팝업을 여는 데 필요한 값이 누락되었습니다.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
누락된 값: {missingParams.join(", ")}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import RdiiChart from "@/app/(map-layout)/map/components/popupComponent/chart/rdiiChart";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function RdiiChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||
|
||||
if (!pplMngNo) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.rdiiChart.title}
|
||||
missingParams={["pplMngNo"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <RdiiChart pplMngNo={pplMngNo} />;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { getDcData } from "@/components/lib/api";
|
||||
import { UiTable } from "@common/business_components/ui";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
type ReportDataFile = {
|
||||
no?: number;
|
||||
fileTitle: string;
|
||||
regDt: string;
|
||||
fileSn: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
export default function ReportFilesPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const clsfSystmSn = getPopupParamValue(searchParams.clsfSystmSn);
|
||||
const [files, setFiles] = useState<ReportDataFile[]>([]);
|
||||
const [allFiles, setAllFiles] = useState<ReportDataFile[]>([]);
|
||||
const [curPage, setCurPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clsfSystmSn) return;
|
||||
|
||||
const fetchFiles = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await getDcData(`/api/v1/file?clsfSystmSn=${clsfSystmSn}`);
|
||||
const normalized = (result ?? []).map((item: ReportDataFile, index: number) => ({
|
||||
...item,
|
||||
no: index + 1,
|
||||
}));
|
||||
setAllFiles(normalized);
|
||||
} catch (error) {
|
||||
console.error("보고서 파일 목록 조회 실패:", error);
|
||||
setAllFiles([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFiles();
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
const paged = allFiles.slice((curPage - 1) * 10, curPage * 10);
|
||||
setFiles(paged);
|
||||
}, [allFiles, curPage]);
|
||||
|
||||
const handleDownload = async (fileSn: string, fileTitle: string, filePath: string) => {
|
||||
if (!filePath) {
|
||||
alert("filePath 정보가 없으므로 다운로드가 불가합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const responseBlob = await getDcData(
|
||||
`/api/v1/fileDownload?filePath=${encodeURIComponent(filePath)}`,
|
||||
"blob",
|
||||
);
|
||||
|
||||
const objectUrl = URL.createObjectURL(responseBlob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = objectUrl;
|
||||
anchor.download = fileTitle;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
} catch (error) {
|
||||
console.error("보고서 파일 다운로드 실패:", error);
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
if (!clsfSystmSn) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.reportFiles.title}
|
||||
missingParams={["clsfSystmSn"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.reportFiles.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={`조회 결과 (${allFiles.length.toLocaleString("ko-KR")})건`}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={allFiles.length}
|
||||
onPageChange={setCurPage}
|
||||
cellClick={(index) => {
|
||||
if (typeof index !== "number" || !files[index]) return;
|
||||
const target = files[index];
|
||||
handleDownload(target.fileSn, target.fileTitle, target.filePath);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import ReportTab from "@/app/(map-layout)/map/searchModal/reportTab";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function ReportTabPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const ctpvNm = getPopupParamValue(searchParams.ctpvNm);
|
||||
const sggNm = getPopupParamValue(searchParams.sggNm);
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
{windowPopupDefinitions.reportTab.title}
|
||||
</h1>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<ReportTab
|
||||
open={true}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm || undefined}
|
||||
sggNm={sggNm || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import SearchModal from "@/app/(map-layout)/map/searchModal/searchModal";
|
||||
|
||||
export default function SearchModalPopupContent() {
|
||||
return <SearchModal renderMode="popup" />;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Ssc from "@/app/(map-layout)/map/components/popupComponent/Ssc";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import PopupMissingParams from "./PopupMissingParams";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
|
||||
export default function SscDetailPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const chmbrXcnts = getPopupParamValue(searchParams.chmbrXcnts);
|
||||
const chmbrYdnts = getPopupParamValue(searchParams.chmbrYdnts);
|
||||
|
||||
const missingParams = [
|
||||
!chmbrXcnts ? "chmbrXcnts" : "",
|
||||
!chmbrYdnts ? "chmbrYdnts" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<PopupMissingParams
|
||||
title={windowPopupDefinitions.sscDetail.title}
|
||||
missingParams={missingParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Ssc
|
||||
featureData={null}
|
||||
isMainSsc={true}
|
||||
chmbrXcnts={chmbrXcnts}
|
||||
chmbrYdnts={chmbrYdnts}
|
||||
renderMode="popup"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const getPopupParamValue = (
|
||||
value: string | string[] | undefined,
|
||||
): string => {
|
||||
if (Array.isArray(value)) return value[0] ?? "";
|
||||
return value ?? "";
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
|
||||
+74
-74
@@ -1,76 +1,76 @@
|
||||
{
|
||||
"name": "st",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@rsone/components": "workspace:*",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@fullcalendar/core": "^6.1.17",
|
||||
"@fullcalendar/daygrid": "^6.1.17",
|
||||
"@fullcalendar/interaction": "^6.1.17",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"@types/xlsx": "^0.0.36",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.8",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"realgrid": "^2.9.5",
|
||||
"realgrid-react": "^0.9.7",
|
||||
"remixicon": "^4.7.0",
|
||||
"sonner": "^2.0.1",
|
||||
"swr": "^2.3.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
"name": "st",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@rsone/components": "workspace:*",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@fullcalendar/core": "^6.1.17",
|
||||
"@fullcalendar/daygrid": "^6.1.17",
|
||||
"@fullcalendar/interaction": "^6.1.17",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@fullcalendar/timegrid": "^6.1.17",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"@types/xlsx": "^0.0.36",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.8",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"realgrid": "^2.9.5",
|
||||
"realgrid-react": "^0.9.7",
|
||||
"remixicon": "^4.7.0",
|
||||
"sonner": "^2.0.1",
|
||||
"swr": "^2.3.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog as RsoneAlertDialog } from "@rsone/components";
|
||||
|
||||
@@ -36,6 +37,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
<SelectPromptDialog />
|
||||
<GlobalProgressOverlay />
|
||||
<Toaster position="top-center" />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
interface ReviewOpinionButtonProps {
|
||||
reviewOpinionFunc: () => void;
|
||||
reviewOpinionFunc: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const ReviewOpinionButton = ({ reviewOpinionFunc }: ReviewOpinionButtonProps) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user