From 599e8a95c783d4956d1343062394618ec55f6c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Tue, 20 Jan 2026 15:00:08 +0900 Subject: [PATCH 01/32] =?UTF-8?q?logout=20=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/app/api/auth/logout/route.ts | 25 ++++- .../cm/src/entities/auth/lib/token-manager.ts | 4 +- .../common/components/src/ui/header/index.tsx | 96 ++++++++++++++++++- 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/apps/cm/app/api/auth/logout/route.ts b/apps/cm/app/api/auth/logout/route.ts index ba8e23374..8740d9d5b 100644 --- a/apps/cm/app/api/auth/logout/route.ts +++ b/apps/cm/app/api/auth/logout/route.ts @@ -9,10 +9,31 @@ import { export async function POST() { const cookieStore = await cookies(); + + // 백엔드에 로그아웃 알림 (토큰 블랙리스트 처리) + const refreshToken = cookieStore.get(REFRESH_TOKEN)?.value; + + if (refreshToken) { + try { + // ⭐ Spring Boot 백엔드에 로그아웃 요청 + await fetch(`${process.env.NEXT_PUBLIC_SERVER_API_URL}/api/auth/logout`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Cookie": `${REFRESH_TOKEN}=${refreshToken}`, + }, + // credentials: "include", // fetch에서는 직접 쿠키 전달 + }); + } catch (error) { + console.error("Backend logout failed:", error); + } + } + + // Next.js 쿠키 삭제 cookieStore.delete(ACCESS_TOKEN); cookieStore.delete(REFRESH_TOKEN); cookieStore.delete(AUTH_USER_ID); cookieStore.delete(CLAIM_NAME); - return NextResponse.json({ message: "Logged out" }, { status: 200 }); -} + return NextResponse.json({ message: "Logged out" }, { status: 200 }); +} \ No newline at end of file diff --git a/apps/cm/src/entities/auth/lib/token-manager.ts b/apps/cm/src/entities/auth/lib/token-manager.ts index 21ed00218..8fcf21984 100644 --- a/apps/cm/src/entities/auth/lib/token-manager.ts +++ b/apps/cm/src/entities/auth/lib/token-manager.ts @@ -1,4 +1,4 @@ -import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; +import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/shared/config"; import { getCookie, setCookie, removeCookie } from "@/shared/lib"; export class TokenManager { @@ -76,6 +76,8 @@ export class TokenManager { this.removeToken(CLAIM_NAME); this.removeToken(AUTH_USER_ID); + this.removeToken(REFRESH_TOKEN); + this.removeToken(ACCESS_TOKEN); } } diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 15b431b6a..d1675d6f1 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -64,7 +64,99 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { queryFn: () => getSiteList(), enabled: isLoggedIn && !viewOnly, }); - + const logout = async () => { + try { + // 1. 백엔드 API 로그아웃 호출 + const response = await fetch("/api/auth/logout", { + method: "POST", + credentials: "include", // ⭐ 쿠키 포함 중요! + }); + + if (!response.ok) { + console.error("Logout API failed"); + // API 실패해도 계속 진행 + } + } catch (error) { + console.error("Logout error:", error); + // 에러가 나도 계속 진행 + } finally { + // 2. 클라이언트 쿠키 삭제 (백업) + clearClientCookies(); + + // 3. 로컬/세션 스토리지 정리 + if (typeof window !== "undefined") { + localStorage.clear(); + sessionStorage.clear(); + } + + // 4. 로그인 페이지로 리다이렉트 + window.location.href = "/auth/login"; + } + }; + + // 쿠키 삭제 헬퍼 함수 + const clearClientCookies = () => { + const actualHost = window.location.hostname; + + // 루트 도메인 추출 + const getRootDomain = (hostname: string): string => { + if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + return hostname; + } + + if (hostname.includes("hasuon.or.kr")) { + return ".hasuon.or.kr"; + } + + const parts = hostname.split("."); + if (parts.length >= 2) { + return `.${parts.slice(-2).join(".")}`; + } + + return hostname; + }; + + const domain = getRootDomain(actualHost); + + // 삭제할 쿠키 목록 + const cookiesToDelete = [ + appConfig.AUTH_USER_ID, + appConfig.REFRESH_TOKEN, + appConfig.ACCESS_TOKEN, + appConfig.CLAIM_NAME, + "site-id-key", + "site-theme-key", + ].filter(Boolean); // undefined 제거 + + // 여러 방법으로 쿠키 삭제 시도 + cookiesToDelete.forEach((cookieName) => { + if (!cookieName) return; + + // 방법 1: removeCookie 함수 사용 + if (domain === "localhost") { + removeCookie(cookieName, {}); + } else { + removeCookie(cookieName, { domain }); + } + + // 방법 2: 직접 document.cookie 설정 (도메인 포함) + if (domain !== "localhost") { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`; + } + + // 방법 3: 도메인 없이 삭제 (현재 도메인만) + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + // 방법 4: Secure 속성 포함 삭제 + if (window.location.protocol === "https:") { + if (domain !== "localhost") { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`; + } + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`; + } + }); + }; + /* const logout = async () => { try { const response = await fetch("/api/auth/logout", { @@ -123,7 +215,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { console.error(error); } }; - +*/ const openPopup = useCallback(async (popup: Popup) => { try { const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`; From 2469a8406859cc59f3363fade7072c3bac1e9561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Wed, 21 Jan 2026 17:52:22 +0900 Subject: [PATCH 02/32] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=A7=81=ED=81=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/app/api/auth/logout/route.ts | 24 +------------------ .../cm/src/pages/dashboard/ui/mypost-list.tsx | 14 +++++++---- .../cm/src/pages/dashboard/ui/notice-list.tsx | 10 ++++---- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/apps/cm/app/api/auth/logout/route.ts b/apps/cm/app/api/auth/logout/route.ts index 8740d9d5b..fdcc9df7c 100644 --- a/apps/cm/app/api/auth/logout/route.ts +++ b/apps/cm/app/api/auth/logout/route.ts @@ -9,31 +9,9 @@ import { export async function POST() { const cookieStore = await cookies(); - - // 백엔드에 로그아웃 알림 (토큰 블랙리스트 처리) - const refreshToken = cookieStore.get(REFRESH_TOKEN)?.value; - - if (refreshToken) { - try { - // ⭐ Spring Boot 백엔드에 로그아웃 요청 - await fetch(`${process.env.NEXT_PUBLIC_SERVER_API_URL}/api/auth/logout`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Cookie": `${REFRESH_TOKEN}=${refreshToken}`, - }, - // credentials: "include", // fetch에서는 직접 쿠키 전달 - }); - } catch (error) { - console.error("Backend logout failed:", error); - } - } - - // Next.js 쿠키 삭제 cookieStore.delete(ACCESS_TOKEN); cookieStore.delete(REFRESH_TOKEN); cookieStore.delete(AUTH_USER_ID); cookieStore.delete(CLAIM_NAME); - return NextResponse.json({ message: "Logged out" }, { status: 200 }); -} \ No newline at end of file +} diff --git a/apps/cm/src/pages/dashboard/ui/mypost-list.tsx b/apps/cm/src/pages/dashboard/ui/mypost-list.tsx index ec5f97d48..2c15b7d7d 100644 --- a/apps/cm/src/pages/dashboard/ui/mypost-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/mypost-list.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel } from "@tanstack/react-table"; -import { PostApi, type MyPost } from "@/entities/post"; +import { PostApi, type MyPost, type NoticePost } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; import { @@ -23,15 +23,21 @@ const MyPostList: React.FC = () => { }), }); + const handleMyPostRowClick = (row: MyPost) => { + router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`); + }; + const { tableComponent: myPostTableComponent } = useDataTable({ query: myPostQuery, tableOptions: { - columns: myPostTableColumns, - getCoreRowModel: getCoreRowModel(), + columns: myPostTableColumns, + getCoreRowModel: getCoreRowModel(), }, dataTableOptions: { - variant: "line", + variant: "line", + onRowClick: handleMyPostRowClick }, + }); return ( diff --git a/apps/cm/src/pages/dashboard/ui/notice-list.tsx b/apps/cm/src/pages/dashboard/ui/notice-list.tsx index 86e21a4a3..502973225 100644 --- a/apps/cm/src/pages/dashboard/ui/notice-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/notice-list.tsx @@ -26,7 +26,7 @@ const NoticeList: React.FC = () => { }); const handleNoticePostRowClick = (row: NoticePost) => { - router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`); + router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`); }; const handleMoreClick = () => { @@ -40,12 +40,12 @@ const NoticeList: React.FC = () => { const { tableComponent: noticePostTableComponent } = useDataTable({ query: noticePostQuery, tableOptions: { - columns: noticePostTableColumns, - getCoreRowModel: getCoreRowModel(), + columns: noticePostTableColumns, + getCoreRowModel: getCoreRowModel(), }, dataTableOptions: { - variant: "line", - onRowClick: handleNoticePostRowClick, + variant: "line", + onRowClick: handleNoticePostRowClick, }, }); From a61fda8b58931c2869dc892bab2d6555de43149f Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 26 Jan 2026 16:13:33 +0900 Subject: [PATCH 03/32] =?UTF-8?q?fo:=20exceldown=EC=8B=9C=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=82=98=EB=A8=B8=EC=A7=80=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityPrecipitation/page.tsx | 16 ++++++++++++++++ .../(with-layout)/facilityWaterQlty/page.tsx | 17 +++++++++++++++++ .../app/pages/(with-layout)/flowRate/page.tsx | 17 +++++++++++++++++ .../app/pages/(with-layout)/planSewage/page.tsx | 17 +++++++++++++++++ .../pages/(with-layout)/processArea/page.tsx | 17 +++++++++++++++++ .../(with-layout)/processFacility/page.tsx | 17 +++++++++++++++++ apps/fo/app/pages/(with-layout)/rdi/page.tsx | 17 +++++++++++++++++ 7 files changed, 118 insertions(+) diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index 18a769871..eecab5768 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -191,6 +191,22 @@ export default function FacilityPrecipitation() { // 엑셀다운로드 const excelDownload = useCallback((searchReturn: ReturnType) => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } searchFacilityPrecipitation(searchReturn, 1, 10000); }, [searchFacilityPrecipitation]); diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx index 86a434d6b..539cc6f31 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx @@ -170,6 +170,23 @@ export default function ProcessFacilitySearch() { }, [upHeadList1, upHeadList2, upHeadList3]); const excelDownload = useCallback(() => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } + setExcelTrigger(true); searchFcltyQltyList(ref.current, 1, 10000); }, [searchFcltyQltyList]); diff --git a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx index a1a5e22d1..696e8f0b2 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx @@ -176,6 +176,23 @@ export default function ProcessFacilitySearch() { }, [upHeadList1, upHeadList2, upHeadList3]); const excelDownload = useCallback(() => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } + searchFlowRate(ref.current, 1, 10000); setExcelTrigger(true); }, [searchFlowRate]); diff --git a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx index 62ae9483a..0f73f1035 100644 --- a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx @@ -178,6 +178,23 @@ export default function PlanSewage() { // 엑셀다운로드 const excelDownload = useCallback((searchReturn: ReturnType) => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } + searchPlanSewage(searchReturn, 1, 10000); setExcelTrigger(true); }, [searchPlanSewage]); diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index ba1e27767..deca4366c 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -132,6 +132,23 @@ export default function ProcessArea() { }, [excelList]); const excelDownload = useCallback(() => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } + searchProcessArea(ref.current, 1, 10000); }, [searchProcessArea]); diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 9c3c612b5..b239970e0 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -236,6 +236,23 @@ export default function ProcessFacilitySearch() { } const excelDownload = useCallback(() => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } + searchFcltyList(ref.current, 1, 10000); }, [searchFcltyList]); diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index 98120523a..ef1691d69 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -175,6 +175,23 @@ export default function Rdi() { // 엑셀다운로드 const excelDownload = useCallback(() => { + if (ref.current.partValue === 'A') { // 행정별 + if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'R') { // 유역별 + if (!ref.current.regionValue || ref.current.regionValue === '0') { + alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + return; + } + } else if (ref.current.partValue === 'G') { // 환경청별 + if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + return; + } + } + searchRdi(ref.current, 1, 5000); setIsInitExcelDownload(true); }, [searchRdi]); From 4356ec8a34855828ece8359188557f5d6658b203 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 28 Jan 2026 18:05:48 +0900 Subject: [PATCH 04/32] =?UTF-8?q?fo:=20=EB=85=84=EB=8F=84=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=B0=8F=20exceldown=EC=8B=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=B2=98=EB=A6=AC=203=EB=B3=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/(with-layout)/dashboard/page.tsx | 8 +++-- .../facilityPrecipitation/page.tsx | 17 +++++---- .../(with-layout)/facilityWaterQlty/page.tsx | 11 ++++-- .../app/pages/(with-layout)/flowRate/page.tsx | 35 +++++++++++-------- .../pages/(with-layout)/planSewage/page.tsx | 13 ++++--- .../pages/(with-layout)/processArea/page.tsx | 17 +++++---- apps/fo/app/pages/(with-layout)/rdi/page.tsx | 11 ++++-- .../pages/(with-layout)/sewageOccur/page.tsx | 25 ++++++++----- .../(with-layout)/underWaterInput/page.tsx | 27 ++++++++------ apps/fo/app/store/common/index.tsx | 30 ++++++++-------- .../src/ui/select/index.tsx | 22 +++++++----- 11 files changed, 136 insertions(+), 80 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx index 6ab18b825..833f706ed 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx @@ -60,6 +60,10 @@ export default function DashBoard() { const [sidoValue, setSidoValue] = useState('0'); const [sigunValue, setSigunValue] = useState('0'); const { isInit, selectSearchYear, btnClass, selectSidoData, selectSigunData, } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; const { planFcltyList } = PlanFclty; const { operationalStatusList } = OperationStatus; const { excelList } = RainManage; @@ -75,7 +79,7 @@ export default function DashBoard() { /******************선택된 데이터******************/ - const [searchYear, setSearchYear] = useState(selectSearchYear[0]?.val || '0'); // 연도 + const [searchYear, setSearchYear] = useState(defaultSearchYear); // 연도 // 시설명 const [fcltyList, setFcltyList] = useState({ bscFctCd: '11000SW001R', @@ -283,7 +287,7 @@ export default function DashBoard() { trtFrfNm: '표준활성슬러지법, MLE, A2O', efflWqltStndrdNm: '500톤 이상(Ⅲ지역)' }); - setSearchYear(selectSearchYear[0]?.val || '0'); + setSearchYear(defaultSearchYear); searchFcltyList.bscFctNm = '난지'; setSidoValue('0'); diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index eecab5768..0f29a28c4 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -37,6 +37,11 @@ export default function FacilityPrecipitation() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { labelArray, } = FacilityPrecipitationSearch; @@ -90,7 +95,7 @@ export default function FacilityPrecipitation() { // 시설상태 const [operationValue, setOperationValue] = useState("0"); // 조회기간 - const [searchYear, setSearchYear] = useState(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState(defaultSearchYear); /** * 페이지 함수 @@ -102,7 +107,7 @@ export default function FacilityPrecipitation() { // 상태값 설정 setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); - setSearchYear(selectSearchYear[0]?.val || "0"); + setSearchYear(defaultSearchYear); // SearchInputSelect 초기화 setFcltyName(""); if (searchInputRef.current) { @@ -111,7 +116,7 @@ export default function FacilityPrecipitation() { input.value = ""; } } - searchFacilityPrecipitationList(1, 10, selectSearchYear[0]?.val || "0"); + searchFacilityPrecipitationList(1, 10, defaultSearchYear); } // 모달 닫기 @@ -134,9 +139,9 @@ export default function FacilityPrecipitation() { useEffect(() => { if (!isInit) { initializeCommonData(); - searchFacilityPrecipitationList(1, 10, selectSearchYear[0]?.val || "0"); + searchFacilityPrecipitationList(1, 10, defaultSearchYear); } - }, [isInit, initializeCommonData, searchFacilityPrecipitationList, selectSearchYear]); + }, [isInit, initializeCommonData, searchFacilityPrecipitationList, defaultSearchYear]); useEffect(() => { if (excelList && excelList.length > 0) { @@ -270,4 +275,4 @@ export default function FacilityPrecipitation() { )} ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx index 539cc6f31..0f8427458 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx @@ -36,6 +36,11 @@ export default function ProcessFacilitySearch() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { mainLabelArray, upHeadList1, @@ -57,7 +62,7 @@ export default function ProcessFacilitySearch() { const [upheadList, setUpheadList] = useState(upHeadList1); const [fcltyName, setFcltyName] = useState(""); const [operationValue, setOperationValue] = useState("0"); - const [searchYearValue, setSearchYearValue] = useState(selectSearchYear[0]?.val || "0"); + const [searchYearValue, setSearchYearValue] = useState(defaultSearchYear); const [fcltCpct, setFcltCpct] = useState("0"); // 모달 상태 추가 @@ -96,7 +101,7 @@ export default function ProcessFacilitySearch() { // 상태값 설정 setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); - setSearchYearValue(selectSearchYear[0]?.val || "0"); + setSearchYearValue(defaultSearchYear); // SearchInputSelect 초기화 setFcltyName(""); if (searchInputRef.current) { @@ -241,4 +246,4 @@ export default function ProcessFacilitySearch() { )} ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx index 696e8f0b2..c80d46699 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx @@ -7,7 +7,7 @@ "use client" import { useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; -import type { ExcelType } from "@/app/store/flowRate"; +// import type { ExcelType } from "@/app/store/flowRate"; import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; @@ -38,6 +38,11 @@ export default function ProcessFacilitySearch() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { flowRateSearchLabelArray } = FlowRateSearch; @@ -57,17 +62,17 @@ export default function ProcessFacilitySearch() { const [fcltCpct, setFcltCpct] = useState("0"); const [fcltyName, setFcltyName] = useState(""); const [operationValue, setOperationValue] = useState("0"); - const [searchYearValue, setSearchYearValue] = useState(selectSearchYear[0]?.val || "0"); + const [searchYearValue, setSearchYearValue] = useState(defaultSearchYear); // const [widthSize, setWidthSize] = useState("w-[120%]"); const [upheadList, setUpheadList] = useState(upHeadList1); const [resetPage, setResetPage] = useState(false); - const prevExcelListRef = useRef(null); + // const prevExcelListRef = useRef(null); // 모달 상태 추가 const [selectedRow, setSelectedRow] = useState(null); const [fcltyNm, setFcltyNm] = useState(""); const [fcltyCd, setFcltyCd] = useState(""); - const [excelTrigger, setExcelTrigger] = useState(false); + const [isExcelDownClick, setIsExcelDownClick] = useState(false); const [initTrigger, setInitTrigger] = useState(false); const searchFlowRate = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { @@ -99,7 +104,7 @@ export default function ProcessFacilitySearch() { // setWidthSize("w-[120%]"); setUpheadList(upHeadList1) // 상태값 설정 - setSearchYearValue(selectSearchYear[0]?.val || "0"); + setSearchYearValue(defaultSearchYear); setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); // SearchInputSelect 초기화 @@ -147,15 +152,17 @@ export default function ProcessFacilitySearch() { }, [isInit, initializeCommonData, searchFlowRateList, selectSearchYear, searchFlowRate, initTrigger]); useEffect(() => { - if(!excelTrigger) return; + if(!isExcelDownClick) return; - if (excelList && excelList.length > 0) { - downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] }); - } else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) { - alert('다운받을 데이터가 없습니다.'); - } - prevExcelListRef.current = excelList; - }, [excelList, excelTrigger]); + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] }); + } else { + alert('조회된 결과가 없습니다.'); + } + }, 1000); // 약간의 지연을 두고 다운로드 실행 + return () => clearTimeout(timer); + }, [excelList, isExcelDownClick]); const ref = useRef({ partValue: '', @@ -194,7 +201,7 @@ export default function ProcessFacilitySearch() { } searchFlowRate(ref.current, 1, 10000); - setExcelTrigger(true); + setIsExcelDownClick(true); }, [searchFlowRate]); const total = flowRateCount; diff --git a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx index 0f73f1035..41b541ce9 100644 --- a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx @@ -41,6 +41,11 @@ export default function PlanSewage() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { labelArray, } = PlanSewageSearch; @@ -85,7 +90,7 @@ export default function PlanSewage() { // 시설상태 const [operationValue, setOperationValue] = useState("0"); // 조회기간 - const [searchYear, setSearchYear] = useState(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState(defaultSearchYear); const [initTrigger, setInitTrigger] = useState(false); @@ -100,7 +105,7 @@ export default function PlanSewage() { // 상태값 설정 setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); - setSearchYear(selectSearchYear[0]?.val || "0"); + setSearchYear(defaultSearchYear); // SearchInputSelect 초기화 setFcltyName(""); if (searchInputRef.current) { @@ -168,7 +173,7 @@ export default function PlanSewage() { setInitTrigger(true); } } - }, [isInit, initializeCommonData, searchPlanSewageList, selectSearchYear, initTrigger, searchPlanSewage]); + }, [isInit, initializeCommonData, initTrigger, searchPlanSewage]); const chkVal = useCallback((returntype: ReturnType) => { // setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") @@ -241,4 +246,4 @@ export default function PlanSewage() { ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index deca4366c..e3b6fc3da 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -42,6 +42,11 @@ export default function ProcessArea() { btnClass, } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { processFacilityLabelArray, } = ProcessAreaSearch; @@ -64,7 +69,7 @@ export default function ProcessArea() { const [fcltyCd, setFcltyCd] = useState(""); const [resetPage, setResetPage] = useState(false); - const [searchYearValue, setSearchYearValue] = useState(selectSearchYear[0]?.val || "0"); + const [searchYearValue, setSearchYearValue] = useState(defaultSearchYear); const ref = useRef({ partValue: '', @@ -95,8 +100,8 @@ export default function ProcessArea() { input.value = ""; } } - setSearchYearValue(selectSearchYear[0]?.val || "0"); - searchProcessAreaList(1, 10, '', '', selectSearchYear[0]?.val || "0"); + setSearchYearValue(defaultSearchYear); + searchProcessAreaList(1, 10, '', '', defaultSearchYear); } // 셀 클릭 시 모달 표시 @@ -116,9 +121,9 @@ export default function ProcessArea() { useEffect(() => { if (!isInit) { initializeCommonData(); - searchProcessAreaList(1, 10, '', '', selectSearchYear[0]?.val || "0"); + searchProcessAreaList(1, 10, '', '', defaultSearchYear); } - }, [isInit, initializeCommonData, searchProcessAreaList]); + }, [isInit, initializeCommonData, searchProcessAreaList, defaultSearchYear]); useEffect(() => { if (excelList && excelList.length > 0) { @@ -200,4 +205,4 @@ export default function ProcessArea() { )} ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index ef1691d69..d166e01a3 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -41,6 +41,11 @@ export default function Rdi() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { labelArray, } = RdiSearch; @@ -83,7 +88,7 @@ export default function Rdi() { // 시설상태 const [operationValue, setOperationValue] = useState("0"); // 조회기간 - const [searchYear, setSearchYear] = useState(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀 다운로드 트리거 const [isInitExcelDownload, setIsInitExcelDownload] = useState(false); @@ -99,7 +104,7 @@ export default function Rdi() { // 상태값 설정 setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); - setSearchYear(selectSearchYear[0]?.val || "0"); + setSearchYear(defaultSearchYear); // SearchInputSelect 초기화 setFcltyName(""); if (searchInputRef.current) { @@ -241,4 +246,4 @@ export default function Rdi() { ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx index e9297d0f5..a7ff0df71 100644 --- a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx +++ b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx @@ -44,6 +44,11 @@ export default function PlanSewage() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { labelArray, } = SewageOccurSearch; @@ -93,9 +98,9 @@ export default function PlanSewage() { // 시설상태 const [operationValue, setOperationValue] = useState("0"); // 조회기간 - const [searchYear, setSearchYear] = useState(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 - const [isInitExcelDownload, setIsInitExcelDownload] = useState(false); + const [isExcelDownClick, setIsExcelDownClick] = useState(false); /** * 페이지 함수 @@ -107,7 +112,7 @@ export default function PlanSewage() { // 상태값 설정 setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); - setSearchYear(selectSearchYear[0]?.val || "0"); + setSearchYear(defaultSearchYear); // SearchInputSelect 초기화 setFcltyName(""); if (searchInputRef.current) { @@ -116,7 +121,7 @@ export default function PlanSewage() { input.value = ""; } } - searchSewageOccurList(1, 10, selectSearchYear[0]?.val || "0"); + searchSewageOccurList(1, 10, defaultSearchYear); } // 모달 닫기 @@ -139,12 +144,12 @@ export default function PlanSewage() { useEffect(() => { if (!isInit) { initializeCommonData(); - searchSewageOccurList(1, 10, selectSearchYear[0]?.val || "0"); + searchSewageOccurList(1, 10, defaultSearchYear); } - }, [isInit, initializeCommonData, searchSewageOccurList, selectSearchYear]); + }, [isInit, initializeCommonData, searchSewageOccurList, defaultSearchYear]); useEffect(() => { - if(!isInitExcelDownload) return ; + if(!isExcelDownClick) return ; const timer = setTimeout(() => { if (excelList && excelList.length > 0) { @@ -154,11 +159,13 @@ export default function PlanSewage() { sheetNameArray: ['오수발생량 추정'], headerRange: 'A1:O1' }); + } else { + alert("조회된 결과가 없습니다."); } }, 500); // 약간의 지연을 두고 다운로드 실행 return () => clearTimeout(timer); - }, [excelList, isInit, isInitExcelDownload]); + }, [excelList, isInit, isExcelDownClick]); /** * 콜백함수 @@ -213,7 +220,7 @@ export default function PlanSewage() { } } searchFacilityPrecipitation(ref.current, 1, 5000); - setIsInitExcelDownload(true); + setIsExcelDownClick(true); }, [searchFacilityPrecipitation]); // 페이지 변경 diff --git a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx index 969ae2a05..849689312 100644 --- a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx +++ b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx @@ -50,6 +50,11 @@ export default function UnderWaterInput() { btnClass } = Common; + const defaultSearchYear = + selectSearchYear.find((item) => item.selected)?.val || + selectSearchYear[0]?.val || + "0"; + const { labelArray, } = UnderWaterSearch; @@ -98,9 +103,9 @@ export default function UnderWaterInput() { // 시설상태 const [operationValue, setOperationValue] = useState("0"); // 조회기간 - const [searchYear, setSearchYear] = useState(selectSearchYear[0]?.val || "0"); + const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 - const [isMount, setIsMount] = useState(false); + const [isExcelDownClick, setIsExcelDownClick] = useState(false); /** * 페이지 함수 @@ -112,7 +117,7 @@ export default function UnderWaterInput() { // 상태값 설정 setFcltCpct(selectUpdownData[0]?.val || "0"); setOperationValue(selectOperationData[0]?.val || "0"); - setSearchYear(selectSearchYear[0]?.val || "0"); + setSearchYear(defaultSearchYear); // SearchInputSelect 초기화 setFcltyName(""); if (searchInputRef.current) { @@ -121,7 +126,7 @@ export default function UnderWaterInput() { input.value = ""; } } - searchUnderWaterList(1, 10, selectSearchYear[0]?.val || "0"); + searchUnderWaterList(1, 10, defaultSearchYear); } // 모달 닫기 @@ -144,12 +149,12 @@ export default function UnderWaterInput() { useEffect(() => { if (!isInit) { initializeCommonData(); - searchUnderWaterList(1, 10, selectSearchYear[0]?.val || "0"); + searchUnderWaterList(1, 10, defaultSearchYear); } - }, [isInit, initializeCommonData, searchUnderWaterList, selectSearchYear]); + }, [isInit, initializeCommonData, searchUnderWaterList, defaultSearchYear]); useEffect(() => { - if(!isMount) return ; + if(!isExcelDownClick) return ; const timer = setTimeout(() => { if (excelList && excelList.length > 0) { @@ -159,10 +164,12 @@ export default function UnderWaterInput() { sheetNameArray: ['지하수유입량 추정'], headerRange: 'A1:O1' }); + } else { + alert("조회된 결과가 없습니다."); } }, 1000); // 약간의 지연을 두고 다운로드 실행 return () => clearTimeout(timer); - }, [excelList, isMount]); + }, [excelList, isExcelDownClick]); /** * 콜백함수 @@ -217,7 +224,7 @@ export default function UnderWaterInput() { } } searchFacilityPrecipitation(ref.current, 1, 5000); - setIsMount(true); + setIsExcelDownClick(true); }, [searchFacilityPrecipitation]); // 페이지 변경 @@ -276,4 +283,4 @@ export default function UnderWaterInput() { )} ); -} \ No newline at end of file +} diff --git a/apps/fo/app/store/common/index.tsx b/apps/fo/app/store/common/index.tsx index a381eb50d..d24305aa9 100644 --- a/apps/fo/app/store/common/index.tsx +++ b/apps/fo/app/store/common/index.tsx @@ -12,6 +12,8 @@ import { getFetch, getFetchAreaList } from "@/app/components/api"; export type SelectDataType = { text: string; val: string; + selected?: boolean; + hidden?: boolean; } // grid header export type TableUpperProps = { @@ -160,23 +162,21 @@ const selectFacilityPartData: SelectDataType[] = [ const getYearData = () => { const today = new Date(); const year = today.getFullYear(); - const month = today.getMonth() + 1; + // const month = today.getMonth() + 1; // 상반기(6월까지)는 2년 전, 하반기는 1년 전부터 시작 - const startYear = month <= 6 ? (year - 2) : (year - 1); + // const startYear = month <= 6 ? (year - 2) : (year - 1); + const startYear = year - 1; - const year1 = startYear; - const year2 = startYear - 1; - const year3 = startYear - 2; - const year4 = startYear - 3; - const year5 = startYear - 4; - return [ - { text: year1 + "년", val: year1.toString() }, - { text: year2 + "년", val: year2.toString() }, - { text: year3 + "년", val: year3.toString() }, - { text: year4 + "년", val: year4.toString() }, - { text: year5 + "년", val: year5.toString() }, - ] + // 최근 10개년도 제공 (기본값: 첫 번째 항목 선택) + return Array.from({ length: 10 }, (_, index) => { + const targetYear = startYear - index; + return { + text: `${targetYear}년`, + val: targetYear.toString(), + selected: index === 0, + }; + }); } const selectSearchYear: SelectDataType[] = getYearData(); @@ -486,4 +486,4 @@ export const CommonReducer = ( } } -export type CommonStore = CommonType & CommonAction; \ No newline at end of file +export type CommonStore = CommonType & CommonAction; diff --git a/packages/common/business_components/src/ui/select/index.tsx b/packages/common/business_components/src/ui/select/index.tsx index f9dac92d6..32510eb18 100644 --- a/packages/common/business_components/src/ui/select/index.tsx +++ b/packages/common/business_components/src/ui/select/index.tsx @@ -1,7 +1,8 @@ // selectBox import { Label } from "@common/components"; -import React, { useEffect, useState } from "react"; +// import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Select, SelectContent, @@ -15,6 +16,7 @@ interface SelectBox1 { text: string; val: string | number; hidden?: boolean; + selected?: boolean; } const SelectBox = React.forwardRef< @@ -51,18 +53,22 @@ const SelectBox = React.forwardRef< const [isOpen, setIsOpen] = useState(false); // hidden이 true인 항목만 드롭다운에서 제외 const visibleItems = selectArray.filter((item) => !item.hidden); + const selectedItem = visibleItems.find((item) => item.selected); + + const resolvedValue = + value === undefined && selectedItem ? selectedItem.val : value; // 현재 값이 hidden 항목이거나 빈 값인 경우 undefined로 설정하여 placeholder가 표시되도록 함 const currentValue = - value !== undefined && + resolvedValue !== undefined && hiddenItem && - (String(value) === String(hiddenItem.val) || - value === "" || - value === "0" || - value === 0) + (String(resolvedValue) === String(hiddenItem.val) || + resolvedValue === "" || + resolvedValue === "0" || + resolvedValue === 0) ? undefined - : value !== undefined - ? String(value) + : resolvedValue !== undefined + ? String(resolvedValue) : undefined; return ( From 5faced2928a4741d8584bdda4ea6c4019e2eb75f Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 29 Jan 2026 09:12:00 +0900 Subject: [PATCH 05/32] =?UTF-8?q?fo:=20default=20=EB=85=84=EB=8F=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/store/common/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/fo/app/store/common/index.tsx b/apps/fo/app/store/common/index.tsx index d24305aa9..f71550f1a 100644 --- a/apps/fo/app/store/common/index.tsx +++ b/apps/fo/app/store/common/index.tsx @@ -162,11 +162,11 @@ const selectFacilityPartData: SelectDataType[] = [ const getYearData = () => { const today = new Date(); const year = today.getFullYear(); - // const month = today.getMonth() + 1; - + const month = today.getMonth() + 1; + // 상반기(6월까지)는 2년 전, 하반기는 1년 전부터 시작 - // const startYear = month <= 6 ? (year - 2) : (year - 1); - const startYear = year - 1; + const defaultIdx = month <= 6 ? 2 : 1; + const startYear = year; // 최근 10개년도 제공 (기본값: 첫 번째 항목 선택) return Array.from({ length: 10 }, (_, index) => { @@ -174,7 +174,7 @@ const getYearData = () => { return { text: `${targetYear}년`, val: targetYear.toString(), - selected: index === 0, + selected: index === defaultIdx, }; }); } From a2504f4c85c1ed4073073d781204e2e812bb39ee Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 29 Jan 2026 18:07:08 +0900 Subject: [PATCH 06/32] =?UTF-8?q?fo:exceldown=EC=8B=9C=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=ED=95=B4=EC=A0=9C=204=EA=B0=9C=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityPrecipitation/page.tsx | 32 +++++++------- .../pages/(with-layout)/processArea/page.tsx | 32 +++++++------- .../(with-layout)/processFacility/page.tsx | 32 +++++++------- apps/fo/app/pages/(with-layout)/rdi/page.tsx | 43 ++++++++++--------- .../src/shared/providers/prompt-provider.tsx | 1 + 5 files changed, 72 insertions(+), 68 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index 0f29a28c4..14d16810d 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -196,22 +196,22 @@ export default function FacilityPrecipitation() { // 엑셀다운로드 const excelDownload = useCallback((searchReturn: ReturnType) => { - if (ref.current.partValue === 'A') { // 행정별 - if (!ref.current.sidoValue || ref.current.sidoValue === '0') { - alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'R') { // 유역별 - if (!ref.current.regionValue || ref.current.regionValue === '0') { - alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'G') { // 환경청별 - if (!ref.current.envPartValue || ref.current.envPartValue === '0') { - alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); - return; - } - } + // if (ref.current.partValue === 'A') { // 행정별 + // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'R') { // 유역별 + // if (!ref.current.regionValue || ref.current.regionValue === '0') { + // alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'G') { // 환경청별 + // if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + // alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } searchFacilityPrecipitation(searchReturn, 1, 10000); }, [searchFacilityPrecipitation]); diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index e3b6fc3da..371a1262d 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -137,22 +137,22 @@ export default function ProcessArea() { }, [excelList]); const excelDownload = useCallback(() => { - if (ref.current.partValue === 'A') { // 행정별 - if (!ref.current.sidoValue || ref.current.sidoValue === '0') { - alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'R') { // 유역별 - if (!ref.current.regionValue || ref.current.regionValue === '0') { - alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'G') { // 환경청별 - if (!ref.current.envPartValue || ref.current.envPartValue === '0') { - alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); - return; - } - } + // if (ref.current.partValue === 'A') { // 행정별 + // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'R') { // 유역별 + // if (!ref.current.regionValue || ref.current.regionValue === '0') { + // alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'G') { // 환경청별 + // if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + // alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } searchProcessArea(ref.current, 1, 10000); }, [searchProcessArea]); diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index b239970e0..9a6535db1 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -236,22 +236,22 @@ export default function ProcessFacilitySearch() { } const excelDownload = useCallback(() => { - if (ref.current.partValue === 'A') { // 행정별 - if (!ref.current.sidoValue || ref.current.sidoValue === '0') { - alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'R') { // 유역별 - if (!ref.current.regionValue || ref.current.regionValue === '0') { - alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'G') { // 환경청별 - if (!ref.current.envPartValue || ref.current.envPartValue === '0') { - alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); - return; - } - } + // if (ref.current.partValue === 'A') { // 행정별 + // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'R') { // 유역별 + // if (!ref.current.regionValue || ref.current.regionValue === '0') { + // alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'G') { // 환경청별 + // if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + // alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } searchFcltyList(ref.current, 1, 10000); }, [searchFcltyList]); diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index d166e01a3..63df65e13 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -90,7 +90,8 @@ export default function Rdi() { // 조회기간 const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀 다운로드 트리거 - const [isInitExcelDownload, setIsInitExcelDownload] = useState(false); + const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const [initTrigger, setInitTrigger] = useState(false); /** @@ -118,7 +119,7 @@ export default function Rdi() { } useEffect(() => { - if(!isInitExcelDownload) return ; + if(!isExcelDownClick) return ; if (excelList && excelList.length > 0) { downloadExcel({ data: excelList as unknown as JSON[], @@ -126,8 +127,10 @@ export default function Rdi() { sheetNameArray: ['RDII추정'], headerRange: 'A1:R1' }); + } else { + alert("조회된 결과가 없습니다."); } - }, [excelList, isInitExcelDownload]); + }, [excelList, isExcelDownClick]); /** * 콜백함수 @@ -180,25 +183,25 @@ export default function Rdi() { // 엑셀다운로드 const excelDownload = useCallback(() => { - if (ref.current.partValue === 'A') { // 행정별 - if (!ref.current.sidoValue || ref.current.sidoValue === '0') { - alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'R') { // 유역별 - if (!ref.current.regionValue || ref.current.regionValue === '0') { - alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); - return; - } - } else if (ref.current.partValue === 'G') { // 환경청별 - if (!ref.current.envPartValue || ref.current.envPartValue === '0') { - alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); - return; - } - } + // if (ref.current.partValue === 'A') { // 행정별 + // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { + // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'R') { // 유역별 + // if (!ref.current.regionValue || ref.current.regionValue === '0') { + // alert('엑셀 다운로드는 유역 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } else if (ref.current.partValue === 'G') { // 환경청별 + // if (!ref.current.envPartValue || ref.current.envPartValue === '0') { + // alert('엑셀 다운로드는 환경청 조건을 선택한 후 이용 가능합니다.'); + // return; + // } + // } searchRdi(ref.current, 1, 5000); - setIsInitExcelDownload(true); + setIsExcelDownClick(true); }, [searchRdi]); // 페이지 변경 diff --git a/apps/fo/src/shared/providers/prompt-provider.tsx b/apps/fo/src/shared/providers/prompt-provider.tsx index 10338c338..7724d0326 100644 --- a/apps/fo/src/shared/providers/prompt-provider.tsx +++ b/apps/fo/src/shared/providers/prompt-provider.tsx @@ -1,3 +1,4 @@ +"use client"; import { createContext, useState } from "react"; interface PromptContext { From 527b0c65672927d495c61d3e170f01800287fdcd Mon Sep 17 00:00:00 2001 From: dongsilove Date: Thu, 29 Jan 2026 20:56:43 +0900 Subject: [PATCH 07/32] =?UTF-8?q?fo:=20exceldownload=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityPrecipitation/page.tsx | 28 ++++++++----- .../(with-layout)/facilityWaterQlty/page.tsx | 40 +++++++++++-------- .../app/pages/(with-layout)/flowRate/page.tsx | 1 + .../pages/(with-layout)/planSewage/page.tsx | 32 +++++++++------ .../pages/(with-layout)/processArea/page.tsx | 29 +++++++++----- .../(with-layout)/processFacility/page.tsx | 18 +++++++-- apps/fo/app/pages/(with-layout)/rdi/page.tsx | 26 +++++++----- .../pages/(with-layout)/sewageOccur/page.tsx | 1 + .../(with-layout)/underWaterInput/page.tsx | 1 + 9 files changed, 116 insertions(+), 60 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index 14d16810d..4df912186 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -96,7 +96,8 @@ export default function FacilityPrecipitation() { const [operationValue, setOperationValue] = useState("0"); // 조회기간 const [searchYear, setSearchYear] = useState(defaultSearchYear); - + // 엑셀다운로드 트리거 + const [isExcelDownClick, setIsExcelDownClick] = useState(false); /** * 페이지 함수 */ @@ -144,14 +145,22 @@ export default function FacilityPrecipitation() { }, [isInit, initializeCommonData, searchFacilityPrecipitationList, defaultSearchYear]); useEffect(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '시설별 강수현황', - sheetNameArray: ['시설별 강수현황'], - headerRange: 'A1:N1' - }); - } + if (!isExcelDownClick) return; + + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '시설별 강수현황', + sheetNameArray: ['시설별 강수현황'], + headerRange: 'A1:N1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + }, 500); + return () => clearTimeout(timer); }, [excelList]); useEffect(() => { @@ -213,6 +222,7 @@ export default function FacilityPrecipitation() { // } // } searchFacilityPrecipitation(searchReturn, 1, 10000); + setIsExcelDownClick(true); }, [searchFacilityPrecipitation]); // 페이지 변경 diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx index 0f8427458..1df3be0cd 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx @@ -54,7 +54,8 @@ export default function ProcessFacilitySearch() { const mainList: FacilityWaterQltyProps[] = facilityWaterQltyList || []; const [initTrigger, setInitTrigger] = useState(false); - const [excelTrigger, setExcelTrigger] = useState(false); + // 엑셀다운로드 트리거 + const [isExcelDownClick, setIsExcelDownClick] = useState(false); // 그리드 관련 const [resetPage, setResetPage] = useState(false); const searchInputRef = useRef(null); @@ -152,21 +153,28 @@ export default function ProcessFacilitySearch() { }, [isInit, initializeCommonData, searchFacilityWaterQltyList, searchFcltyQltyList, initTrigger]); useEffect(() => { - if (!excelTrigger) return; + if (!isExcelDownClick) return; - if (excelList && excelList.length > 0) { - downloadExcelFormat( - { - data: excelList as unknown as JSON[], - fileName: '시설별 수질현황', - sheetNameArray: ['시설별 수질현황'], - headerRange: 'A1:T1', - decimalArray1: [10, 11, 12, 15, 16, 17], - decimalArray3: [13, 14, 18, 19] - } - ); - } - }, [excelList, excelTrigger]); + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcelFormat( + { + data: excelList as unknown as JSON[], + fileName: '시설별 수질현황', + sheetNameArray: ['시설별 수질현황'], + headerRange: 'A1:T1', + decimalArray1: [10, 11, 12, 15, 16, 17], + decimalArray3: [13, 14, 18, 19] + } + ); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + }, 500); + + return () => clearTimeout(timer); + }, [excelList, isExcelDownClick]); const chkVal = useCallback((returntype: ReturnType) => { // setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") @@ -192,7 +200,7 @@ export default function ProcessFacilitySearch() { } } - setExcelTrigger(true); + setIsExcelDownClick(true); searchFcltyQltyList(ref.current, 1, 10000); }, [searchFcltyQltyList]); diff --git a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx index c80d46699..66b8c990d 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx @@ -160,6 +160,7 @@ export default function ProcessFacilitySearch() { } else { alert('조회된 결과가 없습니다.'); } + setIsExcelDownClick(false); }, 1000); // 약간의 지연을 두고 다운로드 실행 return () => clearTimeout(timer); }, [excelList, isExcelDownClick]); diff --git a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx index 41b541ce9..fca74757c 100644 --- a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx @@ -93,8 +93,9 @@ export default function PlanSewage() { const [searchYear, setSearchYear] = useState(defaultSearchYear); const [initTrigger, setInitTrigger] = useState(false); + // 엑셀다운로드 트리거 + const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const [excelTrigger, setExcelTrigger] = useState(false); /** * 페이지 함수 */ @@ -119,17 +120,24 @@ export default function PlanSewage() { } useEffect(() => { - if (!excelTrigger) return; + if (!isExcelDownClick) return; - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '계획하수량 산정지표 현황', - sheetNameArray: ['계획하수량 산정지표 현황'], - headerRange: 'A1:M1' - }); - } - }, [excelList, excelTrigger]); + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '계획하수량 산정지표 현황', + sheetNameArray: ['계획하수량 산정지표 현황'], + headerRange: 'A1:M1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + }, 500); + + return () => clearTimeout(timer); + }, [excelList, isExcelDownClick]); /** * 콜백함수 @@ -201,7 +209,7 @@ export default function PlanSewage() { } searchPlanSewage(searchReturn, 1, 10000); - setExcelTrigger(true); + setIsExcelDownClick(true); }, [searchPlanSewage]); // 페이지 변경 diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index 371a1262d..00ce67077 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -70,6 +70,8 @@ export default function ProcessArea() { const [resetPage, setResetPage] = useState(false); const [searchYearValue, setSearchYearValue] = useState(defaultSearchYear); + + const [isExcelDownClick, setIsExcelDownClick] = useState(false); const ref = useRef({ partValue: '', @@ -126,15 +128,23 @@ export default function ProcessArea() { }, [isInit, initializeCommonData, searchProcessAreaList, defaultSearchYear]); useEffect(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '처리구역정보', - sheetNameArray: ['처리구역정보'], - headerRange: 'A1:W1' - }); - } - }, [excelList]); + if(!isExcelDownClick) return; + + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '처리구역정보', + sheetNameArray: ['처리구역정보'], + headerRange: 'A1:W1' + }); + } else { + alert('조회된 결과가 없습니다.'); + } + setIsExcelDownClick(false); + }, 500); // 약간의 지연을 두고 다운로드 실행 + return () => clearTimeout(timer); + }, [excelList, isExcelDownClick]); const excelDownload = useCallback(() => { // if (ref.current.partValue === 'A') { // 행정별 @@ -155,6 +165,7 @@ export default function ProcessArea() { // } searchProcessArea(ref.current, 1, 10000); + setIsExcelDownClick(true); }, [searchProcessArea]); const handlePageChange = useCallback((pageNo: number) => { diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 9a6535db1..fdc70433e 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -89,6 +89,7 @@ export default function ProcessFacilitySearch() { const [partValue, setPartValue] = useState("A"); // const [widthSize, setWidthSize] = useState("w-[120%]"); const [upHeadList, setUpheadList] = useState(upHeadList1); + const [isExcelDownClick, setIsExcelDownClick] = useState(false); /** * 모달 상태 변수 @@ -150,10 +151,18 @@ export default function ProcessFacilitySearch() { }, [partValue, upHeadList1, upHeadList2, upHeadList3]); useEffect(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); - } - }, [excelList]); + if(!isExcelDownClick) return; + + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); + } else { + alert('조회된 결과가 없습니다.'); + } + setIsExcelDownClick(false); + }, 500); // 약간의 지연을 두고 다운로드 실행 + return () => clearTimeout(timer); + }, [excelList, isExcelDownClick]); const searchFcltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => { let nameCd, partStr @@ -254,6 +263,7 @@ export default function ProcessFacilitySearch() { // } searchFcltyList(ref.current, 1, 10000); + setIsExcelDownClick(true); }, [searchFcltyList]); // 모달 닫기 diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index 63df65e13..03d4017bc 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -120,16 +120,22 @@ export default function Rdi() { useEffect(() => { if(!isExcelDownClick) return ; - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: 'RDII추정', - sheetNameArray: ['RDII추정'], - headerRange: 'A1:R1' - }); - } else { - alert("조회된 결과가 없습니다."); - } + + const timer = setTimeout(() => { + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: 'RDII추정', + sheetNameArray: ['RDII추정'], + headerRange: 'A1:R1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + }, 500); // 약간의 지연을 두고 다운로드 실행 + + return () => clearTimeout(timer); }, [excelList, isExcelDownClick]); /** diff --git a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx index a7ff0df71..4d3578314 100644 --- a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx +++ b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx @@ -162,6 +162,7 @@ export default function PlanSewage() { } else { alert("조회된 결과가 없습니다."); } + setIsExcelDownClick(false); }, 500); // 약간의 지연을 두고 다운로드 실행 return () => clearTimeout(timer); diff --git a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx index 849689312..d03053520 100644 --- a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx +++ b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx @@ -167,6 +167,7 @@ export default function UnderWaterInput() { } else { alert("조회된 결과가 없습니다."); } + setIsExcelDownClick(false); }, 1000); // 약간의 지연을 두고 다운로드 실행 return () => clearTimeout(timer); }, [excelList, isExcelDownClick]); From 1aaa8fcd6b10b7411ae5365d91e0c3e6435b605a Mon Sep 17 00:00:00 2001 From: dongsilove Date: Thu, 29 Jan 2026 21:25:40 +0900 Subject: [PATCH 08/32] =?UTF-8?q?fo:=20pagination=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/store/facilityWaterQlty/index.tsx | 26 ++++++++----- apps/fo/app/store/flowRate/index.tsx | 37 +++++++++++-------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/apps/fo/app/store/facilityWaterQlty/index.tsx b/apps/fo/app/store/facilityWaterQlty/index.tsx index 4a08b429a..1a3508597 100644 --- a/apps/fo/app/store/facilityWaterQlty/index.tsx +++ b/apps/fo/app/store/facilityWaterQlty/index.tsx @@ -466,16 +466,22 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions //--------- const waterQltyList: any[] = []; let recordCount = 0; - for (let idx = 0; idx < pageSize; idx += 100) { - const tmpPageNo = Math.floor(idx / 100) + 1; - const tmpPageSize = Math.min(100, pageSize - idx); - const result = await fetchFacilityWaterQltyList(tmpPageNo, tmpPageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); - if (result?.status === 200 && Array.isArray(result?.data)) { - waterQltyList.push(...result.data); - } - recordCount = result?.count || 0; - if (result?.count < tmpPageNo * tmpPageSize) { - break; // 더 이상 데이터가 없으면 중지 + if (pageSize < 5000) { + const res = await fetchFacilityWaterQltyList(pageNo, pageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); + waterQltyList.push(...res.data); + recordCount = res.count; + } else { + for (let idx = 0; idx < pageSize; idx += 100) { + const tmpPageNo = Math.floor(idx / 100) + 1; + const tmpPageSize = Math.min(100, pageSize - idx); + const result = await fetchFacilityWaterQltyList(tmpPageNo, tmpPageSize, searchYear, part, nameCd, fcltCpct, operationValue, fcltyNm); + if (result?.status === 200 && Array.isArray(result?.data)) { + waterQltyList.push(...result.data); + } + recordCount = result?.count || 0; + if (result?.count < tmpPageNo * tmpPageSize) { + break; // 더 이상 데이터가 없으면 중지 + } } } //------- diff --git a/apps/fo/app/store/flowRate/index.tsx b/apps/fo/app/store/flowRate/index.tsx index 232755c1d..9bca32518 100644 --- a/apps/fo/app/store/flowRate/index.tsx +++ b/apps/fo/app/store/flowRate/index.tsx @@ -285,23 +285,30 @@ export const flowRateSearchReducer: (set: (fn: (state: FlowRateSearchType) => Fl //--------- const flowRateList: any[] = []; const results: any[] = []; - for (let idx = 0; idx < pageSize; idx += 100) { - const tmpPageNo = Math.floor(idx / 100) + 1; - const tmpPageSize = Math.min(100, pageSize - idx); - const result = await fetchFlowRateList( tmpPageNo, tmpPageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm); - results.push(result); - if (result?.count < tmpPageNo * tmpPageSize) { - break; // 더 이상 데이터가 없으면 중지 - } - } - console.log(results); - // 결과를 합치기 - - for (const res of results) { - if (res?.status === 200 && Array.isArray(res?.data)) { - flowRateList.push(...res.data); + if (pageSize < 5000) { + const res = await fetchFlowRateList(pageNo, pageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm); + flowRateList.push(...res.data); + results.push(res); + } else { + for (let idx = 0; idx < pageSize; idx += 100) { + const tmpPageNo = Math.floor(idx / 100) + 1; + const tmpPageSize = Math.min(100, pageSize - idx); + const result = await fetchFlowRateList( tmpPageNo, tmpPageSize, searchYear, partCd, nameCd, fcltCpct, operationValue, fcltyNm); + results.push(result); + if (result?.count < tmpPageNo * tmpPageSize) { + break; // 더 이상 데이터가 없으면 중지 + } + } + // console.log(results); + // 결과를 합치기 + + for (const res of results) { + if (res?.status === 200 && Array.isArray(res?.data)) { + flowRateList.push(...res.data); + } } } + //--------- const recordCount = results[0]?.count || 0; //------- From 39fb1d974abbdb57fffbfd92695b0ff00b98184d Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 30 Jan 2026 10:39:41 +0900 Subject: [PATCH 09/32] =?UTF-8?q?fo:=20excel=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityPrecipitation/page.tsx | 39 +++++++++------ .../(with-layout)/facilityWaterQlty/page.tsx | 48 +++++++++++-------- .../app/pages/(with-layout)/flowRate/page.tsx | 29 +++++++---- .../pages/(with-layout)/planSewage/page.tsx | 40 +++++++++------- .../pages/(with-layout)/processArea/page.tsx | 39 +++++++++------ .../(with-layout)/processFacility/page.tsx | 33 ++++++++----- apps/fo/app/pages/(with-layout)/rdi/page.tsx | 38 +++++++++------ .../pages/(with-layout)/sewageOccur/page.tsx | 41 +++++++++------- .../(with-layout)/underWaterInput/page.tsx | 42 +++++++++------- apps/fo/app/store/processFacility/index.tsx | 4 +- 10 files changed, 217 insertions(+), 136 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index 4df912186..42dbb1be1 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -98,6 +98,7 @@ export default function FacilityPrecipitation() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); /** * 페이지 함수 */ @@ -147,21 +148,28 @@ export default function FacilityPrecipitation() { useEffect(() => { if (!isExcelDownClick) return; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '시설별 강수현황', - sheetNameArray: ['시설별 강수현황'], - headerRange: 'A1:N1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - }, 500); - return () => clearTimeout(timer); - }, [excelList]); + if (isLoading) { + excelWaitRef.current = true; + return; + } + + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '시설별 강수현황', + sheetNameArray: ['시설별 강수현황'], + headerRange: 'A1:N1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); useEffect(() => { if (facilityPrecipitation.length > 0) { @@ -221,6 +229,7 @@ export default function FacilityPrecipitation() { // return; // } // } + excelWaitRef.current = false; searchFacilityPrecipitation(searchReturn, 1, 10000); setIsExcelDownClick(true); }, [searchFacilityPrecipitation]); diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx index 1df3be0cd..316ea78a1 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx @@ -56,6 +56,7 @@ export default function ProcessFacilitySearch() { const [initTrigger, setInitTrigger] = useState(false); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); // 그리드 관련 const [resetPage, setResetPage] = useState(false); const searchInputRef = useRef(null); @@ -155,26 +156,32 @@ export default function ProcessFacilitySearch() { useEffect(() => { if (!isExcelDownClick) return; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcelFormat( - { - data: excelList as unknown as JSON[], - fileName: '시설별 수질현황', - sheetNameArray: ['시설별 수질현황'], - headerRange: 'A1:T1', - decimalArray1: [10, 11, 12, 15, 16, 17], - decimalArray3: [13, 14, 18, 19] - } - ); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - }, 500); + if (isLoading) { + excelWaitRef.current = true; + return; + } - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcelFormat( + { + data: excelList as unknown as JSON[], + fileName: '시설별 수질현황', + sheetNameArray: ['시설별 수질현황'], + headerRange: 'A1:T1', + decimalArray1: [10, 11, 12, 15, 16, 17], + decimalArray3: [13, 14, 18, 19] + } + ); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); const chkVal = useCallback((returntype: ReturnType) => { // setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") @@ -200,8 +207,9 @@ export default function ProcessFacilitySearch() { } } - setIsExcelDownClick(true); + excelWaitRef.current = false; searchFcltyQltyList(ref.current, 1, 10000); + setIsExcelDownClick(true); }, [searchFcltyQltyList]); const handlePageChange = useCallback((page: number) => { diff --git a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx index 66b8c990d..527c8d45e 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx @@ -73,6 +73,7 @@ export default function ProcessFacilitySearch() { const [fcltyNm, setFcltyNm] = useState(""); const [fcltyCd, setFcltyCd] = useState(""); const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); const [initTrigger, setInitTrigger] = useState(false); const searchFlowRate = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { @@ -154,16 +155,23 @@ export default function ProcessFacilitySearch() { useEffect(() => { if(!isExcelDownClick) return; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] }); - } else { - alert('조회된 결과가 없습니다.'); - } - setIsExcelDownClick(false); - }, 1000); // 약간의 지연을 두고 다운로드 실행 - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (isLoading) { + excelWaitRef.current = true; + return; + } + + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] }); + } else { + alert('조회된 결과가 없습니다.'); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); const ref = useRef({ partValue: '', @@ -201,6 +209,7 @@ export default function ProcessFacilitySearch() { } } + excelWaitRef.current = false; searchFlowRate(ref.current, 1, 10000); setIsExcelDownClick(true); }, [searchFlowRate]); diff --git a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx index fca74757c..3ad19c1bb 100644 --- a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx @@ -95,6 +95,7 @@ export default function PlanSewage() { const [initTrigger, setInitTrigger] = useState(false); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); /** * 페이지 함수 @@ -122,22 +123,28 @@ export default function PlanSewage() { useEffect(() => { if (!isExcelDownClick) return; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '계획하수량 산정지표 현황', - sheetNameArray: ['계획하수량 산정지표 현황'], - headerRange: 'A1:M1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - }, 500); - - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (isLoading) { + excelWaitRef.current = true; + return; + } + + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '계획하수량 산정지표 현황', + sheetNameArray: ['계획하수량 산정지표 현황'], + headerRange: 'A1:M1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); /** * 콜백함수 @@ -208,6 +215,7 @@ export default function PlanSewage() { } } + excelWaitRef.current = false; searchPlanSewage(searchReturn, 1, 10000); setIsExcelDownClick(true); }, [searchPlanSewage]); diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index 00ce67077..d4fb0e973 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -72,6 +72,7 @@ export default function ProcessArea() { const [searchYearValue, setSearchYearValue] = useState(defaultSearchYear); const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); const ref = useRef({ partValue: '', @@ -130,21 +131,28 @@ export default function ProcessArea() { useEffect(() => { if(!isExcelDownClick) return; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '처리구역정보', - sheetNameArray: ['처리구역정보'], - headerRange: 'A1:W1' - }); - } else { - alert('조회된 결과가 없습니다.'); - } - setIsExcelDownClick(false); - }, 500); // 약간의 지연을 두고 다운로드 실행 - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (isLoading) { + excelWaitRef.current = true; + return; + } + + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '처리구역정보', + sheetNameArray: ['처리구역정보'], + headerRange: 'A1:W1' + }); + } else { + alert('조회된 결과가 없습니다.'); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); const excelDownload = useCallback(() => { // if (ref.current.partValue === 'A') { // 행정별 @@ -164,6 +172,7 @@ export default function ProcessArea() { // } // } + excelWaitRef.current = false; searchProcessArea(ref.current, 1, 10000); setIsExcelDownClick(true); }, [searchProcessArea]); diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index fdc70433e..995f2e8e9 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -90,6 +90,7 @@ export default function ProcessFacilitySearch() { // const [widthSize, setWidthSize] = useState("w-[120%]"); const [upHeadList, setUpheadList] = useState(upHeadList1); const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); /** * 모달 상태 변수 @@ -151,18 +152,25 @@ export default function ProcessFacilitySearch() { }, [partValue, upHeadList1, upHeadList2, upHeadList3]); useEffect(() => { - if(!isExcelDownClick) return; + if (!isExcelDownClick) return; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); - } else { - alert('조회된 결과가 없습니다.'); - } - setIsExcelDownClick(false); - }, 500); // 약간의 지연을 두고 다운로드 실행 - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (isLoading) { + excelWaitRef.current = true; + return; + } + + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); + } else { + alert('조회된 결과가 없습니다.'); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); const searchFcltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => { let nameCd, partStr @@ -262,6 +270,7 @@ export default function ProcessFacilitySearch() { // } // } + excelWaitRef.current = false; searchFcltyList(ref.current, 1, 10000); setIsExcelDownClick(true); }, [searchFcltyList]); @@ -326,4 +335,4 @@ export default function ProcessFacilitySearch() { )} ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index 03d4017bc..8f601c299 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -91,6 +91,7 @@ export default function Rdi() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀 다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); const [initTrigger, setInitTrigger] = useState(false); @@ -121,22 +122,28 @@ export default function Rdi() { useEffect(() => { if(!isExcelDownClick) return ; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: 'RDII추정', - sheetNameArray: ['RDII추정'], - headerRange: 'A1:R1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - }, 500); // 약간의 지연을 두고 다운로드 실행 + if (isLoading) { + excelWaitRef.current = true; + return; + } - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: 'RDII추정', + sheetNameArray: ['RDII추정'], + headerRange: 'A1:R1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); /** * 콜백함수 @@ -206,6 +213,7 @@ export default function Rdi() { // } // } + excelWaitRef.current = false; searchRdi(ref.current, 1, 5000); setIsExcelDownClick(true); }, [searchRdi]); diff --git a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx index 4d3578314..a9e990e82 100644 --- a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx +++ b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx @@ -101,6 +101,7 @@ export default function PlanSewage() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); /** * 페이지 함수 @@ -151,22 +152,28 @@ export default function PlanSewage() { useEffect(() => { if(!isExcelDownClick) return ; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '오수발생량 추정', - sheetNameArray: ['오수발생량 추정'], - headerRange: 'A1:O1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - }, 500); // 약간의 지연을 두고 다운로드 실행 + if (isLoading) { + excelWaitRef.current = true; + return; + } - return () => clearTimeout(timer); - }, [excelList, isInit, isExcelDownClick]); + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '오수발생량 추정', + sheetNameArray: ['오수발생량 추정'], + headerRange: 'A1:O1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); /** * 콜백함수 @@ -190,7 +197,8 @@ export default function PlanSewage() { break; } if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { - searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue + , fcltyName.trim() !== ""? fcltyName : undefined); } else { searchSewageOccurList(pageNo, pageCnt, searchYear); } @@ -220,6 +228,7 @@ export default function PlanSewage() { return; } } + excelWaitRef.current = false; searchFacilityPrecipitation(ref.current, 1, 5000); setIsExcelDownClick(true); }, [searchFacilityPrecipitation]); diff --git a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx index d03053520..1b90b4584 100644 --- a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx +++ b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx @@ -106,6 +106,7 @@ export default function UnderWaterInput() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); + const excelWaitRef = useRef(false); /** * 페이지 함수 @@ -156,21 +157,28 @@ export default function UnderWaterInput() { useEffect(() => { if(!isExcelDownClick) return ; - const timer = setTimeout(() => { - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '지하수유입량 추정', - sheetNameArray: ['지하수유입량 추정'], - headerRange: 'A1:O1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - }, 1000); // 약간의 지연을 두고 다운로드 실행 - return () => clearTimeout(timer); - }, [excelList, isExcelDownClick]); + if (isLoading) { + excelWaitRef.current = true; + return; + } + + if (!excelWaitRef.current) { + return; + } + + if (excelList && excelList.length > 0) { + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '지하수유입량 추정', + sheetNameArray: ['지하수유입량 추정'], + headerRange: 'A1:O1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + setIsExcelDownClick(false); + excelWaitRef.current = false; + }, [excelList, isExcelDownClick, isLoading]); /** * 콜백함수 @@ -194,7 +202,8 @@ export default function UnderWaterInput() { break; } if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { - searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue + , fcltyName.trim() !== ""? fcltyName : undefined); } else { searchUnderWaterList(pageNo, pageCnt, searchYear); } @@ -224,6 +233,7 @@ export default function UnderWaterInput() { return; } } + excelWaitRef.current = false; searchFacilityPrecipitation(ref.current, 1, 5000); setIsExcelDownClick(true); }, [searchFacilityPrecipitation]); diff --git a/apps/fo/app/store/processFacility/index.tsx b/apps/fo/app/store/processFacility/index.tsx index 8fa38e5bc..240ad2d4d 100644 --- a/apps/fo/app/store/processFacility/index.tsx +++ b/apps/fo/app/store/processFacility/index.tsx @@ -418,7 +418,9 @@ const gridListFcltyWaterQltyInfo: fcltyWaterQltyInfoProps[] = [ */ export const fetchFacilityListInit = async (pageNo: number, pageSize: number, partValue?: string, nameCd?: string, fcltCpct?: string, oprtngSttsCd?: string, facilityPartValue?: string, facilityName?: string): Promise<{ data: facilityListProps[], count: number }> => { try { - const tailStr = partValue ? '/' + (partValue === 'G' ? 'A' : partValue) + '/' + nameCd + '/' + fcltCpct + '/' + oprtngSttsCd + '/' + facilityPartValue + (facilityName && facilityName !== '' ? '/' + facilityName : '') : ''; + const tailStr = partValue ? '/' + (partValue === 'G' ? 'A' : partValue) + '/' + nameCd + '/' + fcltCpct + '/' + oprtngSttsCd + + (facilityPartValue && facilityPartValue !== ''? '/' + facilityPartValue : '') + + (facilityName && facilityName !== ''? '/' + facilityName : '') : ''; const url = '/prcsFclt/info/' + pageNo + '/' + pageSize + tailStr; const res = await getFetch('fo', url); res.data.forEach((item: Record) => { From 164fc1388e73a26a670c183f9ef097b16d79c5f6 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 30 Jan 2026 16:22:51 +0900 Subject: [PATCH 10/32] =?UTF-8?q?fo:=20=EA=B0=95=EC=9A=B0=EC=8B=9C=20?= =?UTF-8?q?=EA=B0=95=EC=9A=B0=EC=9D=BC=EC=9D=98=20=EA=B0=95=EC=9A=B0?= =?UTF-8?q?=EB=9F=89=EC=97=90=20=EC=9D=8C=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/common/CalcRainEffect.tsx | 12 +-- .../(with-layout)/flowRate/modal/page.tsx | 32 +++++-- apps/fo/app/store/facilityWaterQlty/index.tsx | 28 +++--- apps/fo/app/store/underWaterInput/index.tsx | 10 +- .../src/ui/table/index.tsx | 92 ++++++++++--------- 5 files changed, 104 insertions(+), 70 deletions(-) diff --git a/apps/fo/app/components/common/CalcRainEffect.tsx b/apps/fo/app/components/common/CalcRainEffect.tsx index 3ea198016..898bea0ba 100644 --- a/apps/fo/app/components/common/CalcRainEffect.tsx +++ b/apps/fo/app/components/common/CalcRainEffect.tsx @@ -30,18 +30,18 @@ export default function CalcRainEffect(data: any, weather = 'ALL') { rtnDataSunny.push(item); } else if (rainEffectStd >= 0) { // 강우일 + 강우영향일 item.rainEffectDay = 1; - if (idx > 1) { - // 전날 비가 왔고 오늘 강우량 3미만인 경우 전날 강우량이 3이상이면 오늘 toBeInfoRnDay을 전날 강우량으로 설정 - if (data[idx - 1]?.infoRnDay && Number(data[idx]?.infoRnDay) < 3 && Number(data[idx - 1]?.infoRnDay.toString().trim()) >= 3) { - data[idx]!.toBeInfoRnDay = data[idx - 1]!.infoRnDay; - } - } if (idx > 2) { // 전전날 비가 왔고 오늘 강우량 3미만인 경우 전전날 강우량이 3이상이면 오늘 toBeInfoRnDay을 전전날 강우량으로 설정 if (data[idx - 2]?.infoRnDay && Number(data[idx]?.infoRnDay) < 3 && Number(data[idx - 2]?.infoRnDay.toString().trim()) >= 3) { data[idx]!.toBeInfoRnDay = data[idx - 2]!.infoRnDay; } } + if (idx > 1) { + // 전날 비가 왔고 오늘 강우량 3미만인 경우 전날 강우량이 3이상이면 오늘 toBeInfoRnDay을 전날 강우량으로 설정 + if (data[idx - 1]?.infoRnDay && Number(data[idx]?.infoRnDay) < 3 && Number(data[idx - 1]?.infoRnDay.toString().trim()) >= 3) { + data[idx]!.toBeInfoRnDay = data[idx - 1]!.infoRnDay; + } + } rtnDataRain.push(item); } diff --git a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx index 4e0fffeb5..c92fb4325 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx @@ -34,6 +34,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st const { btnClass } = Common; const [copyYearList, setCopyYearList] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [activeTab, setActiveTab] = useState("tab_term_year"); const prevExcelListRef = useRef(null); const { modalFlowRateListYear, upHeadListFlowRateModalYear, } = FlowRateModal; @@ -183,10 +184,6 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st fetchData(); }, [props.facilityCd, props.searchYear, searchFlowRateModalList]); - if (isLoading) { - return
로딩 중...
; - } - const excelDownload = () => { const excelDownList: modalExcelType[] = []; if (copyYearList.length > 0) { @@ -206,14 +203,32 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st if (excelDownList && excelDownList.length > 0) { downloadExcel({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: props.searchYear + '년 ' + props.fcltyName + ' 유량현황', sheetNameArray: [props.fcltyName + ' 유량현황'], headerRange: 'A1:H1' }); } else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) { - alert('다운받을 데이터가 없습니다.'); + alert('조회된 결과가 없습니다.'); } prevExcelListRef.current = excelDownList; } + const rainCellClassName = useCallback( + (row: FlowRateModalInterface, headId: string, rowIndex: number) => { + if (activeTab !== "tab_term_rain") return ""; + if (headId !== "infoRnDay") return ""; + if (rowIndex < 3) return ""; + + const raw = row.infoRnDay; + const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw); + if (!Number.isFinite(value)) return ""; + return value > 3 ? "bg-red-50 font-semibold" : ""; + }, + [activeTab], + ); + + if (isLoading) { + return
로딩 중...
; + } + return ( <> - + @@ -337,7 +352,8 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st />
{ }} className="h-[370px]" boldArray={[0, 1, 2]} /> + cellClick={undefined} onPageChange={() => { }} className="h-[370px]" boldArray={[0, 1, 2]} + cellClassName={rainCellClassName} />
@@ -346,4 +362,4 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st ); -} \ No newline at end of file +} diff --git a/apps/fo/app/store/facilityWaterQlty/index.tsx b/apps/fo/app/store/facilityWaterQlty/index.tsx index 1a3508597..676c0624c 100644 --- a/apps/fo/app/store/facilityWaterQlty/index.tsx +++ b/apps/fo/app/store/facilityWaterQlty/index.tsx @@ -28,9 +28,11 @@ export interface FacilityRdiiDetailProps { export interface FacilityWaterQltyProps { bscFctCd: string, bscFctNm: string, - msrDt: string - sidoNm: string, - sggNm: string, + msrDt: string, + sido: string, + gugun: string, + // sidoNm: string, + // sggNm: string, addr: string, fctCpc: number, inflAssctBfrWqltBod: string | '', @@ -147,8 +149,8 @@ const mainLabelArray: string[] = ['구분', '시도', '시군구', '용량별', // 시설수질리스트 헤더(행정별) const upHeadList1: TableUpperProps[] = [ { id: "C", title: "행정명", colSpan: 2, row: 0, widthPercent: 10 }, - { id: "sidoNm", title: "시도", row: 1, widthPercent: 5 }, - { id: "sggNm", title: "시군구", row: 1, widthPercent: 5 }, + { id: "sido", title: "시도", row: 1, widthPercent: 5 }, + { id: "gugun", title: "시군구", row: 1, widthPercent: 5 }, { id: "bscFctNm", title: "시설명", rowSpan: 2, row: 0, widthPercent: 10 }, { id: "addr", title: "주소", rowSpan: 2, row: 0 }, { id: "fctCpc", title: "시설용량(m³/일)", align: "right", rowSpan: 2, row: 0, widthPercent: 10 }, @@ -197,8 +199,8 @@ const upHeadList3: TableUpperProps[] = [ { id: "mdmbsn", title: "중권역", row: 1, widthPercent: 5 }, { id: "rgnNm", title: "환경청", row: 0, rowSpan: 2, widthPercent: 7 }, { id: "D", title: "행정명", colSpan: 2, row: 0, widthPercent: 10 }, - { id: "sidoNm", title: "시도", upName: "upChangeRe2", row: 1, widthPercent: 5 }, - { id: "sggNm", title: "시군구", upName: "upChangeRe2", row: 1, widthPercent: 5 }, + { id: "sido", title: "시도", upName: "upChangeRe2", row: 1, widthPercent: 5 }, + { id: "gugun", title: "시군구", upName: "upChangeRe2", row: 1, widthPercent: 5 }, { id: "bscFctNm", title: "시설명", row: 0, rowSpan: 2, widthPercent: 8 }, { id: "addr", title: "주소", row: 0, rowSpan: 2 }, { id: "fctCpc", title: "시설용량(m³/L)", align: "right", row: 0, rowSpan: 2, widthPercent: 5 }, @@ -533,8 +535,8 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions waterQltyList.forEach(item => { dataFormSetting(item) item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); - item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; - item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + // item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; + // item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; item.inflAssctBfrWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.inflAssctBfrWqltToc + ")" : item.inflAssctBfrWqltToc; item.efflWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.efflWqltToc + ")" : item.efflWqltToc; excelDownList.push({ @@ -543,8 +545,8 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions 단위유역: item.lgbsn, 중권역: item.mdmbsn, 환경청: item.rgnNm, - 시도: item.sidoNm, - 시군구: item.sggNm, + 시도: item.sido, + 시군구: item.gugun, 시설이름: item.bscFctNm, 주소: item.addr, "시설용량(m³/일)": CommaToNumber(item.fctCpc), @@ -565,8 +567,8 @@ export const facilityWaterQltyReducer = (set: SetFunc): FacilityWaterQltyActions waterQltyList.forEach((item: FacilityWaterQltyProps) => { dataFormSetting(item) item.fctCpc = parseFloat(item.fctCpc.toString().replace(/,/g, '')); - item.sidoNm = item.lclgvrnmntNm.split(' ')[0]?.toString() || ''; - item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; + // item.sidoNm = item.; + // item.sggNm = item.lclgvrnmntNm.split(' ')[1]?.toString() || ''; item.inflAssctBfrWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.inflAssctBfrWqltToc + ")" : item.inflAssctBfrWqltToc; item.efflWqltToc = Number(searchYear) < COD_YEAR ? "(" + item.efflWqltToc + ")" : item.efflWqltToc; }); diff --git a/apps/fo/app/store/underWaterInput/index.tsx b/apps/fo/app/store/underWaterInput/index.tsx index d4d4c39e2..806b12e2b 100644 --- a/apps/fo/app/store/underWaterInput/index.tsx +++ b/apps/fo/app/store/underWaterInput/index.tsx @@ -317,6 +317,14 @@ export type UnderWaterStore = UnderWaterType & UnderWaterActions; export const underWaterReducer: (set: (fn: (state: UnderWaterType) => UnderWaterType) => void) => UnderWaterActions = (set: (fn: (state: UnderWaterType) => UnderWaterType) => void) => { return { searchUnderWaterList: async (pageNo: number, pageSize: number, searchYear: string, part?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { + set((state: UnderWaterType) => ({ + ...state, + UnderWater: { + ...state.UnderWater, + isLoading: true, + }, + })); + const facilityInitData = await fetchFacilityListInit(pageNo, pageSize, part, nameCd, fcltCpct, operationValue, fcltyNm); if (pageSize < 5000) { @@ -724,4 +732,4 @@ export const underWaterReducer: (set: (fn: (state: UnderWaterType) => UnderWater })) } } -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index 1a0fc57c0..94c86d2d8 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -16,14 +16,15 @@ export type ChkClass = { chk: boolean; readonly: boolean; }; -type UiTableProps = { +type UiTableProps = { headName: React.ReactNode | undefined; - tableData: { [key: string]: any }[]; + tableData: T[]; headlist: UiHeadProps[]; pageSize: number; total: number; cellClick: ((index: number | undefined, headId: string) => void) | undefined; onPageChange: (page: number) => void; + cellClassName?: (row: T, headId: string, rowIndex: number) => string; className?: string; resetPageNo?: boolean; children?: React.ReactNode | undefined; @@ -46,7 +47,7 @@ type UiHeadProps = { colSpan?: number; }; -export function UiTable({ +export function UiTable>({ tableData, headlist, pageSize, @@ -55,6 +56,7 @@ export function UiTable({ children, cellClick, onPageChange, + cellClassName, className = "", resetPageNo = false, boldArray = [], @@ -63,7 +65,7 @@ export function UiTable({ curPage = 1, headButton = undefined, widthFixed = false -}: UiTableProps) { +}: UiTableProps) { const [cur, setCur] = useState(curPage); const [idx, setIdx] = useState(index); const [headerHeight, setHeaderHeight] = useState(0); @@ -148,7 +150,7 @@ export function UiTable({ {tableData.length > 0 ? ( - tableData?.map((el: Record, index) => ( + tableData?.map((el: T, index) => ( {headlist .filter((item) => item.visible != false) @@ -163,29 +165,32 @@ export function UiTable({ : cellClick ? "cursor-pointer" : "" - } sticky top-20 z-40 bg-secondary !font-bold whitespace-pre-line`} + } ${cellClassName ? cellClassName(el, head.id, index) : ""} sticky top-20 z-40 bg-secondary !font-bold whitespace-pre-line`} style={{ top: `${headerHeight + index * 32 - 4}px` }} onClick={() => handleCellClick(index, head.id)} key={`cell-${head.id}-${index}`} > - {typeof el[head.id] !== "number" - ? String(el[head.id] ?? "") - : String(el[head.id]).includes(".") - ? String(el[head.id]) - .split(".") - .map((part, index) => - index === 0 - ? part.replace( - /\B(?=(\d{3})+(?!\d))/g, - ",", - ) - : part, - ) - .join(".") - : String(el[head.id] ?? "").replace( - /\B(?=(\d{3})+(?!\d))/g, - ",", - )} + {(() => { + const cellValue = (el as Record)[head.id]; + return typeof cellValue !== "number" + ? String(cellValue ?? "") + : String(cellValue).includes(".") + ? String(cellValue) + .split(".") + .map((part, index) => + index === 0 + ? part.replace( + /\B(?=(\d{3})+(?!\d))/g, + ",", + ) + : part, + ) + .join(".") + : String(cellValue ?? "").replace( + /\B(?=(\d{3})+(?!\d))/g, + ",", + ); + })()} ) : ( handleCellClick(index, head.id)} key={`cell-${head.id}-${index}`} > { //체크박스 유형애서 readOnly가 필요한 경우를 위해서 ChkClass 를 사용해서 readOnly 처리함. - typeof el[head.id] === "object" && - el[head.id] !== null && - "chk" in (el[head.id] as object) && - "readonly" in (el[head.id] as object) ? ( + (() => { + const cellValue = (el as Record)[head.id]; + return typeof cellValue === "object" && + cellValue !== null && + "chk" in (cellValue as object) && + "readonly" in (cellValue as object) ? ( { { - console.log(el[head.id]); + console.log(cellValue); } if ( - (el[head.id] as ChkClass).readonly == true + (cellValue as ChkClass).readonly == true ) return; - (el[head.id] as ChkClass).chk = + (cellValue as ChkClass).chk = Boolean(checked); }} > - ) : typeof el[head.id] === "boolean" ? ( + ) : typeof cellValue === "boolean" ? ( { - el[head.id] = checked; + (el as Record)[head.id] = checked; }} > - ) : typeof el[head.id] !== "number" ? ( - String(el[head.id] ?? "") - ) : String(el[head.id]).includes(".") ? ( - String(el[head.id]) + ) : typeof cellValue !== "number" ? ( + String(cellValue ?? "") + ) : String(cellValue).includes(".") ? ( + String(cellValue) .split(".") .map((part, index) => index === 0 @@ -246,11 +253,12 @@ export function UiTable({ ) .join(".") ) : ( - String(el[head.id] ?? "").replace( + String(cellValue ?? "").replace( /\B(?=(\d{3})+(?!\d))/g, ",", ) - ) + ); + })() } )), From c6cfedb28b36dba4646f2a2b565282a51e285f5d Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 30 Jan 2026 18:14:30 +0900 Subject: [PATCH 11/32] =?UTF-8?q?fo:=20=EC=9C=A0=EB=9F=89=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=ED=8C=9D=EC=97=85=20=EA=B0=95=EC=9A=B0=EC=8B=9C=20?= =?UTF-8?q?=EA=B0=95=EC=9A=B0=EC=9D=BC=20=EC=9D=8C=EC=98=81=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityWaterQlty/modal/page.tsx | 4 +++- .../(with-layout)/flowRate/modal/page.tsx | 23 +++++++++++++++---- apps/fo/app/store/flowRate/index.tsx | 4 ++-- .../src/ui/table/index.tsx | 4 +++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx index 470a44e4c..c1a6afdf8 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx @@ -447,7 +447,9 @@ export default function WaterQualityModal({ fcltyName = '', facilityCd = '', sea

수질 TMS

-

({msrmtDate ? msrmtDate.substring(0, 4) + '-' + msrmtDate.substring(4, 6) + '-' + msrmtDate.substring(6, 8) : yesterDayStr})

+

+ ({msrmtDate ? msrmtDate.substring(0, 4) + '-' + msrmtDate.substring(4, 6) + '-' + msrmtDate.substring(6, 8) : yesterDayStr}) +

{ + // const rainCellClassName = useCallback( + // (row: FlowRateModalInterface, headId: string, rowIndex: number) => { + // if (activeTab !== "tab_term_rain") return ""; + // if (headId !== "infoRnDay") return ""; + // if (rowIndex < 3) return ""; + + // const raw = row.infoRnDay; + // const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw); + // if (!Number.isFinite(value)) return ""; + // return value > 3 ? "bg-red-50 font-semibold" : ""; + // }, + // [activeTab], + // ); + + const rainRowClassName = useCallback( + (row: FlowRateModalInterface, rowIndex: number) => { if (activeTab !== "tab_term_rain") return ""; - if (headId !== "infoRnDay") return ""; if (rowIndex < 3) return ""; const raw = row.infoRnDay; const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw); if (!Number.isFinite(value)) return ""; - return value > 3 ? "bg-red-50 font-semibold" : ""; + return value > 3 ? "bg-slate-50" : ""; }, [activeTab], ); @@ -353,7 +366,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
{ }} className="h-[370px]" boldArray={[0, 1, 2]} - cellClassName={rainCellClassName} /> + rowClassName={rainRowClassName} />
diff --git a/apps/fo/app/store/flowRate/index.tsx b/apps/fo/app/store/flowRate/index.tsx index 9bca32518..b097fc234 100644 --- a/apps/fo/app/store/flowRate/index.tsx +++ b/apps/fo/app/store/flowRate/index.tsx @@ -388,7 +388,7 @@ export const flowRateSearchReducer: (set: (fn: (state: FlowRateSearchType) => Fl const yearSunnyAmnt = sunnyDay > 0 ? sunnyInflAmnt / sunnyDay : 0; // 청천시 연평균유입유량 const yearSunnyBod = sunnyDay > 0 ? sunnyBod / sunnyDay : 0; // 청천시 연평균유입BOD - console.log(inflData); + //console.log(inflData); const rtnData = inflData.map((item: FlowRateModalProps) => { const targetWqItem = wqltData?.find(wqitem => wqitem.msrDt === item.msrDt.replaceAll('-', '')); const dayBod = targetWqItem ? Number(targetWqItem.inflAssctBfrWqltBod) : 0; @@ -404,7 +404,7 @@ export const flowRateSearchReducer: (set: (fn: (state: FlowRateSearchType) => Fl : item.msrDt, }; }); - console.log(rtnData); + //console.log(rtnData); set((state: FlowRateSearchType) => ({ ...state, FlowRateModal: { diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index 94c86d2d8..d2fd81de6 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -25,6 +25,7 @@ type UiTableProps = { cellClick: ((index: number | undefined, headId: string) => void) | undefined; onPageChange: (page: number) => void; cellClassName?: (row: T, headId: string, rowIndex: number) => string; + rowClassName?: (row: T, rowIndex: number) => string; className?: string; resetPageNo?: boolean; children?: React.ReactNode | undefined; @@ -57,6 +58,7 @@ export function UiTable>({ cellClick, onPageChange, cellClassName, + rowClassName, className = "", resetPageNo = false, boldArray = [], @@ -151,7 +153,7 @@ export function UiTable>({ {tableData.length > 0 ? ( tableData?.map((el: T, index) => ( - + {headlist .filter((item) => item.visible != false) .map( From 85615a826e42da53570f4d755041d80ac5755acb Mon Sep 17 00:00:00 2001 From: dongsilove Date: Sat, 31 Jan 2026 16:05:21 +0900 Subject: [PATCH 12/32] =?UTF-8?q?fo:=20exceldown=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityPrecipitation/page.tsx | 67 ++++++++-------- .../(with-layout)/facilityWaterQlty/page.tsx | 76 +++++++++---------- .../app/pages/(with-layout)/flowRate/page.tsx | 58 ++++++-------- .../pages/(with-layout)/planSewage/page.tsx | 68 ++++++++--------- .../pages/(with-layout)/processArea/page.tsx | 65 +++++++--------- .../(with-layout)/processFacility/page.tsx | 55 ++++++-------- apps/fo/app/pages/(with-layout)/rdi/page.tsx | 68 ++++++++--------- .../pages/(with-layout)/sewageOccur/page.tsx | 70 ++++++++--------- .../(with-layout)/underWaterInput/page.tsx | 67 ++++++++-------- .../src/ui/search_base_form/index.tsx | 6 +- 10 files changed, 267 insertions(+), 333 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index 42dbb1be1..7fd141694 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -5,11 +5,11 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import PrecipitationModal from "./modal/page"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; import UnderComment from "@/app/components/common/UnderComment"; @@ -22,6 +22,11 @@ export default function FacilityPrecipitation() { ,initializeCommonData ,searchFacilityPrecipitationList } = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }}; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -50,7 +55,6 @@ export default function FacilityPrecipitation() { upHeadList1, upHeadList2, upHeadList3, - excelList, facilityPrecipitation, facilityPrecipitationCount, isLoading, @@ -98,7 +102,6 @@ export default function FacilityPrecipitation() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); /** * 페이지 함수 */ @@ -145,32 +148,6 @@ export default function FacilityPrecipitation() { } }, [isInit, initializeCommonData, searchFacilityPrecipitationList, defaultSearchYear]); - useEffect(() => { - if (!isExcelDownClick) return; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '시설별 강수현황', - sheetNameArray: ['시설별 강수현황'], - headerRange: 'A1:N1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - useEffect(() => { if (facilityPrecipitation.length > 0) { @@ -181,7 +158,7 @@ export default function FacilityPrecipitation() { * 콜백함수 */ // 조회 - const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + const searchFacilityPrecipitation = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { let nameCd, partStr switch (partValue) { case 'R': @@ -199,9 +176,9 @@ export default function FacilityPrecipitation() { break; } if (partStr.length > 1 || operationValue!== '0' || fcltCpct !== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { - searchFacilityPrecipitationList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + await searchFacilityPrecipitationList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); } else { - searchFacilityPrecipitationList(pageNo, pageCnt, searchYear); + await searchFacilityPrecipitationList(pageNo, pageCnt, searchYear); } }, [operationValue, fcltyName, searchFacilityPrecipitationList, searchYear, fcltCpct]); @@ -212,7 +189,8 @@ export default function FacilityPrecipitation() { }, [upHeadList1, upHeadList2, upHeadList3]); // 엑셀다운로드 - const excelDownload = useCallback((searchReturn: ReturnType) => { + const excelDownload = useCallback(async (searchReturn: ReturnType) => { + if (isExcelDownClick) return; // if (ref.current.partValue === 'A') { // 행정별 // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -229,10 +207,24 @@ export default function FacilityPrecipitation() { // return; // } // } - excelWaitRef.current = false; - searchFacilityPrecipitation(searchReturn, 1, 10000); setIsExcelDownClick(true); - }, [searchFacilityPrecipitation]); + try { + await searchFacilityPrecipitation(searchReturn, 1, 10000); + const latestExcelList = store.getState().FacilityPrecipitation.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ + data: latestExcelList as unknown as JSON[], + fileName: '시설별 강수현황', + sheetNameArray: ['시설별 강수현황'], + headerRange: 'A1:N1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchFacilityPrecipitation, store]); // 페이지 변경 const handlePageChange = useCallback((page: number) => { @@ -267,6 +259,7 @@ export default function FacilityPrecipitation() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false}/> diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx index 316ea78a1..265c9788f 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx @@ -6,13 +6,13 @@ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; import type { FacilityWaterQltyProps } from "@/app/store/facilityWaterQlty"; import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import WaterQualityModal from "./modal/page"; export default function ProcessFacilitySearch() { @@ -21,6 +21,11 @@ export default function ProcessFacilitySearch() { , initializeCommonData , searchFacilityWaterQltyList } = useFoStore((state) => state) || { FacilityWaterQltyList: { facilityWaterQltyList: [], facilityWaterQltyCount: 0 } }; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -48,7 +53,6 @@ export default function ProcessFacilitySearch() { upHeadList3, facilityWaterQltyList, facilityWaterQltyCount, - excelList, isLoading, } = FacilityWaterQltyMain; @@ -56,7 +60,6 @@ export default function ProcessFacilitySearch() { const [initTrigger, setInitTrigger] = useState(false); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); // 그리드 관련 const [resetPage, setResetPage] = useState(false); const searchInputRef = useRef(null); @@ -73,7 +76,7 @@ export default function ProcessFacilitySearch() { const [fcltyCd, setFcltyCd] = useState(""); const [modalYear, setModalYear] = useState(""); - const searchFcltyQltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => { + const searchFcltyQltyList = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => { let nameCd, partStr; switch (partValue) { case 'R': @@ -91,9 +94,9 @@ export default function ProcessFacilitySearch() { break; } if (partStr === 'G') { - searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue); + await searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue); } else { - searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + await searchFacilityWaterQltyList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); } }, [operationValue, fcltyName, searchFacilityWaterQltyList, searchYearValue, fcltCpct]); @@ -153,43 +156,14 @@ export default function ProcessFacilitySearch() { } }, [isInit, initializeCommonData, searchFacilityWaterQltyList, searchFcltyQltyList, initTrigger]); - useEffect(() => { - if (!isExcelDownClick) return; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcelFormat( - { - data: excelList as unknown as JSON[], - fileName: '시설별 수질현황', - sheetNameArray: ['시설별 수질현황'], - headerRange: 'A1:T1', - decimalArray1: [10, 11, 12, 15, 16, 17], - decimalArray3: [13, 14, 18, 19] - } - ); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - const chkVal = useCallback((returntype: ReturnType) => { // setWidthSize(returntype.partValue=='A' ?"w-[120%]":"w-[130%]") setUpheadList(returntype.partValue == 'R' ? upHeadList2 : returntype.partValue == 'G' ? upHeadList3 : upHeadList1); setResetPage(true); }, [upHeadList1, upHeadList2, upHeadList3]); - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; if (ref.current.partValue === 'A') { // 행정별 if (!ref.current.sidoValue || ref.current.sidoValue === '0') { alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -206,11 +180,28 @@ export default function ProcessFacilitySearch() { return; } } - - excelWaitRef.current = false; - searchFcltyQltyList(ref.current, 1, 10000); setIsExcelDownClick(true); - }, [searchFcltyQltyList]); + try { + await searchFcltyQltyList(ref.current, 1, 10000); + const latestExcelList = store.getState().FacilityWaterQltyMain.excelList || []; + if (latestExcelList.length > 0) { + downloadExcelFormat( + { + data: latestExcelList as unknown as JSON[], + fileName: '시설별 수질현황', + sheetNameArray: ['시설별 수질현황'], + headerRange: 'A1:T1', + decimalArray1: [10, 11, 12, 15, 16, 17], + decimalArray3: [13, 14, 18, 19] + } + ); + } else { + alert("조회된 결과가 없습니다."); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchFcltyQltyList, store]); const handlePageChange = useCallback((page: number) => { setResetPage(false); @@ -239,6 +230,7 @@ export default function ProcessFacilitySearch() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false} initSidoValue={"11"} /> diff --git a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx index 527c8d45e..d83386dee 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/page.tsx @@ -5,13 +5,13 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; // import type { ExcelType } from "@/app/store/flowRate"; import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import FlowRateModal from "./modal/page"; export default function ProcessFacilitySearch() { @@ -21,6 +21,11 @@ export default function ProcessFacilitySearch() { ,initializeCommonData ,searchFlowRateList } = useFoStore((state) => state); + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const searchInputRef = useRef(null); @@ -54,7 +59,6 @@ export default function ProcessFacilitySearch() { upHeadList3, flowRateList, flowRateCount, - excelList, isLoading, } = FlowRateList; @@ -73,10 +77,9 @@ export default function ProcessFacilitySearch() { const [fcltyNm, setFcltyNm] = useState(""); const [fcltyCd, setFcltyCd] = useState(""); const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); const [initTrigger, setInitTrigger] = useState(false); - const searchFlowRate = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + const searchFlowRate = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { let nameCd, partStr; switch (partValue) { @@ -95,9 +98,9 @@ export default function ProcessFacilitySearch() { break; } if(partStr==='G'){ - searchFlowRateList(pageNo, pageCnt, searchYearValue); + await searchFlowRateList(pageNo, pageCnt, searchYearValue); }else{ - searchFlowRateList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + await searchFlowRateList(pageNo, pageCnt, searchYearValue, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); } }, [searchFlowRateList, searchYearValue, fcltCpct, operationValue, fcltyName]); @@ -152,27 +155,6 @@ export default function ProcessFacilitySearch() { } }, [isInit, initializeCommonData, searchFlowRateList, selectSearchYear, searchFlowRate, initTrigger]); - useEffect(() => { - if(!isExcelDownClick) return; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] }); - } else { - alert('조회된 결과가 없습니다.'); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - const ref = useRef({ partValue: '', sidoValue: '11', @@ -191,7 +173,8 @@ export default function ProcessFacilitySearch() { setResetPage(true); }, [upHeadList1, upHeadList2, upHeadList3]); - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; if (ref.current.partValue === 'A') { // 행정별 if (!ref.current.sidoValue || ref.current.sidoValue === '0') { alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -208,11 +191,19 @@ export default function ProcessFacilitySearch() { return; } } - - excelWaitRef.current = false; - searchFlowRate(ref.current, 1, 10000); setIsExcelDownClick(true); - }, [searchFlowRate]); + try { + await searchFlowRate(ref.current, 1, 10000); + const latestExcelList = store.getState().FlowRateList.excelList || []; + if (latestExcelList.length > 0) { + downloadExcelFormat({ data: JSON.parse(JSON.stringify(latestExcelList)), fileName: '시설별 유량현황', sheetNameArray: ['시설별 유량현황'], headerRange: 'A1:O1', decimalArray0: [9,10,11,12,13,14] }); + } else { + alert('조회된 결과가 없습니다.'); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchFlowRate, store]); const total = flowRateCount; return ( @@ -237,6 +228,7 @@ export default function ProcessFacilitySearch() { queryClick={chkVal} excelClick={excelDownload} initClick={resetVal} + excelDisabled={isExcelDownClick} ref={ref} isAdmin={false} initSidoValue={'11'}/> diff --git a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx index 3ad19c1bb..8b41524d1 100644 --- a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx @@ -5,11 +5,11 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; import { downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; import UnderComment from "@/app/components/common/UnderComment"; @@ -26,6 +26,11 @@ export default function PlanSewage() { , initializeCommonData , searchPlanSewageList } = useFoStore((state) => state) || { processAreaList: { processAreaList: [] } }; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -56,7 +61,6 @@ export default function PlanSewage() { upHeadList3, planSewageList, planSewageCount, - excelList, isLoading, } = PlanSewage; @@ -95,7 +99,6 @@ export default function PlanSewage() { const [initTrigger, setInitTrigger] = useState(false); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); /** * 페이지 함수 @@ -120,37 +123,11 @@ export default function PlanSewage() { searchPlanSewage(ref.current, 1, 10); } - useEffect(() => { - if (!isExcelDownClick) return; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '계획하수량 산정지표 현황', - sheetNameArray: ['계획하수량 산정지표 현황'], - headerRange: 'A1:M1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - /** * 콜백함수 */ // 조회 - const searchPlanSewage = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => { + const searchPlanSewage = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt: number = 20) => { let nameCd, partStr switch (partValue) { case 'R': @@ -170,10 +147,10 @@ export default function PlanSewage() { if (partStr === 'G') { partValue = partValue === 'G' ? 'A' : partValue; - searchPlanSewageList(pageNo, pageCnt, searchYear, partValue); + await searchPlanSewageList(pageNo, pageCnt, searchYear, partValue); } else { - searchPlanSewageList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + await searchPlanSewageList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); } }, [operationValue, fcltyName, searchPlanSewageList, searchYear, fcltCpct]); @@ -197,7 +174,8 @@ export default function PlanSewage() { }, [upHeadList1, upHeadList2, upHeadList3]); // 엑셀다운로드 - const excelDownload = useCallback((searchReturn: ReturnType) => { + const excelDownload = useCallback(async (searchReturn: ReturnType) => { + if (isExcelDownClick) return; if (ref.current.partValue === 'A') { // 행정별 if (!ref.current.sidoValue || ref.current.sidoValue === '0') { alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -214,11 +192,24 @@ export default function PlanSewage() { return; } } - - excelWaitRef.current = false; - searchPlanSewage(searchReturn, 1, 10000); setIsExcelDownClick(true); - }, [searchPlanSewage]); + try { + await searchPlanSewage(searchReturn, 1, 10000); + const latestExcelList = store.getState().PlanSewage.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ + data: latestExcelList as unknown as JSON[], + fileName: '계획하수량 산정지표 현황', + sheetNameArray: ['계획하수량 산정지표 현황'], + headerRange: 'A1:M1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchPlanSewage, store]); // 페이지 변경 const handlePageChange = useCallback((page: number) => { @@ -249,6 +240,7 @@ export default function PlanSewage() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false} initSidoValue={"11"} /> diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index d4fb0e973..84d8ee8b1 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -5,11 +5,11 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import { useShallow } from 'zustand/react/shallow'; import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import ProcessFacilityInfoModal from "./modal/page"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; @@ -29,6 +29,11 @@ export default function ProcessArea() { searchProcessAreaList: state.searchProcessAreaList, })) ); + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -55,7 +60,6 @@ export default function ProcessArea() { upHeadList1, processAreaList, processAreaCount, - excelList, isLoading, } = ProcessArea; @@ -72,7 +76,6 @@ export default function ProcessArea() { const [searchYearValue, setSearchYearValue] = useState(defaultSearchYear); const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); const ref = useRef({ partValue: '', @@ -86,8 +89,8 @@ export default function ProcessArea() { sigunText: '', }); - const searchProcessArea = useCallback(({ sidoText, sigunText }: ReturnType, pageNo: number, pageCnt: number = 20) => { - searchProcessAreaList(pageNo, pageCnt, sidoText, sigunText, searchYearValue); + const searchProcessArea = useCallback(async ({ sidoText, sigunText }: ReturnType, pageNo: number, pageCnt: number = 20) => { + await searchProcessAreaList(pageNo, pageCnt, sidoText, sigunText, searchYearValue); }, [searchProcessAreaList, searchYearValue]); // 조회 @@ -128,33 +131,8 @@ export default function ProcessArea() { } }, [isInit, initializeCommonData, searchProcessAreaList, defaultSearchYear]); - useEffect(() => { - if(!isExcelDownClick) return; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '처리구역정보', - sheetNameArray: ['처리구역정보'], - headerRange: 'A1:W1' - }); - } else { - alert('조회된 결과가 없습니다.'); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; // if (ref.current.partValue === 'A') { // 행정별 // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -172,10 +150,24 @@ export default function ProcessArea() { // } // } - excelWaitRef.current = false; - searchProcessArea(ref.current, 1, 10000); setIsExcelDownClick(true); - }, [searchProcessArea]); + try { + await searchProcessArea(ref.current, 1, 10000); + const latestExcelList = store.getState().ProcessArea.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ + data: latestExcelList as unknown as JSON[], + fileName: '처리구역정보', + sheetNameArray: ['처리구역정보'], + headerRange: 'A1:W1' + }); + } else { + alert('조회된 결과가 없습니다.'); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchProcessArea, store]); const handlePageChange = useCallback((pageNo: number) => { setResetPage(false); @@ -201,6 +193,7 @@ export default function ProcessArea() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={true} diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 995f2e8e9..e73213bb1 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -6,11 +6,11 @@ "use client" import UnderComment from "@/app/components/common/UnderComment"; -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import ProcessFacilityInfoModal from "./modal/page"; export default function ProcessFacilitySearch() { @@ -25,6 +25,11 @@ export default function ProcessFacilitySearch() { //시설데이터 수질정보 조회 ,searchProcessFacilityWaterQlty } = useFoStore((state) => state) || { ProcessFacilityList: { processFacilityList: [], processFacilityCount: 0 }}; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -69,7 +74,6 @@ export default function ProcessFacilitySearch() { upHeadList1, upHeadList2, upHeadList3, - excelList, isLoading, } = ProcessFacilityList; @@ -90,7 +94,6 @@ export default function ProcessFacilitySearch() { // const [widthSize, setWidthSize] = useState("w-[120%]"); const [upHeadList, setUpheadList] = useState(upHeadList1); const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); /** * 모달 상태 변수 @@ -151,28 +154,7 @@ export default function ProcessFacilitySearch() { } }, [partValue, upHeadList1, upHeadList2, upHeadList3]); - useEffect(() => { - if (!isExcelDownClick) return; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ data: excelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); - } else { - alert('조회된 결과가 없습니다.'); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - - const searchFcltyList = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => { + const searchFcltyList = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=10) => { let nameCd, partStr switch (partValue) { case 'R': @@ -189,7 +171,7 @@ export default function ProcessFacilitySearch() { partStr = envPartValue !== '0' ? envPartValue:partValue; break; } - searchProcessFacility(pageNo, pageCnt, partStr, nameCd || '0', fcltCpct, operationValue, facilityPartValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); + await searchProcessFacility(pageNo, pageCnt, partStr, nameCd || '0', fcltCpct, operationValue, facilityPartValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); if(isSearch) { setIsSearch(false); } @@ -252,7 +234,8 @@ export default function ProcessFacilitySearch() { } } - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; // if (ref.current.partValue === 'A') { // 행정별 // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -270,10 +253,19 @@ export default function ProcessFacilitySearch() { // } // } - excelWaitRef.current = false; - searchFcltyList(ref.current, 1, 10000); setIsExcelDownClick(true); - }, [searchFcltyList]); + try { + await searchFcltyList(ref.current, 1, 10000); + const latestExcelList = store.getState().ProcessFacilityList.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ data: latestExcelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); + } else { + alert('조회된 결과가 없습니다.'); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchFcltyList, store]); // 모달 닫기 const closeModal = () => { @@ -302,6 +294,7 @@ export default function ProcessFacilitySearch() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false} /> diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index 8f601c299..459cd44ca 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -5,11 +5,11 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; import UnderComment from "@/app/components/common/UnderComment"; @@ -26,6 +26,11 @@ export default function Rdi() { ,RdiSearch ,searchRdiList } = useFoStore((state) => state) || { processAreaList: { processAreaList: [] }}; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -56,7 +61,6 @@ export default function Rdi() { upHeadList3, rdiList, rdiCount, - excelList, isLoading, } = Rdi; @@ -91,7 +95,6 @@ export default function Rdi() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀 다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); const [initTrigger, setInitTrigger] = useState(false); @@ -119,37 +122,11 @@ export default function Rdi() { searchRdi(ref.current, 1, 10); } - useEffect(() => { - if(!isExcelDownClick) return ; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: 'RDII추정', - sheetNameArray: ['RDII추정'], - headerRange: 'A1:R1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - /** * 콜백함수 */ // 조회 - const searchRdi = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + const searchRdi = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { let nameCd, partStr switch (partValue) { case 'R': @@ -168,10 +145,9 @@ export default function Rdi() { } if(partStr === 'G'){ - searchRdiList(pageNo, pageCnt, searchYear); + await searchRdiList(pageNo, pageCnt, searchYear); }else{ - searchRdiList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); - + await searchRdiList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue, fcltyName.trim() !== "" || fcltyName.trim() !== null ? fcltyName : undefined); } }, [operationValue, fcltyName, searchRdiList, searchYear, fcltCpct]); @@ -195,7 +171,8 @@ export default function Rdi() { }, [upHeadList1, upHeadList2, upHeadList3]); // 엑셀다운로드 - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; // if (ref.current.partValue === 'A') { // 행정별 // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { // alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -213,10 +190,24 @@ export default function Rdi() { // } // } - excelWaitRef.current = false; - searchRdi(ref.current, 1, 5000); setIsExcelDownClick(true); - }, [searchRdi]); + try { + await searchRdi(ref.current, 1, 5000); + const latestExcelList = store.getState().Rdi.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ + data: latestExcelList as unknown as JSON[], + fileName: 'RDII추정', + sheetNameArray: ['RDII추정'], + headerRange: 'A1:R1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchRdi, store]); // 페이지 변경 const handlePageChange = useCallback((page: number) => { @@ -248,6 +239,7 @@ export default function Rdi() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false} initSidoValue={"11"}/> diff --git a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx index a9e990e82..4dc3125d1 100644 --- a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx +++ b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx @@ -6,13 +6,13 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import { type TableUpperProps } from "@/app/store/common"; import type { SewageOccurProps } from "@/app/store/sewageOccur"; import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import SewageOccurModal from "./modal/page"; export default function PlanSewage() { @@ -29,6 +29,11 @@ export default function PlanSewage() { searchSewageOccurList: () => {}, calculateModalDataForAllFacilities: async () => [] }; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -59,7 +64,6 @@ export default function PlanSewage() { upHeadList3, sewageOccurList, sewageOccurCount, - excelList, isLoading, } = SewageOccur; @@ -99,10 +103,8 @@ export default function PlanSewage() { const [operationValue, setOperationValue] = useState("0"); // 조회기간 const [searchYear, setSearchYear] = useState(defaultSearchYear); - // 엑셀다운로드 트리거 + // 엑셀 조회 중 상태 const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); - /** * 페이지 함수 */ @@ -149,37 +151,11 @@ export default function PlanSewage() { } }, [isInit, initializeCommonData, searchSewageOccurList, defaultSearchYear]); - useEffect(() => { - if(!isExcelDownClick) return ; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '오수발생량 추정', - sheetNameArray: ['오수발생량 추정'], - headerRange: 'A1:O1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - /** * 콜백함수 */ // 조회 - const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + const searchFacilityPrecipitation = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { let nameCd, partStr switch (partValue) { case 'R': @@ -197,10 +173,10 @@ export default function PlanSewage() { break; } if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { - searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue + await searchSewageOccurList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue , fcltyName.trim() !== ""? fcltyName : undefined); } else { - searchSewageOccurList(pageNo, pageCnt, searchYear); + await searchSewageOccurList(pageNo, pageCnt, searchYear); } }, [fcltCpct, operationValue, fcltyName, searchSewageOccurList, searchYear]); @@ -211,7 +187,8 @@ export default function PlanSewage() { }, [upHeadList1, upHeadList2, upHeadList3]); // 엑셀다운로드 - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; if (ref.current.partValue === 'A') { // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -228,10 +205,24 @@ export default function PlanSewage() { return; } } - excelWaitRef.current = false; - searchFacilityPrecipitation(ref.current, 1, 5000); setIsExcelDownClick(true); - }, [searchFacilityPrecipitation]); + try { + await searchFacilityPrecipitation(ref.current, 1, 5000); + const latestExcelList = store.getState().SewageOccur.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ + data: latestExcelList as unknown as JSON[], + fileName: '오수발생량 추정', + sheetNameArray: ['오수발생량 추정'], + headerRange: 'A1:O1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchFacilityPrecipitation]); // 페이지 변경 const handlePageChange = useCallback((page: number) => { @@ -263,6 +254,7 @@ export default function PlanSewage() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false} diff --git a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx index 1b90b4584..4404403e3 100644 --- a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx +++ b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx @@ -7,11 +7,11 @@ */ "use client" -import { useFoStore } from "@/app/store"; +import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import UnderWaterInputModal from "./modal/page"; import type { UnderWaterProps } from "@/app/store/underWaterInput/types"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; @@ -35,6 +35,11 @@ export default function UnderWaterInput() { initializeCommonData: () => {}, searchUnderWaterList: () => {} }; + const store = useContext(StoreContext); + + if (!store) { + throw new Error("StoreContext is not available. Ensure StoreProvider is mounted."); + } const { isInit, @@ -65,7 +70,6 @@ export default function UnderWaterInput() { upHeadList3, underWaterList, underWaterCount, - excelList, isLoading, } = UnderWater; @@ -106,7 +110,6 @@ export default function UnderWaterInput() { const [searchYear, setSearchYear] = useState(defaultSearchYear); // 엑셀다운로드 트리거 const [isExcelDownClick, setIsExcelDownClick] = useState(false); - const excelWaitRef = useRef(false); /** * 페이지 함수 @@ -154,37 +157,11 @@ export default function UnderWaterInput() { } }, [isInit, initializeCommonData, searchUnderWaterList, defaultSearchYear]); - useEffect(() => { - if(!isExcelDownClick) return ; - - if (isLoading) { - excelWaitRef.current = true; - return; - } - - if (!excelWaitRef.current) { - return; - } - - if (excelList && excelList.length > 0) { - downloadExcel({ - data: excelList as unknown as JSON[], - fileName: '지하수유입량 추정', - sheetNameArray: ['지하수유입량 추정'], - headerRange: 'A1:O1' - }); - } else { - alert("조회된 결과가 없습니다."); - } - setIsExcelDownClick(false); - excelWaitRef.current = false; - }, [excelList, isExcelDownClick, isLoading]); - /** * 콜백함수 */ // 조회 - const searchFacilityPrecipitation = useCallback(({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { + const searchFacilityPrecipitation = useCallback(async ({ partValue, regionValue, unitWaterShedValue, middleRegionValue, envPartValue, sidoValue, sigunValue }: ReturnType, pageNo: number, pageCnt:number=20) => { let nameCd, partStr switch (partValue) { case 'R': @@ -202,10 +179,10 @@ export default function UnderWaterInput() { break; } if (partStr.length > 1 || fcltCpct !== '0' || operationValue!== '0' ||nameCd ||(fcltyName.trim() !== "" && fcltyName.trim() !== null)) { - searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue + await searchUnderWaterList(pageNo, pageCnt, searchYear, partStr, nameCd || '0', fcltCpct, operationValue , fcltyName.trim() !== ""? fcltyName : undefined); } else { - searchUnderWaterList(pageNo, pageCnt, searchYear); + await searchUnderWaterList(pageNo, pageCnt, searchYear); } }, [fcltCpct, operationValue, fcltyName, searchUnderWaterList, searchYear]); @@ -216,7 +193,8 @@ export default function UnderWaterInput() { }, [upHeadList1, upHeadList2, upHeadList3]); // 엑셀다운로드 - const excelDownload = useCallback(() => { + const excelDownload = useCallback(async () => { + if (isExcelDownClick) return; if (ref.current.partValue === 'A') { // if (!ref.current.sidoValue || ref.current.sidoValue === '0') { alert('엑셀 다운로드는 시도 조건을 선택한 후 이용 가능합니다.'); @@ -233,10 +211,24 @@ export default function UnderWaterInput() { return; } } - excelWaitRef.current = false; - searchFacilityPrecipitation(ref.current, 1, 5000); setIsExcelDownClick(true); - }, [searchFacilityPrecipitation]); + try { + await searchFacilityPrecipitation(ref.current, 1, 5000); + const latestExcelList = store.getState().UnderWater.excelList || []; + if (latestExcelList.length > 0) { + downloadExcel({ + data: latestExcelList as unknown as JSON[], + fileName: '지하수유입량 추정', + sheetNameArray: ['지하수유입량 추정'], + headerRange: 'A1:O1' + }); + } else { + alert("조회된 결과가 없습니다."); + } + } finally { + setIsExcelDownClick(false); + } + }, [isExcelDownClick, searchFacilityPrecipitation, store]); // 페이지 변경 const handlePageChange = useCallback((page: number) => { @@ -268,6 +260,7 @@ export default function UnderWaterInput() { btnClass={btnClass} queryClick={chkVal} excelClick={excelDownload} + excelDisabled={isExcelDownClick} initClick={resetVal} ref={ref} isAdmin={false} diff --git a/packages/common/business_components/src/ui/search_base_form/index.tsx b/packages/common/business_components/src/ui/search_base_form/index.tsx index 82798db0b..9cef315c3 100644 --- a/packages/common/business_components/src/ui/search_base_form/index.tsx +++ b/packages/common/business_components/src/ui/search_base_form/index.tsx @@ -26,6 +26,7 @@ type SearchBaseType = { leftSelectClassName?: string; buttonOrder?: ("query" | "init" | "excel")[]; initSidoValue?: string | ''; + excelDisabled?: boolean; } export type ReturnType = { partValue: string; @@ -60,6 +61,7 @@ function SearchBaseForm ({ leftSelectClassName, buttonOrder, initSidoValue='', + excelDisabled=false, }: SearchBaseType) { const [partValue, setPartValue] = useState("A"); const [sidoValue, setSidoValue] = useState(initSidoValue?initSidoValue:"0"); @@ -87,7 +89,7 @@ function SearchBaseForm ({ ); case "excel": return ( - ); @@ -191,4 +193,4 @@ function SearchBaseForm ({ } export { SearchBaseForm }; - \ No newline at end of file + From ddb36933589a2dc18b4c7b89515103f8d402cbfd Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 2 Feb 2026 18:14:45 +0900 Subject: [PATCH 13/32] =?UTF-8?q?fo:=20=EB=AA=A8=EB=9E=98=EC=8B=9C?= =?UTF-8?q?=EA=B3=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/(with-layout)/dashboard/facilityInfo/page.tsx | 2 +- apps/fo/app/store/dashboard/index.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx index dd689f2fa..e65fe3cc8 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx @@ -271,7 +271,7 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}: (head.id === 'flowRate' && index === 0)?{el[head.id]}: (head.id === 'flowRate' && index === 5)?{el[head.id]}: (head.id === 'flowRate' && index === 11)?{el[head.id]}: - (head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?{el[head.id]}: + (head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?{Number.isNaN(el[head.id]) ? "" : el[head.id]}: (head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 7)?{el[head.id]}: (head.id === 'rdi'&&index === 0)?{el[head.id]}: (head.id === 'rdi'&&index === 7)?{el[head.id]}: diff --git a/apps/fo/app/store/dashboard/index.tsx b/apps/fo/app/store/dashboard/index.tsx index 8f3b4dd8a..398f82c0e 100644 --- a/apps/fo/app/store/dashboard/index.tsx +++ b/apps/fo/app/store/dashboard/index.tsx @@ -1712,7 +1712,7 @@ export const dashBoardReducer = ( // RDII 추정 = 하절기(6~8월) 중 최저 유입 BOD일때 유입유량 x ((동절기 청천시 유입BOD - 하절기(6~8월) 중 최저 유입BOD) / (동절기 청천시 유입BOD)) // const resp = await fetchMainGraph(searchYear, fcltyCd); const resp = mainGraphData; - if (resp && resp.data && resp.data.wqltObjs) { + if (resp && resp.data && resp.data.wqltObjs && resp.data.wqltObjs.length > 0) { const wqltData = resp.data.wqltObjs; const wqltDataTot = CalcRainEffect(clone(wqltData)); const wqltDataSunny = CalcRainEffect(clone(wqltData), 'SUNNY'); @@ -1800,10 +1800,11 @@ export const dashBoardReducer = ( const rtnData = await calcWaterQltyData(operationalStatusList, searchYear, fcltyCd, mainGraphData); // 수질 데이터 청천시, 강우시 재계산 + let tpFctYn = 'N'; + if (mainGraphData.data.wqltObjs && mainGraphData.data.wqltObjs.length > 0) + tpFctYn = mainGraphData.data.wqltObjs?.[0].tpFctYn; // 총인설비가 없을 경우 제외 - console.log('총인시설 여부 : ', mainGraphData.data.wqltObjs?.[0].tpFctYn); - - const tpFctYn = mainGraphData.data.wqltObjs?.[0].tpFctYn; + console.log('총인시설 여부 : ', tpFctYn); // console.log('rtnData : ', rtnData); if (tpFctYn !== 'Y') { From 8a160dc97ce09dee7fd8f54d1c623f631abda80d Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 3 Feb 2026 10:47:23 +0900 Subject: [PATCH 14/32] =?UTF-8?q?[cs]=20feat:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=ED=8C=90=20=ED=86=B5=EA=B3=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=94=EC=9D=B8=EB=94=A9=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + apps/cs/docs/표준단어정의서.md | 831 ++++++++++++++++++ .../api/board-statistics-service.ts | 45 +- .../model/board-statistics-interface.ts | 44 +- .../board-statistics/model/chart-data.ts | 23 +- .../ui/board-statistics-page.tsx | 21 +- apps/cs/src/shared/config/api.ts | 1 + 7 files changed, 887 insertions(+), 79 deletions(-) create mode 100644 apps/cs/docs/표준단어정의서.md diff --git a/.gitignore b/.gitignore index 878447ead..ab07be55d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ apps/fo/.next/**/* pnpm-lock.yaml settings.json .npmrc +.cursor \ No newline at end of file diff --git a/apps/cs/docs/표준단어정의서.md b/apps/cs/docs/표준단어정의서.md new file mode 100644 index 000000000..6950eb84b --- /dev/null +++ b/apps/cs/docs/표준단어정의서.md @@ -0,0 +1,831 @@ +# 표준단어정의서 + +AI 이해용으로 정리된 표준단어명, 영문약어명, 영문명 목록입니다. + +| 표준단어명 | 표준단어 영문약어명 | 표준단어 영문명 | +|-----------|-------------------|----------------| +| BOD | BOD | Biochemical Oxygen Demand | +| C/N비 | CNR | Carbon-Nitrogen Ratio | +| COD | COD | Chemical Oxygen Demand | +| F/M비 | FMR | Food-Microorganism Ratio | +| GISD | GISDATA | Geographic Information System Data | +| HTML | HTML | Hypertext Markup Language | +| ID | ID | Id | +| IP | IP | Internet Protocol | +| KEY | KEY | Key | +| ND | ND | Not Detected | +| NON | NON | Non | +| NTU | NTU | Nephelometry Turbidity Unit | +| OPT | OPT | Opt | +| PAC | PAC | Polyaluminium Chloride | +| PH | PH | Hydrogen Exponent | +| PKI | PKI | Public Key Infrastructure | +| POOL | POOL | Pool | +| SS | SS | Suspended Solids (Ss) | +| TCE | TCE | Trichloroethane | +| TM | TM | Transverse Mercator | +| T-N | TN | Total Nitrogen | +| TOC | TOC | Total Organic Carbon | +| T-P | TP | Total Phosphorus | +| URL | URL | Uniform Resource Locator | +| 가구 | HSHD | Household | +| 가능 | PSBL | Possible | +| 가동 | OPR | Operation | +| 가산 | ADTN | Addition | +| 가성소다 | NAOH | Caustic Soda(Nach) | +| 가축분뇨 | ANMA | Animal Manure | +| 값 | VAL | Value | +| 강도 | IN | Intensity | +| 강우 | RAIN | Rain | +| 개방 | OPN | Opening | +| 개별 | INDIV | Individual | +| 개보수비 | RPAFEE | Repair Fee | +| 개설 | ESTBL | Establishment | +| 개시 | STRT | Start | +| 개정 | RVSN | Revision | +| 개조 | REMDEL | Remodeling | +| 개체 | INDVD | Individual | +| 개체군 | POPU | Population | +| 개폐 | RORGN | Reorganization | +| 갱신 | UPDT | Update | +| 건물 | BLDG | Building | +| 건설 | CONC | Construction | +| 건수 | NOCS | The Number Of Cases | +| 건조 | DRY | Dry | +| 검사 | INSP | Inspection | +| 검사서 | EXRPT | Examination Report | +| 검정 | RCGN | Recognition | +| 검증 | VRFC | Verification | +| 검출 | DETECT | Detection | +| 검토 | RVW | Review | +| 격자 | LATTC | Lattice | +| 결과 | RSLT | Result | +| 결의 | DCIS | Decision | +| 결정 | DCSN | Decision | +| 경계 | BNDRY | Boundary | +| 경과 | PROC | Process | +| 경도 | LOT | Longitude | +| 경보 | ALARM | Alarm | +| 경사판 | ICNTPT | Inclination plate | +| 계 | SUM | Summation | +| 계량기 | MRNR | Measuring Instrument | +| 계량기분리 | METRSPRT | Meter Separation | +| 계면 | SGLV | Sludge Level | +| 계산식 | FMUL | Formula | +| 계약 | CTRT | Contract | +| 계약기간 | CNTRPD | Contract Period | +| 계절 | SEASON | Season | +| 계획 | PLAN | Plan | +| 고도처리 | AWTR | Advanced Water Treatment | +| 고시 | ANCMNT | Announcement | +| 고액분리 | SLSPRT | Solid-Liquid Separation | +| 고형물체류시간 | SRT | Solid Retention Time | +| 공간데이터 | SPCEDATA | Space Data | +| 공공 | PUBLIC | Public | +| 공급 | SPLY | Supply | +| 공기 | AIR | Air | +| 공기여과 | ARFT | Air Filter | +| 공문 | ODOC | Official Documnet | +| 공법 | MSRC | Method Of Construction | +| 공사 | CSTRN | Construction | +| 공사비 | CONSTEXPN | Construction Expenses | +| 공업지역 | MNFTR_ARA | Manufacturing Area | +| 공용면적 | CMAR | Common Area | +| 공통 | CMMN | Commonness | +| 관 | PP | Pipe | +| 관계 | REL | Relation | +| 관련 | RLTN | Relation | +| 관로 | PPLINE | Pipe Line | +| 관리 | MNG | Management | +| 관리기관 | MGC | Management Agency | +| 관리자 | MNGR | Manager | +| 관찰 | WTCH | Watch | +| 관측 | OBSR | Observation | +| 관측소 | OBSRVT | Observatory | +| 교반기 | AGITOR | Agirator | +| 교정 | CRRC | Correction | +| 구간 | SCTN | Section | +| 구동 | DRIVE | Drive | +| 구분 | SE | Section | +| 구성 | CMPSG | Composing | +| 구역 | ZONE | Zone | +| 구조 | STRCT | Structure | +| 권역 | DSTR | District | +| 권한 | AUTH | Authority | +| 규격 | STNDRD | Standard | +| 규모 | SCAL | Scale | +| 규제 | REGL | Regulation | +| 균등 | EQLTY | Equality | +| 금액 | AMT | Amount | +| 급수 | WSP | Water Supply | +| 기간 | WHL | While | +| 기관 | INST | Institution | +| 기록 | RECD | Recording | +| 기록부 | REGI | Register | +| 기밀 | SCRET | Secret | +| 기법 | TECQU | Techniques | +| 기본 | BSIC | Basics | +| 기본정보 | BASINF | Bassinfo | +| 기본키 | PK | Primary Key | +| 기상 | WETHER | Weather | +| 기안 | DRFTG | Drafting | +| 기재 | WRIT | Write | +| 기점 | CDPNT | Cardinal Point | +| 기존 | LGCY | Legacy | +| 기준 | CRTR | Criteria | +| 기타 | ETC | Etc | +| 기한 | TERM | Term | +| 기후 | CLMT | Climate | +| 긴급 | EMRG | Emergency | +| 날개벽 | WW | Wing Wall | +| 날씨 | WETHR | Weather | +| 날짜 | DATE | DATE | +| 남조류 | BLAL | Blue-Green Algae | +| 납 | PB | Plumbum | +| 내려받기 | DWLD | Download | +| 내부반송 | ITNRCC | Internal Recycl | +| 내용 | CN | Content | +| 내용연수 | SVLF | Service Life | +| 냄새 | SMELL | Smell | +| 년 | YY | Year | +| 년간 | FAYR | For A Year | +| 노즐 | NZL | Nozzle | +| 노출 | EXPSR | Exposure | +| 농도 | DNSTY | Density | +| 농축 | TKN | Thickened | +| 누적 | ACML | Accumulate | +| 누출 | LKG | Leakage | +| 눈 | SNW | Snow | +| 단가 | UNPRC | Unit Price | +| 단기 | SRTPD | Short Period | +| 단순 | SIMPL | Simplicity | +| 단위 | UNIT | Unit | +| 대구역 | LZONE | Large Zone | +| 대분류 | LCLAS | Large Classification | +| 대장 | REGSTR | Register | +| 대장균 | COLI | Coliform, Fecal Coliform | +| 대장균군 | COLIB | Coliform Bacteria | +| 대지면적 | SIAR | Site Area | +| 대체 | RPLC | Replace | +| 덮개 | COVR | Cover | +| 데이타 | DAT | Data | +| 도구 | UNT | Unit | +| 도래 | ADVNT | Advent | +| 도로 | ROAD | Road | +| 도로명 | RN | Road Name | +| 도시 | CTY | City | +| 도엽 | MAPDMC | Map Demarcation | +| 도표 | CHART | Chart | +| 독성 | TOXCTY | Toxicity | +| 동의 | AGRE | Agreement | +| 동절기 | WSESN | The Winter Season | +| 등급 | GRD | Grade | +| 등기 | RG | Registered | +| 등록 | REG | Registration | +| 디바이스 | DVICE | Device | +| 디코딩 | DCD | Decoding | +| 레이어 | LYR | Layer | +| 로그인 | LGN | Login | +| 로우 | ROW | Row | +| 마감 | DDLN | Deadline | +| 마이크로필터 | MCRFT | Micro Filter | +| 막 | MBRN | Membrane | +| 만족도 | STFDG | Satisfaction Degree | +| 맑음 | CLAR | Clear | +| 매각 | SAL | Sale | +| 맨홀 | MH | Manhole | +| 맵 | MAP | Map | +| 메인 | MAIN | Main | +| 메일 | EMAIL | Email | +| 메탄올 | MTN | Methanol(CH3OH) | +| 명 | NM | Name | +| 모니터링 | MTRG | Monitoring | +| 모델 | MODL | Model | +| 모델링 | MODLNG | Modeling | +| 모래 | SAND | Sand | +| 목 | MOK | Mok | +| 무산소 | ANX | Anoxic | +| 무산소조 | ANX_TANK | Anoxic Tank | +| 무인센서 | MNLSENSR | Manless Sensor | +| 문서 | DCMT | Document | +| 물 | WTR | Water | +| 미급수 | NONWTRSPL | Non Water Supply | +| 미생물 | BLGC | Biological | +| 미세먼지 | PM | Particulate Matter | +| 미세목 | MCRB | Micro Bar | +| 민간위탁 | ETSVN | Entrustment To Civilian | +| 민감 | SST | Sensitive | +| 민원 | CVLCPT | Civil Complaint | +| 바이러스 | VIRS | Virus | +| 반기 | HT | Half Term | +| 반류수 | SIDSWT | Side Stream Water | +| 반송 | SNDBK | Send Back | +| 반송수 | RCCWT | Recycled Water | +| 반송슬러지 | RTNSLG | Return Sludge | +| 반영 | RFLT | Reflection | +| 반응조 | RETNK | Reactant Tank | +| 반출 | EXPRT | Export | +| 반토 | ALUM | Alumina | +| 반환 | GVBK | Giving Back | +| 받이 | INL | Inlet | +| 발생 | OCRN | Occurrence | +| 발전 | PWGNRT | Power Generation | +| 발주기관 | ORNT | Order Institution | +| 방류 | DCWTR | Discharge Water Stock Etc | +| 방류구 | DSCHG | Discharge | +| 방류수 | EFLEC | Effluence | +| 방문 | VST | Visit | +| 방문자 | VSTR | Visitor | +| 방법 | MTHD | Method | +| 방사 | RLSE | Release | +| 배관 | PLINE | Pipeline | +| 배분 | DSTRB | Distirbution | +| 배설물 | EXCRTN | Excretion | +| 배수 | DRNG | Drainage | +| 배수구역 | DRANGZN | Drainage Zone | +| 배출 | EXHST | Exhaust | +| 배출가스 | EMGA | Emission Gases | +| 배출구 | VENT | Vent | +| 배출시설 | DHGP | Discharge Equipment | +| 배출시험 | EXHTST | Exhaust Test | +| 배출자 | DCHGR | Discharge Person | +| 밸브 | VALVE | Valve | +| 번호 | NO | Number | +| 법 | LAW | Law | +| 법적 | LGL | Legal | +| 법정 | STTY | Statutory | +| 법정동 | STDG | Statutory Dong(Administrative District) | +| 법정리 | STLI | Statutory Li(Administrative District) | +| 변경 | CHG | Change | +| 변동 | FLCTN | Fluctuation | +| 변속기 | TRMIS | Transmission | +| 보고 | RPT | Report | +| 보고서 | RPOT | Report | +| 보관 | STORG | Storage | +| 보수 | MEND | Mend | +| 보유 | HOLD | Hold | +| 보정 | RVSION | Revision | +| 보조 | ASSISC | Assistance | +| 보조금 | SBSIDY | Subsidy | +| 복개 | COVE | Cover | +| 복토 | CWTH | Covering With Soil | +| 본류 | MI_RV | Main River | +| 본번 | MNO | Main Number | +| 부검 | ATPSY | Autopsy | +| 부과 | LEVY | Levy | +| 부과금 | LVYAMT | Levy Amount | +| 부기 | BUKIP | Bookkeeping | +| 부담금 | ALOTM | Allotment | +| 부번 | SNO | Sub Number | +| 부산물 | BYPRD | By-Product | +| 부서 | DEPT | Department | +| 부숙화 | DECPZ | Decomposed ~Ize | +| 부적합 | IMPPRTY | Impropriety | +| 부착 | ASN | Adhesion | +| 부품 | CMPNT | Components | +| 부하량 | LOADQTY | Load Quantity | +| 분과 | DIVSN | Division | +| 분뇨 | ENSO | Excreta, Night Soil | +| 분류 | CL | Classification | +| 분류군 | TAXON | Taxon | +| 분리막 | SPRTMBR | Separation Membrane | +| 분배 | DTRBT | Distribution | +| 분배조 | DTRBT_TANK | Distribution Tank | +| 분야 | RLM | Realm | +| 분원성 | FECL | Fecal | +| 분원성대장균군 | FCOLI | Fecal Coliform | +| 분원성연쇄상구균 | FCSTRCCI | Fecal Streptococci | +| 분할 | PRTIN | Partition | +| 비밀번호 | PSWD | Password | +| 비상 | EMGNY | Emergency | +| 비정상 | ABNRM | Abnormality | +| 사양 | SPECS | Specification | +| 사업비 | WCT | Working Cost | +| 사업장 | BPLC | Business Place | +| 사여과지 | SAND_ FILTR | Sand Filter | +| 사용 | USE | Use | +| 사용가능 | USABLE | Usable | +| 사용권 | RIOUS | Right Of Using | +| 사용량 | USG | USAGE | +| 사용자 | USER | User | +| 사위 | FDG | Four Directions Girth | +| 사유 | RESN | Reason | +| 사이클 | CYCLE | Cycle | +| 사이트 | SITE | Site | +| 사진 | PHOT | Photograph | +| 사항 | MATTR | Matter | +| 사후 | AFTFAT | After Fact | +| 삭제 | DEL | Deletion | +| 산기관 | DFS | Diffuser | +| 산업 | INDUST | Industrial | +| 산화환원전위 | ORP | Oxidation Reduction Potential | +| 살모넬라 | SLMN | Salmonella | +| 삼원촉매장치 | TWCTLC | Three Way Catalytic Converter | +| 상세 | DTL | Details | +| 상세주소 | DADDR | Detailed Address | +| 상수 | WT | Water | +| 상업지역 | CMCL_ARA | Commercial Area | +| 상태 | STUS | Status | +| 상품 | GDS | Goods | +| 상향류 | UFW | Upflow | +| 상호 | CONM | Company Name | +| 색도 | CRMTY | Chromaticity | +| 생략 | OMS | Omission | +| 생물 | LVB | Living Being | +| 생물반응조 | BIRAT | Bioreactor | +| 생물서식처 | ORGNHBT | Organism Habitat | +| 생물종 | BISP | Biological Species | +| 생성 | CRT | Creation | +| 생슬러지 | RSLG | Raw Sludge | +| 생태 | ECLGY | Ecology | +| 생태독성 | ECTOX | ecotoxicity | +| 서류 | PAPE | Papers | +| 서명 | SIGN | Signing | +| 서버 | SRVR | Server | +| 서비스 | SRVC | Service | +| 설계 | DESGN | Design | +| 설계용량 | DESGN_CPCT | Design Capacity | +| 설립 | FNDN | Foundation | +| 설정 | STNG | Setting | +| 설치 | INSTL | Installation | +| 성격 | CHTR | Character | +| 성공 | SCS | Success | +| 성능 | PRFMNC | Performance | +| 성능검사 | EFTST | Efficiency Test | +| 성적서 | SCRRP | Score Report | +| 세균 | BCLS | Bacillus | +| 세목 | FB | Fine bar | +| 세부 | DETAIL | Detail | +| 세정 | WS | Washing | +| 세정제 | CNML | Cleaning Material | +| 소 | SML | Small | +| 소독 | DSNF | Disinfection | +| 소독제 | DSINF | Disinfectant | +| 소분류 | SCLAS | Small Classification | +| 소수점 | DCPT | Decimal Point | +| 소요 | REQ | Required | +| 소유 | OWN | Own | +| 소유역 | SBASIN | Sub Basin | +| 소재지 | PLAC | Place Of Location | +| 소포 | ATFM | Antifoaming | +| 소포수 | FBW | Foam Breaking Water | +| 소포제 | ANAG | Antifoaming Agent | +| 송부 | SENDNG | Sending | +| 송신 | SNDM | Send Message | +| 송풍기 | BLW | Blower | +| 수 | CNT | Count | +| 수거 | TKAWY | Take Away | +| 수계 | WWBH | Watershed, Water Basin, Hydrosphere | +| 수동 | HOPR | Hand Operation | +| 수리학적 체류시간 | HRT | Hydraulic Retention Time | +| 수문 | Gate | Gate | +| 수산화나트륨 | SOHY | Sodium Hydroxide | +| 수소이온 | HION | Hydrogen Ion | +| 수신 | RCPTN | Reception | +| 수용가 | RCPTR | Receptor | +| 수위 | WAL | Water Level | +| 수정 | MDFCN | Modification | +| 수정자 | MDFR | Modifier | +| 수제 | SPRDIKE | Superdike | +| 수조 | Tank | Tank | +| 수질 | QTWT | Quality Of Water | +| 수질개선 | WTRQLIMP | Water Quality Improvement | +| 수질기준 | WQST | Water Quality Standards | +| 수질배출정보 | WTDI | Water Discharge Information | +| 수집 | CLCT | Collection | +| 순환 | CCL | Cycling | +| 쉬겔라 | SHIGLA | Shigella | +| 스컴 | SCM | Scum | +| 스크린 | SCR | Screen | +| 슬러지 | SLG | Sludge | +| 슬러지 밀도지수 | SDI | sludge density index | +| 슬러지 용량 지표 | SVI | Sludge Volume Index | +| 습지 | WTLN | Wetland | +| 승인 | APRV | Approval | +| 승인권 | APPRVRGT | Right Of Approval | +| 시각 | TIME | Time | +| 시간 | HR | Hour | +| 시군구 | SGG | Si/Gun/Gu | +| 시나리오 | SCEN | Scenario | +| 시도 | CTPV | Cities And Provinces | +| 시료 | SMPL | Sample | +| 시설 | FCLT | Facility | +| 시설면적 | FCAR | Facility Area | +| 시설물 | FCLTS | Facilities | +| 시스템 | SYS | System | +| 시안 | CYN | Cyaan | +| 시운전 | TSTRN | Test Run | +| 시작 | BGNG | Beginning | +| 시점 | PTM | Point Time | +| 시행 | ENFC | Enforcement | +| 시험 | TEST | Test | +| 신고 | DCLR | Declaration | +| 신고자 | DCL | Declarer | +| 신규 | NEW | New | +| 신기술 | NTCH | New Technology | +| 신뢰 | CNFDNC | Confidence | +| 신뢰성 | CRDIBLTY | Credibility | +| 실시 | OPRT | Operation | +| 실적 | ACRS | Actual Result | +| 실행 | EXCN | Execution | +| 심사 | SRNG | Screening | +| 심의 | DLBR | Deliberation | +| 아황산환원혐기성포자형성균 | SFSRA | Spore Forming Sulfite Reducing Anaerobes | +| 안정화 | STBLZT | Stabilization | +| 암롤박스(컨테이너) | ARTCT | Arm roll type container | +| 암모니아성질소 | NHN | Ammonia Nitrogen | +| 압력 | PRESSR | Pressure | +| 압송 | PUMPNG | Pumping | +| 압축 | CMPRS | Compress | +| 액비 | LQFERTLZ | Liquid Fertilizer | +| 액상 | LIQD | Liquid | +| 약 | GENTLE | Gentle | +| 약품 | CMC | Chemical | +| 약품비 | MEDFEE | Medicine Fee | +| 약품사용 | USMD | Use Medicines | +| 업로드 | UPLD | Upload | +| 업무 | DTY | Duty | +| 없음 | NOT | Nothing | +| 에너지 | ENR | Energy | +| 에틸벤젠 | ETBNZN | Ethylbenzene | +| 여과 | FILTR | Filteration | +| 여과수 | FTWT | Filtered Water | +| 여과재 | FTMTRL | Filter Material | +| 여과형 | FILTRFOM | Filter Form | +| 여부 | YN | Yes or No | +| 여시니아균 | YERSNA | Yersinia | +| 역률 | PWRER_FCT | Power Factor | +| 역세척 | BWS | Back Wash | +| 연결 | CONN | Connection | +| 연계 | LINK | Link | +| 연도 | YR | Year | +| 연락 | CNTC | Contact | +| 연령 | AGE | Age | +| 연료화 | FUELZ | Fuelize | +| 연월 | YM | Year and Month | +| 염소 | CLRN | Chlorine | +| 염소소독 | CHDI | Chlorine Disinfection | +| 염소이온 | CLION | Chlorine Ion | +| 염화제이철 | IRCLD | Iron(III) Chloride | +| 예보 | FRCST | Forecast | +| 예비 | PRPT | Preparation | +| 예산 | BGT | Budget | +| 예상 | EXPCT | Expectation | +| 예약 | RSVT | Reservation | +| 예외 | EXCPT | Exception | +| 예정 | PRNG | Prearrangement | +| 예찰 | PRDICT | Prediction | +| 오류 | ERR | Error | +| 오수받이 | HUS_INL | House Inlet | +| 오염물질 | CNTMN | Contaminant | +| 오염원 | POLUSRC | Pollution Source | +| 오존 | OZN | Ozone | +| 오프로드 | OFFRD | Off Road | +| 옥살산 | OXACID | Oxalic Acid | +| 와류 | VORTEX | Vortex | +| 와류형 | VORTXFOM | Vortex From | +| 완결 | COMPLT | Completion | +| 완료 | CMPTN | Completion | +| 완전 | PRFECT | Perfection | +| 요건 | REQSIT | Requisite | +| 요막액 | ALLNTFLD | Allantoic Fluid | +| 요청 | DMND | Demand | +| 용량 | CPCT | Capacity | +| 용역 | SERVC | Service | +| 용존산소 | DO | Dissolved Oxygen | +| 용해 | DSLV | Dissolving | +| 우량 | EXLT | Excellent | +| 우수 | ST | Storm | +| 우수받이 | STRT_INL | Street Inlet | +| 우수토구 | STM_OVFL_MUT | Storm Overflow Mouth | +| 우회 | BP | BY PASS | +| 운반 | CNVY | Conveyance | +| 운영 | OPER | Operation | +| 운영비 | OPEXP | Operation Expense | +| 운행 | OPRAT | Operation | +| 원격 | RMRT | Rmrt | +| 원격감시체계 | TMS | Tele Monitoring System | +| 원료 | MTRAL | Material | +| 원수 | ORGWT | Origin'S Water | +| 원인자 | ORGPR | The Origin Person | +| 월 | MM | Month | +| 위도 | LAT | Latitude | +| 위반 | CNTR | Contravene | +| 위촉 | ENTRST | Entrusting | +| 위치 | LOC | Location | +| 위탁 | CNSG | Consign | +| 위탁자 | CONSGNR | Consignor | +| 유기인 | ORGP | Organic Phosphorus | +| 유량 | FLUX | Flux | +| 유량조정조 | EQLZT__TANK | Equalization Tank | +| 유입 | INFlOW | Inflow | +| 유입수 | INFWTR | Influent Water | +| 유주 | SWARMG | Swarming | +| 유지 | MNTNC | Maintenance | +| 유지용수공급 | MWRS | Maintenance Water Requirement Supply | +| 유체 | FLUID | Fluid | +| 유출 | EFFLUX | Efflux | +| 유충 | LAVL | Larval | +| 유형 | TY | Type | +| 유효 | VLD | Validity | +| 음용 | DKPS | For Drinking Purpose | +| 읍면동 | EMD | Eup/Myun/Dong | +| 응답 | RSPNS | Response | +| 응집제 | COAG | Cohesive Agents | +| 이관 | TRNSF | Transfer | +| 이동 | MVMN | Movement | +| 이력 | HST | History | +| 이메일 | EML | Email | +| 이상 | ABOVE | Above | +| 이송 | TRSF | Transfer | +| 이의 | OBJC | Objection | +| 이차 | SCDR | Secondary | +| 이차침전지 | SCDR_SDMT_TANK | Secondary Sedimaentation | +| 이행 | MIG | Migratation | +| 인건비 | PEXP | Personnel Expenses{Expenditure] | +| 인계 | HNDVR | Handover | +| 인계서 | HDOVDC | Handover Document | +| 인구 | POPL | Population | +| 인산 | PHACID | Phosphoric Acid | +| 인산염인 | POP | phosphate | +| 인양기 | LFT | Lifter | +| 인원수 | NOPE | The Number of Persons | +| 인증 | CERT | Certification | +| 인처리 | PPRCS | Phosphorus Process | +| 인허가 | APVL | Approval | +| 일 | DY | Day | +| 일련번호 | SN | Serial Number | +| 일반 | GNRL | General | +| 일별 | DALY | Daily | +| 일시 | DT | Date and Time | +| 일자 | YMD | YEAR Month Day | +| 일지 | DIARY | Diary | +| 일차 | PRM | Primary | +| 일차침전지 | PST_SDMT_TANK | Primary Sedimentation Tank | +| 일평균 | DYRG | Day Average | +| 임시 | TMPR | Temporary | +| 임의 | RNDM | Random | +| 입고 | WRHS | Warehousing | +| 입력 | INPT | Input | +| 잉여 | SPS | Surplus | +| 잉여슬러지 | EC_SLG | Excess Sludge | +| 자 | MAN | Man | +| 자갈 | GRAVEL | Gravel | +| 자료 | DATA | Data | +| 자리 | DIGIT | Digit | +| 자립화 | SLFRLI | Self Reliance | +| 자연형 | NATRLFOM | Natural Form | +| 자외선 | URLI | Ultraviolet Rays, Light | +| 자재 | MTRIL | Material | +| 자체 | ITSL | Itself | +| 작성 | WRT | Writing | +| 작업 | JOB | Job | +| 잠금 | LOCK | Lock | +| 장기 | LNGTR | Long Term | +| 장소 | PLCE | Place | +| 장착 | MNTNG | Mounting | +| 재 | RE | Re | +| 재이용 | REUSE | Reuse | +| 재이용수 | | Reuse Water | +| 재활용 | RUSE | Reuse | +| 저감 | REDUC | Reduction | +| 저류조 | DTT_TANK | Detention Tank | +| 저온 | LOWTMP | Low Temperature | +| 저장 | STRG | Storage | +| 저장소 | STORGE | Storage | +| 적발 | DSCL | Disclose | +| 적산 | ADD | Addition | +| 적용 | APLCN | Application | +| 적합 | STBLT | Suitability | +| 전기 | ELCTY | Electricity | +| 전기료 | ELFEE | Electricity Fee | +| 전력 | PWRER | Power | +| 전문 | SPCY | Specially | +| 전산 | CMPUTT | Computation | +| 전송 | TRSM | Transmission | +| 전압 | VLTGE | Voltage | +| 전용 | EXUSE | Exclusive Use | +| 전원 | SELCT | Source Of Electricity | +| 전처리 | PRETR | Pretreatment | +| 점검 | CHCK | Check | +| 접속 | CNTN | Connection | +| 정밀 | PRCIS | Precision | +| 정보 | INFO | Information | +| 정산 | CLCLN | Calculation | +| 정상 | NRMLT | Normality | +| 정수장 | WPPW | Water Purification Plant, Waterworks | +| 정제 | REFINE | Refining | +| 정지 | STP | Stop | +| 제거 | RMV | Removal | +| 제수변 | SL_VALVE | Sluice Valve | +| 제작 | MNFCT | Manufacture | +| 제조사 | MKR | Maker | +| 제출 | SBMSN | Submission | +| 제한 | LIMT | Limitation | +| 조류 | ALG | Algae | +| 조목 | CSB | Coarse Bar | +| 조사 | EXMN | Examination | +| 조성 | MAKE | Make | +| 조정 | EQLT | Equalization | +| 조치 | ACTN | Action | +| 조회 | INQ | Inquiry | +| 존재 | EXST | Existence | +| 종결 | TRMN | Termination | +| 종균제 | MICBA | Microbial Agent | +| 종료 | END | End | +| 종류 | KND | Kind | +| 종목 | IEM | Item | +| 종별 | ASORT | Assortment | +| 종사자 | ENFSN | Enfilade Person | +| 종점 | TMNL | Terminal | +| 좌표 | COOR | Coordinate | +| 주간 | WIK | Week | +| 주변 | CFR | Circumference | +| 주소 | ADDR | Address | +| 주입 | IJTN | Injection | +| 주파수 | FQNC | Frequency | +| 준공 | CMCN | Completion Of Construction | +| 중권역 | MDSTRC | Medium District | +| 중복 | DPCN | Duplication | +| 중분류 | MLSFC | Middle Classification | +| 중온 | MESPH | Mesophilic | +| 중첩 | RETRTN | Reiteration | +| 중화설비 | NUTEQP | Neutralization Equipment | +| 중화제 | CNTAG | Counteragent | +| 증가 | INCRS | Increase | +| 지도점검표사본 | CITBMR | Copy of Instruction Checklist | +| 지방자치단체 | LGOV | Local Governments | +| 지번 | LOTNO | Lot Number | +| 지상 | GRND | Ground | +| 지역 | ARA | Area | +| 지질 | NTRFS | Nature Of Soil | +| 지질도 | GEOMAP | Geological Map | +| 지하 | UDGD | Underground | +| 지하수 | GROU | Groundwater | +| 지형 | TPGRPH | Topography | +| 진공 | VCM | Vacuum | +| 진단 | DGNSS | Diagnosis | +| 진동 | VIBRAT | Vibration | +| 진행 | PRGRE | Progress | +| 질산 | NIACID | Nitric Acid. | +| 질산성질소 | NO3N | Nitrate Nitrogen | +| 집계 | TOT | Total | +| 집계구 | SUMDST | Summation District | +| 집행 | IMPL | Implementation | +| 찌꺼기 | LEES | Lees | +| 차아염소산 | NAOCL | Hypochlorous Acid | +| 차집 | ITCT | Intercepting | +| 착공 | BGNCST | Begin Construction | +| 채수 | SAMWT | Sampling Of Water | +| 채수기 | WTRSMPLR | Water Sampler | +| 채수병 | WTRBOTL | Water Bottle | +| 처리 | PRCS | Process | +| 처리비 | PSSFEE | Process Fee | +| 처리수 | TRT_WT | Treated Water | +| 처리수조 | TRT_WT_TANK | Treated Water Tank | +| 처리시설 | TRTMTFCLT | Treatment Facility | +| 처리자 | PRCR | Processor | +| 처분 | DSPS | Disposition | +| 철염 | FESLT | Ferric Salt | +| 첨부 | ATCH | Attachment | +| 첨부파일 | ATFI | Attachment File | +| 초과 | EXCS | Excess | +| 초기화 | INITL | Initialization | +| 촉매 | CTLY | Catalyst | +| 총계 | TSM | Summation | +| 총대장균군 | TCOLFGRP | Total Coliform Group | +| 총량 | TQT | Total Quantity | +| 총면적 | GFA | Gross Floor Area | +| 총사업비 | TPCO | Total Project Cost | +| 총인구 | TPOPLTN | Total Population | +| 총인처리시설 | TPFCT | Total Phosphorus facility | +| 최근 | RCNT | Recent | +| 최대값 | MAXV | Maximum Value | +| 최소값 | MINV | Minimum Value | +| 최적 | BMTB | Being The Most Suitable | +| 최종 | LAST | Last | +| 최초 | FRST | First | +| 추출 | EXTR | Extraction | +| 추출정보 | EXTRINFO | Extraction Information | +| 축 | AXS | Axis | +| 출력 | OTPT | Output | +| 출발 | LVE | Leave | +| 충전기 | CHRGR | Charger | +| 취득 | ACQS | Acquisition | +| 취소 | RTRCN | Retraction | +| 측정 | MSUR | Measurement | +| 측정기 | MSRR | Measurement Instrument | +| 측정소 | MSRSTN | Measurement Station | +| 침사지 | GTCB | Grit Chamber | +| 침전 | SDMT | Sediment | +| 침전지 | SDMT_TANK | sedimentation tank | +| 컨베이어 | CVY | Conveyor | +| 컬럼 | COL | Column | +| 코드 | CD | Code | +| 크기 | SZ | Size | +| 크실렌 | XYLEN | Xylene | +| 탄화 | CARB | Carbonization | +| 탈거 | CRYOFF | Carry Off | +| 탈리액 | SPNT | Supernatant | +| 탈수 | DWTR | Dewatering | +| 탈수기 | DEHYM | Dehydration Machine | +| 탈질 | DNTFCT | Denitrificatio | +| 탈취 | DODRZ | Deodorization | +| 탈취기 | DEODRZ | Deodorizer | +| 탈취제 | DEODR | Deodorant | +| 탈황제 | DSLFA | Desulfurizing Agent | +| 터널형 | YUNLTP | Tunnel Type | +| 테이블 | TBL | Table | +| 토구 | OTFL | Outfall | +| 토지 | LAD | Land | +| 토지면적 | LDAR | Land Area | +| 통계 | STATS | Statistics | +| 통보 | NTFCTN | Notification | +| 통신 | COMM | Communication | +| 통지 | AVTSMT | Avertissement | +| 통합 | UNTY | Unity | +| 특성 | CHRT | Character | +| 특수 | SPECL | Special | +| 특정유해물질 | SHCH | Specified Hazardous Chemicals | +| 파일 | FILE | File | +| 판정 | JGMT | Judgement | +| 펌프 | PUMP | Pump | +| 펌프장 | PUMP_STT | Pump Station | +| 페놀 | PHNL | Phenol | +| 평가 | EVL | Evaluation | +| 폐기 | DSCD | Discard | +| 폐기물 | WSTE | Waste | +| 폐쇄 | CLSG | Closing | +| 폐수 | WAWA | Waste Water | +| 폐수처리 | WSWT | Wastewater Treatment | +| 폐열 | WASTHET | Waste Heat | +| 포기 | ARTN | Aeration | +| 포트 | PORT | Port | +| 폭기조 | ARTN_TANK | Aeration Tank | +| 폴리머 | PLYMR | Polymer | +| 폴리철 | FRRCSU | Ferric Sulfate | +| 표고 | AL | Altitude | +| 표기 | MARK | Mark | +| 표시 | INDCT | Indication | +| 표준 | STD | Standard | +| 표출 | EPRSS | Expression | +| 플래그 | FLAG | Flag | +| 필드 | FILD | Field | +| 필수 | ESNTL | Essential | +| 필증 | CERFIN | Certificate Of Finishing | +| 필터 | FLTER | Filter | +| 하수 | SWGE | Sewage | +| 하수도 | DRNGSYS | Drainage System | +| 하수미처리 | SEWGUNTTM | Un Sewage Treatment | +| 하수처리 | SEWGTRTM | Sewage Treatment | +| 하수처리수 | STWTR | Sewage Treatment Water | +| 하위 | LOWR | Lower | +| 하향류 | DWFW | Downflow | +| 할인 | DSCNT | Discount | +| 함수율 | MISTR_CTNT | Moisture content | +| 합계 | SMM | Summation | +| 해당 | RLVT | Relevant | +| 해석 | INTRPRTT | Interpretation | +| 해양투기 | OCDU | Ocean Dumping | +| 행망 | ADMNW | Administration Network | +| 행정 | ADMN | Administration | +| 행정구역 | ADMI | Administrative District | +| 행정동 | DONG | Dong (Administrative District) | +| 허가 | PRMSN | Permission | +| 허용 | PERM | Permission | +| 현상 | PHNMN | Phenomenon | +| 혐기 | AARB | Anaerobic | +| 혐기조 | AARB_TANK | Anaerobic tank | +| 협잡물 | CCMT | Concomitant | +| 형태 | SHAP | Shape | +| 호기 | ARB | Aerobic | +| 호기조 | ARB_TANK | Aerobic Tank | +| 호소수 | LMSHWT | Lake And Marsh Water | +| 호수 | LKE | Lake | +| 호안 | RVTMNT | Revetment | +| 호이스트 | HIST | Hoist | +| 혼합 | CMPN | Compound | +| 혼합액현탁고형물 | MLSS | Mixed liquor suspended solids | +| 혼화지 | MXG_TNAK | Mixing tank | +| 홈페이지 | HMPG | Homepage | +| 홍수 | FLOOD | Flood | +| 확인 | IDNTY | Identify | +| 확인서 | CNFRMN | Confirmation | +| 확인자 | IDFR | Identifier | +| 확장자 | EXTN | Extension | +| 확정 | CFMTN | Confirmation | +| 환경 | ENVR | Environment | +| 환경기초시설 | ENINF | Environmental Infrastructure | +| 환원 | RSTORE | Restoration | +| 활성탄 | AC | Activated Carbon | +| 활성화 | PRMT | Promote/Facilitate | +| 활용 | PRUS | Practical Use | +| 황산 | ECCBT | Encyclopaedia Britannica (H2So4) | +| 회사 | CO | Company | +| 회수 | RTRVL | Retrieval | +| 회신 | RPLY | Reply | +| 효율 | EFC | Efficiency | +| 효율성 | EFFCTV | Effectiveness | +| 휴대 | MBL | Mobile | +| 흡수정 | EXTTW | Extraction Well | +| 흡착 | ADST | Adsorption | diff --git a/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts b/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts index 03291223c..9380132e8 100644 --- a/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts +++ b/apps/cs/src/pages/board-statistics/api/board-statistics-service.ts @@ -1,53 +1,22 @@ -// import { client } from "@/shared/api"; -// import { ENDPOINT } from "@/shared/config"; +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; import { type BoardStatisticsParams, type BoardStatistics, type BoardStatisticsService, } from "../model/board-statistics-interface"; -// 임시 모의 데이터 -const mockBoardStatisticsData: BoardStatistics = { - totalPosts: 2456, - totalComments: 156, - averageRating: 4.2, - totalViews: 45678, - dayList: [ - { date: "1월", posts: 10, comments: 5, views: 800 }, - { date: "2월", posts: 15, comments: 8, views: 1200 }, - { date: "3월", posts: 12, comments: 10, views: 1000 }, - { date: "4월", posts: 18, comments: 15, views: 1500 }, - { date: "5월", posts: 22, comments: 20, views: 1800 }, - { date: "6월", posts: 20, comments: 18, views: 1600 }, - { date: "7월", posts: 25, comments: 22, views: 2000 }, - ], - boardList: [ - { boardType: "notice", boardName: "공지사항", count: 45 }, - { boardType: "faq", boardName: "FAQ", count: 120 }, - { boardType: "qna", boardName: "Q&A", count: 180 }, - { boardType: "free", boardName: "자유게시판", count: 95 }, - ], - statusList: [ - { status: "completed", statusName: "답변완료", count: 75 }, - { status: "waiting", statusName: "답변대기", count: 25 }, - ], -}; - class BoardStatisticsApiService implements BoardStatisticsService { async getBoardStatistics( params: BoardStatisticsParams, ): Promise { - console.log("params: ", params); try { // 실제 API 연동 시 아래 코드 사용 - // const response = await client.get( - // `${ENDPOINT.CMS_SERVICE.BOARD_STATISTICS}`, - // { params }, - // ); - // return response.data; - - // 임시 모의 데이터 반환 - return Promise.resolve(mockBoardStatisticsData); + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.STATISTICS}`, + { params }, + ); + return response.data; } catch (error) { console.error("게시판 통계 데이터 조회 실패:", error); throw error; diff --git a/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts b/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts index 1f113475f..3d7a97c87 100644 --- a/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts +++ b/apps/cs/src/pages/board-statistics/model/board-statistics-interface.ts @@ -12,32 +12,36 @@ export interface BoardStatisticsParams extends PageableParams { } export interface BoardStatistics { - totalPosts: number; - totalComments: number; - averageRating: number; - totalViews: number; - dayList: DayStatistics[]; - boardList: BoardTypeStatistics[]; - statusList: StatusStatistics[]; + bbsTotalDto: BbsTotalDto; // 게시판 전체 통계 + pstAnsList: PstAnswerStatistics[]; // 게시물 답변 상태별 통계 목록 + bbsDailyList: BbsDailyStatistics[]; // 게시판 일별 통계 목록 + bbsPstCntList: BbsPostCountStatistics[]; // 게시판별 게시물 건수 통계 목록 } -interface DayStatistics { - date: string; - posts: number; - comments: number; - views: number; +interface BbsTotalDto { + pstInqCnt: number; // 게시물 조회 건수 + pstEvlAvg: number; // 게시물 평가 평균 + pstTotCnt: number; // 게시물 총 건수 + pstAnsWaitCnt: number; // 게시물 답변 대기 건수 } -interface BoardTypeStatistics { - boardType: string; - boardName: string; - count: number; +interface PstAnswerStatistics { + answerNm: string; // 답변 명 + answerYn: string; // 답변 여부 (Y/N) + ansCnt: number; // 답변 건수 } -interface StatusStatistics { - status: string; - statusName: string; - count: number; +interface BbsDailyStatistics { + dayYmd: string; // 일자 (YYYY-MM-DD) + pstInqCnt: number; // 게시물 조회 건수 + pstCmntCnt: number; // 게시물 댓글 건수 + pstNewCnt: number; // 게시물 신규 건수 +} + +interface BbsPostCountStatistics { + bbsId: number; // 게시판 ID + bbsNm: string; // 게시판 명 + pstCnt: number; // 게시물 건수 } export interface BoardStatisticsService { diff --git a/apps/cs/src/pages/board-statistics/model/chart-data.ts b/apps/cs/src/pages/board-statistics/model/chart-data.ts index 2a9b6671f..487d70976 100644 --- a/apps/cs/src/pages/board-statistics/model/chart-data.ts +++ b/apps/cs/src/pages/board-statistics/model/chart-data.ts @@ -11,20 +11,20 @@ export const chartData = { } => { return { data: { - labels: data.dayList.map((item) => item.date), + labels: data.bbsDailyList.map((item) => item.dayYmd), datasets: [ { type: "line", label: "신규게시물", - data: data.dayList.map((item) => item.posts), + data: data.bbsDailyList.map((item) => item.pstNewCnt), borderColor: getCssVariableValue("--chart-1"), backgroundColor: "rgba(59, 130, 246, 0.1)", tension: 0.4, }, { type: "line", - label: "답변대기", - data: data.dayList.map((item) => item.comments), + label: "댓글수", + data: data.bbsDailyList.map((item) => item.pstCmntCnt), borderColor: getCssVariableValue("--chart-2"), backgroundColor: "rgba(236, 72, 153, 0.1)", tension: 0.4, @@ -32,11 +32,10 @@ export const chartData = { { type: "line", label: "조회수", - data: data.dayList.map((item) => item.views), + data: data.bbsDailyList.map((item) => item.pstInqCnt), borderColor: getCssVariableValue("--chart-3"), backgroundColor: "rgba(168, 85, 247, 0.1)", tension: 0.4, - hidden: true, }, ], }, @@ -67,13 +66,13 @@ export const chartData = { } => { return { data: { - labels: data.boardList.map((item) => item.boardName), + labels: data.bbsPstCntList.map((item) => item.bbsNm), datasets: [ { type: "bar", label: "게시판별 분포", - data: data.boardList.map((item) => item.count), - backgroundColor: data.boardList.map((_, index) => + data: data.bbsPstCntList.map((item) => item.pstCnt), + backgroundColor: data.bbsPstCntList.map((_, index) => getCssVariableValue(`--chart-${(index % 5) + 1}`), ), }, @@ -106,12 +105,12 @@ export const chartData = { } => { return { data: { - labels: data.statusList.map((item) => item.statusName), + labels: data.pstAnsList.map((item) => item.answerNm), datasets: [ { type: "doughnut", - data: data.statusList.map((item) => item.count), - backgroundColor: data.statusList.map((_, index) => + data: data.pstAnsList.map((item) => item.ansCnt), + backgroundColor: data.pstAnsList.map((_, index) => getCssVariableValue(`--chart-${(index % 5) + 1}`), ), }, diff --git a/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx b/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx index 13dcbe812..716c390ab 100644 --- a/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx +++ b/apps/cs/src/pages/board-statistics/ui/board-statistics-page.tsx @@ -26,7 +26,7 @@ import { SelectValue, } from "@common/components"; import { SiteSelect } from "@/entities/site"; -// import { ENDPOINT } from "@/shared/config"; +import { ENDPOINT } from "@/shared/config"; import { NoData } from "@/shared/ui/no-data"; import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; import { BoardStatisticsApi } from "../api/board-statistics-service"; @@ -66,7 +66,7 @@ function BoardStatisticsPage() { }); const { data: boardStatisticsData } = useQuery({ - queryKey: ["board-statistics", searchParams], + queryKey: [ENDPOINT.BOARD_SERVICE.STATISTICS, searchParams], queryFn: () => BoardStatisticsApi.getBoardStatistics({ ...searchParams, @@ -176,7 +176,8 @@ function BoardStatisticsPage() {

총 게시물

- {boardStatisticsData?.totalPosts.toLocaleString() || 0} + {boardStatisticsData?.bbsTotalDto?.pstTotCnt?.toLocaleString() || + 0}
답변대기
- {boardStatisticsData?.totalComments.toLocaleString() || 0} + {boardStatisticsData?.bbsTotalDto?.pstAnsWaitCnt?.toLocaleString() || + 0}
평균평점
- {boardStatisticsData?.averageRating.toFixed(1) || 0} + {boardStatisticsData?.bbsTotalDto?.pstEvlAvg?.toFixed(1) || 0}
총 조회수
- {boardStatisticsData?.totalViews.toLocaleString() || 0} + {boardStatisticsData?.bbsTotalDto?.pstInqCnt?.toLocaleString() || + 0}
일별 게시물 현황
- {boardStatisticsData?.dayList?.length ? ( + {boardStatisticsData?.bbsDailyList?.length ? ( ) : ( @@ -329,7 +332,7 @@ function BoardStatisticsPage() {

답변상태

- {boardStatisticsData?.statusList?.length ? ( + {boardStatisticsData?.pstAnsList?.length ? ( ) : ( @@ -342,7 +345,7 @@ function BoardStatisticsPage() {

게시판별 분포

- {boardStatisticsData?.boardList?.length ? ( + {boardStatisticsData?.bbsPstCntList?.length ? ( ) : ( diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index ce6f61381..4f7f5ef9f 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -63,6 +63,7 @@ export const ENDPOINT = { POSTS: "/board-service/api/v1/posts", POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", POST_EVALUATION: "/board-service/api/v1/pstEvl", + STATISTICS: "/board-service/api/v1/stat/bbs", }, }; From 912e335b279724a0511c7e625f1a9e7c799f445a Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 3 Feb 2026 17:48:15 +0900 Subject: [PATCH 15/32] =?UTF-8?q?[cs]=20feat:=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8B=A4?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=95=8C=EB=A6=BC=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/dashboard/api/dashboard-service.ts | 16 +++++++- .../dashboard/model/dashboard-interface.ts | 13 +++++++ .../src/pages/dashboard/ui/dashboard-page.tsx | 39 ++++++++++--------- apps/cs/src/shared/config/api.ts | 1 + .../src/ui/header/notice-popover.tsx | 15 +++---- 5 files changed, 58 insertions(+), 26 deletions(-) diff --git a/apps/cs/src/pages/dashboard/api/dashboard-service.ts b/apps/cs/src/pages/dashboard/api/dashboard-service.ts index 94bf807a1..784f1df82 100644 --- a/apps/cs/src/pages/dashboard/api/dashboard-service.ts +++ b/apps/cs/src/pages/dashboard/api/dashboard-service.ts @@ -1,6 +1,11 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; -import type { Dashboard, DashboardService } from "../model/dashboard-interface"; +import type { + Dashboard, + DashboardService, + NoticeUser, +} from "../model/dashboard-interface"; +import type { PageableList } from "@/shared/model"; export class DashboardApiService implements DashboardService { public async getDashboard(): Promise { @@ -9,6 +14,15 @@ export class DashboardApiService implements DashboardService { ); return response.data; } + + public async getNoticeUserList( + recvUnqId: string, + ): Promise> { + const response = await client.get>( + `${ENDPOINT.CMS_SERVICE.NOTICE_USER}/${recvUnqId}/list`, + ); + return response.data; + } } export const DashboardApi = new DashboardApiService(); diff --git a/apps/cs/src/pages/dashboard/model/dashboard-interface.ts b/apps/cs/src/pages/dashboard/model/dashboard-interface.ts index 9e038eef7..77a4649cc 100644 --- a/apps/cs/src/pages/dashboard/model/dashboard-interface.ts +++ b/apps/cs/src/pages/dashboard/model/dashboard-interface.ts @@ -1,3 +1,5 @@ +import type { PageableList } from "@/shared/model"; + export interface SiteStatisticsContent { siteId: string; siteNm: string; @@ -27,6 +29,17 @@ export interface Dashboard { menuAccessList: MenuStatisticsContent[]; } +export interface NoticeUser { + notiId: number; + recvUnqId: string; + senderUnqId: string; + notiType: string; + notiTtl: string; + notReadYn: string; + regDt: string; +} + export interface DashboardService { getDashboard(): Promise; + getNoticeUserList(recvUnqId: string): Promise>; } diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx index 65fd0030a..32ea14966 100644 --- a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -1,5 +1,6 @@ "use client"; +import dayjs from "dayjs"; import { useRouter } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel } from "@tanstack/react-table"; @@ -17,8 +18,8 @@ import { } from "chart.js"; import { Bar, Line } from "react-chartjs-2"; import { PostApi, type RecentPost } from "@/entities/post"; -import { ENDPOINT } from "@/shared/config"; -import { useDataTable } from "@/shared/lib"; +import { AUTH_USER_ID, ENDPOINT } from "@/shared/config"; +import { getCookie, useDataTable } from "@/shared/lib"; import { DashboardApi } from "../api/dashboard-service"; import { chartData } from "../model/chart-data"; import { @@ -39,12 +40,21 @@ ChartJS.register( ); function DashboardPage() { + const userId = getCookie(AUTH_USER_ID) ?? ""; + const router = useRouter(); + const { data } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD], queryFn: () => DashboardApi.getDashboard(), }); + const { data: noticeUserList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER], + queryFn: () => DashboardApi.getNoticeUserList(userId), + enabled: !!userId, + }); + const noticePostQuery = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], queryFn: () => @@ -323,22 +333,15 @@ function DashboardPage() {
-
- 신규 사용자 가입 - {(1).toLocaleString()}분 전 -
-
- 메뉴 수정 요청 - {(5).toLocaleString()}분 전 -
-
- 게시물 신고 - {(10).toLocaleString()}분 전 -
-
- 컨텐츠 승인 요청 - {(15).toLocaleString()}분 전 -
+ {noticeUserList?.content?.map((item) => ( +
+ {item.notiTtl} + {dayjs(item.regDt).fromNow()} +
+ ))}
diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 4f7f5ef9f..db8eda72c 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -56,6 +56,7 @@ export const ENDPOINT = { APILOG: "/cms-service/api/v1/api-logs", SITE_AUTHORITY: "/cms-service/api/v1/site-authrt", CONNECTION_LOG: "/cms-service/api/v1/cntnlog", + NOTICE_USER: "/cms-service/api/v1/noti/user", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", diff --git a/packages/common/components/src/ui/header/notice-popover.tsx b/packages/common/components/src/ui/header/notice-popover.tsx index 6ee35371b..251756a94 100644 --- a/packages/common/components/src/ui/header/notice-popover.tsx +++ b/packages/common/components/src/ui/header/notice-popover.tsx @@ -34,6 +34,7 @@ export function NoticePopover() { queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER], queryFn: () => getNoticeUserList(userId!), enabled: !!userId, + select: (data) => data.content.filter((item) => item.notReadYn === "N"), }); const { data: unreadNoticeCount } = useQuery({ @@ -53,11 +54,11 @@ export function NoticePopover() { }; const handleSelectAll = () => { - if (noticeUserList?.content) { - if (selectedNoticeIds.length === noticeUserList.content.length) { + if (noticeUserList) { + if (selectedNoticeIds.length === noticeUserList.length) { setSelectedNoticeIds([]); } else { - const allIds = noticeUserList.content.map((item) => item.notiId); + const allIds = noticeUserList.map((item) => item.notiId); setSelectedNoticeIds(allIds); } } @@ -79,9 +80,9 @@ export function NoticePopover() { } }; - const isAllSelected = noticeUserList?.content - ? selectedNoticeIds.length === noticeUserList.content.length && - noticeUserList.content.length > 0 + const isAllSelected = noticeUserList + ? selectedNoticeIds.length === noticeUserList.length && + noticeUserList.length > 0 : false; useEffect(() => { @@ -107,7 +108,7 @@ export function NoticePopover() {
    - {noticeUserList?.content?.map((item) => ( + {noticeUserList?.map((item) => (
  • Date: Wed, 4 Feb 2026 09:34:50 +0900 Subject: [PATCH 16/32] =?UTF-8?q?[cs]=20feat:=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=ED=8C=A8=EB=84=90=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/dashboard/api/dashboard-service.ts | 12 +++ .../dashboard/model/dashboard-interface.ts | 25 ++++++ .../src/pages/dashboard/ui/dashboard-page.tsx | 83 ++++++++++++++----- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/apps/cs/src/pages/dashboard/api/dashboard-service.ts b/apps/cs/src/pages/dashboard/api/dashboard-service.ts index 784f1df82..998215864 100644 --- a/apps/cs/src/pages/dashboard/api/dashboard-service.ts +++ b/apps/cs/src/pages/dashboard/api/dashboard-service.ts @@ -4,6 +4,8 @@ import type { Dashboard, DashboardService, NoticeUser, + Schedule, + ScheduleQueryParams, } from "../model/dashboard-interface"; import type { PageableList } from "@/shared/model"; @@ -23,6 +25,16 @@ export class DashboardApiService implements DashboardService { ); return response.data; } + + public async getScheduleList( + params?: ScheduleQueryParams, + ): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.SCHEDULES, + { params }, + ); + return response.data; + } } export const DashboardApi = new DashboardApiService(); diff --git a/apps/cs/src/pages/dashboard/model/dashboard-interface.ts b/apps/cs/src/pages/dashboard/model/dashboard-interface.ts index 77a4649cc..e2a01173d 100644 --- a/apps/cs/src/pages/dashboard/model/dashboard-interface.ts +++ b/apps/cs/src/pages/dashboard/model/dashboard-interface.ts @@ -39,7 +39,32 @@ export interface NoticeUser { regDt: string; } +export interface Schedule { + schdlBgngDt: string; + schdlEndDt: string; + actualDate: string; + schdlSn: number; + siteId: string; + schdlTtl: string; + schdlSeCd: string; + schdlCn: string; + reptYn: string; + commSchdlYn: string; + atflCd: string | null; +} + +export interface ScheduleQueryParams { + siteId?: string; + schdlTtl?: string; + schdlSeCd?: string; + schdlBgngYmd?: string; + schdlEndYmd?: string; + reptYn?: string; + commSchdlYn?: string; +} + export interface DashboardService { getDashboard(): Promise; getNoticeUserList(recvUnqId: string): Promise>; + getScheduleList(params?: ScheduleQueryParams): Promise; } diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx index 32ea14966..d787a6ab7 100644 --- a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -18,7 +18,7 @@ import { } from "chart.js"; import { Bar, Line } from "react-chartjs-2"; import { PostApi, type RecentPost } from "@/entities/post"; -import { AUTH_USER_ID, ENDPOINT } from "@/shared/config"; +import { AUTH_USER_ID, ENDPOINT, SITE_ID } from "@/shared/config"; import { getCookie, useDataTable } from "@/shared/lib"; import { DashboardApi } from "../api/dashboard-service"; import { chartData } from "../model/chart-data"; @@ -53,6 +53,29 @@ function DashboardPage() { queryKey: [ENDPOINT.CMS_SERVICE.NOTICE_USER], queryFn: () => DashboardApi.getNoticeUserList(userId), enabled: !!userId, + select: (data) => { + // 오늘 ~ 한 달 전 사이의 일정 + const today = dayjs().endOf("day"); + const monthAgo = dayjs().subtract(1, "month").startOf("day"); + return data.content?.filter((item) => { + const regDt = dayjs(item.regDt); + return ( + (regDt.isSame(monthAgo) || regDt.isAfter(monthAgo)) && + (regDt.isSame(today) || regDt.isBefore(today)) + ); + }); + }, + }); + + const { data: scheduleList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES], + queryFn: () => + DashboardApi.getScheduleList({ + siteId: SITE_ID, + schdlBgngYmd: dayjs().format("YYYY-MM-DD"), + schdlEndYmd: dayjs().add(1, "month").format("YYYY-MM-DD"), + commSchdlYn: "Y", + }), }); const noticePostQuery = useQuery({ @@ -333,7 +356,7 @@ function DashboardPage() {
- {noticeUserList?.content?.map((item) => ( + {noticeUserList?.map((item) => (
-
- - {(3).toLocaleString()}월 정기 시스템 점검 - - 내일 -
-
- 서버 업데이트 예정 - {(3).toLocaleString()}일 후 -
-
- 백업 스케줄 - {(5).toLocaleString()}일 후 -
-
- 정기 보안 점검 - {(7).toLocaleString()}일 후 -
+ {scheduleList && scheduleList.length > 0 ? ( + scheduleList.map((schedule, index) => { + const today = dayjs(); + const scheduleDate = dayjs(schedule.actualDate); + const daysUntil = scheduleDate.diff(today, "day"); + + let timeText = ""; + if (daysUntil === 0) { + timeText = "오늘"; + } else if (daysUntil === 1) { + timeText = "내일"; + } else if (daysUntil > 1) { + timeText = `${daysUntil}일 후`; + } else { + timeText = scheduleDate.format("YYYY-MM-DD"); + } + + return ( +
+ + {schedule.schdlTtl} + + {timeText} +
+ ); + }) + ) : ( +
+ 예정된 일정이 없습니다. +
+ )}
From 206cd10844df87abcf31f1623a36f1d4f6d21525 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 4 Feb 2026 13:50:45 +0900 Subject: [PATCH 17/32] =?UTF-8?q?sn:=20=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A7=9D=EB=8F=84=20=ED=8C=9D=EC=97=85-=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/common/table.ts | 88 +++++++++++++++++-- .../map/searchModal/searchModal.tsx | 48 +++++----- .../map/searchModal/useBasicTab.ts | 28 ++++-- .../src/ui/map/lib/gis.core.js | 2 +- 4 files changed, 129 insertions(+), 37 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/common/table.ts b/apps/sn/app/(map-layout)/map/components/common/table.ts index 081e13d1f..2aa3d0ac5 100644 --- a/apps/sn/app/(map-layout)/map/components/common/table.ts +++ b/apps/sn/app/(map-layout)/map/components/common/table.ts @@ -27,7 +27,8 @@ export const MsSwagePplCol = [ // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, // { id: 'lyrTypeNm', title: '레이어타입명' }, ] -export const MsSwagePplCol2 = [ +// 하수관로 searchModal 기본정보tab +export const MsSwagePplCol2 = [ { id: 'rowNumber', title: '일련\n번호' }, { id: 'fcltMngNo', title: '시설\n관리번호' }, { id: 'instMngNo', title: '기관\n관리번호' }, @@ -44,13 +45,13 @@ export const MsSwagePplCol2 = [ { id: 'tmptMhlMngNo', title: '시점맨홀\n관리번호' }, { id: 'scendDpth', title: '종점깊이\n(m)' }, { id: 'scendMhlMngNo', title: '종점맨홀\n관리번호' }, - // { id: 'vrtcLen', title: '세로길이\n(m)' }, - // { id: 'wdthLen', title: '가로길이\n(m)' }, - // { id: 'fcltyShpCd', title: '시설물형태코드' }, + { id: 'vrtcLen', title: '세로길이\n(m)' }, + { id: 'wdthLen', title: '가로길이\n(m)' }, + { id: 'fcltyShpCd', title: '시설물형태코드' }, // { id: 'fid', title: 'FID' }, // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, // { id: 'mngInstCd', title: '관리기관코드' }, - // { id: 'mqltCd', title: '재질코드' }, + { id: 'mqltCd', title: '재질코드' }, // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, // { id: 'usgCd', title: '용도코드' }, // { id: 'lyrTypeNm', title: '레이어타입명' }, @@ -69,6 +70,10 @@ export const MsSwagePplExcelCol = { 'tmptMhlMngNo': '시점맨홀 관리번호', 'scendDpth': '종점 깊이(m)', 'scendMhlMngNo': '종점맨홀 관리번호', + 'vrtcLen': '세로길이(m)', + 'wdthLen': '가로길이(m)', + 'fcltyShpCd': '시설물형태코드', + 'mqltCd': '재질코드', }; // 하수관로 용도 코드표 export const MS_SWAGE_PPL_USE_CODES = [ @@ -275,7 +280,80 @@ export const MS_SWAGE_PPL_CODES = [ { val: "MOP977", text: "전주용입상관" }, { val: "MOP978", text: "MD관" }, ]; +// 하수관로 'fcltyShpCd' '시설물형태코드' +export const MS_SWAGE_PPL_SHAPE = [ + { val: "FOR000 ", text: "미분류" }, + { val: "FOR001 ", text: "원형" }, + { val: "FOR002 ", text: "원추형" }, + { val: "FOR003 ", text: "원통형" }, + { val: "FOR004 ", text: "구형" }, + { val: "FOR005 ", text: "타원형" }, + { val: "FOR006 ", text: "난원" }, + { val: "FOR007 ", text: "반원형" }, + { val: "FOR010 ", text: "피라미드형" }, + { val: "FOR011 ", text: "사면체형" }, + { val: "FOR012 ", text: "기둥,축" }, + { val: "FOR013 ", text: "돛대,활대처럼 둥글고 긴 원재" }, + { val: "FOR014 ", text: "아치형" }, + { val: "FOR015 ", text: "다중아치형" }, + { val: "FOR016 ", text: "돔형" }, + { val: "FOR017 ", text: "고형체형" }, + + { val: "FOR020 ", text: "사각형" }, + { val: "FOR021 ", text: "평방형" }, + { val: "FOR022 ", text: "다이아몬드형" }, + { val: "FOR023 ", text: "사다리꼴" }, + { val: "FOR024 ", text: "각형" }, + { val: "FOR025 ", text: "마제형관" }, + { val: "FOR026 ", text: "태형" }, + + { val: "FOR030 ", text: "A형" }, + { val: "FOR031 ", text: "H형" }, + { val: "FOR032 ", text: "I형" }, + { val: "FOR033 ", text: "Y형" }, + { val: "FOR034 ", text: "T형" }, + + { val: "FOR050 ", text: "불규칙한 형태" }, + + { val: "FOR900 ", text: "계란형" }, + { val: "FOR901 ", text: "구(사각)형" }, + { val: "FOR902 ", text: "높이조절철개형" }, + { val: "FOR903 ", text: "박스형(RCB)" }, + { val: "FOR904 ", text: "반월개거" }, + { val: "FOR905 ", text: "반월개거(측구)" }, + { val: "FOR906 ", text: "벽돌조" }, + { val: "FOR907 ", text: "사각철판" }, + { val: "FOR908 ", text: "석축개거" }, + { val: "FOR909 ", text: "석축개거(측구)" }, + { val: "FOR910 ", text: "승수로" }, + { val: "FOR911 ", text: "옹벽개거" }, + { val: "FOR912 ", text: "옹벽개거(측구)" }, + { val: "FOR913 ", text: "이단" }, + { val: "FOR914 ", text: "이중침전형" }, + { val: "FOR915 ", text: "자연개거" }, + { val: "FOR916 ", text: "자연개거(측구)" }, + { val: "FOR917 ", text: "조립식" }, + { val: "FOR918 ", text: "지상관" }, + { val: "FOR919 ", text: "지상단구" }, + { val: "FOR920 ", text: "지상쌍구" }, + { val: "FOR921 ", text: "지하단구" }, + { val: "FOR922 ", text: "지하쌍구" }, + { val: "FOR923 ", text: "직사각형" }, + { val: "FOR924 ", text: "타설식" }, + { val: "FOR925 ", text: "토류형" }, + { val: "FOR926 ", text: "흄관원형" }, + { val: "FOR927 ", text: "L형측구" }, + { val: "FOR928 ", text: "LO형측구" }, + { val: "FOR929 ", text: "LU형측구" }, + { val: "FOR930 ", text: "PVC이중벽관" }, + { val: "FOR931 ", text: "RCB 맨홀" }, + { val: "FOR932 ", text: "U형" }, + { val: "FOR933 ", text: "U형측구(미복개)" }, + { val: "FOR934 ", text: "U형측구(복개)" }, + + { val: "FOR999 ", text: "기타" } +]; // 하수처리시설 export const SwtrmtFcltCol = [ { id: 'swtrmt_fclt_nm', title: '하수처리시설명' }, diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 0906ce0ca..d179652f3 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -125,32 +125,30 @@ const SearchModal: React.FC = () => { {/* 📋 결과 리스트 */} {/* 기본정보 탭 */} {activeTab === 'basic' && ( -
- {/*
*/} +
- { - const label = BASIC_TYPES.find((type) => type.val === basicType)?.text - return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건); - })() - } - tableData={tableData} - headlist={dynamicHeadlist} - pageSize={10} - total={tableCount} - cellClick={handleBasicCellClick} - onPageChange={setCurPage} - resetPageNo={isResetPage} - resetSelection={isResetSelection} - curPage={curPage} - headButton={ -
- - -
- } - /> - {/*
*/} + { + const label = BASIC_TYPES.find((type) => type.val === basicType)?.text + return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건); + })() + } + tableData={tableData} + headlist={dynamicHeadlist} + pageSize={10} + total={tableCount} + cellClick={handleBasicCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + curPage={curPage} + headButton={ +
+ + +
+ } + />
)} {/* 유지관리 정보 탭 */} diff --git a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts index 80d3deb50..9184286ed 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts +++ b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts @@ -14,7 +14,7 @@ */ import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui'; import { - MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, + MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol } from '../components/common/table'; import { getData } from "@/components/lib/api"; @@ -46,6 +46,7 @@ export default function useBasicTab() { } = useSearchModalStore(); const { ctpv, sgg } = useStore(); + const fetchBasicFilteredData = async () => { let cqlFilters: string[] = []; const queryParams = new URLSearchParams({ @@ -79,8 +80,15 @@ export default function useBasicTab() { if (!sggCd && code) sggCd = code.slice(0, 5); } - const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()); - const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); + const matchedMqltCode = basicType === 'tb_ms_swage_ppl' + ? MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 하수관로 재질 + : basicType === 'tb_ms_swage_mhl' + ? MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 맨홀 재질 + : undefined; + const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); // 맨홀 용도 + const matchedPplShpCode = MS_SWAGE_PPL_SHAPE.find(code => code.val === item?.fcltyShpCd); // 하수관로 시설물형태코드 + + const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm; const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm; const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); @@ -90,15 +98,17 @@ export default function useBasicTab() { ...formattedItem, rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가 scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경 - mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd, + mqltCd: matchedMqltCode ? matchedMqltCode.text : item?.mqltCd, usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd), ctpvNm: ctpvNm ?? '-', sggNm: sggNm ?? '-', swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-', + fcltyShpCd: matchedPplShpCode? matchedPplShpCode.text : item?.fcltyShpCd, }; }); setTableData(dataWithRowNumber); + // console.log(tableData); setTableCount(res.count || []); }; @@ -150,7 +160,7 @@ export default function useBasicTab() { // 하수관로 cqlFilters = swagePplCqlFilters() headerMapping = MsSwagePplExcelCol; - headerRange = "A1:M1"; + headerRange = "A1:Q1"; break; } case 'tb_il_swage_prcs_fclt': { @@ -196,6 +206,8 @@ export default function useBasicTab() { const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text])); const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text])); + const pplMqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val?.trim(), code.text])); + const pplShpCdMap = new Map(MS_SWAGE_PPL_SHAPE.map(code => [code.val?.trim(), code.text])); // 관로 시설물형태코드 const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text])); const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text])); @@ -232,6 +244,10 @@ export default function useBasicTab() { tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-', scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-', scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-', + vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-', + wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-', + fcltyShpCd: pplShpCdMap.get(item?.fcltyShpCd?.trim()) || item.fcltyShpCd || formattedItem.fcltyShpCd || '-', + mqltCd: pplMqltCdMap.get(item?.mqltCd?.trim()) || item.mqltCd || formattedItem.mqltCd || '-', }; } case 'tb_il_swage_prcs_fclt': { @@ -329,4 +345,4 @@ export default function useBasicTab() { return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload }; -}; \ No newline at end of file +}; diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index e8e05f037..2e97e8af5 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -172,7 +172,7 @@ GisApp.core.prototype = { wmtsSource.setTileUrlFunction((tileCoord) => { const [z, x, y] = tileCoord; const matrix = matrixIds[z]; - console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y)); + // console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y)); return GisApp.NGII_WMTS_URL .replace("{z}", matrix) .replace("{x}", x) From 8565331e68dccf652cc5f9f0953141966f3af6fa Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 6 Feb 2026 11:26:40 +0900 Subject: [PATCH 18/32] =?UTF-8?q?[cs]=20feat:=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=8B=AB=ED=9E=90=20=EB=95=8C=20?= =?UTF-8?q?=ED=8F=BC=20=EB=B0=8F=20=EC=BF=BC=EB=A6=AC=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/ui/add-admin-dialog.tsx | 19 +++++++++- apps/cs/src/pages/code/ui/lwpocode-dialog.tsx | 6 ++- apps/cs/src/pages/code/ui/upcode-dialog.tsx | 6 ++- .../pages/groups/ui/menu-authority-dialog.tsx | 38 ++++++++++--------- .../module/ui/management-authority-dialog.tsx | 6 ++- .../src/pages/role/ui/mapping-role-dialog.tsx | 6 ++- 6 files changed, 58 insertions(+), 23 deletions(-) diff --git a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx index 859af4058..c49d75530 100644 --- a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx +++ b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { keepPreviousData, useQuery, @@ -75,7 +75,7 @@ function AddAdminDialog() { }); const query = useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination, "normal-user"], + queryKey: [ENDPOINT.USER_SERVICE.USERS, "findPageUser", pagination], queryFn: () => UserApi.findPageUser({ ...userSearchQuery, @@ -181,6 +181,21 @@ function AddAdminDialog() { query.refetch(); }; + useEffect(() => { + if (!open) { + setRowSelection({}); + setSelectedGroupId(""); + setUserSearchQuery({ + keywordType: "userName", + keyword: "", + }); + setPagination({ pageIndex: 0, pageSize: 10 }); + queryClient.removeQueries({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, "findPageUser"], + }); + } + }, [open]); + return ( diff --git a/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx index d21f5b60a..3179303dd 100644 --- a/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx +++ b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -140,6 +140,10 @@ const LwpoCodeManagementDialog = memo( } }; + useEffect(() => { + if (!open) form.reset(); + }, [open]); + return ( {children} diff --git a/apps/cs/src/pages/code/ui/upcode-dialog.tsx b/apps/cs/src/pages/code/ui/upcode-dialog.tsx index fafb7429f..6774bd17e 100644 --- a/apps/cs/src/pages/code/ui/upcode-dialog.tsx +++ b/apps/cs/src/pages/code/ui/upcode-dialog.tsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -96,6 +96,10 @@ const UpCodeManagementDialog = memo( if (await confirm(confirmMessage)) callback(data); }; + useEffect(() => { + if (!open) form.reset(); + }, [open]); + return ( {children} diff --git a/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx b/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx index 38c6597fc..cde3cf641 100644 --- a/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -48,25 +48,14 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) { keywordType: "", siteId: "", }); - - const handleReset = () => { - if (inputRef.current) inputRef.current.value = ""; - setKeywordType(""); - setSiteId(""); - setSearchParams({ - keyword: "", - keywordType: "", - siteId: "", - }); - }; - const query = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, groupId, searchParams], - queryFn: () => MenuAuthorityApi.getGroupMenuAuthorityList({ - groupId, - ...searchParams - }), + queryFn: () => + MenuAuthorityApi.getGroupMenuAuthorityList({ + groupId, + ...searchParams, + }), enabled: open && !!groupId, }); @@ -117,6 +106,21 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) { }); }; + const handleReset = () => { + if (inputRef.current) inputRef.current.value = ""; + setSiteId(""); + setKeywordType(""); + setSearchParams({ + keyword: "", + keywordType: "", + siteId: "", + }); + }; + + useEffect(() => { + if (!open) handleReset(); + }, [open]); + return ( diff --git a/apps/cs/src/pages/module/ui/management-authority-dialog.tsx b/apps/cs/src/pages/module/ui/management-authority-dialog.tsx index 0459ba804..b3e6f3fe5 100644 --- a/apps/cs/src/pages/module/ui/management-authority-dialog.tsx +++ b/apps/cs/src/pages/module/ui/management-authority-dialog.tsx @@ -1,4 +1,4 @@ -import { useState, memo } from "react"; +import { useState, memo, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -90,6 +90,10 @@ const ManagementAuthorityDialog = memo( } }; + useEffect(() => { + if (!open) form.reset(); + }, [open]); + return ( diff --git a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx index 5377f6a7b..e327fb88e 100644 --- a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx +++ b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table"; import { @@ -113,6 +113,10 @@ function MappingRoleDialog({ }, }); + useEffect(() => { + if (!open) handleReset(); + }, [open]); + return ( From 21dd8f47a1ee5b8a075fae47cb5f082186089483 Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 9 Feb 2026 18:18:11 +0900 Subject: [PATCH 19/32] =?UTF-8?q?wr:=20=EC=A7=80=EB=8F=84=EC=A0=90?= =?UTF-8?q?=EA=B2=80=EA=B2=B0=EA=B3=BC=20=EB=93=B1=EB=A1=9D=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=9A=B4=EC=98=81=EA=B4=80=EB=A6=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruseFcltMng/prfmncReg/chkgdnReg/page.tsx | 4 ++-- .../ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx index aea5c4b39..0fe594630 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx @@ -94,7 +94,7 @@ export default function Page() { const cellClick = () => { if (searchYear) - if (selFcltDiv === "RNB") { + if (selFcltDiv === "RNB") { // 빗물 if (fcltNm) { rainChkNmSearch( rainPageNation, @@ -107,7 +107,7 @@ export default function Page() { } else { rainChkInit(rainPageNation, rainPageSize, searchYear, laeSeCd); } - } else { + } else { // 중수도 if (fcltNm) { rswtChkNmSearch( rainPageNation, diff --git a/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx b/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx index e931cf706..3a59edb49 100644 --- a/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/rswtChkgd/rswtChkgdTable.tsx @@ -13,6 +13,7 @@ import { chRowspanTable, rswtChkTableData, } from "@/components/lib/func/TableData"; +import { codeToText } from "@/components/lib/func/commom"; import UiPageNations from "@common/business_components/ui/table/uiPageNations"; import { Table, TableBody, TableHeader } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; @@ -99,9 +100,16 @@ export default function RswtChkgdTable({ const tableTotalData = rswtChkTableData(rswtChkData); const makeRswtChkData = tableTotalData.map((item: any) => { const bud = tableData([item.building], { code }); + // 운영관리형태 코드 → 한글 텍스트로 치환 (빗물 화면과 동일한 표시) + const operMngCdText = + codeToText(item.operMngCd?.trim(), "rainRswtMng") ?? + codeToText(item.reuseBuilding?.operMngCd?.trim(), "rainRswtMng") ?? + item.operMngCd; + return { ...item, ...bud[0], + operMngCd: operMngCdText, rowspan: item.rswtrDetails?.length, }; }); From 8d481aab099d61ad157f2550c6e18bc299a1fc99 Mon Sep 17 00:00:00 2001 From: dongsilove Date: Mon, 9 Feb 2026 21:40:02 +0900 Subject: [PATCH 20/32] =?UTF-8?q?fo:=20=EC=B2=98=EB=A6=AC=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EC=8B=9C=EC=84=A4=EC=A0=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processFacility/modal/page.tsx | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx index 7e3f7c52f..512b0be19 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx @@ -5,6 +5,7 @@ */ "use client" +import { useCallback, useRef } from "react"; import { useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/dashboard"; import { downloadExcelFormat, filePickedBlobArray, UITab, UiTable } from "@common/business_components/ui"; @@ -121,6 +122,21 @@ export default function ProcessFacilityInfoModal(props: ModalType) { const upHeadListPlanInfoChange = headMakeList(upHeadListPlanInfo, 2); const upHeadListFcltyHistInfoChange = headMakeList(upHeadListFcltyHistInfo, 3); const upHeadListFcltyWaterQltyInfoChange = headMakeList(upHeadListFcltyWaterQltyInfo, 4); + const historyTopScrollRef = useRef(null); + const historyBottomScrollRef = useRef(null); + const syncLockRef = useRef(false); + + const syncScroll = useCallback((source: HTMLDivElement | null, target: HTMLDivElement | null) => { + if (!source || !target || syncLockRef.current) return; + syncLockRef.current = true; + const nextTop = source.scrollTop; + if (target.scrollTop !== nextTop) { + target.scrollTop = nextTop; + } + requestAnimationFrame(() => { + syncLockRef.current = false; + }); + }, []); const excelDownloadPlan = () => { const excelDownList: any[] = []; @@ -218,7 +234,7 @@ export default function ProcessFacilityInfoModal(props: ModalType) {

시설 기본정보

- +
{'시설명'} @@ -262,7 +278,11 @@ export default function ProcessFacilityInfoModal(props: ModalType) { -
+
syncScroll(historyTopScrollRef.current, historyBottomScrollRef.current)} + > @@ -274,7 +294,11 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
-
+
syncScroll(historyBottomScrollRef.current, historyTopScrollRef.current)} + > @@ -292,4 +316,4 @@ export default function ProcessFacilityInfoModal(props: ModalType) {
); -} \ No newline at end of file +} From 3787f824df88b6b879e18c7d36a1830c1d7a2eea Mon Sep 17 00:00:00 2001 From: dongsilove Date: Mon, 9 Feb 2026 22:38:10 +0900 Subject: [PATCH 21/32] =?UTF-8?q?fo:=20exceldown=EC=8B=9C=20,=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facilityPrecipitation/page.tsx | 7 ++-- .../(with-layout)/facilityWaterQlty/page.tsx | 3 +- .../pages/(with-layout)/planSewage/page.tsx | 32 ++++++++++++++--- .../pages/(with-layout)/processArea/page.tsx | 11 ++++-- .../(with-layout)/processFacility/page.tsx | 10 ++++-- apps/fo/app/pages/(with-layout)/rdi/page.tsx | 36 ++++++++++++++++--- .../pages/(with-layout)/sewageOccur/page.tsx | 34 +++++++++++++++--- .../(with-layout)/underWaterInput/page.tsx | 33 ++++++++++++++--- 8 files changed, 140 insertions(+), 26 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx index 7fd141694..9570320d4 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/page.tsx @@ -7,7 +7,7 @@ "use client" import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; -import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import PrecipitationModal from "./modal/page"; @@ -212,11 +212,12 @@ export default function FacilityPrecipitation() { await searchFacilityPrecipitation(searchReturn, 1, 10000); const latestExcelList = store.getState().FacilityPrecipitation.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ + downloadExcelFormat({ data: latestExcelList as unknown as JSON[], fileName: '시설별 강수현황', sheetNameArray: ['시설별 강수현황'], - headerRange: 'A1:N1' + headerRange: 'A1:N1', + decimalArray0: [9, 11, 12, 13], }); } else { alert("조회된 결과가 없습니다."); diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx index 265c9788f..a90929225 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/page.tsx @@ -191,8 +191,7 @@ export default function ProcessFacilitySearch() { fileName: '시설별 수질현황', sheetNameArray: ['시설별 수질현황'], headerRange: 'A1:T1', - decimalArray1: [10, 11, 12, 15, 16, 17], - decimalArray3: [13, 14, 18, 19] + decimalArray0: [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] } ); } else { diff --git a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx index 8b41524d1..bdfd9c4b4 100644 --- a/apps/fo/app/pages/(with-layout)/planSewage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/planSewage/page.tsx @@ -7,7 +7,7 @@ "use client" import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; -import { downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; +import { downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; @@ -197,11 +197,35 @@ export default function PlanSewage() { await searchPlanSewage(searchReturn, 1, 10000); const latestExcelList = store.getState().PlanSewage.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ - data: latestExcelList as unknown as JSON[], + const toNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') return ''; + const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, "")); + return Number.isFinite(parsed) ? parsed : ''; + }; + + const numericKeys = [ + "시설용량(m³/일)", + "여유용량(m³/일)", + "변동부하율(%)", + "분류식화율(%)", + ]; + + const excelData = latestExcelList.map((row) => { + const nextRow = { ...row } as Record; + numericKeys.forEach((key) => { + if (key in nextRow) { + nextRow[key] = toNumber(nextRow[key]); + } + }); + return nextRow; + }); + + downloadExcelFormat({ + data: excelData as unknown as JSON[], fileName: '계획하수량 산정지표 현황', sheetNameArray: ['계획하수량 산정지표 현황'], - headerRange: 'A1:M1' + headerRange: 'A1:M1', + decimalArray0: [9, 10, 11, 12] }); } else { alert("조회된 결과가 없습니다."); diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index 84d8ee8b1..60dfff2cc 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -7,7 +7,7 @@ "use client" import { StoreContext, useFoStore } from "@/app/store"; import { useShallow } from 'zustand/react/shallow'; -import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import ProcessFacilityInfoModal from "./modal/page"; @@ -155,11 +155,16 @@ export default function ProcessArea() { await searchProcessArea(ref.current, 1, 10000); const latestExcelList = store.getState().ProcessArea.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ + downloadExcelFormat({ data: latestExcelList as unknown as JSON[], fileName: '처리구역정보', sheetNameArray: ['처리구역정보'], - headerRange: 'A1:W1' + headerRange: 'A1:W1', + decimalArray0: [ + 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, + 17, 18, 19, 20, 21 + ] }); } else { alert('조회된 결과가 없습니다.'); diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index e73213bb1..76546b4d7 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -7,7 +7,7 @@ "use client" import UnderComment from "@/app/components/common/UnderComment"; import { StoreContext, useFoStore } from "@/app/store"; -import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; @@ -258,7 +258,13 @@ export default function ProcessFacilitySearch() { await searchFcltyList(ref.current, 1, 10000); const latestExcelList = store.getState().ProcessFacilityList.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ data: latestExcelList as unknown as JSON[], fileName: '처리시설 시설정보', sheetNameArray: ['처리시설 시설정보'], headerRange: 'A1:P1' }); + downloadExcelFormat({ + data: latestExcelList as unknown as JSON[], + fileName: '처리시설 시설정보', + sheetNameArray: ['처리시설 시설정보'], + headerRange: 'A1:P1', + decimalArray0: [9, 10, 11], + }); } else { alert('조회된 결과가 없습니다.'); } diff --git a/apps/fo/app/pages/(with-layout)/rdi/page.tsx b/apps/fo/app/pages/(with-layout)/rdi/page.tsx index 459cd44ca..ff4b8d205 100644 --- a/apps/fo/app/pages/(with-layout)/rdi/page.tsx +++ b/apps/fo/app/pages/(with-layout)/rdi/page.tsx @@ -7,7 +7,7 @@ "use client" import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; -import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; +import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; @@ -195,11 +195,39 @@ export default function Rdi() { await searchRdi(ref.current, 1, 5000); const latestExcelList = store.getState().Rdi.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ - data: latestExcelList as unknown as JSON[], + const toNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') return ''; + const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, "")); + return Number.isFinite(parsed) ? parsed : ''; + }; + + const numericKeys = [ + "시설용량(m³/일)", + "청천시유입하수량(m³/일)", + "하절기강우일최저유입BOD(mg/L)", + "하절기최저유입BOD유입유량(m³/일)", + "동절기청천시유입BOD(mg/L)", + "RDII추정", + "RDII추정시일강수량(mm)", + "강우시총추정유입량(m³/일)", + ]; + + const excelData = latestExcelList.map((row) => { + const nextRow = { ...row } as Record; + numericKeys.forEach((key) => { + if (key in nextRow) { + nextRow[key] = toNumber(nextRow[key]); + } + }); + return nextRow; + }); + + downloadExcelFormat({ + data: excelData as unknown as JSON[], fileName: 'RDII추정', sheetNameArray: ['RDII추정'], - headerRange: 'A1:R1' + headerRange: 'A1:R1', + decimalArray0: [9, 10, 11, 12, 13, 14, 15, 16], }); } else { alert("조회된 결과가 없습니다."); diff --git a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx index 4dc3125d1..95aa8961a 100644 --- a/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx +++ b/apps/fo/app/pages/(with-layout)/sewageOccur/page.tsx @@ -9,7 +9,7 @@ import { StoreContext, useFoStore } from "@/app/store"; import { type TableUpperProps } from "@/app/store/common"; import type { SewageOccurProps } from "@/app/store/sewageOccur"; -import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import LoadingOverlay from "@common/business_components/ui/loadingBar"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; @@ -210,11 +210,37 @@ export default function PlanSewage() { await searchFacilityPrecipitation(ref.current, 1, 5000); const latestExcelList = store.getState().SewageOccur.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ - data: latestExcelList as unknown as JSON[], + const toNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') return ''; + const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, "")); + return Number.isFinite(parsed) ? parsed : ''; + }; + + const numericKeys = [ + "시설용량(m³/일)", + "물사용량(m³/일)", + "오수전환율(%)", + "오수발생량추정(m³/일)", + "연간하수유입량(m³/일)", + "청천시하수유입량(m³/일)", + ]; + + const excelData = latestExcelList.map((row) => { + const nextRow = { ...row } as Record; + numericKeys.forEach((key) => { + if (key in nextRow) { + nextRow[key] = toNumber(nextRow[key]); + } + }); + return nextRow; + }); + + downloadExcelFormat({ + data: excelData as unknown as JSON[], fileName: '오수발생량 추정', sheetNameArray: ['오수발생량 추정'], - headerRange: 'A1:O1' + headerRange: 'A1:O1', + decimalArray0: [9, 10, 11, 12, 13, 14] }); } else { alert("조회된 결과가 없습니다."); diff --git a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx index 4404403e3..36fbff308 100644 --- a/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx +++ b/apps/fo/app/pages/(with-layout)/underWaterInput/page.tsx @@ -9,7 +9,7 @@ "use client" import { StoreContext, useFoStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common"; -import { ContentsDiv, downloadExcel, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; +import { ContentsDiv, downloadExcelFormat, MainDiv, SearchBaseForm, SearchInput, SelectBox, Title, UiModal, UiTable, type ReturnType } from "@common/business_components"; import ListDiv from "@common/business_components/ui/list_div"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import UnderWaterInputModal from "./modal/page"; @@ -216,11 +216,36 @@ export default function UnderWaterInput() { await searchFacilityPrecipitation(ref.current, 1, 5000); const latestExcelList = store.getState().UnderWater.excelList || []; if (latestExcelList.length > 0) { - downloadExcel({ - data: latestExcelList as unknown as JSON[], + const toNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') return ''; + const parsed = typeof value === "number" ? value : Number(String(value).replace(/,/g, "")); + return Number.isFinite(parsed) ? parsed : ''; + }; + + const numericKeys = [ + "시설용량(m³/일)", + "평균유입하수량(m³/일)", + "연간지하수유입량(m³/일)", + "하절기지하수유입량(m³/일)", + "동절기지하수유입량(m³/일)", + ]; + + const excelData = latestExcelList.map((row) => { + const nextRow = { ...row } as Record; + numericKeys.forEach((key) => { + if (key in nextRow) { + nextRow[key] = toNumber(nextRow[key]); + } + }); + return nextRow; + }); + + downloadExcelFormat({ + data: excelData as unknown as JSON[], fileName: '지하수유입량 추정', sheetNameArray: ['지하수유입량 추정'], - headerRange: 'A1:O1' + headerRange: 'A1:O1', + decimalArray0: [9, 10, 11, 12, 13] }); } else { alert("조회된 결과가 없습니다."); From aacf652dac15d4be3fb559d49c1afea9aa40cc22 Mon Sep 17 00:00:00 2001 From: dongsilove Date: Mon, 9 Feb 2026 23:14:23 +0900 Subject: [PATCH 22/32] =?UTF-8?q?fo:=20=EC=B2=98=EB=A6=AC=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=ED=8C=9D=EC=97=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(with-layout)/processFacility/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 76546b4d7..75bd371e0 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -327,7 +327,7 @@ export default function ProcessFacilitySearch() { {selectedRow !== null && ( } - bodyClass="sm:max-w-[70vw] h-[100dvh] max-h-[800px]" + bodyClass="sm:max-w-[100vw] w-[1100px] h-[100dvh] max-h-[800px]" open={selectedRow !== null} onOpenChange={(open) => !open && closeModal()} /> From c5816985918a0571966a9d831e15d800c7e533ee Mon Sep 17 00:00:00 2001 From: ljpark Date: Tue, 10 Feb 2026 10:13:32 +0900 Subject: [PATCH 23/32] =?UTF-8?q?fo:=20=EA=B3=B5=EB=B2=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(with-layout)/processFacility/modal/page.tsx | 10 +++++++++- .../pages/(with-layout)/processFacility/page.tsx | 2 +- apps/fo/app/store/processFacility/index.tsx | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx index 512b0be19..20c885797 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx @@ -153,11 +153,19 @@ export default function ProcessFacilityInfoModal(props: ModalType) { "시운전완료일": item.trlrnCmplDt, "가동개시일": item.rnDt, "시설용량(m³/일)": item.fctCpc&&item.fctCpc.toString().trim()!=''?Number(item.fctCpc.toString().replaceAll(',','')):'', + "공법구분": item.trtSrsHanNm, + "공법계열": item.trtMocHanNm, + "방법": item.trtFrfHanNm, }); }); } if (excelDownList && excelDownList.length > 0) { - downloadExcelFormat({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: props.fcltyName + ' 시설이력관리', sheetNameArray: [props.fcltyName + ' 시설이력관리'], headerRange: 'A1:I1' }); + downloadExcelFormat({ + data: JSON.parse(JSON.stringify(excelDownList)), + fileName: props.fcltyName + ' 시설이력관리', + sheetNameArray: [props.fcltyName + ' 시설이력관리'], + decimalArray0: [8], + headerRange: 'A1:L1' }); } else if (gridListFcltyHistInfo.length == 0) { alert('다운받을 데이터가 없습니다.'); } diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 75bd371e0..def1ca0e8 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -327,7 +327,7 @@ export default function ProcessFacilitySearch() { {selectedRow !== null && ( } - bodyClass="sm:max-w-[100vw] w-[1100px] h-[100dvh] max-h-[800px]" + bodyClass="sm:max-w-[100vw] w-[1300px] h-[100dvh] max-h-[800px]" open={selectedRow !== null} onOpenChange={(open) => !open && closeModal()} /> diff --git a/apps/fo/app/store/processFacility/index.tsx b/apps/fo/app/store/processFacility/index.tsx index 240ad2d4d..006185e6d 100644 --- a/apps/fo/app/store/processFacility/index.tsx +++ b/apps/fo/app/store/processFacility/index.tsx @@ -113,7 +113,10 @@ export type fcltyHistInfoProps = { sitArea: number, trtDstrArea: number, swrTrtPopl: number, - fctCpc: number | '' + fctCpc: number | '', + trtSrsHanNm?: string, // 공법구분 + trtMocHanNm?: string, // 공법계열 + trtFrfHanNm?: string, // 방법 } // 시설 수질정보 @@ -328,6 +331,9 @@ const upHeadListFcltyHistInfo: TableUpperProps[] = [ { id: "trlrnCmplDt", title: "시운전완료일" }, { id: "rnDt", title: "가동개시일" }, { id: "fctCpc", title: "시설용량(m³/일)", align: 'right' }, + { id: "trtSrsHanNm", title: "공법구분" }, + { id: "trtMocHanNm", title: "공법계열" }, + { id: "trtFrfHanNm", title: "방법" }, ]; // 시설 이력관리 (리스트) @@ -344,7 +350,10 @@ const gridListFcltyHistInfo: fcltyHistInfoProps[] = [ sitArea: 0, trtDstrArea: 0, swrTrtPopl: 0, - fctCpc: 860000 + fctCpc: 860000, + trtFrfHanNm: "", + trtMocHanNm: "", + trtSrsHanNm: "", } ]; @@ -902,4 +911,4 @@ export const processFacilityReducer: (set: SetStateFunction) => ProcessFacilityA } } } -} \ No newline at end of file +} From a7d7ffb7673c9fd9bfcc241337cc502dcd8f7e12 Mon Sep 17 00:00:00 2001 From: ljpark Date: Tue, 10 Feb 2026 13:49:48 +0900 Subject: [PATCH 24/32] =?UTF-8?q?wr:=20chRowspanTable=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/components/lib/func/TableData.tsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/wr/components/lib/func/TableData.tsx b/apps/wr/components/lib/func/TableData.tsx index 1a66802f5..0c613bed0 100644 --- a/apps/wr/components/lib/func/TableData.tsx +++ b/apps/wr/components/lib/func/TableData.tsx @@ -190,12 +190,10 @@ export const chRowspanTable = ( value={formatCommaNumber(value) || ""} disabled={!selectedIds.includes(row.chkgdnSn)} maxLength={numSize} - onInput={(e: React.ChangeEvent) => { - if ( - head.type === "number" && - e.target.value.length > numSize - ) { - e.target.value = e.target.value.slice(0, numSize); + onInput={(e: React.FormEvent) => { + const el = e.currentTarget; + if (head.type === "number" && el.value.length > numSize) { + el.value = el.value.slice(0, numSize); } }} /> @@ -225,12 +223,10 @@ export const chRowspanTable = ( value={value || ""} disabled={!selectedIds.includes(row.chkgdnSn)} maxLength={numSize} - onInput={(e: React.ChangeEvent) => { - if ( - head.type === "number" && - e.target.value.length > numSize - ) { - e.target.value = e.target.value.slice(0, numSize); + onInput={(e: React.FormEvent) => { + const el = e.currentTarget; + if (head.type === "number" && el.value.length > numSize) { + el.value = el.value.slice(0, numSize); } }} /> From c8912357b2821af700186fd93a211839aeccb07f Mon Sep 17 00:00:00 2001 From: ljpark Date: Tue, 10 Feb 2026 18:02:47 +0900 Subject: [PATCH 25/32] =?UTF-8?q?fo:=20exceldowntl=20,=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=ED=91=9C=EB=A1=9C=EB=B3=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/(with-layout)/dashboard/page.tsx | 22 ++- .../src/ui/excel/index.tsx | 137 +++++++++++++++--- 2 files changed, 135 insertions(+), 24 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx index 833f706ed..8728cfe1d 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx @@ -7,7 +7,7 @@ "use client" import { useFoStore } from "@/app/store"; import { - ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv + ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv, downloadExcelFormat , SelectBox, UITab, type SelectDataType } from "@common/business_components"; import { Button, TabsContent } from "@common/components/ui"; @@ -142,13 +142,11 @@ export default function DashBoard() { } } - const excelDownload = () => { + const excelDownload = async () => { if (!fcltyList.bscFctCd) return; switch (activeTab) { case 'facilityInfo': // 시설정보 및 운영현황 - searchOperationStatus(false, searchYear, fcltyList.bscFctCd).then(() => { setTimeout(() => { makeExcelData(); }, 200); }); - const makeExcelData = async () => { fetch(`/mainDashBoard.xlsx`, {}).then(async (response) => { if (!response.ok) { @@ -198,20 +196,26 @@ export default function DashBoard() { headerNum: 3, headerRange: 'A1:G2', sheetName: '운영실태', - startCol: 'A' + startCol: 'A', + decimalRange0: 'B9:B11', }, { data: excelData2, headerNum: 4, headerRange: 'A1:Q3', sheetName: '운영실태', - startCol: 'C' + startCol: 'C', + decimalRange0: 'C4:O8', + decimalRange1: 'C9:L10,M11:O11', + decimalRange3: 'C11:L16', } ]; // 버퍼에 있는 엑셀데이터 전달 후 json으로 변환 await filePickedBlobArray(blobRet, '시설정보및운영현황', sheetParameter); }); }; + await searchOperationStatus(false, searchYear, fcltyList.bscFctCd); + await makeExcelData(); break; case 'mainGraph': // 주요그래프 @@ -262,7 +266,11 @@ export default function DashBoard() { case 'rainManage': // 강우일 운영현황 searchRainManage(searchYear, fcltyList.bscFctCd, true); if (excelList && excelList.length > 0) { - downloadExcel({ data: excelList as unknown as JSON[], fileName: '강우일 운영현황', sheetNameArray: ['강우일 운영현황'], headerRange: 'A1:T1' }); + downloadExcel({ + data: excelList as unknown as JSON[], + fileName: '강우일 운영현황', + sheetNameArray: ['강우일 운영현황'], + headerRange: 'A1:T1' }); } break; // Add other cases for different tabs as needed diff --git a/packages/common/business_components/src/ui/excel/index.tsx b/packages/common/business_components/src/ui/excel/index.tsx index 048a0cc42..662705837 100644 --- a/packages/common/business_components/src/ui/excel/index.tsx +++ b/packages/common/business_components/src/ui/excel/index.tsx @@ -11,6 +11,10 @@ type ExcelDownloadProps = { decimalArray1? : number[]; decimalArray2? : number[]; decimalArray3? : number[]; + decimalRange0?: string; + decimalRange1?: string; + decimalRange2?: string; + decimalRange3?: string; } export const downloadExcel = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"], headerRange='A1:V3'}: ExcelDownloadProps) => { @@ -29,7 +33,20 @@ export const downloadExcelHeaders = ({ data, fileName = new Date().getTime().toS excelStyle(XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }), headerRange, fileName); }; -export const downloadExcelFormat = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"], headerRange='A1:V3', decimalArray0=[], decimalArray1=[], decimalArray2=[], decimalArray3=[]}: ExcelDownloadProps) => { +export const downloadExcelFormat = ({ + data, + fileName = new Date().getTime().toString() + '.xlsx', + sheetNameArray = ["Sheet1"], + headerRange = 'A1:V3', + decimalArray0 = [], + decimalArray1 = [], + decimalArray2 = [], + decimalArray3 = [], + decimalRange0, + decimalRange1, + decimalRange2, + decimalRange3, +}: ExcelDownloadProps) => { const worksheet = XLSX.utils.json_to_sheet(Array.isArray(data) ? data : [data]); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, sheetNameArray[0]); @@ -61,10 +78,35 @@ export const downloadExcelFormat = ({ data, fileName = new Date().getTime().toSt } } }; + const applyRangeFormat = (rangeStr: string | undefined, decimal: number) => { + if (!rangeStr) return; + const rangeInfo = XLSX.utils.decode_range(rangeStr); + for (let row = rangeInfo.s.r; row <= rangeInfo.e.r; row++) { + for (let col = rangeInfo.s.c; col <= rangeInfo.e.c; col++) { + const cellAddr = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = worksheet[cellAddr]; + if (cell && typeof cell.v === "number") { + if (decimal === 1) { + cell.z = "#,##0.0"; + } else if (decimal === 2) { + cell.z = "#,##0.00"; + } else if (decimal === 3) { + cell.z = "#,##0.000"; + } else { + cell.z = "#,##0"; + } + } + } + } + }; chaneValue(decimalArray0, 0); chaneValue(decimalArray1, 1); chaneValue(decimalArray2, 2); chaneValue(decimalArray3, 3); + applyRangeFormat(decimalRange0, 0); + applyRangeFormat(decimalRange1, 1); + applyRangeFormat(decimalRange2, 2); + applyRangeFormat(decimalRange3, 3); excelStyle(XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }), headerRange, fileName); }; @@ -74,6 +116,10 @@ export type SheetParameter = { headerRange: string; sheetName: string; startCol?: string; + decimalRange0?: string; + decimalRange1?: string; + decimalRange2?: string; + decimalRange3?: string; }; export const downloadMultipleExcel = (fileName = new Date().getTime().toString() + '.xlsx', sheetArray: SheetParameter[]): Promise => { @@ -201,23 +247,80 @@ export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, header }); } -export const filePickedBlobArray = (oFile: ArrayBuffer, fileNm: string, sheetArray: SheetParameter[]): Promise => { - return new Promise(async () => { - const wb = XLSX.read(oFile, { type: 'binary' }); - wb.SheetNames.forEach(function (sheetName, index) { - if (sheetArray[index]) { - const sheet = wb.Sheets[sheetName]; - if (sheet) { - const startCol = sheetArray[index].startCol ? sheetArray[index].startCol : 'A'; - XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol + sheetArray[index].headerNum }); - } else { - console.error(`Sheet "${sheetName}" is undefined.`); - } +// export const filePickedBlobArray = (oFile: ArrayBuffer, fileNm: string, sheetArray: SheetParameter[]): Promise => { +// return new Promise(async () => { +// const wb = XLSX.read(oFile, { type: 'binary' }); +// wb.SheetNames.forEach(function (sheetName, index) { +// if (sheetArray[index]) { +// const sheet = wb.Sheets[sheetName]; +// if (sheet) { +// const startCol = sheetArray[index].startCol ? sheetArray[index].startCol : 'A'; +// XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol + sheetArray[index].headerNum }); +// } else { +// console.error(`Sheet "${sheetName}" is undefined.`); +// } +// } +// }); +// await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm) +// }); +// } +export const filePickedBlobArray = async ( + oFile: ArrayBuffer, + fileNm: string, + sheetArray: SheetParameter[] +): Promise => { + const wb = XLSX.read(oFile, { type: "binary" }); + + wb.SheetNames.forEach((sheetName, index) => { + if (sheetArray[index]) { + const sheet = wb.Sheets[sheetName]; + if (sheet) { + const startCol = sheetArray[index].startCol ?? "A"; + XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { + origin: startCol + sheetArray[index].headerNum, + }); + const applyRangeFormat = (rangeStr: string | undefined, decimal: number) => { + if (!rangeStr) return; + const parts = rangeStr.split(",").map((part) => part.trim()).filter(Boolean); + for (const part of parts) { + const rangeInfo = XLSX.utils.decode_range(part); + for (let row = rangeInfo.s.r; row <= rangeInfo.e.r; row++) { + for (let col = rangeInfo.s.c; col <= rangeInfo.e.c; col++) { + const cellAddr = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = sheet[cellAddr]; + if (cell && typeof cell.v === "number") { + if (decimal === 1) { + cell.z = "#,##0.0"; + } else if (decimal === 2) { + cell.z = "#,##0.00"; + } else if (decimal === 3) { + cell.z = "#,##0.000"; + } else { + cell.z = "#,##0"; + } + } + } + } + } + }; + applyRangeFormat(sheetArray[index].decimalRange0, 0); + applyRangeFormat(sheetArray[index].decimalRange1, 1); + applyRangeFormat(sheetArray[index].decimalRange2, 2); + applyRangeFormat(sheetArray[index].decimalRange3, 3); + } else { + console.error(`Sheet "${sheetName}" is undefined.`); } - }); - await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm) + } }); -} + + await excelArrayStyle( + XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), + sheetArray, + fileNm + ); + + return undefined; +}; type ExcelDownloadWithHeaderProps = { data: JSON | JSON[]; @@ -251,4 +354,4 @@ export const downloadExcelWithKoreanHeaders = async ({ // 3. 헤더만 한글로 교체하는 커스텀 스타일 함수 호출 await excelStyleWithKoreanHeaders(buffer, headerRange, fileName, headerMapping, dataArray[0]); -}; \ No newline at end of file +}; From 7650ec30e877e3ce5b0cb74b3138b9ceb4dc0f71 Mon Sep 17 00:00:00 2001 From: dongsilove Date: Tue, 10 Feb 2026 20:30:41 +0900 Subject: [PATCH 26/32] =?UTF-8?q?fo:=20dashboard=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(with-layout)/dashboard/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx index 8728cfe1d..fec75a0b7 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx @@ -7,7 +7,7 @@ "use client" import { useFoStore } from "@/app/store"; import { - ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv, downloadExcelFormat + ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv , SelectBox, UITab, type SelectDataType } from "@common/business_components"; import { Button, TabsContent } from "@common/components/ui"; From e264159323dfdf429795043796d2821768da254e Mon Sep 17 00:00:00 2001 From: dongsilove Date: Tue, 10 Feb 2026 22:41:56 +0900 Subject: [PATCH 27/32] =?UTF-8?q?fo:=20=EC=88=98=EC=A7=88=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=ED=8C=9D=EC=97=85=20exceldown=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/(with-layout)/dashboard/page.tsx | 15 ++++++++----- .../facilityWaterQlty/modal/page.tsx | 2 +- .../(with-layout)/flowRate/modal/page.tsx | 11 ++++++++-- .../processFacility/modal/page.tsx | 2 +- .../src/ui/excel/index.tsx | 22 +++++++++++++++++++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx index fec75a0b7..24d71e719 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/page.tsx @@ -7,7 +7,7 @@ "use client" import { useFoStore } from "@/app/store"; import { - ContentsDiv, downloadExcel, downloadMultipleExcel, filePickedBlobArray, MainDiv + ContentsDiv, downloadExcelFormat, downloadMultipleExcel, filePickedBlobArrayFormat, MainDiv , SelectBox, UITab, type SelectDataType } from "@common/business_components"; import { Button, TabsContent } from "@common/components/ui"; @@ -197,7 +197,8 @@ export default function DashBoard() { headerRange: 'A1:G2', sheetName: '운영실태', startCol: 'A', - decimalRange0: 'B9:B11', + decimalRange0: 'B3:G2,B9:B11', + decimalRange1: 'B5:G5', }, { data: excelData2, @@ -211,7 +212,7 @@ export default function DashBoard() { } ]; // 버퍼에 있는 엑셀데이터 전달 후 json으로 변환 - await filePickedBlobArray(blobRet, '시설정보및운영현황', sheetParameter); + await filePickedBlobArrayFormat(blobRet, '시설정보및운영현황', sheetParameter); }); }; await searchOperationStatus(false, searchYear, fcltyList.bscFctCd); @@ -266,11 +267,15 @@ export default function DashBoard() { case 'rainManage': // 강우일 운영현황 searchRainManage(searchYear, fcltyList.bscFctCd, true); if (excelList && excelList.length > 0) { - downloadExcel({ + downloadExcelFormat({ data: excelList as unknown as JSON[], fileName: '강우일 운영현황', sheetNameArray: ['강우일 운영현황'], - headerRange: 'A1:T1' }); + headerRange: 'A1:T1', + decimalArray0: [2,3], + decimalArray1: [1,4,5,6,7,10,11,12,15,16,17], + decimalArray3: [8,9,13,14,18,19], + }); } break; // Add other cases for different tabs as needed diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx index c1a6afdf8..9c6d5360a 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/modal/page.tsx @@ -394,7 +394,7 @@ export default function WaterQualityModal({ fcltyName = '', facilityCd = '', sea "유입BOD(mg/L)": CommaToNumber(item.tpInflWqltBod), "유입TP(mg/L)": CommaToNumber(item.tpInflWqltTp), "유출BOD(mg/L)": CommaToNumber(item.efflWqltBod), - "유출TP(mg/L)": CommaToNumber(item.efflWqltTn), + "유출TP(mg/L)": CommaToNumber(item.efflWqltTp), }); }); } diff --git a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx index b214e0a48..0cffa775e 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx @@ -9,7 +9,7 @@ import FacilityTermModalHeader from "@/app/components/common/FacilityTermModalHe import { useFoStore } from "@/app/store"; import type { FlowRateModalInterface, modalExcelType } from "@/app/store/flowRate"; import { dataFormSetting } from "@/app/store/flowRate"; -import { downloadExcel, UITab, UiTable } from "@common/business_components/ui"; +import { downloadExcelFormat, UITab, UiTable } from "@common/business_components/ui"; import { Button, TabsContent } from "@common/components/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import type { ReturnType } from "../../../../components/common/FacilityTermModalHeader"; @@ -201,7 +201,14 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st }); } if (excelDownList && excelDownList.length > 0) { - downloadExcel({ data: JSON.parse(JSON.stringify(excelDownList)), fileName: props.searchYear + '년 ' + props.fcltyName + ' 유량현황', sheetNameArray: [props.fcltyName + ' 유량현황'], headerRange: 'A1:H1' }); + downloadExcelFormat({ + data: JSON.parse(JSON.stringify(excelDownList)), + fileName: props.searchYear + '년 ' + props.fcltyName + ' 유량현황', + sheetNameArray: [props.fcltyName + ' 유량현황'], + headerRange: 'A1:H1', + decimalArray0: [1, 2, 3, 4, 5, 6], + decimalArray1: [0], + }); } else if (prevExcelListRef.current && prevExcelListRef.current.length > 0) { alert('조회된 결과가 없습니다.'); } diff --git a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx index 20c885797..3118e008f 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx @@ -206,7 +206,7 @@ export default function ProcessFacilityInfoModal(props: ModalType) { const sheetParameter = [ { data: excelData, - headerNum: 2, + headerNum: 3, headerRange: 'A1:S2', sheetName: '시설 수질정보', startCol: 'A' diff --git a/packages/common/business_components/src/ui/excel/index.tsx b/packages/common/business_components/src/ui/excel/index.tsx index 662705837..80acd9696 100644 --- a/packages/common/business_components/src/ui/excel/index.tsx +++ b/packages/common/business_components/src/ui/excel/index.tsx @@ -268,6 +268,27 @@ export const filePickedBlobArray = async ( oFile: ArrayBuffer, fileNm: string, sheetArray: SheetParameter[] +): Promise => { + const wb = XLSX.read(oFile, { type: 'binary' }); + wb.SheetNames.forEach(function (sheetName, index) { + if (sheetArray[index]) { + const sheet = wb.Sheets[sheetName]; + if (sheet) { + const startCol = sheetArray[index].startCol ? sheetArray[index].startCol : 'A'; + XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol + sheetArray[index].headerNum }); + } else { + console.error(`Sheet "${sheetName}" is undefined.`); + } + } + }); + await excelArrayStyle(XLSX.write(wb, { bookType: "xlsx", type: "buffer" }), sheetArray, fileNm); + return undefined; +}; + +export const filePickedBlobArrayFormat = async ( + oFile: ArrayBuffer, + fileNm: string, + sheetArray: SheetParameter[] ): Promise => { const wb = XLSX.read(oFile, { type: "binary" }); @@ -276,6 +297,7 @@ export const filePickedBlobArray = async ( const sheet = wb.Sheets[sheetName]; if (sheet) { const startCol = sheetArray[index].startCol ?? "A"; + XLSX.utils.sheet_add_aoa(sheet, sheetArray[index].data, { origin: startCol + sheetArray[index].headerNum, }); From 8d5546abd58254ea516da2ba64dfe46665166500 Mon Sep 17 00:00:00 2001 From: dongsilove Date: Tue, 10 Feb 2026 22:55:58 +0900 Subject: [PATCH 28/32] =?UTF-8?q?fo:=20=EC=B2=98=EB=A6=AC=EA=B5=AC?= =?UTF-8?q?=EC=97=AD=20exceldown=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(with-layout)/processArea/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/fo/app/pages/(with-layout)/processArea/page.tsx b/apps/fo/app/pages/(with-layout)/processArea/page.tsx index 60dfff2cc..bca5a7b91 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/page.tsx @@ -160,11 +160,8 @@ export default function ProcessArea() { fileName: '처리구역정보', sheetNameArray: ['처리구역정보'], headerRange: 'A1:W1', - decimalArray0: [ - 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, - 17, 18, 19, 20, 21 - ] + decimalArray0: [3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15], + decimalArray2: [16, 17, 18, 19, 20, 21], }); } else { alert('조회된 결과가 없습니다.'); From 36ce09b7544f88f35e18f6f21ad7e8ebb49bf00b Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 12 Feb 2026 18:18:08 +0900 Subject: [PATCH 29/32] =?UTF-8?q?dm=5Fnew=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dm_new/.env.local | 26 + apps/dm_new/.env.mindone | 24 + apps/dm_new/.env.production | 24 + .../reuse/_components/CaseTabsCarousel.tsx | 519 ++++++++++++++++++ .../reuse/_components/ImageSlider.tsx | 167 ++++++ .../app/(landing)/reuse/_components/Lnb.tsx | 171 ++++++ .../reuse/_components/LoginStatus.tsx | 51 ++ .../reuse/_components/SideIndicator.tsx | 99 ++++ .../reuse/_components/SubPageHeader.tsx | 64 +++ .../(landing)/reuse/_components/TopNav.tsx | 465 ++++++++++++++++ .../app/(landing)/reuse/_constants/lnb.ts | 52 ++ .../(landing)/reuse/_hooks/useScrollSpy.ts | 55 ++ .../app/(landing)/reuse/_sections/Cases.tsx | 65 +++ .../app/(landing)/reuse/_sections/Hero.tsx | 29 + .../(landing)/reuse/_sections/IntroCards.tsx | 86 +++ .../(landing)/reuse/_sections/ProcessFlow.tsx | 22 + .../app/(landing)/reuse/apply/layout.tsx | 13 + .../apply/modal/WaterQualityRefModal.tsx | 99 ++++ .../app/(landing)/reuse/apply/place/page.tsx | 350 ++++++++++++ .../(landing)/reuse/apply/register/page.tsx | 476 ++++++++++++++++ .../app/(landing)/reuse/apply/status/page.tsx | 338 ++++++++++++ .../app/(landing)/reuse/business/layout.tsx | 16 + .../reuse/business/rain/suwon/page.tsx | 93 ++++ .../reuse/business/sewage/bolyeong/page.tsx | 111 ++++ .../reuse/business/sewage/cheongju/page.tsx | 94 ++++ .../reuse/business/sewage/guri/page.tsx | 171 ++++++ .../reuse/business/sewage/hwanggu/page.tsx | 171 ++++++ .../reuse/business/sewage/paju/page.tsx | 112 ++++ .../reuse/business/sewage/yeosu/page.tsx | 101 ++++ .../(landing)/reuse/cases/greywater/page.tsx | 96 ++++ .../app/(landing)/reuse/cases/layout.tsx | 16 + .../app/(landing)/reuse/cases/rain/page.tsx | 86 +++ .../(landing)/reuse/cases/treated/page.tsx | 96 ++++ .../(landing)/reuse/intro/greywater/page.tsx | 86 +++ .../app/(landing)/reuse/intro/law/page.tsx | 122 ++++ .../app/(landing)/reuse/intro/layout.tsx | 16 + .../app/(landing)/reuse/intro/rain/page.tsx | 76 +++ .../(landing)/reuse/intro/treated/page.tsx | 76 +++ apps/dm_new/app/(landing)/reuse/layout.tsx | 12 + apps/dm_new/app/(landing)/reuse/page.tsx | 58 ++ .../app/(landing)/reuse/reuse.module.css | 238 ++++++++ apps/dm_new/components.json | 21 + apps/dm_new/eslint.config.mjs | 22 + apps/dm_new/middleware.ts | 179 ++++++ apps/dm_new/next-env.d.ts | 5 + apps/dm_new/next.config.ts | 57 ++ apps/dm_new/package.json | 37 ++ apps/dm_new/postcss.config.mjs | 8 + .../images/landing/business/rain/rain_01.png | Bin 0 -> 263405 bytes .../images/landing/business/rain/rain_02.png | Bin 0 -> 314034 bytes .../images/landing/business/rain/rain_03.png | Bin 0 -> 333203 bytes .../images/landing/business/rain/rain_04.png | Bin 0 -> 344317 bytes .../landing/business/sewage/bolyeong_01.png | Bin 0 -> 185353 bytes .../landing/business/sewage/cheongju_01.png | Bin 0 -> 428282 bytes .../landing/business/sewage/hwanggu_01.png | Bin 0 -> 285722 bytes .../landing/business/sewage/paju_01.png | Bin 0 -> 356780 bytes .../landing/business/sewage/yeosu_01.png | Bin 0 -> 351736 bytes .../dm_new/public/images/landing/calendar.svg | 3 + apps/dm_new/public/images/landing/card_01.jpg | Bin 0 -> 105869 bytes apps/dm_new/public/images/landing/card_02.jpg | Bin 0 -> 188410 bytes apps/dm_new/public/images/landing/card_03.jpg | Bin 0 -> 181172 bytes apps/dm_new/public/images/landing/card_04.jpg | Bin 0 -> 255362 bytes .../landing/cases/greywater_building.png | Bin 0 -> 51551 bytes .../landing/cases/greywater_factory.png | Bin 0 -> 56622 bytes .../landing/cases/greywater_overall.png | Bin 0 -> 72692 bytes .../images/landing/cases/greywater_usage.png | Bin 0 -> 45483 bytes .../landing/cases/rainwater_building.png | Bin 0 -> 41751 bytes .../images/landing/cases/rainwater_usage.png | Bin 0 -> 62674 bytes .../landing/cases/rainwater_usage_amount.png | Bin 0 -> 41171 bytes .../landing/cases/treated_water_facility.png | Bin 0 -> 58845 bytes .../landing/cases/treated_water_region.png | Bin 0 -> 106934 bytes .../landing/cases/treated_water_usage.png | Bin 0 -> 74707 bytes .../landing/cases/treated_water_year.png | Bin 0 -> 72692 bytes .../public/images/landing/drop_arrow.svg | 3 + .../public/images/landing/global_01.png | Bin 0 -> 85203 bytes apps/dm_new/public/images/landing/hero_bg.jpg | Bin 0 -> 6418756 bytes .../public/images/landing/icon_left_arrow.png | Bin 0 -> 250 bytes .../public/images/landing/icon_login.png | Bin 0 -> 353 bytes .../public/images/landing/icon_logout.png | Bin 0 -> 362 bytes .../images/landing/icon_right_arrow.png | Bin 0 -> 258 bytes .../images/landing/intro/domestic_textbox.png | Bin 0 -> 237540 bytes .../images/landing/intro/greywater_intro.png | Bin 0 -> 321224 bytes .../landing/intro/greywater_mandatory.png | Bin 0 -> 64590 bytes .../landing/intro/greywater_standard.png | Bin 0 -> 53834 bytes .../images/landing/intro/law_timeline.png | Bin 0 -> 173354 bytes .../public/images/landing/intro/linkage.png | Bin 0 -> 445248 bytes .../images/landing/intro/performance_left.png | Bin 0 -> 363152 bytes .../images/landing/intro/policy_timeline.png | Bin 0 -> 1121906 bytes .../images/landing/intro/rainreuse_intro.png | Bin 0 -> 331677 bytes .../landing/intro/rainreuse_mandatory.png | Bin 0 -> 74345 bytes .../public/images/landing/intro/rel_note.png | Bin 0 -> 175900 bytes .../landing/intro/treatedwater_intro.png | Bin 0 -> 141470 bytes .../landing/intro/treatedwater_mandatory.png | Bin 0 -> 25186 bytes apps/dm_new/public/images/landing/logo.png | Bin 0 -> 9223 bytes apps/dm_new/public/images/landing/rain_01.png | Bin 0 -> 148115 bytes .../public/images/landing/scroll_down.svg | 4 + .../public/images/landing/section_03.png | Bin 0 -> 260096 bytes .../public/images/landing/sewage_01.png | Bin 0 -> 192090 bytes .../public/images/landing/sewage_02.png | Bin 0 -> 111450 bytes .../public/images/landing/sewage_03.png | Bin 0 -> 184355 bytes .../images/landing/water_quality_ref_1.png | Bin 0 -> 215652 bytes .../src/entities/auth/api/login-service.ts | 78 +++ apps/dm_new/src/entities/auth/index.ts | 2 + .../src/entities/auth/lib/auth-wrapper.tsx | 45 ++ .../src/entities/auth/lib/token-manager.ts | 59 ++ .../entities/auth/model/login-interface.ts | 9 + apps/dm_new/src/shared/api/client.ts | 11 + apps/dm_new/src/shared/api/index.ts | 1 + apps/dm_new/src/shared/config/api.ts | 58 ++ apps/dm_new/src/shared/config/env.ts | 13 + apps/dm_new/src/shared/config/index.ts | 2 + apps/dm_new/src/shared/lib/cookie.ts | 30 + apps/dm_new/src/shared/lib/index.ts | 8 + apps/dm_new/tailwind.config.ts | 69 +++ apps/dm_new/tsconfig.json | 54 ++ package.json | 1 + 116 files changed, 5821 insertions(+) create mode 100644 apps/dm_new/.env.local create mode 100644 apps/dm_new/.env.mindone create mode 100644 apps/dm_new/.env.production create mode 100644 apps/dm_new/app/(landing)/reuse/_components/CaseTabsCarousel.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_components/ImageSlider.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_components/Lnb.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_components/LoginStatus.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_components/SideIndicator.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_constants/lnb.ts create mode 100644 apps/dm_new/app/(landing)/reuse/_hooks/useScrollSpy.ts create mode 100644 apps/dm_new/app/(landing)/reuse/_sections/Cases.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_sections/Hero.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_sections/IntroCards.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/_sections/ProcessFlow.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/apply/layout.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/apply/modal/WaterQualityRefModal.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/apply/place/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/apply/register/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/apply/status/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/layout.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/rain/suwon/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/sewage/bolyeong/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/sewage/cheongju/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/sewage/guri/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/sewage/hwanggu/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/sewage/paju/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/business/sewage/yeosu/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/cases/greywater/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/cases/layout.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/cases/rain/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/cases/treated/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/intro/greywater/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/intro/law/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/intro/layout.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/intro/rain/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/intro/treated/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/layout.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/page.tsx create mode 100644 apps/dm_new/app/(landing)/reuse/reuse.module.css create mode 100644 apps/dm_new/components.json create mode 100644 apps/dm_new/eslint.config.mjs create mode 100644 apps/dm_new/middleware.ts create mode 100644 apps/dm_new/next-env.d.ts create mode 100644 apps/dm_new/next.config.ts create mode 100644 apps/dm_new/package.json create mode 100644 apps/dm_new/postcss.config.mjs create mode 100644 apps/dm_new/public/images/landing/business/rain/rain_01.png create mode 100644 apps/dm_new/public/images/landing/business/rain/rain_02.png create mode 100644 apps/dm_new/public/images/landing/business/rain/rain_03.png create mode 100644 apps/dm_new/public/images/landing/business/rain/rain_04.png create mode 100644 apps/dm_new/public/images/landing/business/sewage/bolyeong_01.png create mode 100644 apps/dm_new/public/images/landing/business/sewage/cheongju_01.png create mode 100644 apps/dm_new/public/images/landing/business/sewage/hwanggu_01.png create mode 100644 apps/dm_new/public/images/landing/business/sewage/paju_01.png create mode 100644 apps/dm_new/public/images/landing/business/sewage/yeosu_01.png create mode 100644 apps/dm_new/public/images/landing/calendar.svg create mode 100644 apps/dm_new/public/images/landing/card_01.jpg create mode 100644 apps/dm_new/public/images/landing/card_02.jpg create mode 100644 apps/dm_new/public/images/landing/card_03.jpg create mode 100644 apps/dm_new/public/images/landing/card_04.jpg create mode 100644 apps/dm_new/public/images/landing/cases/greywater_building.png create mode 100644 apps/dm_new/public/images/landing/cases/greywater_factory.png create mode 100644 apps/dm_new/public/images/landing/cases/greywater_overall.png create mode 100644 apps/dm_new/public/images/landing/cases/greywater_usage.png create mode 100644 apps/dm_new/public/images/landing/cases/rainwater_building.png create mode 100644 apps/dm_new/public/images/landing/cases/rainwater_usage.png create mode 100644 apps/dm_new/public/images/landing/cases/rainwater_usage_amount.png create mode 100644 apps/dm_new/public/images/landing/cases/treated_water_facility.png create mode 100644 apps/dm_new/public/images/landing/cases/treated_water_region.png create mode 100644 apps/dm_new/public/images/landing/cases/treated_water_usage.png create mode 100644 apps/dm_new/public/images/landing/cases/treated_water_year.png create mode 100644 apps/dm_new/public/images/landing/drop_arrow.svg create mode 100644 apps/dm_new/public/images/landing/global_01.png create mode 100644 apps/dm_new/public/images/landing/hero_bg.jpg create mode 100644 apps/dm_new/public/images/landing/icon_left_arrow.png create mode 100644 apps/dm_new/public/images/landing/icon_login.png create mode 100644 apps/dm_new/public/images/landing/icon_logout.png create mode 100644 apps/dm_new/public/images/landing/icon_right_arrow.png create mode 100644 apps/dm_new/public/images/landing/intro/domestic_textbox.png create mode 100644 apps/dm_new/public/images/landing/intro/greywater_intro.png create mode 100644 apps/dm_new/public/images/landing/intro/greywater_mandatory.png create mode 100644 apps/dm_new/public/images/landing/intro/greywater_standard.png create mode 100644 apps/dm_new/public/images/landing/intro/law_timeline.png create mode 100644 apps/dm_new/public/images/landing/intro/linkage.png create mode 100644 apps/dm_new/public/images/landing/intro/performance_left.png create mode 100644 apps/dm_new/public/images/landing/intro/policy_timeline.png create mode 100644 apps/dm_new/public/images/landing/intro/rainreuse_intro.png create mode 100644 apps/dm_new/public/images/landing/intro/rainreuse_mandatory.png create mode 100644 apps/dm_new/public/images/landing/intro/rel_note.png create mode 100644 apps/dm_new/public/images/landing/intro/treatedwater_intro.png create mode 100644 apps/dm_new/public/images/landing/intro/treatedwater_mandatory.png create mode 100644 apps/dm_new/public/images/landing/logo.png create mode 100644 apps/dm_new/public/images/landing/rain_01.png create mode 100644 apps/dm_new/public/images/landing/scroll_down.svg create mode 100644 apps/dm_new/public/images/landing/section_03.png create mode 100644 apps/dm_new/public/images/landing/sewage_01.png create mode 100644 apps/dm_new/public/images/landing/sewage_02.png create mode 100644 apps/dm_new/public/images/landing/sewage_03.png create mode 100644 apps/dm_new/public/images/landing/water_quality_ref_1.png create mode 100644 apps/dm_new/src/entities/auth/api/login-service.ts create mode 100644 apps/dm_new/src/entities/auth/index.ts create mode 100644 apps/dm_new/src/entities/auth/lib/auth-wrapper.tsx create mode 100644 apps/dm_new/src/entities/auth/lib/token-manager.ts create mode 100644 apps/dm_new/src/entities/auth/model/login-interface.ts create mode 100644 apps/dm_new/src/shared/api/client.ts create mode 100644 apps/dm_new/src/shared/api/index.ts create mode 100644 apps/dm_new/src/shared/config/api.ts create mode 100644 apps/dm_new/src/shared/config/env.ts create mode 100644 apps/dm_new/src/shared/config/index.ts create mode 100644 apps/dm_new/src/shared/lib/cookie.ts create mode 100644 apps/dm_new/src/shared/lib/index.ts create mode 100644 apps/dm_new/tailwind.config.ts create mode 100644 apps/dm_new/tsconfig.json diff --git a/apps/dm_new/.env.local b/apps/dm_new/.env.local new file mode 100644 index 000000000..88b6f8067 --- /dev/null +++ b/apps/dm_new/.env.local @@ -0,0 +1,26 @@ +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PORT = 3005 +NEXT_PUBLIC_PROXY_HOST = http://localhost:3005 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1 +# NEXT_PUBLIC_FO_SERVER_API_URL = http://192.168.1.222:8883/operation-service/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_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_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 API +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login + + +NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/dm_new/.env.mindone b/apps/dm_new/.env.mindone new file mode 100644 index 000000000..da6f80177 --- /dev/null +++ b/apps/dm_new/.env.mindone @@ -0,0 +1,24 @@ +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_PORT = 11035 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1 +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/sewerage-service # 개발 서버 API +NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 API +# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login + + +NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/dm_new/.env.production b/apps/dm_new/.env.production new file mode 100644 index 000000000..5a401c89a --- /dev/null +++ b/apps/dm_new/.env.production @@ -0,0 +1,24 @@ +# 공단 운영 +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +#NEXT_PUBLIC_PROXY_HOST = https://devreuse.hasuon.or.kr +#NEXT_PUBLIC_CM_LOGIN_PAGE_URL = https://devqueen.hasuon.or.kr/auth/login +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service +NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +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_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= diff --git a/apps/dm_new/app/(landing)/reuse/_components/CaseTabsCarousel.tsx b/apps/dm_new/app/(landing)/reuse/_components/CaseTabsCarousel.tsx new file mode 100644 index 000000000..2d936ffc5 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/CaseTabsCarousel.tsx @@ -0,0 +1,519 @@ +"use client"; + +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; + +/** + * CaseTabsCarousel.tsx (FINAL OPTIMIZED) + * - 탭별 데이터 + * - 카드 perPage(1/2/3) 반응형 + * - 카드 perPage개 = 1그룹, 좌/우 클릭 시 1그룹 단위 이동 + * - 3그룹 가상화(prev/cur/next)로 DOM 최소화 + * - px 기반 트랙 이동 + "측정 박스 분리"로 좌/우 비침 방지 + * - stale state 제거(방향/인덱스 ref 관리)로 슬라이드 안정성 강화 + * - BUGFIX 1) onTransitionEnd 버블링 방지 (target===currentTarget, transform만 처리) + * - BUGFIX 2) 슬라이드 중(busy) clipWidth 변화로 트랙 스냅(-W) 덮어쓰지 않게 분리 + */ + +type TabKey = "rain" | "reuse" | "sewage" | "global"; + +type CaseItem = { + id: string; + title: string; + desc: string; + imageSrc: string; + href?: string; +}; + +const TABS: { key: TabKey; label: string }[] = [ + { key: "rain", label: "빗물 재이용 사례" }, + { key: "reuse", label: "중수 재이용 사례" }, + { key: "sewage", label: "하수 재이용 사례" }, + { key: "global", label: "해외 재이용 사례" }, +]; + +// 실제 데이터로 교체해서 쓰세요(구조 유지) +const MOCK: Record = { + rain: [ + { + id: "rain-0", + title: "경기도 수원시 빗물이용과 중수도", + desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기…", + imageSrc: "/images/landing/rain_01.png", + href: "/reuse/business/rain/suwon", + }, + // { + // id: "rain-2", + // title: "경기도 수원시 빗물이용과 중수도", + // desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기…", + // imageSrc: "/images/landing/rain_01.png", + // href: "#", + // }, + ], + reuse: Array.from({ length: 6 }).map((_, i) => ({ + id: `reuse-${i}`, + title: `중수 재이용 사례 ${i + 1}`, + desc: "설명 문구가 들어갑니다. (최대 2~3줄) …", + imageSrc: "/images/landing/sample_case.png", + href: "#", + })), + sewage: [ + { + id: "sewage-1", + title: "경기도 파주시 하수 재이용", + desc: "운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에...", + imageSrc: "/images/landing/sewage_01.png", + href: "#", + }, + { + id: "sewage-2", + title: "충남보령 하수재이용", + desc: "보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업...", + imageSrc: "/images/landing/sewage_02.png", + href: "#", + }, + { + id: "sewage-3", + title: "여수시 하수처리수 재이용", + desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로...", + imageSrc: "/images/landing/sewage_03.png", + href: "#", + }, + ...Array.from({ length: 6 }).map((_, i) => ({ + id: `sewage-x-${i}`, + title: `하수 재이용 사례 ${i + 4}`, + desc: "설명 문구가 들어갑니다. (최대 2~3줄) …", + imageSrc: "/images/landing/sample_case.png", + href: "#", + })), + ], + global: Array.from({ length: 6 }).map((_, i) => ({ + id: `global-${i}`, + title: `해외 재이용 사례 ${i + 1}`, + desc: "설명 문구가 들어갑니다. (최대 2~3줄) …", + imageSrc: "/images/landing/sample_case.png", + href: "#", + })), +}; + +/* ---------------- utils ---------------- */ +function chunk(arr: T[], size: number) { + const res: T[][] = []; + if (size <= 0) return res; + for (let i = 0; i < arr.length; i += size) res.push(arr.slice(i, i + size)); + return res; +} +function mod(n: number, m: number) { + return ((n % m) + m) % m; +} +function usePrefersReducedMotion() { + const [reduced, setReduced] = useState(false); + useEffect(() => { + const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); + const onChange = () => setReduced(mq.matches); + onChange(); + mq.addEventListener?.("change", onChange); + return () => mq.removeEventListener?.("change", onChange); + }, []); + return reduced; +} + +/** + * wrapRef: perPage 계산용 + * clipRef: "패딩 없는 측정 박스" 폭(px) 계산용 + */ +function useCarouselMeasure() { + const wrapRef = useRef(null); + const clipRef = useRef(null); + + const [perPage, setPerPage] = useState(3); + const [clipWidth, setClipWidth] = useState(0); + + // perPage: container width로 반응형 결정 (rAF로 디바운스) + useEffect(() => { + const el = wrapRef.current; + if (!el) return; + + let raf = 0; + + const calc = () => { + const w = el.clientWidth; + if (w < 640) setPerPage(1); + else if (w < 1024) setPerPage(2); + else setPerPage(3); + }; + + const ro = new ResizeObserver(() => { + cancelAnimationFrame(raf); + raf = requestAnimationFrame(calc); + }); + + calc(); + ro.observe(el); + return () => { + cancelAnimationFrame(raf); + ro.disconnect(); + }; + }, []); + + // clipWidth: 패딩 없는 박스에서 width를 정확히 측정 (sub-pixel 반올림) + useEffect(() => { + const el = clipRef.current; + if (!el) return; + + let raf = 0; + + const calc = () => { + const w = Math.round(el.getBoundingClientRect().width); + setClipWidth(w); + }; + + const ro = new ResizeObserver(() => { + cancelAnimationFrame(raf); + raf = requestAnimationFrame(calc); + }); + + calc(); + ro.observe(el); + return () => { + cancelAnimationFrame(raf); + ro.disconnect(); + }; + }, []); + + return { wrapRef, clipRef, perPage, clipWidth }; +} + +/* ---------------- component ---------------- */ +export default function CaseTabsCarousel() { + const [active, setActive] = useState("rain"); + + // 탭별 마지막 그룹 인덱스 기억 + const tabIndexRef = useRef>({ + rain: 0, + reuse: 0, + sewage: 0, + global: 0, + }); + + const reducedMotion = usePrefersReducedMotion(); + const { wrapRef, clipRef, perPage, clipWidth } = useCarouselMeasure(); + + const items = useMemo(() => MOCK[active] ?? [], [active]); + const groups = useMemo(() => chunk(items, perPage), [items, perPage]); + const groupCount = groups.length; + + const [cur, setCur] = useState(0); + const curRef = useRef(0); + + // px 기반 트랙 + const [trackPx, setTrackPx] = useState(0); + const [anim, setAnim] = useState(false); + const [busy, setBusy] = useState(false); + + // 방향 ref (onTransitionEnd에서 trackPx 비교하지 않음) + const dirRef = useRef<-1 | 1 | 0>(0); + + // cur 최신값 유지 + useEffect(() => { + curRef.current = cur; + }, [cur]); + + /** + * 리셋 로직 분리: + * - 탭/perPage/groupCount 바뀔 때만 "인덱스/상태" 리셋 + * - clipWidth 변화는 별도 effect에서 "busy 아닐 때만" 중앙(-W)로 배치 + */ + useLayoutEffect(() => { + const saved = tabIndexRef.current[active] ?? 0; + const nextCur = groupCount ? mod(saved, groupCount) : 0; + + dirRef.current = 0; + setCur(nextCur); + setBusy(false); + setAnim(false); + }, [active, perPage, groupCount]); + + // clipWidth 변화로 트랙 위치를 잡되, 슬라이드 중(busy)엔 덮어쓰지 않음 + useLayoutEffect(() => { + if (busy) return; + if (clipWidth > 0) setTrackPx(-clipWidth); + else setTrackPx(0); + }, [clipWidth, busy]); + + // 3그룹 가상화 + const viewGroups = useMemo(() => { + if (!groupCount) return [[], [], []] as CaseItem[][]; + return [ + groups[mod(cur - 1, groupCount)] ?? [], + groups[mod(cur, groupCount)] ?? [], + groups[mod(cur + 1, groupCount)] ?? [], + ]; + }, [groups, cur, groupCount]); + + const canSlide = groupCount > 1 && clipWidth > 0; + + const go = useCallback( + (dir: -1 | 1) => { + if (!canSlide) return; + if (busy) return; + + dirRef.current = dir; + + setBusy(true); + setAnim(!reducedMotion); + + // 중앙(-W) 기준: prev=0, next=-2W + setTrackPx(dir === 1 ? -2 * clipWidth : 0); + + // reduced motion이면 즉시 확정 + if (reducedMotion) { + const nextCur = mod(curRef.current + dir, groupCount); + tabIndexRef.current[active] = nextCur; + + setCur(nextCur); + setAnim(false); + setTrackPx(-clipWidth); + setBusy(false); + dirRef.current = 0; + } + }, + [active, busy, canSlide, clipWidth, groupCount, reducedMotion] + ); + + // BUGFIX 1) 자식 transitionend 버블링 방지 + transform만 처리 + const onTransitionEnd = useCallback( + (e: React.TransitionEvent) => { + // 자식 transitionend 버블링 방지 + if (e.target !== e.currentTarget) return; + if (e.propertyName !== "transform") return; + + if (!busy || reducedMotion) return; + + const dir = dirRef.current; + if (dir !== 1 && dir !== -1) return; + + const nextCur = mod(curRef.current + dir, groupCount); + tabIndexRef.current[active] = nextCur; + + // 1) 먼저 transition을 끄고(중요) + setAnim(false); + + // 2) cur를 회전 + 트랙을 즉시 중앙(-W)로 스냅 + // (이 순간은 "무조건 애니메이션 없이" 처리되어야 함) + setCur(nextCur); + setTrackPx(-clipWidth); + + // 3) 다음 프레임(2번 rAF)에서 다시 애니메이션 켜고 busy 해제 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setAnim(!reducedMotion); + setBusy(false); + dirRef.current = 0; + }); + }); + }, + [active, busy, clipWidth, groupCount, reducedMotion] +); + + const goDot = useCallback( + (idx: number) => { + if (!groupCount) return; + if (busy) return; + + const target = mod(idx, groupCount); + tabIndexRef.current[active] = target; + + dirRef.current = 0; + setCur(target); + setAnim(false); + if (clipWidth > 0) setTrackPx(-clipWidth); + }, + [active, busy, clipWidth, groupCount] + ); + + const trackStyle: React.CSSProperties = useMemo( + () => ({ + transform: `translate3d(${trackPx}px, 0, 0)`, + transition: anim ? "transform 700ms cubic-bezier(.22,.61,.36,1)" : "none", + willChange: busy ? "transform" : undefined, + }), + [trackPx, anim, busy] + ); + + return ( +
+ {/* 탭 바 */} +
+
+
+ {TABS.map((t) => { + const isActive = t.key === active; + + return ( + + ); + })} +
+
+
+ + {/* 슬라이더 */} +
+
+ {/* 좌우 버튼 */} + + + + + {/* 중요: clip(가림)과 measure(측정) 분리 */} +
+
+ {/* 측정은 패딩 없는 박스에서 */} +
+ {/* track */} +
+ {viewGroups.map((group, gi) => ( +
+ {/* 그룹 내부: perPage 컬럼 */} +
+ {(group ?? []).map((c) => ( + + ))} +
+
+ ))} +
+
+
+
+ + {/* 도트 */} +
+ {Array.from({ length: Math.max(groupCount, 1) }).map((_, i) => ( +
+
+
+
+ ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_components/ImageSlider.tsx b/apps/dm_new/app/(landing)/reuse/_components/ImageSlider.tsx new file mode 100644 index 000000000..6c50d5a5c --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/ImageSlider.tsx @@ -0,0 +1,167 @@ +// apps/dm/app/(landing)/reuse/_components/ImageSlider.tsx +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; + +type Slide = { src: string; alt?: string }; + +type Props = { + slides: Slide[]; + className?: string; + maxWidthClassName?: string; // 예: "max-w-[760px]" +}; + +export default function CaseImageSlider({ + slides, + className = "", + maxWidthClassName = "max-w-[760px]", +}: Props) { + const safeSlides = useMemo(() => slides.filter(Boolean), [slides]); + const [idx, setIdx] = useState(0); + const [dragging, setDragging] = useState(false); + + const startX = useRef(0); + const deltaX = useRef(0); + + const total = safeSlides.length; + const canSlide = total > 1; + + const go = (dir: number) => { + if (!canSlide) return; + setIdx((prev) => { + const next = prev + dir; + if (next < 0) return total - 1; + if (next >= total) return 0; + return next; + }); + }; + + // ← → 키보드 지원(선택) + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (!canSlide) return; + if (e.key === "ArrowLeft") go(-1); + if (e.key === "ArrowRight") go(1); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [canSlide, total]); + + // 드래그(마우스/터치)로 넘기기 + const onPointerDown = (e: React.PointerEvent) => { + if (!canSlide) return; + setDragging(true); + startX.current = e.clientX; + deltaX.current = 0; + (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + }; + + const onPointerMove = (e: React.PointerEvent) => { + if (!dragging) return; + deltaX.current = e.clientX - startX.current; + }; + + const onPointerUp = () => { + if (!dragging) return; + setDragging(false); + + const threshold = 60; // 넘김 감도 + if (deltaX.current > threshold) go(-1); + else if (deltaX.current < -threshold) go(1); + + deltaX.current = 0; + }; + + if (!total) return null; + + return ( +
+
+
+ {/* 좌측 버튼 */} + + + + {/* 우측 버튼 */} + + + + {/* 뷰포트 */} +
+
+ {safeSlides.map((s, i) => ( +
+ {s.alt +
+ ))} +
+
+
+ + {/* 도트 */} + {canSlide && ( +
+ {safeSlides.map((_, i) => { + const active = i === idx; + return ( +
+ )} +
+
+ ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_components/Lnb.tsx b/apps/dm_new/app/(landing)/reuse/_components/Lnb.tsx new file mode 100644 index 000000000..c7d834844 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/Lnb.tsx @@ -0,0 +1,171 @@ +// apps/dm/app/(landing)/reuse/_components/Lnb.tsx +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useMemo, useState } from "react"; + +export type LnbItem = { + label: string; + href?: string; // href 없는 부모(2뎁스 컨테이너)도 허용 + children?: LnbItem[]; +}; + +function isExactActive(pathname: string, href?: string) { + if (!href) return false; + return pathname === href; +} + +export default function Lnb({ title, items }: { title: string; items: LnbItem[] }) { + const pathname = usePathname(); + + // ✅ 현재 경로가 속한 2depth 그룹을 기본 open + const defaultOpenLabel = useMemo(() => { + const found = items.find((it) => it.children?.some((c) => isExactActive(pathname, c.href))); + return found?.label ?? null; + }, [items, pathname]); + + // ✅ 아코디언: 한 번에 하나만 열리도록 openLabel 1개만 관리 + const [openLabel, setOpenLabel] = useState(defaultOpenLabel); + + return ( + + ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_components/LoginStatus.tsx b/apps/dm_new/app/(landing)/reuse/_components/LoginStatus.tsx new file mode 100644 index 000000000..4ddc08812 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/LoginStatus.tsx @@ -0,0 +1,51 @@ +// apps/dm/app/(landing)/reuse/_components/LoginStatus.tsx +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function LoginStatus() { + const router = useRouter(); + const [userName, setUserName] = useState(null); + + useEffect(() => { + // 예시: 로그인 시 저장한 값 + const name = localStorage.getItem("userName"); + setUserName(name); + }, []); + + const handleLogout = () => { + localStorage.removeItem("accessToken"); + localStorage.removeItem("userName"); + router.refresh(); // 헤더 상태 갱신 + }; + + // 🔹 로그인 전 + if (!userName) { + return ( + + 로그인 + → + + ); + } + + // 🔹 로그인 후 + return ( +
+ + {userName}님 환영합니다. + + +
+ ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_components/SideIndicator.tsx b/apps/dm_new/app/(landing)/reuse/_components/SideIndicator.tsx new file mode 100644 index 000000000..8f4cea164 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/SideIndicator.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import styles from "../reuse.module.css"; + +export interface SideIndicatorItem { + id: string; + label: string; // 상단/공용 라벨 + sideLabel?: string; // 사이드 인디게이터 전용 라벨(옵션) +} + +export default function SideIndicator({ + items, + activeId, +}: { + items: SideIndicatorItem[]; + activeId: string; +}) { + const [manualActiveId, setManualActiveId] = useState(null); + const clearTimerRef = useRef(null); + + const currentId = (manualActiveId ?? activeId ?? "main") || "main"; + + // ✅ manualActiveId가 observer(activeId)와 불일치하기 시작하면 즉시 해제 (stuck 방지) + useEffect(() => { + if (!manualActiveId) return; + if (activeId && activeId !== manualActiveId) setManualActiveId(null); + }, [activeId, manualActiveId]); + + // ✅ fallback: 스무스 스크롤 시간이 길어도 manual이 오래 남지 않게 타임아웃 해제 + useEffect(() => { + return () => { + if (clearTimerRef.current) window.clearTimeout(clearTimerRef.current); + }; + }, []); + + const scrollTo = (id: string) => { + setManualActiveId(id); + + // 기존 타이머 제거 후 새로 설정 + if (clearTimerRef.current) window.clearTimeout(clearTimerRef.current); + clearTimerRef.current = window.setTimeout(() => { + setManualActiveId(null); + }, 1600); + + if (id === "main") { + window.scrollTo({ top: 0, behavior: "smooth" }); + return; + } + + document.getElementById(id)?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }; + + return ( + + ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx b/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx new file mode 100644 index 000000000..fc3bade23 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx @@ -0,0 +1,64 @@ +"use client"; + +import Link from "next/link"; + +export type Crumb = { label: string; href?: string }; + +type Props = { + title: string; + crumbs: Crumb[]; + note?: string; + underlineWidthClass?: string; + className?: string; + wrapClassName?: string; + hideFooterLine?: boolean; + hideGuideText?: boolean; +}; + +export default function SubPageHeader({ + title, + crumbs, + note = "* 는 필수 값입니다.", + underlineWidthClass = "w-40", + className = "", + wrapClassName = "", + hideFooterLine = false, + hideGuideText = false, +}: Props) { + return ( +
+
+
+ {/* 좌측 타이틀 */} +
+

{title}

+
+
+ + {/* 우측 브레드크럼 */} + +
+ {/* 하단 라인 + 안내문 */} + {!hideGuideText && ( +
+ * + {note.replace("*", "")} +
+ )} +
+
+ ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx b/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx new file mode 100644 index 000000000..078d5e0d4 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx @@ -0,0 +1,465 @@ +// apps/dm/app/(landing)/reuse/_components/TopNav.tsx +"use client"; + +import Link from "next/link"; +import { useEffect, useId, useState } from "react"; +import { useRouter } from "next/navigation"; + +const NAV_ITEMS = [ + { href: "/reuse/intro/law", label: "물 재이용 소개" }, + { href: "/reuse/cases/rain", label: "물 재이용 현황" }, + { href: "/reuse/business/rain/suwon", label: "물 재이용 사례" }, + { href: "/reuse/apply/place", label: "수요신청" }, +]; + +// ✅ 필요 시 실제 “수요처등록” 페이지로 경로만 교체하세요. +const REGISTER_HREF = "/reuse/apply/place"; + +// ✅ 로그인 정보 저장 키(원하는대로 변경 가능) +const LS_TOKEN_KEY = "reuse_accessToken"; +const LS_USER_KEY = "reuse_userName"; + +type LoginModalProps = { + open: boolean; + onClose: () => void; + onSuccess?: (payload: { userName: string; accessToken?: string }) => void; +}; + +type FieldKey = "bizNo" | "workerNo" | "pw"; + +function isBizNoValid(v: string) { + // 예: 123-45-67890 + return /^\d{3}-\d{2}-\d{5}$/.test(v); +} + +function inputCls(opts: { hasError?: boolean; disabled?: boolean }) { + const base = + "w-full h-[48px] rounded-[4px] border px-5 text-[18px] outline-none"; + if (opts.disabled) return `${base} border-gray-200 bg-gray-50 text-gray-400`; + if (opts.hasError) + return `${base} border-red-500 focus:border-red-500 ring-1 ring-red-400`; + return `${base} border-gray-300 focus:border-sky-400`; +} + +function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { + const titleId = useId(); + const router = useRouter(); + + const [bizNo, setBizNo] = useState(""); + const [workerNo, setWorkerNo] = useState(""); + const [noWorker, setNoWorker] = useState(false); + const [pw, setPw] = useState(""); + + // ✅ 유효성/서버 에러 표시용 + const [errors, setErrors] = useState>>({}); + const [touched, setTouched] = useState>>({}); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (!open) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKeyDown); + + // 스크롤 잠금 + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + // 모달 오픈 시 에러/터치 초기화 + setErrors({}); + setTouched({}); + + return () => { + window.removeEventListener("keydown", onKeyDown); + document.body.style.overflow = prev; + }; + }, [open, onClose]); + + useEffect(() => { + if (noWorker) { + setWorkerNo(""); + setErrors((p) => { + const { workerNo: _w, ...rest } = p; + return rest; + }); + } + }, [noWorker]); + + if (!open) return null; + + const validate = () => { + const next: Partial> = {}; + + const b = bizNo.trim(); + const w = workerNo.trim(); + const p = pw.trim(); + + if (!b) next.bizNo = "사업자등록번호를 입력해주세요."; + else if (!isBizNoValid(b)) + next.bizNo = "사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)"; + + if (!noWorker && !w) next.workerNo = "종사자등록번호를 입력해주세요."; + + if (!p) next.pw = "비밀번호를 입력해주세요."; + + setErrors(next); + return next; + }; + + const markTouched = (key: FieldKey) => + setTouched((p) => ({ ...p, [key]: true })); + + const markAllTouched = () => + setTouched({ bizNo: true, workerNo: true, pw: true }); + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + markAllTouched(); + + const v = validate(); + if (Object.keys(v).length) return; + + try { + setSubmitting(true); + + // ✅ TODO: 실제 로그인 API 연결 지점 + // const res = await fetch("/api/demand/login", { + // method: "POST", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify({ bizNo, workerNo: noWorker ? null : workerNo, pw }), + // }); + // if (!res.ok) { ... } + // const data = await res.json(); + // onSuccess?.({ userName: data.userName, accessToken: data.token }); + + // --- 데모: 비밀번호가 "1234"면 성공 처리(원하면 제거) --- + const serverOk = pw.trim() === "1234"; + if (!serverOk) { + setErrors((p) => ({ ...p, pw: "잘못된 비밀번호입니다. (데모: 1234 입력 시 성공)" })); + setTouched((p) => ({ ...p, pw: true })); + return; + } + + onSuccess?.({ userName: "마인드원", accessToken: "demo_token" }); + onClose(); + router.refresh(); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ {/* Backdrop */} + + +

+ 수요처 로그인 +

+ + {/* title underline gradient bar */} +
+
+ + {/* Body */} +
+
+ {/* 사업자 등록번호 */} +
+ + { + setBizNo(e.target.value); + setErrors((p) => ({ ...p, bizNo: undefined })); + }} + onBlur={() => { + markTouched("bizNo"); + validate(); + }} + placeholder="사업자등록번호를 입력하세요. (예: 123-45-67890)" + className={inputCls({ hasError: !!(touched.bizNo && errors.bizNo) })} + /> + {touched.bizNo && errors.bizNo && ( +

+ {errors.bizNo} +

+ )} +
+ + {/* 종사자등록번호 + 해당없음 */} +
+
+ + + +
+ + { + setWorkerNo(e.target.value); + setErrors((p) => ({ ...p, workerNo: undefined })); + }} + onBlur={() => { + markTouched("workerNo"); + validate(); + }} + disabled={noWorker} + placeholder="종사자등록번호를 입력하세요." + className={inputCls({ + disabled: noWorker, + hasError: !noWorker && !!(touched.workerNo && errors.workerNo), + })} + /> + + {!noWorker && touched.workerNo && errors.workerNo && ( +

+ {errors.workerNo} +

+ )} +
+ + {/* 비밀번호 */} +
+ + { + setPw(e.target.value); + setErrors((p) => ({ ...p, pw: undefined })); + }} + onBlur={() => { + markTouched("pw"); + validate(); + }} + placeholder="비밀번호를 입력하세요." + className={inputCls({ hasError: !!(touched.pw && errors.pw) })} + /> + {touched.pw && errors.pw && ( +

+ {errors.pw} +

+ )} +
+ + {/* 로그인 버튼 */} + + + {/* 비밀번호 초기화 요청 */} + + + {/* 하단 안내 박스 + 수요처등록 버튼 */} +
+
+
+ 수요처를 등록하지 않았다면 +
+
+ 먼저 수요처등록을 해주세요. +
+
+ + +
+
+ +
+
+
+ ); +} + +export default function TopNav() { + const router = useRouter(); + + const [loginOpen, setLoginOpen] = useState(false); + const [userName, setUserName] = useState(null); + + // ✅ 초기 로드 시 로그인 상태 반영 + useEffect(() => { + const name = localStorage.getItem(LS_USER_KEY); + setUserName(name); + }, []); + + const openLogin = () => setLoginOpen(true); + + const logout = () => { + localStorage.removeItem(LS_TOKEN_KEY); + localStorage.removeItem(LS_USER_KEY); + setUserName(null); + router.refresh(); + }; + + return ( + <> +
+ {/* ✅ 좌/중/우 정렬이 되도록 컨테이너 구성 */} +
+
+ {/* 좌측: 로고 */} +
+ + HASUON + +
+ + {/* 중앙: 상단 메뉴 */} + + + {/* ✅ 우측: 로그인 버튼 / 로그인 정보 + 로그아웃 */} +
+ {!userName ? ( + + ) : ( + <> + + {userName}님 환영합니다. + + + + )} +
+
+
+
+ + {/* ✅ 로그인 모달 */} + setLoginOpen(false)} + onSuccess={({ userName, accessToken }) => { + // ✅ 로그인 성공 처리: 저장 + 헤더 갱신 + localStorage.setItem(LS_USER_KEY, userName); + if (accessToken) localStorage.setItem(LS_TOKEN_KEY, accessToken); + setUserName(userName); + setLoginOpen(false); + router.refresh(); + }} + /> + + ); +} diff --git a/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts b/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts new file mode 100644 index 000000000..6f9706e68 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts @@ -0,0 +1,52 @@ +// apps/dm/app/(landing)/reuse/_constants/lnb.ts +import type { LnbItem } from "../_components/Lnb"; + +export const LNB_DEMAND: LnbItem[] = [ + { href: "/reuse/apply/place", label: "수요처등록" }, + { href: "/reuse/apply/register", label: "수요정보등록" }, + { href: "/reuse/apply/status", label: "수요신청현황" }, +]; + +export const LNB_INTRO = [ + { label: "물 재이용 법", href: "/reuse/intro/law" }, + { label: "빗물 이용시설", href: "/reuse/intro/rain" }, + { label: "중수도", href: "/reuse/intro/greywater" }, + { label: "하수처리수", href: "/reuse/intro/treated" }, +]; + +export const LNB_CASES = [ + { label: "빗물이용시설(현황)", href: "/reuse/cases/rain" }, + { label: "중수도(현황)", href: "/reuse/cases/greywater" }, + { label: "하수처리수 재이용시설(현황)", href: "/reuse/cases/treated" }, +]; + +// 물 재이용 사례처럼 2depth가 있는 메뉴 +export const LNB_BUSINESS: LnbItem[] = [ + { + label: "빗물 재이용 사례", + children: [ + { label: "경기도 수원시", href: "/reuse/business/rain/suwon" }, + // { label: "충청남도 보령시", href: "/reuse/business/rain/bolyeong" }, + // { label: "충청북도 청주시", href: "/reuse/business/rain/cheongju" }, + ], + }, + { + label: "중수 재이용 사례", + children: [{ label: "사례 목록", href: "/reuse/business/graywater" }], + }, + { + label: "하수 재이용 사례", + children: [ + { label: "파주시", href: "/reuse/business/sewage/paju" }, + { label: "충청남도 보령시", href: "/reuse/business/sewage/bolyeong" }, + { label: "여수", href: "/reuse/business/sewage/yeosu" }, + { label: "청주", href: "/reuse/business/sewage/cheongju" }, + { label: "수원시 황구지천", href: "/reuse/business/sewage/hwanggu" }, + { label: "구리시 도로자동청소시스템", href: "/reuse/business/sewage/guri" }, + ], + }, + { + label: "해외 재이용 사례", + children: [{ label: "사례 목록", href: "/reuse/business/global" }], + }, +]; diff --git a/apps/dm_new/app/(landing)/reuse/_hooks/useScrollSpy.ts b/apps/dm_new/app/(landing)/reuse/_hooks/useScrollSpy.ts new file mode 100644 index 000000000..3a253e95d --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_hooks/useScrollSpy.ts @@ -0,0 +1,55 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; + +export function useScrollSpy(sectionIds: string[], options?: IntersectionObserverInit) { + const [activeId, setActiveId] = useState(sectionIds[0] ?? ""); + const ratiosRef = useRef>(new Map()); + + useEffect(() => { + const els = sectionIds + .map((id) => document.getElementById(id)) + .filter(Boolean) as HTMLElement[]; + + if (!els.length) return; + + const DEFAULT_ROOT_MARGIN = "83px 0px -65% 0px"; + + // 초기값 세팅(안정화) + ratiosRef.current.clear(); + els.forEach((el) => ratiosRef.current.set(el.id, 0)); + + const io = new IntersectionObserver( + (entries) => { + for (const e of entries) { + const id = (e.target as HTMLElement).id; + ratiosRef.current.set(id, e.isIntersecting ? e.intersectionRatio : 0); + } + + // ✅ 전체 섹션 중 가장 ratio 큰 항목을 active로 + let bestId = activeId; + let bestRatio = -1; + + for (const [id, ratio] of ratiosRef.current.entries()) { + if (ratio > bestRatio) { + bestRatio = ratio; + bestId = id; + } + } + + if (bestId) setActiveId(bestId); + }, + { + root: null, + threshold: [0, 0.01, 0.1, 0.25, 0.5, 0.75], + rootMargin: DEFAULT_ROOT_MARGIN, + ...options, + } + ); + + els.forEach((el) => io.observe(el)); + return () => io.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sectionIds.join("|"), options?.rootMargin, JSON.stringify(options?.threshold)]); + + return activeId; +} diff --git a/apps/dm_new/app/(landing)/reuse/_sections/Cases.tsx b/apps/dm_new/app/(landing)/reuse/_sections/Cases.tsx new file mode 100644 index 000000000..3d91bb5d5 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_sections/Cases.tsx @@ -0,0 +1,65 @@ +// apps/dm/app/(landing)/reuse/_sections/Cases.tsx +"use client"; + +export default function Cases() { + return ( +
+
+
+ + {/* 좌측 : 카피 영역 */} +
+
+

+ 빗물+중수도+하수도 +

+ +

+ + 데이터로 보는 + + + 물 재이용 현황 + +

+ + +
+
+ + {/* 우측 : 이미지 카드 */} +
+
+ 물 재이용 현황 데이터 테이블 +
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/_sections/Hero.tsx b/apps/dm_new/app/(landing)/reuse/_sections/Hero.tsx new file mode 100644 index 000000000..8f93bdd72 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_sections/Hero.tsx @@ -0,0 +1,29 @@ +// apps/dm/app/(landing)/reuse/_sections/Hero.tsx +import styles from "../reuse.module.css"; + +export default function Hero() { + return ( +
+
+
+ +
+
+

물 재이용

+

+ 빗물, 오수, 하수처리수, 폐수처리수, 발전소 온배수를 재이용처리하여
+ 생활, 공업, 농업, 조경, 하천유지 등의 용도로 재이용합니다. +

+

+ 전국 하수 재이용률 15.6%(2024년 통계 기준),
+ 물재이용 활성화를 위한 재이용 수요신청, 매칭을 추진하고 있습니다. +

+
+
+
+ + scroll down +
+
+ ); +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/_sections/IntroCards.tsx b/apps/dm_new/app/(landing)/reuse/_sections/IntroCards.tsx new file mode 100644 index 000000000..070d9ba91 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_sections/IntroCards.tsx @@ -0,0 +1,86 @@ +// apps/dm/app/(landing)/reuse/_sections/IntroCards.tsx +"use client"; + +import Link from "next/link"; + +type IntroCardItem = { + title: string; + desc: string; + image: string; + href: string; +}; + +const INTRO_CARDS: IntroCardItem[] = [ + { + title: "물 재이용 법", + desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.", + image: "/images/landing/card_01.jpg", + href: "/reuse/intro/law", + }, + { + title: "친환경 수자원 빗물", + desc: "가뭄을 극복하고 홍수를 예방하기 위해 비가 내릴 때 빗물을 모아두었다가 사용합니다.", + image: "/images/landing/card_02.jpg", + href: "/reuse/intro/rain", + }, + { + title: "알뜰하게 재이용되는 중수", + desc: "한번 사용한 물을 중수도 시설을 통해 재생하여 개별적 또는 지역적으로 재이용할 수 있습니다.", + image: "/images/landing/card_03.jpg", + href: "/reuse/intro/greywater", + }, + { + title: "하수처리수 재이용", + desc: "사용하고 버려진 공공하수처리장으로 모여진 물을 용도에 맞는 처리 후 재이용 할 수 있습니다.", + image: "/images/landing/card_04.jpg", + href: "/reuse/intro/treated", + }, +]; + +export default function IntroCards() { + return ( +
+
+
+
+

물 재이용 소개

+
+
+

+ 지속 가능한 미래를 위한 약속, 깨끗한 물의 새로운 순환을 시작합니다. +

+
+ +
+
+ {INTRO_CARDS.map((card, idx) => ( + + {/* 배경 이미지 (이미 그라디언트 포함) */} + {card.title} + + {/* 텍스트 영역 */} +
+

+ {card.title} +

+

+ {card.desc} +

+
+ + ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/_sections/ProcessFlow.tsx b/apps/dm_new/app/(landing)/reuse/_sections/ProcessFlow.tsx new file mode 100644 index 000000000..761a79201 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_sections/ProcessFlow.tsx @@ -0,0 +1,22 @@ +// apps/dm/app/(landing)/reuse/_sections/ProcessFlow.tsx +import CaseTabsCarousel from "../_components/CaseTabsCarousel"; + + +export default function ProcessFlow() { + return ( +
+
+
+
+

물 재이용 사례

+
+
+
+ + {/* 탭메뉴 영역 */} + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/apply/layout.tsx b/apps/dm_new/app/(landing)/reuse/apply/layout.tsx new file mode 100644 index 000000000..e9e7e5cb0 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/apply/layout.tsx @@ -0,0 +1,13 @@ +// 예) apps/dm/app/(landing)/reuse/apply/layout.tsx +import type { ReactNode } from "react"; +import Lnb from "../_components/Lnb"; +import { LNB_DEMAND } from "../_constants/lnb"; + +export default function ApplyLayout({ children }: { children: ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/apps/dm_new/app/(landing)/reuse/apply/modal/WaterQualityRefModal.tsx b/apps/dm_new/app/(landing)/reuse/apply/modal/WaterQualityRefModal.tsx new file mode 100644 index 000000000..877eb7d8c --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/apply/modal/WaterQualityRefModal.tsx @@ -0,0 +1,99 @@ +// apps/dm/app/(landing)/reuse/apply/modal/WaterQualityRefModal.tsx (수질기준참고 모달) +"use client"; + +import { useEffect, useId } from "react"; +import Image from "next/image"; + +type Props = { + open: boolean; + onClose: () => void; + imageSrc: string; + title?: string; +}; + +export default function WaterQualityRefModal({ + open, + onClose, + imageSrc, + title = "수질 기준 참고", +}: Props) { + const titleId = useId(); + + useEffect(() => { + if (!open) return; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKeyDown); + + // 스크롤 잠금 + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + return () => { + window.removeEventListener("keydown", onKeyDown); + document.body.style.overflow = prev; + }; + }, [open, onClose]); + + if (!open) return null; + + return ( +
+ {/* Backdrop */} + + +

+ {title} +

+ +
+
+ + {/* Body (스크롤 영역) */} +
+
+ {/* 이미지가 길어도 여기서 스크롤 */} + +
+
+
+ + + ); +} diff --git a/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx new file mode 100644 index 000000000..2deb0334e --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx @@ -0,0 +1,350 @@ +// apps/dm/app/(landing)/reuse/apply/place/page.tsx +"use client"; + +import { useMemo, useState } from "react"; +import SubPageHeader from "../../_components/SubPageHeader"; + +type Option = { value: string; label: string }; +type SidoValue = "" | "seoul" | "busan" | "daegu" | "incheon" | "gyeonggi"; + +const SIDO: { value: SidoValue; label: string }[] = [ + { value: "", label: "선택" }, + { value: "seoul", label: "서울특별시" }, + { value: "busan", label: "부산광역시" }, + { value: "daegu", label: "대구광역시" }, + { value: "incheon", label: "인천광역시" }, + { value: "gyeonggi", label: "경기도" }, +]; + +const SIGUNGU_MAP: Record = { + "": [{ value: "", label: "선택" }], + seoul: [ + { value: "", label: "선택" }, + { value: "gangnam", label: "강남구" }, + { value: "yeongdeungpo", label: "영등포구" }, + { value: "mapo", label: "마포구" }, + ], + busan: [ + { value: "", label: "선택" }, + { value: "haeundae", label: "해운대구" }, + { value: "suyeong", label: "수영구" }, + ], + daegu: [ + { value: "", label: "선택" }, + { value: "suseong", label: "수성구" }, + { value: "dalseo", label: "달서구" }, + ], + incheon: [ + { value: "", label: "선택" }, + { value: "namdong", label: "남동구" }, + { value: "yeonsu", label: "연수구" }, + ], + gyeonggi: [ + { value: "", label: "선택" }, + { value: "bundang", label: "성남시 분당구" }, + { value: "suji", label: "용인시 수지구" }, + { value: "ilsandong", label: "고양시 일산동구" }, + ], +}; + +/* --------------------------- + * register/page.tsx 스타일과 맞춘 공통 UI (FormRow/Divider) + * -------------------------- */ + +function Divider() { + return
; +} + +function FormRow({ + label, + required, + children, +}: { + label: React.ReactNode; + required?: boolean; + children: React.ReactNode; +}) { + return ( +
+ {/* 라벨(좌측) */} +
+
+ {label} + {required ? * : null} +
+
+ + {/* 입력(우측) */} +
+ {children} +
+
+ ); +} + +function TextInput({ + value, + onChange, + placeholder, + type = "text", + disabled, + className = "", +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; + type?: string; + disabled?: boolean; + className?: string; +}) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + className={[ + "h-11 w-full rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800", + "placeholder:text-neutral-400 focus:border-neutral-400 focus:outline-none", + disabled ? "bg-neutral-100 text-neutral-400" : "", + className, + ].join(" ")} + /> + ); +} + +function SelectBox({ + value, + onChange, + options, + className = "", +}: { + value: T; + onChange: (v: T) => void; + options: { value: T; label: string }[]; + className?: string; +}) { + return ( + + ); +} + +export default function DemandPlaceRegisterPage() { + const [sido, setSido] = useState(""); + const [sigungu, setSigungu] = useState(""); + + const [company, setCompany] = useState(""); + const [bizNo, setBizNo] = useState(""); + const [workerNo, setWorkerNo] = useState(""); + const [noWorkerNo, setNoWorkerNo] = useState(false); + + const [zip, setZip] = useState(""); + const [addr1, setAddr1] = useState(""); + const [addr2, setAddr2] = useState(""); + + const [tel, setTel] = useState(""); + const [pw, setPw] = useState(""); + const [pw2, setPw2] = useState(""); + + // ✅ undefined 불가(Record + sido가 SidoValue) + const sigunguOptions = useMemo(() => SIGUNGU_MAP[sido], [sido]); + + const onSubmit = () => { + if (!sido || !sigungu) return alert("시도/시군구를 선택해 주세요."); + if (!company) return alert("회사명을 입력해 주세요."); + if (!bizNo) return alert("사업자등록번호를 입력해 주세요."); + if (!noWorkerNo && !workerNo) + return alert("종사자등록번호를 입력하거나 '해당없음'을 선택해 주세요."); + if (!addr1) return alert("사업자주소를 입력해 주세요."); + if (!tel) return alert("전화번호를 입력해 주세요."); + if (!pw) return alert("비밀번호를 입력해 주세요."); + if (pw !== pw2) return alert("비밀번호가 일치하지 않습니다."); + + alert("수요처 등록(샘플) 완료!"); + }; + + return ( +
+ + + {/* register/page.tsx와 동일한 테이블 룩앤필 */} +
+ + + value={sido} + onChange={(v) => { + setSido(v); + setSigungu(""); // 시도 변경 시 초기화 + }} + options={SIDO} + /> + + + + + + + + + + + +
+ +
+
+ + + + +
+ +
+
+ + + + +
+
+ +
+ + +
+
+ + + + +
+
+
+ +
+ +
+ + + +
+
+ + + + +
+ +
+
+ + + + +
+ +
+
+ + + + +
+ +
+
+
+ + {/* register/page.tsx와 동일하게 하단 중앙 버튼 */} +
+ +
+
+ ); +} diff --git a/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx new file mode 100644 index 000000000..07a8bf244 --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx @@ -0,0 +1,476 @@ +// apps/dm/app/(landing)/reuse/apply/register/page.tsx +"use client"; + +import { useMemo, useState, useRef } from "react"; +import SubPageHeader from "../../_components/SubPageHeader"; +import WaterQualityRefModal from "../modal/WaterQualityRefModal"; + +type PHRange = { min: string; max: string }; + +export default function DemandRegisterPage() { + const today = useMemo(() => { + const d = new Date(); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; + }, []); + + const [openRef, setOpenRef] = useState(false); + + // 기본정보 + const [reqDate, setReqDate] = useState(today); + const [reqQty, setReqQty] = useState(""); + const [usePurpose, setUsePurpose] = useState(""); + const [hopePrice, setHopePrice] = useState(""); + + // 요구수질 + const [coliform, setColiform] = useState(""); + const [noDetect, setNoDetect] = useState(false); + + const [tn, setTn] = useState(""); + const [residualCl, setResidualCl] = useState(""); + const [tp, setTp] = useState(""); + const [turbidity, setTurbidity] = useState(""); + const [ph, setPh] = useState({ min: "", max: "" }); + const [bod, setBod] = useState(""); + const [chloride, setChloride] = useState(""); + const [odor, setOdor] = useState("불쾌하지 않을 것"); + const [ec, setEc] = useState(""); + const [color, setColor] = useState(""); + const [remark, setRemark] = useState(""); + + // 공급처 후보지 선택(필수아님) + const [cand1, setCand1] = useState(""); + const [cand2, setCand2] = useState(""); + const [cand3, setCand3] = useState(""); + + return ( + <> + +
+ + {/* 서브 타이틀 + 브레드크럼 */} + + + {/* 컨텐츠 영역 */} +
+ + {/* 기본정보 테이블 */} +
+ + + + + + + + + + + + + + + + + + + + + +
+ + {/* 요구수질 섹션 타이틀 바 */} +
+
+ 요구수질 + + {/* 수질기준참고 모달 */} + setOpenRef(false)} + imageSrc="/images/landing/water_quality_ref_1.png" + /> +
+
+ + {/* 요구수질 테이블 */} +
+ {/* 1행 */} + + + +
+ } + rightLabel="총질소(T-N)(mg/L)" + rightControl={ + + } + /> + + + {/* 2행 */} + } + rightLabel="총인(T-P)(mg/L)" + rightControl={ + + } + /> + + + {/* 3행 */} + + } + rightLabel="수소이온농도(pH)" + rightControl={ +
+ setPh((p) => ({ ...p, min: v }))} + className="w-32" + /> + ~ + setPh((p) => ({ ...p, max: v }))} + className="w-32" + /> +
+ } + /> + + + {/* 4행 */} + + 생물화학적산소요구량 +
+ (BOD)(mg/L) + + } + leftRequired + leftControl={} + rightLabel="염화물(mgCL/L)" + rightControl={} + /> + + + {/* 5행 */} + setOdor(e.target.value)} + className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500" + > + + + + + } + rightLabel="전기전도도(µs/cm)" + rightControl={} + /> + + + {/* 6행 */} + } + rightLabel="비고" + rightControl={ +