From 811dedd12e3b9f3db482d9157bdf978ad00a8da4 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Tue, 13 May 2025 21:45:50 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20props=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20useMapStore=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/BottomSheet.tsx | 30 +++++---- src/pages/MapPage/MapPage.tsx | 62 +------------------ src/pages/MapPage/components/InfoTable.tsx | 7 ++- .../MapPage/components/map/MapComponent.tsx | 30 ++++----- 4 files changed, 42 insertions(+), 87 deletions(-) diff --git a/src/pages/MapPage/BottomSheet.tsx b/src/pages/MapPage/BottomSheet.tsx index 984a08a..400351a 100644 --- a/src/pages/MapPage/BottomSheet.tsx +++ b/src/pages/MapPage/BottomSheet.tsx @@ -1,22 +1,28 @@ import { useRef, TouchEvent } from 'react'; import styled from 'styled-components'; import InfoTable from './components/InfoTable'; -import { BottomSheetProps } from './interface'; import ListContainer from './components/ListContainer'; import CommentList from './components/comment/CommentList'; import NoInfo from './components/NoInfo'; import SearchResult from './components/SearchResult'; import { Slope } from '../../apis/slopeMap'; +import { useMapStore } from './mapStore'; + +const BottomSheet = () => { + const { + slopeData, + selectedMarkerId, + chooseSelectItem, + bottomSheetHeight, + setBottomSheetHeight, + searchMod, + } = useMapStore(); + + const height = bottomSheetHeight; + const setHeight = setBottomSheetHeight; + const selectItem = + selectedMarkerId !== null ? slopeData[selectedMarkerId] : null; -const BottomSheet: React.FC = ({ - slopeData, - selectItem, - onItemClick, - height, - setHeight, - onCloseInfo, - searchMod, -}) => { const startY = useRef(0); const currentHeight = useRef(200); //현재 높이 const isDragging = useRef(false); //드래그 상태 @@ -128,7 +134,7 @@ const BottomSheet: React.FC = ({ if (selectItem !== null) { return (
- +
); @@ -162,7 +168,7 @@ const BottomSheet: React.FC = ({ key={index} item={item} onClick={() => { - onItemClick(item, index); + chooseSelectItem(item, index); }} > )) diff --git a/src/pages/MapPage/MapPage.tsx b/src/pages/MapPage/MapPage.tsx index 2519f4b..3ee721d 100644 --- a/src/pages/MapPage/MapPage.tsx +++ b/src/pages/MapPage/MapPage.tsx @@ -9,22 +9,7 @@ import SearchComponent from './components/map/Search'; import { useMapStore } from './mapStore'; import ButtonGroup from './components/ButtonGroup'; const MapPage = () => { - const { - selectedMarkerId, - allTextShow, - userLocation, - slopeData, - searchMod, - bottomSheetHeight, - mapInstance, - setBottomSheetHeight, - fetchSlopes, - handleSearch, - chooseSelectItem, - setUserLocation, - setMapInstance, - setSelectedMarkerId, - } = useMapStore(); + const { userLocation, searchMod, fetchSlopes, handleSearch } = useMapStore(); useEffect(() => { if (!searchMod) fetchSlopes(); @@ -33,50 +18,9 @@ const MapPage = () => { return ( <> - - { - setSelectedMarkerId(null); - }} - searchMod={searchMod} - /> - + + - {/* { - setAllTextShow(!allTextShow); - }} - > - {allTextShow ? '위성지도' : '일반지도'} - - { - setAllTextShow(!allTextShow); - }} - > - {allTextShow ? '전체표기' : '개별표기'} - - - - */} diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx index 02ae269..a2b51b1 100644 --- a/src/pages/MapPage/components/InfoTable.tsx +++ b/src/pages/MapPage/components/InfoTable.tsx @@ -1,7 +1,12 @@ import styled from 'styled-components'; import { InfotableProps } from '../interface'; +import { useMapStore } from '../mapStore'; -const InfoTable: React.FC = ({ selectItem, onCloseInfo }) => { +const InfoTable: React.FC = ({ selectItem }) => { + const { setSelectedMarkerId } = useMapStore(); + const onCloseInfo = () => { + setSelectedMarkerId(null); + }; if (!selectItem) return null; const grade = selectItem.priority?.grade?.includes('A') ? 'A' diff --git a/src/pages/MapPage/components/map/MapComponent.tsx b/src/pages/MapPage/components/map/MapComponent.tsx index a42e06e..280ba8c 100644 --- a/src/pages/MapPage/components/map/MapComponent.tsx +++ b/src/pages/MapPage/components/map/MapComponent.tsx @@ -12,7 +12,6 @@ import CmarkerIcon from '../../../../assets/c.webp'; import DmarkerIcon from '../../../../assets/d.webp'; import FmarkerIcon from '../../../../assets/f.webp'; import UserPosIcon from '../../../../assets/current_position.png'; -import { MapComponentProps } from '../../interface'; import { MapTypeId, useMapStore } from '../../mapStore'; declare global { interface Window { @@ -22,17 +21,18 @@ declare global { } } -const MapComponent: React.FC = ({ - selectedMarkerId, - escarpmentData, - allTextShow, - userLocation, - setUserLocation, - //추가 - mapInstance, - setMapInstance, - onMarkerClick, -}) => { +const MapComponent = () => { + const { + selectedMarkerId, + slopeData, + allTextShow, + userLocation, + setUserLocation, + mapInstance, + setMapInstance, + chooseSelectItem, + } = useMapStore(); + const { mapTypeId, setIsMapReady } = useMapStore(); const navermaps = useNavermaps(); const [_errorMessage, setErrorMessage] = useState(null); @@ -188,8 +188,8 @@ const MapComponent: React.FC = ({ `, }} /> - {escarpmentData.length > 0 - ? escarpmentData.map((item, index) => { + {slopeData.length > 0 + ? slopeData.map((item, index) => { // console.log(item); const grade = item.priority?.grade.includes('A') ? 'A' @@ -247,7 +247,7 @@ const MapComponent: React.FC = ({ anchor: new navermaps.Point(16, 16), }} onClick={() => { - onMarkerClick(item, index); + chooseSelectItem(item, index); }} /> ); From f239d0e8679897f9e43df128f270c0e975873809 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Tue, 13 May 2025 21:48:35 +0900 Subject: [PATCH 02/13] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/components/ButtonGroup.tsx | 81 -------------------- 1 file changed, 81 deletions(-) diff --git a/src/pages/MapPage/components/ButtonGroup.tsx b/src/pages/MapPage/components/ButtonGroup.tsx index f061b9b..49dd4b6 100644 --- a/src/pages/MapPage/components/ButtonGroup.tsx +++ b/src/pages/MapPage/components/ButtonGroup.tsx @@ -1,84 +1,3 @@ -// import styled from 'styled-components' -// import { useMapStore } from '../mapStore'; -// import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; - -// const ButtonGroup = () => { -// const {allTextShow,setAllTextShow,moveToMyLocation}=useMapStore(); -// return ( -// -// { -// setAllTextShow(!allTextShow); -// }} -// > -// {allTextShow ? '위성지도' : '일반지도'} -// -// { -// setAllTextShow(!allTextShow); -// }} -// > -// {allTextShow ? '전체표기' : '개별표기'} -// -// -// -// -// -// ); -// }; - -// export default ButtonGroup; - -// const Container=styled.div` -// position: absolute; -// top: 50px; -// right: 10px; -// ` -// const AllShowButton = styled.button<{ $isSelect: boolean }>` -// position: absolute; -// top: 50px; -// right: 10px; -// border: none; -// border-radius: 8px; -// height: 30px; -// padding: 5px 10px; -// box-shadow: ${({ theme }) => theme.shadows.sm}; -// font-weight: ${({ theme }) => theme.fonts.weights.bold}; -// font-size: ${({ theme }) => theme.fonts.sizes.ms}; -// background-color: ${({ $isSelect, theme }) => -// $isSelect ? theme.colors.primaryDark : '#fff'}; -// color: ${({ $isSelect, theme }) => -// !$isSelect ? theme.colors.primaryDark : '#fff'}; -// &:focus { -// outline: none; -// } -// transition: all 0.15s ease-in-out; - -// &:active { -// transform: scale(1.1); -// } -// `; - -// const MyPosition = styled.button` -// position: absolute; -// top: 90px; -// right: 10px; -// border: none; -// border-radius: 8px; -// padding: 5px 10px; -// box-shadow: ${({ theme }) => theme.shadows.sm}; -// font-weight: 550; -// background-color: #fff; -// transition: all 0.15s ease-in-out; -// &:hover { -// background-color: ${({ theme }) => theme.colors.grey[200]}; -// } -// &:active { -// transform: scale(1.1); -// } -// `; import styled from 'styled-components'; import { useMapStore, MapTypeId } from '../mapStore'; import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; From 9bb882a1028406e8324f41343185a0176efc000d Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Tue, 13 May 2025 21:49:57 +0900 Subject: [PATCH 03/13] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/mapStore.ts | 132 ---------------------------------- 1 file changed, 132 deletions(-) diff --git a/src/pages/MapPage/mapStore.ts b/src/pages/MapPage/mapStore.ts index e19bd83..21a3637 100644 --- a/src/pages/MapPage/mapStore.ts +++ b/src/pages/MapPage/mapStore.ts @@ -1,135 +1,3 @@ -// import { create } from 'zustand'; -// import { Slope, slopeMapAPI } from '../../apis/slopeMap'; - -// export interface MapState { -// // 맵 상태 -// selectedMarkerId: number | null; -// allTextShow: boolean; -// userLocation: naver.maps.LatLng | null; -// slopeData: Slope[]; -// searchMod: boolean; -// bottomSheetHeight: number; -// mapInstance: naver.maps.Map | null; -// mapTypeId: naver.maps.MapTypeId; - -// // 액션 -// setSelectedMarkerId: (id: number | null) => void; -// setAllTextShow: (show: boolean) => void; -// setUserLocation: (location: naver.maps.LatLng | null) => void; -// setSlopeData: (data: Slope[]) => void; -// setSearchMod: (mod: boolean) => void; -// setBottomSheetHeight: (height: number) => void; -// setMapInstance: (map: naver.maps.Map | null) => void; -// setMapTypeId: (typeId: naver.maps.MapTypeId) => void; - -// // 비즈니스 로직 -// fetchSlopes: () => Promise; -// handleSearch: (searchValue: string) => void; -// chooseSelectItem: (item: Slope, index: number) => void; -// moveToMyLocation: () => void; -// closeInfo: () => void; -// } - -// export const useMapStore = create((set, get) => ({ -// // 초기 상태 -// selectedMarkerId: null, -// allTextShow: false, -// userLocation: null, -// slopeData: [], -// searchMod: false, -// bottomSheetHeight: 200, -// mapInstance: null, -// mapTypeId: naver.maps.MapTypeId.NORMAL, - -// // 액션 -// setSelectedMarkerId: (id) => set({ selectedMarkerId: id }), -// setAllTextShow: (show) => set({ allTextShow: show }), -// setUserLocation: (location) => set({ userLocation: location }), -// setSlopeData: (data) => set({ slopeData: data }), -// setSearchMod: (mod) => set({ searchMod: mod }), -// setBottomSheetHeight: (height) => set({ bottomSheetHeight: height }), -// setMapInstance: (map) => set({ mapInstance: map }), -// setMapTypeId: (typeId) => set({ mapTypeId: typeId }), - -// // 비즈니스 로직 -// fetchSlopes: async () => { -// const { userLocation } = get(); -// if (!userLocation?.lat() || !userLocation?.lng()) return; - -// try { -// const data = await slopeMapAPI.fetchNearbySlopes( -// userLocation.lat(), -// userLocation.lng() -// ); -// set({ slopeData: data || [] }); -// } catch (error) { -// console.error('Error fetching slopes:', error); -// set({ slopeData: [] }); -// } -// }, - -// handleSearch: async (searchValue) => { -// const { fetchSlopes, userLocation, mapInstance } = get(); - -// if (searchValue === '') { -// set({ searchMod: false, selectedMarkerId: null }); -// fetchSlopes(); -// return; -// } - -// set({ selectedMarkerId: null, searchMod: true }); - -// if (!userLocation?.lat() || !userLocation?.lng()) return; - -// try { -// const data = await slopeMapAPI.searchSlopes( -// searchValue, -// userLocation.lat(), -// userLocation.lng() -// ); -// set({ slopeData: data || [] }); - -// if (mapInstance && data && data.length > 0) { -// const coordinates = data[0].location.coordinates.start.coordinates; -// mapInstance.panTo( -// new naver.maps.LatLng(coordinates[1], coordinates[0]) -// ); -// } -// } catch (error) { -// console.error('Error search slopes:', error); -// set({ slopeData: [] }); -// } -// }, - -// chooseSelectItem: (item, index) => { -// const { mapInstance, selectedMarkerId } = get(); - -// if (mapInstance && item) { -// const coordinates = item.location.coordinates.start.coordinates; -// mapInstance.panTo(new naver.maps.LatLng(coordinates[1], coordinates[0])); - -// set({ selectedMarkerId: selectedMarkerId === index ? null : index }); -// } -// }, - -// moveToMyLocation: () => { -// const { mapInstance, userLocation, fetchSlopes } = get(); - -// if (!mapInstance || !userLocation) return; - -// mapInstance.setZoom(15); - -// setTimeout(() => { -// mapInstance.panTo(userLocation); -// }, 100); - -// fetchSlopes(); -// }, - -// closeInfo: () => { -// set({ selectedMarkerId: null }); -// }, -// })); import { create } from 'zustand'; import { Slope, slopeMapAPI } from '../../apis/slopeMap'; From 3a4be6119512eb002dcf74a44011c1fbb48ac274 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Tue, 13 May 2025 22:27:39 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor:=20props=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20type->interface.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/BottomSheet.tsx | 2 +- src/pages/MapPage/MapPage.tsx | 4 +- src/pages/MapPage/components/ButtonGroup.tsx | 2 +- src/pages/MapPage/components/InfoTable.tsx | 4 +- .../MapPage/components/ListContainer.tsx | 3 +- src/pages/MapPage/components/NoInfo.tsx | 4 +- src/pages/MapPage/components/SearchResult.tsx | 12 +-- .../MapPage/components/map/LeftModal.tsx | 5 +- .../MapPage/components/map/MapComponent.tsx | 2 +- src/pages/MapPage/interface.ts | 97 ++++++++++++------- 10 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/pages/MapPage/BottomSheet.tsx b/src/pages/MapPage/BottomSheet.tsx index 400351a..c2a04ca 100644 --- a/src/pages/MapPage/BottomSheet.tsx +++ b/src/pages/MapPage/BottomSheet.tsx @@ -6,7 +6,7 @@ import CommentList from './components/comment/CommentList'; import NoInfo from './components/NoInfo'; import SearchResult from './components/SearchResult'; import { Slope } from '../../apis/slopeMap'; -import { useMapStore } from './mapStore'; +import { useMapStore } from '../../stores/mapStore'; const BottomSheet = () => { const { diff --git a/src/pages/MapPage/MapPage.tsx b/src/pages/MapPage/MapPage.tsx index 3ee721d..913c48c 100644 --- a/src/pages/MapPage/MapPage.tsx +++ b/src/pages/MapPage/MapPage.tsx @@ -1,12 +1,10 @@ import { useEffect } from 'react'; - +import { useMapStore } from '../../stores/mapStore'; import styled from 'styled-components'; - import BottomSheet from './BottomSheet'; import MapComponent from './components/map/MapComponent'; import SearchComponent from './components/map/Search'; -import { useMapStore } from './mapStore'; import ButtonGroup from './components/ButtonGroup'; const MapPage = () => { const { userLocation, searchMod, fetchSlopes, handleSearch } = useMapStore(); diff --git a/src/pages/MapPage/components/ButtonGroup.tsx b/src/pages/MapPage/components/ButtonGroup.tsx index 49dd4b6..c2e6f0e 100644 --- a/src/pages/MapPage/components/ButtonGroup.tsx +++ b/src/pages/MapPage/components/ButtonGroup.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { useMapStore, MapTypeId } from '../mapStore'; +import { useMapStore, MapTypeId } from '../../../stores/mapStore'; import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; const ButtonGroup = () => { diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx index a2b51b1..0abb75b 100644 --- a/src/pages/MapPage/components/InfoTable.tsx +++ b/src/pages/MapPage/components/InfoTable.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components'; import { InfotableProps } from '../interface'; -import { useMapStore } from '../mapStore'; +import { useMapStore } from '../../../stores/mapStore'; -const InfoTable: React.FC = ({ selectItem }) => { +const InfoTable = ({ selectItem }: InfotableProps) => { const { setSelectedMarkerId } = useMapStore(); const onCloseInfo = () => { setSelectedMarkerId(null); diff --git a/src/pages/MapPage/components/ListContainer.tsx b/src/pages/MapPage/components/ListContainer.tsx index 12c5b7d..fecab9e 100644 --- a/src/pages/MapPage/components/ListContainer.tsx +++ b/src/pages/MapPage/components/ListContainer.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import styled from 'styled-components'; import { ListProps } from '../interface'; -const ListContainer: React.FC = ({ item, onClick }) => { +const ListContainer = ({ item, onClick }: ListProps) => { if (!item) return null; const grade = item.priority?.grade.includes('A') ? 'A' diff --git a/src/pages/MapPage/components/NoInfo.tsx b/src/pages/MapPage/components/NoInfo.tsx index 3275554..dc10cf8 100644 --- a/src/pages/MapPage/components/NoInfo.tsx +++ b/src/pages/MapPage/components/NoInfo.tsx @@ -1,9 +1,7 @@ import styled from 'styled-components'; import ErrorOutlineRoundedIcon from '@mui/icons-material/ErrorOutlineRounded'; +import { NoInfoProps } from '../interface'; -interface NoInfoProps { - text: string; -} const NoInfo = ({ text }: NoInfoProps) => ( diff --git a/src/pages/MapPage/components/SearchResult.tsx b/src/pages/MapPage/components/SearchResult.tsx index 85577fc..6cde673 100644 --- a/src/pages/MapPage/components/SearchResult.tsx +++ b/src/pages/MapPage/components/SearchResult.tsx @@ -1,15 +1,5 @@ import styled from 'styled-components'; - -interface SearchResultProps { - resultCount: number; - gradeCount: { - A: number; - B: number; - C: number; - D: number; - F: number; - }; -} +import { SearchResultProps } from '../interface'; const SearchResult = ({ resultCount, gradeCount }: SearchResultProps) => { return ( diff --git a/src/pages/MapPage/components/map/LeftModal.tsx b/src/pages/MapPage/components/map/LeftModal.tsx index ee93168..f359a16 100644 --- a/src/pages/MapPage/components/map/LeftModal.tsx +++ b/src/pages/MapPage/components/map/LeftModal.tsx @@ -6,10 +6,7 @@ import { userAPI } from '../../../../apis/User'; import TermsofUseModal from './TermsofUseModal'; import ArrowBackIcon from '@mui/icons-material/ArrowBackIosNewRounded'; import { useNotificationStore } from '../../../../hooks/notificationStore'; -interface LeftModalProps { - isOpen: boolean; - onClose: () => void; -} +import { LeftModalProps } from '../../interface'; const LeftModal = ({ isOpen, onClose }: LeftModalProps) => { const [animationOpen, setAnimationOpen] = useState(false); diff --git a/src/pages/MapPage/components/map/MapComponent.tsx b/src/pages/MapPage/components/map/MapComponent.tsx index 280ba8c..7b4eb9b 100644 --- a/src/pages/MapPage/components/map/MapComponent.tsx +++ b/src/pages/MapPage/components/map/MapComponent.tsx @@ -12,7 +12,7 @@ import CmarkerIcon from '../../../../assets/c.webp'; import DmarkerIcon from '../../../../assets/d.webp'; import FmarkerIcon from '../../../../assets/f.webp'; import UserPosIcon from '../../../../assets/current_position.png'; -import { MapTypeId, useMapStore } from '../../mapStore'; +import { MapTypeId, useMapStore } from '../../../../stores/mapStore'; declare global { interface Window { ReactNativeWebView?: { diff --git a/src/pages/MapPage/interface.ts b/src/pages/MapPage/interface.ts index eab8932..6651915 100644 --- a/src/pages/MapPage/interface.ts +++ b/src/pages/MapPage/interface.ts @@ -1,27 +1,25 @@ import { Slope } from '../../apis/slopeMap'; -export interface MapComponentProps { - selectedMarkerId: number | null; - escarpmentData: Slope[]; - allTextShow: boolean; - userLocation: naver.maps.LatLng | null; - setUserLocation: (location: naver.maps.LatLng | null) => void; - mapInstance: naver.maps.Map | null; - setMapInstance: (map: naver.maps.Map | null) => void; - onMarkerClick: (item: Slope, index: number) => void; -} -export interface BottomSheetProps { - slopeData: Slope[]; - selectItem: Slope | null; - onItemClick: (item: Slope, index: number) => void; - height: number; - setHeight: (height: number) => void; - onCloseInfo: () => void; - searchMod: boolean; +export interface CommentData { + _id: string; + slopeId: string; + userId: UserInfo; + content: string; + imageUrls: string[]; + createdAt: string; + updatedAt: string; + __v: number; } +interface UserInfo { + _id: string; + name: string; + organization: string; + isAdmin: boolean; +} + +//props관련 export interface InfotableProps { selectItem: Slope | null; - onCloseInfo: () => void; } export interface ListProps { item: Slope | null; @@ -31,24 +29,57 @@ export interface ListProps { export interface SearchComponentProps { onSearch: (value: string) => void; } +export interface NoInfoProps { + text: string; +} +export interface CommentContainerProps { + comment: CommentData; + fetchComment: () => Promise; +} +export interface SearchResultProps { + resultCount: number; + gradeCount: { + A: number; + B: number; + C: number; + D: number; + F: number; + }; +} +export interface DeleteModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: () => void; +} +export interface CommentListProps { + slopeId: string; +} -interface UserInfo { +//모달 관련 + +export interface DeleteConfirmModalProps { + isOpen: boolean; + onClose: () => void; +} + +export interface UserDelete { _id: string; + isAdmin: string; name: string; organization: string; - isAdmin: boolean; + phone: string; } -export interface CommentData { - _id: string; - slopeId: string; - userId: UserInfo; - content: string; - imageUrls: string[]; - createdAt: string; - updatedAt: string; - __v: number; + +export interface LeftModalProps { + isOpen: boolean; + onClose: () => void; } -export interface CommentContainerProps { - comment: CommentData; - fetchComment: () => Promise; + +export interface PrivacyPolicyModalProps { + isOpen: boolean; + onClose: () => void; +} +export interface TermsofUseModalProps { + isOpen: boolean; + onClose: () => void; } From a0539e1e4b728ace47f1efcd4944789d2bc6308c Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Tue, 13 May 2025 22:28:55 +0900 Subject: [PATCH 05/13] refactor: modal type->interface.ts --- src/pages/MapPage/components/map/DeleteIdModal.tsx | 14 ++------------ .../MapPage/components/map/PrivacyPolicyModal.tsx | 6 +----- .../MapPage/components/map/TermsofUseModal.tsx | 6 +----- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/pages/MapPage/components/map/DeleteIdModal.tsx b/src/pages/MapPage/components/map/DeleteIdModal.tsx index 7a3b468..6b9ba5d 100644 --- a/src/pages/MapPage/components/map/DeleteIdModal.tsx +++ b/src/pages/MapPage/components/map/DeleteIdModal.tsx @@ -2,18 +2,8 @@ import styled from 'styled-components'; import { useState, useEffect } from 'react'; import { userAPI } from '../../../../apis/User'; import { useNotificationStore } from '../../../../hooks/notificationStore'; -interface DeleteConfirmModalProps { - isOpen: boolean; - onClose: () => void; -} - -interface UserDelete { - _id: string; - isAdmin: string; - name: string; - organization: string; - phone: string; -} +import { DeleteConfirmModalProps, UserDelete } from '../../interface'; + const DeleteIdModal = ({ isOpen, onClose }: DeleteConfirmModalProps) => { const [inputValue, setInputValue] = useState(''); const [user, setUser] = useState(null); diff --git a/src/pages/MapPage/components/map/PrivacyPolicyModal.tsx b/src/pages/MapPage/components/map/PrivacyPolicyModal.tsx index c67c5ac..3b3103e 100644 --- a/src/pages/MapPage/components/map/PrivacyPolicyModal.tsx +++ b/src/pages/MapPage/components/map/PrivacyPolicyModal.tsx @@ -1,10 +1,6 @@ import { useEffect, useState } from 'react'; import styled from 'styled-components'; - -interface PrivacyPolicyModalProps { - isOpen: boolean; - onClose: () => void; -} +import { PrivacyPolicyModalProps } from '../../interface'; const PrivacyPolicyModal = ({ isOpen, onClose }: PrivacyPolicyModalProps) => { const [animationOpen, setAnimationOpen] = useState(false); diff --git a/src/pages/MapPage/components/map/TermsofUseModal.tsx b/src/pages/MapPage/components/map/TermsofUseModal.tsx index 3b5ae11..9035447 100644 --- a/src/pages/MapPage/components/map/TermsofUseModal.tsx +++ b/src/pages/MapPage/components/map/TermsofUseModal.tsx @@ -1,11 +1,7 @@ import { useEffect, useState } from 'react'; +import { TermsofUseModalProps } from '../../interface'; import styled from 'styled-components'; -interface TermsofUseModalProps { - isOpen: boolean; - onClose: () => void; -} - const TermsofUseModal = ({ isOpen, onClose }: TermsofUseModalProps) => { const [animationOpen, setAnimationOpen] = useState(false); From a7255e5dea98d970db94758b99c9fdd0f377259f Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Thu, 15 May 2025 21:46:50 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20store=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stores/commentStore.ts | 25 +++++++++++++++++++++++ src/{pages/MapPage => stores}/mapStore.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/stores/commentStore.ts rename src/{pages/MapPage => stores}/mapStore.ts (98%) diff --git a/src/stores/commentStore.ts b/src/stores/commentStore.ts new file mode 100644 index 0000000..f7e3780 --- /dev/null +++ b/src/stores/commentStore.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +interface commentStore { + //초기상태 + isMoreOpen: boolean; + isDeleteOpen: boolean; + isModiOpen: boolean; + //액션 + setIsMore: (value: boolean) => void; + setIsDelete: (value: boolean) => void; + setIsModi: (value: boolean) => void; +} + +export const useCommentStore = create((set) => ({ + //초기상태 + isMoreOpen: false, + isDeleteOpen: false, + isModiOpen: false, + //액션 + setIsMore: (value) => set({ isMoreOpen: value }), + setIsDelete: (value) => set({ isDeleteOpen: value }), + setIsModi: (value) => set({ isModiOpen: value }), + + //비즈니스 로직 +})); diff --git a/src/pages/MapPage/mapStore.ts b/src/stores/mapStore.ts similarity index 98% rename from src/pages/MapPage/mapStore.ts rename to src/stores/mapStore.ts index 21a3637..7a19f5f 100644 --- a/src/pages/MapPage/mapStore.ts +++ b/src/stores/mapStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { Slope, slopeMapAPI } from '../../apis/slopeMap'; +import { Slope, slopeMapAPI } from '../apis/slopeMap'; // 문자열 상수로 맵 타입 정의 export enum MapTypeId { From 7ba83afbbc49b990106404562bd9cc22f169e50e Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Thu, 15 May 2025 21:47:16 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20comment=20store=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95=20=EB=B0=8F=20=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/comment/CommentContainer.tsx | 14 +++-------- .../components/comment/CommentCreateModal.tsx | 24 ++++++++----------- .../components/comment/CommentDeleteModal.tsx | 19 +++++++-------- .../components/comment/CommentList.tsx | 17 ++++--------- .../components/comment/CommentUpdateModal.tsx | 24 ++++++++----------- src/pages/MapPage/interface.ts | 12 +++++++--- 6 files changed, 45 insertions(+), 65 deletions(-) diff --git a/src/pages/MapPage/components/comment/CommentContainer.tsx b/src/pages/MapPage/components/comment/CommentContainer.tsx index 90aad9d..1cbca43 100644 --- a/src/pages/MapPage/components/comment/CommentContainer.tsx +++ b/src/pages/MapPage/components/comment/CommentContainer.tsx @@ -1,15 +1,13 @@ -import { useState } from 'react'; import styled from 'styled-components'; import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded'; import { CommentContainerProps } from '../../interface'; import { slopeCommentAPI } from '../../../../apis/slopeMap'; import CommentDeleteModal from './CommentDeleteModal'; import CommentUpdateModal from './CommentUpdateModal'; +import { useCommentStore } from '../../../../stores/commentStore'; const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { - const [isMore, setIsMore] = useState(false); - const [isDelete, setIsDelete] = useState(false); - const [isModi, setIsModi] = useState(false); + const { isMoreOpen, setIsMore, setIsDelete, setIsModi } = useCommentStore(); //수정 삭제에 접근 가능한지 const accessible = (): boolean => { @@ -42,10 +40,6 @@ const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { <> {/* 삭제모달 */} { - setIsDelete(false); - }} onSubmit={() => { handleDelete(); setIsMore(false); @@ -54,8 +48,6 @@ const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { /> {/*수정모달 */} setIsModi(false)} onSubmit={handleUpdate} defaultComment={comment.content} defaultImages={comment.imageUrls.map( @@ -87,7 +79,7 @@ const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { sx={{ width: '20px', height: '20px', opacity: '0.6' }} /> - {isMore && ( + {isMoreOpen && ( <> { diff --git a/src/pages/MapPage/components/comment/CommentCreateModal.tsx b/src/pages/MapPage/components/comment/CommentCreateModal.tsx index 92825bb..5e14d10 100644 --- a/src/pages/MapPage/components/comment/CommentCreateModal.tsx +++ b/src/pages/MapPage/components/comment/CommentCreateModal.tsx @@ -1,12 +1,8 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { useNotificationStore } from '../../../../hooks/notificationStore'; - -interface CommentAddModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: (comment: string, images: File[]) => void; -} +import { CommentAddModalProps } from '../../interface'; +import { useCommentStore } from '../../../../stores/commentStore'; interface ImageFile { file: File; @@ -23,11 +19,11 @@ interface MobileImageAsset { dataUrl?: string; } -const CommentAddModal = ({ - isOpen, - onClose, - onSubmit, -}: CommentAddModalProps) => { +const CommentAddModal = ({ onSubmit }: CommentAddModalProps) => { + const { isMoreOpen, setIsMore } = useCommentStore(); + const onClose = () => { + setIsMore(false); + }; const [comment, setComment] = useState(''); const [images, setImages] = useState([]); //전역 알람 @@ -38,11 +34,11 @@ const CommentAddModal = ({ typeof window !== 'undefined' && window.ReactNativeWebView != null; //모달이 열릴 때 초기상태로 복원 useEffect(() => { - if (isOpen) { + if (isMoreOpen) { setComment(''); setImages([]); } - }, [isOpen]); + }, [isMoreOpen]); //사진 권한 요청 useEffect(() => { @@ -243,7 +239,7 @@ const CommentAddModal = ({ }; return ( - +

코멘트 작성

diff --git a/src/pages/MapPage/components/comment/CommentDeleteModal.tsx b/src/pages/MapPage/components/comment/CommentDeleteModal.tsx index 05c5d42..bf3c0e8 100644 --- a/src/pages/MapPage/components/comment/CommentDeleteModal.tsx +++ b/src/pages/MapPage/components/comment/CommentDeleteModal.tsx @@ -1,17 +1,14 @@ import styled from 'styled-components'; +import { DeleteModalProps } from '../../interface'; +import { useCommentStore } from '../../../../stores/commentStore'; -interface DeleteModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: () => void; -} -const CommentDeleteModal = ({ - isOpen, - onClose, - onSubmit, -}: DeleteModalProps) => { +const CommentDeleteModal = ({ onSubmit }: DeleteModalProps) => { + const { isDeleteOpen, setIsDelete } = useCommentStore(); + const onClose = () => { + setIsDelete(false); + }; return ( - +

삭제 확인

diff --git a/src/pages/MapPage/components/comment/CommentList.tsx b/src/pages/MapPage/components/comment/CommentList.tsx index 3c1d9e8..5183075 100644 --- a/src/pages/MapPage/components/comment/CommentList.tsx +++ b/src/pages/MapPage/components/comment/CommentList.tsx @@ -3,16 +3,13 @@ import styled from 'styled-components'; import { useEffect, useState } from 'react'; import CommentAddModal from './CommentCreateModal'; import { slopeCommentAPI } from '../../../../apis/slopeMap'; -import { CommentData } from '../../interface'; +import { CommentData, CommentListProps } from '../../interface'; import NoInfo from '../NoInfo'; - -interface CommentListProps { - slopeId: string; -} +import { useCommentStore } from '../../../../stores/commentStore'; const CommentList = ({ slopeId }: CommentListProps) => { const [commentData, setCommentData] = useState([]); - const [isCreate, setIsCreate] = useState(false); + const { setIsMore } = useCommentStore(); const handleCreate = async (content: string, images: File[]) => { try { const formData = new FormData(); @@ -29,7 +26,7 @@ const CommentList = ({ slopeId }: CommentListProps) => { // 생성 후 코멘트 목록 다시 조회 const newData = await slopeCommentAPI.getComment(slopeId); setCommentData(newData); - setIsCreate(false); + setIsMore(false); } catch (error) { console.log('코멘트 생성 실패', error); } @@ -50,15 +47,11 @@ const CommentList = ({ slopeId }: CommentListProps) => { return ( { - setIsCreate(false); - }} onSubmit={(content, images) => handleCreate(content, images)} /> { - setIsCreate(true); + setIsMore(true); }} > 글 등록/사진 등록 diff --git a/src/pages/MapPage/components/comment/CommentUpdateModal.tsx b/src/pages/MapPage/components/comment/CommentUpdateModal.tsx index 314074c..7b58444 100644 --- a/src/pages/MapPage/components/comment/CommentUpdateModal.tsx +++ b/src/pages/MapPage/components/comment/CommentUpdateModal.tsx @@ -1,15 +1,8 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { useNotificationStore } from '../../../../hooks/notificationStore'; - -interface CommentUpdateModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: (formData: FormData) => void; - defaultComment: string; - defaultImages: string[]; - commentId: string; -} +import { useCommentStore } from '../../../../stores/commentStore'; +import { CommentUpdateModalProps } from '../../interface'; interface ImageFile { file: File | null; @@ -28,13 +21,15 @@ interface MobileImageAsset { dataUrl?: string; } const CommentUpdateModal = ({ - isOpen, - onClose, onSubmit, defaultComment, defaultImages, commentId, }: CommentUpdateModalProps) => { + const { isModiOpen, setIsModi } = useCommentStore(); + const onClose = () => { + setIsModi(false); + }; const [comment, setComment] = useState(defaultComment); const [images, setImages] = useState(() => defaultImages.map((url) => ({ @@ -45,6 +40,7 @@ const CommentUpdateModal = ({ url: url, })) ); + const isReactNativeWebView = typeof window !== 'undefined' && window.ReactNativeWebView != null; const showNotification = useNotificationStore( @@ -52,7 +48,7 @@ const CommentUpdateModal = ({ ); // 모달이 열릴 때마다 초기 상태로 재설정 useEffect(() => { - if (isOpen) { + if (isModiOpen) { setComment(defaultComment); setImages( defaultImages.map((url) => ({ @@ -64,7 +60,7 @@ const CommentUpdateModal = ({ })) ); } - }, [isOpen]); + }, [isModiOpen]); // 디버깅용 로그 useEffect(() => { @@ -314,7 +310,7 @@ const CommentUpdateModal = ({ } }; return ( - +

코멘트 수정

diff --git a/src/pages/MapPage/interface.ts b/src/pages/MapPage/interface.ts index 6651915..76cef0d 100644 --- a/src/pages/MapPage/interface.ts +++ b/src/pages/MapPage/interface.ts @@ -46,15 +46,21 @@ export interface SearchResultProps { F: number; }; } +export interface CommentAddModalProps { + onSubmit: (comment: string, images: File[]) => void; +} export interface DeleteModalProps { - isOpen: boolean; - onClose: () => void; onSubmit: () => void; } export interface CommentListProps { slopeId: string; } - +export interface CommentUpdateModalProps { + onSubmit: (formData: FormData) => void; + defaultComment: string; + defaultImages: string[]; + commentId: string; +} //모달 관련 export interface DeleteConfirmModalProps { From 17ba2a3904f9ad1b98048ce0f1a08da080ba17d1 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 17 May 2025 21:00:17 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20=EC=97=AC=EB=9F=AC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EB=B0=8F=20=EC=A0=84=EC=B2=B4=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSlope/components/DeleteModal.tsx | 35 ++++++++- .../StepSlope/components/table/DataTable.tsx | 48 +++++++++--- .../components/table/TableAction.tsx | 22 +++++- .../components/table/TableCheckbox.tsx | 53 ++++++++++++++ .../components/table/TableModals.tsx | 3 + .../StepSlope/components/table/coloums.ts | 31 +++++++- .../components/table/store/tableStore.ts | 37 ++++++++-- .../StepSlope/pages/SteepSlopeLookUp.tsx | 73 +++++++++++++++++-- 8 files changed, 272 insertions(+), 30 deletions(-) create mode 100644 src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx diff --git a/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx b/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx index 3977664..fe809db 100644 --- a/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx @@ -1,11 +1,13 @@ import styled from 'styled-components'; import { Slope } from '../../../../apis/slopeMap'; +import { useEffect, useState } from 'react'; interface DeleteConfirmModalProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; selectedRow: Slope | null; + selectedRows: Slope[]; } const DeleteConfirmModal = ({ @@ -13,13 +15,27 @@ const DeleteConfirmModal = ({ onClose, onConfirm, selectedRow, + selectedRows = [], }: DeleteConfirmModalProps) => { + const [count, setCount] = useState(0); + const [isMulti, setIsMulti] = useState(false); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onConfirm(); onClose(); }; - + useEffect(() => { + if (isOpen) { + const selectedCount = selectedRows.length; + setCount(selectedCount); + setIsMulti(selectedCount > 1); + console.log('Modal opened with:', { + selectedCount, + isMulti: selectedCount > 1, + selectedRows, + }); + } + }, [isOpen, selectedRows]); return ( @@ -29,8 +45,20 @@ const DeleteConfirmModal = ({
- {selectedRow?.name}를 삭제하시려면 입력창에 - 삭제라고 입력해 주세요. + {isMulti ? ( + // 다중 선택 시 메시지 + <> + 선택한 {count}개의 항목을 삭제하시려면 + 입력창에 + 삭제라고 입력해 주세요. + + ) : ( + // 단일 선택 시 메시지 + <> + {selectedRow?.name}를 삭제하시려면 입력창에 + 삭제라고 입력해 주세요. + + )} ); }; - const ModalOverlay = styled.div<{ $isOpen: boolean }>` display: ${(props) => (props.$isOpen ? 'flex' : 'none')}; position: fixed; diff --git a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx index b509695..5e26230 100644 --- a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx @@ -32,6 +32,22 @@ const DataTable: React.FC = ({ rowVirtualizer.getTotalSize() - (paddingTop + rowVirtualizer.getVirtualItems().length * 40); + // 셀 렌더링 함수 + const renderCell = (cell: any) => { + // 체크박스 열인 경우 별도 처리 + if (cell.column.id === 'select') { + // 컬럼 정의에서 cell 함수를 호출 + const cellContent = (cell.column.columnDef as any).cell; + if (typeof cellContent === 'function') { + return cellContent({ row: cell.row, table, column: cell.column }); + } + return cellContent; + } + + // 일반 셀 + return cell.getValue() as string; + }; + return ( @@ -39,7 +55,14 @@ const DataTable: React.FC = ({ {table.getHeaderGroups()[0].headers.map((header) => ( - {header.column.columnDef.header as string} + {header.column.id === 'select' + ? typeof (header.column.columnDef as any).header === + 'function' + ? ((header.column.columnDef as any).header as any)({ + table, + }) + : (header.column.columnDef as any).header + : (header.column.columnDef.header as string)} = ({ return ( { - setSelectedRow( - selectedRow?.managementNo === row.original.managementNo - ? null - : row.original - ); + onClick={(e) => { + // 체크박스 열이 아닌 부분을 클릭했을 때만 기존 선택 동작 수행 + if ( + !(e.target as HTMLElement).closest('input[type="checkbox"]') + ) { + setSelectedRow( + selectedRow?.managementNo === row.original.managementNo + ? null + : row.original + ); + } }} $selected={ - selectedRow?.managementNo === row.original.managementNo + selectedRow?.managementNo === row.original.managementNo || + row.getIsSelected?.() || + false } > {row.getVisibleCells().map((cell) => ( - {cell.getValue() as string} + {renderCell(cell)} ))} diff --git a/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx b/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx index 6d848b9..92ff5f7 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx @@ -2,27 +2,41 @@ import styled from 'styled-components'; import React from 'react'; import { Slope } from '../../../../../apis/slopeMap'; import LoadingMessage from '../../../components/LoadingMessage'; + interface TableActionProps { isLoading: boolean; selectedRow: Slope | null; + // 선택된 행들 배열 추가 + selectedRows: Slope[]; openEditModal: () => void; openDeleteModal: () => void; } + const TableAction: React.FC = ({ isLoading, selectedRow, + selectedRows, openEditModal, openDeleteModal, }) => { + // 선택된 행 수 + const selectedCount = selectedRows?.length || 0; + return ( <> {isLoading && } - {/* 하단 버튼 컨테이너 */} - {selectedRow && ( + + {/* 하단 버튼 컨테이너 - 단일 선택 또는 다중 선택 시 표시 */} + {(selectedRow || selectedCount > 0) && ( - 수정 + {/* 단일 선택일 때만 수정 버튼 표시 */} + {selectedCount <= 1 && selectedRow && ( + 수정 + )} + + {/* 삭제 버튼은 항상 표시, 다중 선택 시 개수 표시 */} - 삭제 + {selectedCount > 1 ? `${selectedCount}개 항목 삭제` : '삭제'} )} diff --git a/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx b/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx new file mode 100644 index 0000000..e546ff9 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx @@ -0,0 +1,53 @@ +import React, { useRef, useEffect } from 'react'; +import styled from 'styled-components'; + +interface TableCheckboxProps { + indeterminate?: boolean; + checked?: boolean; + disabled?: boolean; + onChange: (e: React.ChangeEvent) => void; +} + +const TableCheckbox: React.FC = ({ + indeterminate = false, + checked = false, + disabled = false, + onChange, + ...rest +}) => { + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + ref.current.indeterminate = indeterminate; + } + }, [indeterminate]); + + return ( + + + + ); +}; + +export default TableCheckbox; + +const CheckboxContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const StyledCheckbox = styled.input` + cursor: pointer; + width: 16px; + height: 16px; +`; diff --git a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx index 1a4e4e1..478d249 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx @@ -17,6 +17,7 @@ interface TableModalProps { closeDeleteModal: () => void; handleDelete: () => void; selectedRow: Slope | null; + selectedRows: Slope[]; isEditModalOpen: boolean; closeEditModal: () => void; handleEdit: (updatedSlope: Slope) => void; @@ -33,6 +34,7 @@ const TableModals: React.FC = ({ closeDeleteModal, handleDelete, selectedRow, + selectedRows, isEditModalOpen, closeEditModal, handleEdit, @@ -49,6 +51,7 @@ const TableModals: React.FC = ({ isOpen={isDeleteModalOpen} onClose={closeDeleteModal} onConfirm={handleDelete} + selectedRows={selectedRows} selectedRow={selectedRow} /> (); export const getSlopeColumns = () => [ + columnHelper.display({ + id: 'select', + header: ({ table }) => { + // 이벤트 핸들러를 올바르게 정의 + const handleToggleAll = (e: React.ChangeEvent) => { + table.toggleAllRowsSelected?.(e.target.checked); + }; + + return React.createElement(TableCheckbox, { + checked: table.getIsAllRowsSelected?.() || false, + indeterminate: table.getIsSomeRowsSelected?.() || false, + onChange: handleToggleAll, // 함수 자체를 전달, 호출 결과 아님 + }); + }, + cell: ({ row }) => { + // 이벤트 핸들러를 올바르게 정의 + const handleToggleRow = (e: React.ChangeEvent) => { + row.toggleSelected?.(e.target.checked); + }; + + return React.createElement(TableCheckbox, { + checked: row.getIsSelected?.() || false, + disabled: !row.getCanSelect?.() || false, + onChange: handleToggleRow, // 함수 자체를 전달, 호출 결과 아님 + }); + }, + size: 50, + }), columnHelper.accessor( (_row, index) => { return index + 1; diff --git a/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts b/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts index b874e04..f71131e 100644 --- a/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts +++ b/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts @@ -3,6 +3,7 @@ import { VisibilityState, ColumnSizingState, OnChangeFn, + RowSelectionState, } from '@tanstack/react-table'; import { getDefaultColumnVisibility } from '..//coloums'; @@ -17,14 +18,18 @@ export interface TableState { selectedRegion: { city: string; county: string } | null; grade: string; + // 행 선택 상태 추가 + rowSelection: RowSelectionState; + // 모달 상태 isModalOpen: boolean; isRegionModalOpen: boolean; isDeleteModalOpen: boolean; isEditModalOpen: boolean; - // 선택된 행 - selectedRow: T | null; + // 선택된 행 - 단일 선택에서 다중 선택으로 변경 + selectedRow: T | null; // 기존 호환성을 위해 유지 + selectedRows: T[]; // 다중 선택을 위한 배열 추가 // 액션 - 타입 수정됨 setColumnVisibility: OnChangeFn; @@ -34,6 +39,11 @@ export interface TableState { setInputValue: (value: string) => void; setSelectedRegion: (region: { city: string; county: string } | null) => void; setGrade: (value: string) => void; + + // 행 선택 상태 설정 함수 추가 + setRowSelection: OnChangeFn; + setSelectedRows: (rows: T[]) => void; + openModal: () => void; closeModal: () => void; openRegionModal: () => void; @@ -58,12 +68,17 @@ export function createTableStore(initialState: Partial> = {}) { inputValue: '', selectedRegion: null, grade: '선택안함', + + // 행 선택 상태 초기화 + rowSelection: {}, + isModalOpen: false, isRegionModalOpen: false, isDeleteModalOpen: false, isEditModalOpen: false, selectedRow: null, + selectedRows: [], // 다중 선택 배열 초기화 // 액션 메서드들 - TanStack Table 호환 타입으로 수정 setColumnVisibility: (updaterOrValue) => { @@ -88,6 +103,19 @@ export function createTableStore(initialState: Partial> = {}) { } }, + // 행 선택 상태 설정 함수 추가 + setRowSelection: (updaterOrValue) => { + if (typeof updaterOrValue === 'function') { + set((state) => ({ + rowSelection: updaterOrValue(state.rowSelection), + })); + } else { + set({ rowSelection: updaterOrValue }); + } + }, + + setSelectedRows: (rows) => set({ selectedRows: rows }), + setTotalCount: (count) => set({ totalCount: count }), setSearchQuery: (query) => set({ searchQuery: query }), setInputValue: (value) => set({ inputValue: value }), @@ -111,12 +139,11 @@ export function createTableStore(initialState: Partial> = {}) { inputValue: '', selectedRegion: null, columnVisibility: getDefaultColumnVisibility(), + rowSelection: {}, // 선택 상태도 초기화 + selectedRows: [], // 선택된 행 배열도 초기화 }), // 초기 상태 오버라이드 ...initialState, })); } - -// 특정 타입의 테이블 스토어 생성 예시 -// export const useSlopeOutlierStore = createTableStore(); diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index 9d4fdab..c69c548 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -1,9 +1,10 @@ -import React, { useRef, useCallback, useEffect } from 'react'; +import React, { useRef, useCallback, useEffect, useState } from 'react'; import { useReactTable, getCoreRowModel, ColumnResizeMode, FilterFn, + RowSelectionState, } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; import { rankItem } from '@tanstack/match-sorter-utils'; @@ -66,6 +67,8 @@ const SteepSlopeLookUp = () => { selectedRow, setSelectedRow, resetFilters, + setSelectedRows, + selectedRows, } = useSteepSlopeStore(); // 테이블 컨테이너 ref는 훅 내에서 직접 생성 @@ -128,6 +131,8 @@ const SteepSlopeLookUp = () => { addMeta({ itemRank }); return itemRank.passed; }; + // 행 선택 상태 추가 + const [rowSelection, setRowSelection] = useState({}); //테이블 선언 const table = useReactTable({ @@ -136,11 +141,14 @@ const SteepSlopeLookUp = () => { state: { columnVisibility, columnSizing, + rowSelection, }, onColumnVisibilityChange: setColumnVisibility, onColumnSizingChange: setColumnSizing, columnResizeMode: 'onChange' as ColumnResizeMode, enableColumnResizing: true, + onRowSelectionChange: setRowSelection, + enableRowSelection: true, getCoreRowModel: getCoreRowModel(), defaultColumn: { minSize: 80, @@ -152,6 +160,17 @@ const SteepSlopeLookUp = () => { globalFilterFn: 'fuzzy', }); + useEffect(() => { + // rowSelection 상태에서 선택된 행 추출 + const selectedRowsArray = Object.keys(rowSelection) + .filter((key) => rowSelection[key]) + .map((key) => flatData[parseInt(key)]); + + // 선택된 행 정보 업데이트 + setSelectedRows(selectedRowsArray); + setSelectedRow(selectedRowsArray.length > 0 ? selectedRowsArray[0] : null); + }, [rowSelection, flatData]); + // 스크롤 이벤트 핸들러(무한스크롤 기능) const handleScroll = useCallback(() => { if (!tableContainerRef.current || !hasNextPage || isFetching) return; @@ -181,20 +200,58 @@ const SteepSlopeLookUp = () => { setSelectedRegion({ city, county }); }; + // const handleDelete = async () => { + // try { + // if (selectedRow) { + // await slopeManageAPI.deleteSlope([selectedRow._id]); + // // 삭제 성공 후 데이터 갱신 + // await queryClient.invalidateQueries({ queryKey: ['slopes'] }); + // setSelectedRow(null); + // closeDeleteModal(); + // } + // } catch (error) { + // console.error('삭제 실패:', error); + // } + // }; const handleDelete = async () => { try { - if (selectedRow) { - await slopeManageAPI.deleteSlope([selectedRow._id]); - // 삭제 성공 후 데이터 갱신 - await queryClient.invalidateQueries({ queryKey: ['slopes'] }); + // 선택된 행들이 있는지 확인 (단일 선택 또는 다중 선택) + const rowsToDelete = + selectedRows.length > 0 + ? selectedRows + : selectedRow + ? [selectedRow] + : []; + + if (rowsToDelete.length > 0) { + const idsToDelete = rowsToDelete.map((row) => row._id); // 선택된 모든 항목의 ID 추출 + await slopeManageAPI.deleteSlope(idsToDelete); // API 호출로 삭제 처리 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); // 삭제 성공 후 데이터 갱신 + + // rowSelection 상태를 사용하는 경우 + if (setRowSelection) setRowSelection({}); + // 선택된 행 상태 초기화 + if (setSelectedRows) setSelectedRows([]); setSelectedRow(null); - closeDeleteModal(); + + closeDeleteModal(); // 모달 닫기 + + if (showNotification) { + showNotification(`${idsToDelete.length}개 항목이 삭제되었습니다.`, { + severity: 'success', + }); + } } } catch (error) { console.error('삭제 실패:', error); + // 실패 알림 (알림 기능이 있는 경우) + if (showNotification) { + showNotification('삭제 중 오류가 발생했습니다.', { + severity: 'error', + }); + } } }; - const handleEdit = async (updatedSlope: Slope) => { try { await slopeManageAPI.updateSlope(updatedSlope); @@ -246,6 +303,7 @@ const SteepSlopeLookUp = () => { closeDeleteModal={closeDeleteModal} handleDelete={handleDelete} selectedRow={selectedRow} + selectedRows={selectedRows} isEditModalOpen={isEditModalOpen} closeEditModal={closeEditModal} handleEdit={handleEdit} @@ -281,6 +339,7 @@ const SteepSlopeLookUp = () => { From 390d087dc221d0ea05a54af02d4d99e50fe479c2 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 17 May 2025 21:14:34 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20store=20=EC=9C=84=EC=B9=98=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 --- .../ManagePage/StepSlope/components/table/TableToolbar.tsx | 2 +- src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx | 2 +- .../components/table/store => stores}/steepSlopeStore.ts | 2 +- .../StepSlope/components/table/store => stores}/tableStore.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/{pages/ManagePage/StepSlope/components/table/store => stores}/steepSlopeStore.ts (74%) rename src/{pages/ManagePage/StepSlope/components/table/store => stores}/tableStore.ts (97%) diff --git a/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx b/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx index ece622e..691b347 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx @@ -6,7 +6,7 @@ import CachedRoundedIcon from '@mui/icons-material/CachedRounded'; import TravelExploreRoundedIcon from '@mui/icons-material/TravelExploreRounded'; import DownloadRoundedIcon from '@mui/icons-material/DownloadRounded'; import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; -import { useSteepSlopeStore } from './store/steepSlopeStore'; +import { useSteepSlopeStore } from '../../../../../stores/steepSlopeStore'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; interface Region { diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index c69c548..f5849c1 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -19,7 +19,7 @@ import styled from 'styled-components'; import { slopeManageAPI } from '../../../../apis/slopeManage'; import { useNotificationStore } from '../../../../hooks/notificationStore'; import { getSlopeColumns } from '../components/table/coloums'; -import { useSteepSlopeStore } from '../components/table/store/steepSlopeStore'; +import { useSteepSlopeStore } from '../../../../stores/steepSlopeStore'; import TableToolbar from '../components/table/TableToolbar'; import DataTable from '../components/table/DataTable'; diff --git a/src/pages/ManagePage/StepSlope/components/table/store/steepSlopeStore.ts b/src/stores/steepSlopeStore.ts similarity index 74% rename from src/pages/ManagePage/StepSlope/components/table/store/steepSlopeStore.ts rename to src/stores/steepSlopeStore.ts index 254876e..394d18c 100644 --- a/src/pages/ManagePage/StepSlope/components/table/store/steepSlopeStore.ts +++ b/src/stores/steepSlopeStore.ts @@ -1,5 +1,5 @@ import { createTableStore } from './tableStore'; -import { Slope } from '../../../../../../apis/slopeMap'; +import { Slope } from '../apis/slopeMap'; // 급경사지 스토어 생성 - 팩토리 함수 사용 export const useSteepSlopeStore = createTableStore(); diff --git a/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts b/src/stores/tableStore.ts similarity index 97% rename from src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts rename to src/stores/tableStore.ts index f71131e..10e7e83 100644 --- a/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts +++ b/src/stores/tableStore.ts @@ -5,7 +5,7 @@ import { OnChangeFn, RowSelectionState, } from '@tanstack/react-table'; -import { getDefaultColumnVisibility } from '..//coloums'; +import { getDefaultColumnVisibility } from '../pages/ManagePage/StepSlope/components/table/coloums'; // 제네릭 타입 T는 각 테이블에서 사용할 데이터 타입입니다 (Slope, SlopeOutlier 등) export interface TableState { From 239b6dc175caadcde094f383f17126e9f12df979 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 17 May 2025 21:55:38 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20props=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AddSlopeContainer.tsx | 6 +- .../components/ColumnFilterModal.tsx | 9 +- .../StepSlope/components/DeleteModal.tsx | 10 +- .../StepSlope/components/EditModal.tsx | 8 +- .../components/RegionFilterModal.tsx | 15 ++- .../StepSlope/components/SlopeForm.tsx | 10 +- .../StepSlope/components/table/DataTable.tsx | 18 +-- .../components/table/TableAction.tsx | 11 +- .../components/table/TableCheckbox.tsx | 8 +- .../components/table/TableModals.tsx | 20 +--- .../components/table/TableToolbar.tsx | 17 +-- .../StepSlope/pages/SteepSlopeAdd.tsx | 6 +- src/pages/ManagePage/interface.ts | 113 ++++++++++++++++++ 13 files changed, 131 insertions(+), 120 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx b/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx index 84941e0..1c37c70 100644 --- a/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx +++ b/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx @@ -1,11 +1,7 @@ import { Slope } from '../../../../apis/slopeMap'; import SlopeForm from './SlopeForm'; import { slopeManageAPI } from '../../../../apis/slopeManage'; - -interface AddSlopeProps { - isOpen: boolean; - onClose: () => void; -} +import { AddSlopeProps } from '../../interface'; const AddSlope = ({ isOpen, onClose }: AddSlopeProps) => { const onSubmit = async (newSlopeData: Slope) => { diff --git a/src/pages/ManagePage/StepSlope/components/ColumnFilterModal.tsx b/src/pages/ManagePage/StepSlope/components/ColumnFilterModal.tsx index dc3057f..f2f50b6 100644 --- a/src/pages/ManagePage/StepSlope/components/ColumnFilterModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/ColumnFilterModal.tsx @@ -1,12 +1,5 @@ import styled from 'styled-components'; -import { Table } from '@tanstack/react-table'; -import { Slope } from '../../../../apis/slopeMap'; - -interface FilterModalProps { - isOpen: boolean; - onClose: () => void; - table: Table; -} +import { FilterModalProps } from '../../interface'; const FilterModal = ({ isOpen, onClose, table }: FilterModalProps) => { return ( diff --git a/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx b/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx index fe809db..da4aff3 100644 --- a/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/DeleteModal.tsx @@ -1,14 +1,6 @@ import styled from 'styled-components'; -import { Slope } from '../../../../apis/slopeMap'; import { useEffect, useState } from 'react'; - -interface DeleteConfirmModalProps { - isOpen: boolean; - onClose: () => void; - onConfirm: () => void; - selectedRow: Slope | null; - selectedRows: Slope[]; -} +import { DeleteConfirmModalProps } from '../../interface'; const DeleteConfirmModal = ({ isOpen, diff --git a/src/pages/ManagePage/StepSlope/components/EditModal.tsx b/src/pages/ManagePage/StepSlope/components/EditModal.tsx index 468e518..db2d01a 100644 --- a/src/pages/ManagePage/StepSlope/components/EditModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/EditModal.tsx @@ -1,13 +1,7 @@ import { Slope } from '../../../../apis/slopeMap'; import { useState, useEffect } from 'react'; import SlopeForm from './SlopeForm'; - -interface EditModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: (updatedSlope: Slope) => void; - selectedRow: Slope | null; -} +import { EditModalProps } from '../../interface'; const EditModal = ({ isOpen, diff --git a/src/pages/ManagePage/StepSlope/components/RegionFilterModal.tsx b/src/pages/ManagePage/StepSlope/components/RegionFilterModal.tsx index 1041453..d5e9578 100644 --- a/src/pages/ManagePage/StepSlope/components/RegionFilterModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/RegionFilterModal.tsx @@ -1,14 +1,13 @@ import styled from 'styled-components'; import { useState } from 'react'; import { CityOptions, CountyOptions } from './regionData'; +import { RegionFilterModalProps } from '../../interface'; -interface FilterModalProps { - isOpen: boolean; - onClose: () => void; - onRegionSelect: (city: string, county: string) => void; -} - -const FilterModal = ({ isOpen, onClose, onRegionSelect }: FilterModalProps) => { +const RegionFilterModal = ({ + isOpen, + onClose, + onRegionSelect, +}: RegionFilterModalProps) => { const [selectedCity, setSelectedCity] = useState('지역'); const [selectedCounty, setSelectedCounty] = useState('모두'); @@ -60,7 +59,7 @@ const FilterModal = ({ isOpen, onClose, onRegionSelect }: FilterModalProps) => { ); }; -export default FilterModal; +export default RegionFilterModal; const ModalOverlay = styled.div<{ $isOpen: boolean }>` display: ${(props) => (props.$isOpen ? 'flex' : 'none')}; diff --git a/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx b/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx index 8d9bb17..ff9408d 100644 --- a/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx +++ b/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx @@ -1,15 +1,7 @@ import styled from 'styled-components'; import { Slope } from '../../../../apis/slopeMap'; import { useState, useEffect } from 'react'; - -interface SlopeFormProps { - titleText: string; - initialData: Slope; - isOpen: boolean; - onClose: () => void; - onSubmit: (data: Slope) => void; - submitButtonText: string; -} +import { SlopeFormProps } from '../../interface'; const SlopeForm = ({ titleText, diff --git a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx index 5e26230..b61baa7 100644 --- a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx @@ -1,22 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { - Table as TableInstance, - Row as RowInstance, -} from '@tanstack/react-table'; - -import { Virtualizer } from '@tanstack/react-virtual'; -import { Slope } from '../../../../../apis/slopeMap'; - -interface DataTableProps { - tableContainerRef: React.RefObject; - handleScroll: () => void; - table: TableInstance; - rows: RowInstance[]; - rowVirtualizer: Virtualizer; - selectedRow: Slope | null; - setSelectedRow: (row: Slope | null) => void; -} +import { DataTableProps } from '../../../interface'; const DataTable: React.FC = ({ tableContainerRef, diff --git a/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx b/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx index 92ff5f7..26bfe22 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx @@ -1,16 +1,7 @@ import styled from 'styled-components'; import React from 'react'; -import { Slope } from '../../../../../apis/slopeMap'; import LoadingMessage from '../../../components/LoadingMessage'; - -interface TableActionProps { - isLoading: boolean; - selectedRow: Slope | null; - // 선택된 행들 배열 추가 - selectedRows: Slope[]; - openEditModal: () => void; - openDeleteModal: () => void; -} +import { TableActionProps } from '../../../interface'; const TableAction: React.FC = ({ isLoading, diff --git a/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx b/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx index e546ff9..827d5bb 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableCheckbox.tsx @@ -1,12 +1,6 @@ import React, { useRef, useEffect } from 'react'; import styled from 'styled-components'; - -interface TableCheckboxProps { - indeterminate?: boolean; - checked?: boolean; - disabled?: boolean; - onChange: (e: React.ChangeEvent) => void; -} +import { TableCheckboxProps } from '../../../interface'; const TableCheckbox: React.FC = ({ indeterminate = false, diff --git a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx index 478d249..ba37f3c 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx @@ -1,27 +1,9 @@ import React from 'react'; -import { Slope } from '../../../../../apis/slopeMap'; import FilterModal from '../ColumnFilterModal'; import DeleteConfirmModal from '../DeleteModal'; import EditModal from '../EditModal'; import RegionFilterModal from '../RegionFilterModal'; -import { Table as TableInstance } from '@tanstack/react-table'; - -interface TableModalProps { - isModalOpen: boolean; - closeModal: () => void; - table: TableInstance; - isRegionModalOpen: boolean; - closeRegionModal: () => void; - handleRegionSelect: (city: string, county: string) => void; - isDeleteModalOpen: boolean; - closeDeleteModal: () => void; - handleDelete: () => void; - selectedRow: Slope | null; - selectedRows: Slope[]; - isEditModalOpen: boolean; - closeEditModal: () => void; - handleEdit: (updatedSlope: Slope) => void; -} +import { TableModalProps } from '../../../interface'; const TableModals: React.FC = ({ isModalOpen, diff --git a/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx b/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx index 691b347..8808436 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx @@ -9,22 +9,7 @@ import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; import { useSteepSlopeStore } from '../../../../../stores/steepSlopeStore'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -interface Region { - city: string; - county: string; -} - -interface TableToolbarProps { - title: string; - setSearchQuery: (query: string) => void; - inputValue: string; - setInputValue: (value: string) => void; - selectedRegion: Region | null; - resetFilters: () => void; - downloadExcel: () => void; - isDownloading: boolean; - totalCount: number; -} +import { TableToolbarProps } from '../../../interface'; const TableToolbar: React.FC = ({ title, diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx index 70313c5..b499146 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx @@ -5,11 +5,7 @@ import { slopeManageAPI } from '../../../../apis/slopeManage'; import CloudUploadRoundedIcon from '@mui/icons-material/CloudUploadRounded'; import { useNotificationStore } from '../../../../hooks/notificationStore'; import AddSlope from '../components/AddSlopeContainer'; -interface FileInputContainerProps { - $isDragActive?: boolean; - $hasFile?: boolean; - theme?: any; -} +import { FileInputContainerProps } from '../../interface'; const SteepSlopeAdd: React.FC = () => { const [isDragActive, setIsDragActive] = useState(false); diff --git a/src/pages/ManagePage/interface.ts b/src/pages/ManagePage/interface.ts index 7860da5..d356838 100644 --- a/src/pages/ManagePage/interface.ts +++ b/src/pages/ManagePage/interface.ts @@ -35,3 +35,116 @@ export interface PaginationProps { onLastPage: () => void; onPageSizeChange: (size: number) => void; } + +//급경사지 테이블 관련 props타입 +import { + Table as TableInstance, + Row as RowInstance, +} from '@tanstack/react-table'; + +import { Virtualizer } from '@tanstack/react-virtual'; +import { Slope } from '../../apis/slopeMap'; + +export interface DataTableProps { + tableContainerRef: React.RefObject; + handleScroll: () => void; + table: TableInstance; + rows: RowInstance[]; + rowVirtualizer: Virtualizer; + selectedRow: Slope | null; + setSelectedRow: (row: Slope | null) => void; +} + +export interface TableActionProps { + isLoading: boolean; + selectedRow: Slope | null; + // 선택된 행들 배열 추가 + selectedRows: Slope[]; + openEditModal: () => void; + openDeleteModal: () => void; +} + +export interface TableCheckboxProps { + indeterminate?: boolean; + checked?: boolean; + disabled?: boolean; + onChange: (e: React.ChangeEvent) => void; +} + +export interface TableModalProps { + isModalOpen: boolean; + closeModal: () => void; + table: TableInstance; + isRegionModalOpen: boolean; + closeRegionModal: () => void; + handleRegionSelect: (city: string, county: string) => void; + isDeleteModalOpen: boolean; + closeDeleteModal: () => void; + handleDelete: () => void; + selectedRow: Slope | null; + selectedRows: Slope[]; + isEditModalOpen: boolean; + closeEditModal: () => void; + handleEdit: (updatedSlope: Slope) => void; +} + +export interface TableToolbarProps { + title: string; + setSearchQuery: (query: string) => void; + inputValue: string; + setInputValue: (value: string) => void; + selectedRegion: Region | null; + resetFilters: () => void; + downloadExcel: () => void; + isDownloading: boolean; + totalCount: number; +} + +interface Region { + city: string; + county: string; +} + +export interface AddSlopeProps { + isOpen: boolean; + onClose: () => void; +} + +export interface FilterModalProps { + isOpen: boolean; + onClose: () => void; + table: TableInstance; +} +export interface RegionFilterModalProps { + isOpen: boolean; + onClose: () => void; + onRegionSelect: (city: string, county: string) => void; +} +export interface DeleteConfirmModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + selectedRow: Slope | null; + selectedRows: Slope[]; +} + +export interface EditModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (updatedSlope: Slope) => void; + selectedRow: Slope | null; +} + +export interface SlopeFormProps { + titleText: string; + initialData: Slope; + isOpen: boolean; + onClose: () => void; + onSubmit: (data: Slope) => void; + submitButtonText: string; +} +export interface FileInputContainerProps { + $isDragActive?: boolean; + $hasFile?: boolean; + theme?: any; +} From d5634bfc3defb30ce45dbbe6376130d778feccb5 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 17 May 2025 21:55:52 +0900 Subject: [PATCH 11/13] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index f5849c1..3c50e76 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -200,19 +200,6 @@ const SteepSlopeLookUp = () => { setSelectedRegion({ city, county }); }; - // const handleDelete = async () => { - // try { - // if (selectedRow) { - // await slopeManageAPI.deleteSlope([selectedRow._id]); - // // 삭제 성공 후 데이터 갱신 - // await queryClient.invalidateQueries({ queryKey: ['slopes'] }); - // setSelectedRow(null); - // closeDeleteModal(); - // } - // } catch (error) { - // console.error('삭제 실패:', error); - // } - // }; const handleDelete = async () => { try { // 선택된 행들이 있는지 확인 (단일 선택 또는 다중 선택) From c6253baaa64d23e75a239315dc1380cd5337decb Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 17 May 2025 21:58:28 +0900 Subject: [PATCH 12/13] =?UTF-8?q?fix:=20useEffect=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index 3c50e76..dc29da9 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -169,7 +169,7 @@ const SteepSlopeLookUp = () => { // 선택된 행 정보 업데이트 setSelectedRows(selectedRowsArray); setSelectedRow(selectedRowsArray.length > 0 ? selectedRowsArray[0] : null); - }, [rowSelection, flatData]); + }, [rowSelection, flatData, setSelectedRow, setSelectedRows]); // 스크롤 이벤트 핸들러(무한스크롤 기능) const handleScroll = useCallback(() => { From 86f5fcbcf1fc41bbe9fc87654e864761dfb23fe9 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 18 May 2025 02:28:43 +0900 Subject: [PATCH 13/13] =?UTF-8?q?rebuild:=20=EC=9D=B4=EC=83=81=EA=B0=92=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EC=A0=84=EB=A9=B4=20?= =?UTF-8?q?=EA=B0=9C=ED=8E=B8=20=EB=B0=8F=20=EB=AA=A9=EC=B0=A8&=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 9 +- src/apis/slopeManage.tsx | 23 +- src/pages/ManagePage/ManagePage.tsx | 6 +- .../StepSlopeOutlier/SteepSlopeDup.tsx | 347 +++++++ .../StepSlopeOutlier/SteepSlopeEmpty.tsx | 344 +++++++ .../StepSlopeOutlier/SteepSlopeLocation.tsx | 344 +++++++ .../StepSlope/components/OutlierDup.tsx | 933 ------------------ .../StepSlope/components/OutlierEmpty.tsx | 932 ----------------- .../StepSlope/pages/SteepSlopeLookUp.tsx | 11 +- .../StepSlope/pages/SteepSlopeOutlier.tsx | 21 - .../ManagePage/components/SideComponents.tsx | 58 +- src/pages/ManagePage/interface.ts | 2 + src/stores/steepSlopeStore.ts | 3 + 13 files changed, 1117 insertions(+), 1916 deletions(-) create mode 100644 src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeDup.tsx create mode 100644 src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeEmpty.tsx create mode 100644 src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeLocation.tsx delete mode 100644 src/pages/ManagePage/StepSlope/components/OutlierDup.tsx delete mode 100644 src/pages/ManagePage/StepSlope/components/OutlierEmpty.tsx delete mode 100644 src/pages/ManagePage/StepSlope/pages/SteepSlopeOutlier.tsx diff --git a/src/Router.tsx b/src/Router.tsx index c35799e..ef2e222 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -4,11 +4,12 @@ import LoginPage from './pages/LoginPage/LoginPage'; import MapPage from './pages/MapPage/MapPage'; import ManagePage from './pages/ManagePage/ManagePage'; import SteepSlopeLookUp from './pages/ManagePage/StepSlope/pages/SteepSlopeLookUp'; -import SteepSlopeOutlier from './pages/ManagePage/StepSlope/pages/SteepSlopeOutlier'; import SteepSlopeAdd from './pages/ManagePage/StepSlope/pages/SteepSlopeAdd'; import UserLookUp from './pages/ManagePage/User/pages/UserLookUp'; import UserModi from './pages/ManagePage/User/pages/UserModi'; import Home from './pages/ManagePage/Home'; +import SteepSlopeDup from './pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeDup'; +import SteepSlopeEmpty from './pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeEmpty'; const Router = () => { return ( @@ -21,9 +22,13 @@ const Router = () => { } /> } /> - } /> } /> + + } /> + } /> + } /> + } /> } /> diff --git a/src/apis/slopeManage.tsx b/src/apis/slopeManage.tsx index 5ece429..6f72cf0 100644 --- a/src/apis/slopeManage.tsx +++ b/src/apis/slopeManage.tsx @@ -8,7 +8,15 @@ interface FetchSlopeParams { grade?: string; county?: string; } - +interface OutlierFetchParams { + page: number; + pageSize: number; + searchQuery?: string; + city?: string; + grade?: string; + county?: string; + outlierType?: string; +} export const slopeManageAPI = { batchSlope: async (params: FetchSlopeParams) => { const response = await api.get('/slopes/batch', { params }); @@ -69,9 +77,14 @@ export const slopeManageAPI = { throw error; } }, - findOutlier: async () => { - const response = await api.get('/slopes/outlier'); - console.log(' 경사지 이상값 조회', response.data.data); - return response.data.data; + findOutlierDup: async (params: OutlierFetchParams) => { + const response = await api.get('/slopes/outlier/dup', { params }); + console.log(' 경사지 이상값 조회', response.data); + return response.data; + }, + findOutlierEmpty: async (params: OutlierFetchParams) => { + const response = await api.get('/slopes/outlier/empty', { params }); + console.log(' 경사지 이상값 조회', response.data); + return response.data; }, }; diff --git a/src/pages/ManagePage/ManagePage.tsx b/src/pages/ManagePage/ManagePage.tsx index 3a5dc43..d9c0cd6 100644 --- a/src/pages/ManagePage/ManagePage.tsx +++ b/src/pages/ManagePage/ManagePage.tsx @@ -14,13 +14,17 @@ const ManagePage = () => { false, false, false, + false, + false, ]); const Page = [ './home', './slope/list', './slope/add', - './slope/outlier', + './outlier/empty', + './outlier/dup', + './outlier/location', './member/list', './member/edit', './map', diff --git a/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeDup.tsx b/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeDup.tsx new file mode 100644 index 0000000..a7223ed --- /dev/null +++ b/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeDup.tsx @@ -0,0 +1,347 @@ +import React, { useRef, useCallback, useEffect, useState } from 'react'; +import { + useReactTable, + getCoreRowModel, + ColumnResizeMode, + FilterFn, + RowSelectionState, +} from '@tanstack/react-table'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { rankItem } from '@tanstack/match-sorter-utils'; +import { + useInfiniteQuery, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; +import { Slope } from '../../../../apis/slopeMap'; +import styled from 'styled-components'; + +import { slopeManageAPI } from '../../../../apis/slopeManage'; +import { useNotificationStore } from '../../../../hooks/notificationStore'; +import { getSlopeColumns } from '../components/table/coloums'; +import { useSteepSlopeDupStore } from '../../../../stores/steepSlopeStore'; + +import TableToolbar from '../components/table/TableToolbar'; +import DataTable from '../components/table/DataTable'; +import TableAction from '../components/table/TableAction'; +import TableModals from '../components/table/TableModals'; + +const FETCH_SIZE = 50; +declare module '@tanstack/react-table' { + interface FilterFns { + fuzzy: FilterFn; + } +} + +const SteepSlopeDup = () => { + const queryClient = useQueryClient(); + + // Zustand 스토어에서 상태 및 액션 가져오기 + const { + columnVisibility, + setColumnVisibility, + columnSizing, + setColumnSizing, + totalCount, + setTotalCount, + searchQuery, + setSearchQuery, + inputValue, + setInputValue, + selectedRegion, + setSelectedRegion, + grade, + + isModalOpen, + closeModal, + isRegionModalOpen, + + closeRegionModal, + isDeleteModalOpen, + openDeleteModal, + closeDeleteModal, + isEditModalOpen, + openEditModal, + closeEditModal, + + selectedRow, + setSelectedRow, + resetFilters, + setSelectedRows, + selectedRows, + } = useSteepSlopeDupStore(); + + // 테이블 컨테이너 ref는 훅 내에서 직접 생성 + const tableContainerRef = useRef(null); + + // 알림 함수 가져오기 + const showNotification = useNotificationStore( + (state) => state.showNotification + ); + + //데이터 조회 쿼리 + const { + data, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + refetch, + } = useInfiniteQuery({ + queryKey: ['slopes', searchQuery, selectedRegion, grade], + queryFn: async ({ pageParam = 0 }) => { + const response = await slopeManageAPI.findOutlierDup({ + page: pageParam, + pageSize: FETCH_SIZE, + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + grade: grade, + county: selectedRegion?.county, + }); + return response; + }, + getNextPageParam: (lastPage) => + lastPage?.meta?.hasMore ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 0, + }); + + //flatData를 통해 페이지 계산 + const flatData = React.useMemo(() => { + return data?.pages.flatMap((page) => page.data) ?? []; + }, [data]); + + // 검색어나 지역 필터 변경시 데이터 리페치 + useEffect(() => { + refetch(); + }, [searchQuery, selectedRegion, refetch]); + + //데이터 전체 수 + useEffect(() => { + if (data?.pages[0]?.meta?.totalCount) + setTotalCount(data.pages[0].meta.totalCount); + else setTotalCount(0); + }, [data, setTotalCount]); + + //데이터 열 선언 + const columns = React.useMemo(() => getSlopeColumns(), []); + + //필터 선언 + const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ itemRank }); + return itemRank.passed; + }; + // 행 선택 상태 추가 + const [rowSelection, setRowSelection] = useState({}); + + //테이블 선언 + const table = useReactTable({ + data: flatData, + columns, + state: { + columnVisibility, + columnSizing, + rowSelection, + }, + onColumnVisibilityChange: setColumnVisibility, + onColumnSizingChange: setColumnSizing, + columnResizeMode: 'onChange' as ColumnResizeMode, + enableColumnResizing: true, + onRowSelectionChange: setRowSelection, + enableRowSelection: true, + getCoreRowModel: getCoreRowModel(), + defaultColumn: { + minSize: 80, + maxSize: 400, + }, + filterFns: { + fuzzy: fuzzyFilter, + }, + globalFilterFn: 'fuzzy', + }); + useEffect(() => { + console.log('API 응답:', data); + }, [data]); + useEffect(() => { + // rowSelection 상태에서 선택된 행 추출 + const selectedRowsArray = Object.keys(rowSelection) + .filter((key) => rowSelection[key]) + .map((key) => flatData[parseInt(key)]); + + // 선택된 행 정보 업데이트 + setSelectedRows(selectedRowsArray); + setSelectedRow(selectedRowsArray.length > 0 ? selectedRowsArray[0] : null); + }, [rowSelection, flatData, setSelectedRow, setSelectedRows]); + + // 스크롤 이벤트 핸들러(무한스크롤 기능) + const handleScroll = useCallback(() => { + if (!tableContainerRef.current || !hasNextPage || isFetching) return; + + const { scrollTop, scrollHeight, clientHeight } = tableContainerRef.current; + if (scrollHeight - scrollTop - clientHeight < 300) { + fetchNextPage(); + } + }, [fetchNextPage, hasNextPage, isFetching]); + + const { rows } = table.getRowModel(); + + //보이는 행만 보일 수 있도록 가상화 + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => 40, + overscan: 10, + }); + + const handleRegionSelect = (city: string, county: string) => { + if (county === '모두') { + console.log(`${city}`); + } else { + console.log(`${city} ${county} `); + } + setSelectedRegion({ city, county }); + }; + + const handleDelete = async () => { + try { + // 선택된 행들이 있는지 확인 (단일 선택 또는 다중 선택) + const rowsToDelete = + selectedRows.length > 0 + ? selectedRows + : selectedRow + ? [selectedRow] + : []; + + if (rowsToDelete.length > 0) { + const idsToDelete = rowsToDelete.map((row) => row._id); // 선택된 모든 항목의 ID 추출 + await slopeManageAPI.deleteSlope(idsToDelete); // API 호출로 삭제 처리 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); // 삭제 성공 후 데이터 갱신 + + // rowSelection 상태를 사용하는 경우 + if (setRowSelection) setRowSelection({}); + // 선택된 행 상태 초기화 + if (setSelectedRows) setSelectedRows([]); + setSelectedRow(null); + + closeDeleteModal(); // 모달 닫기 + + if (showNotification) { + showNotification(`${idsToDelete.length}개 항목이 삭제되었습니다.`, { + severity: 'success', + }); + } + } + } catch (error) { + console.error('삭제 실패:', error); + // 실패 알림 (알림 기능이 있는 경우) + if (showNotification) { + showNotification('삭제 중 오류가 발생했습니다.', { + severity: 'error', + }); + } + } + }; + const handleEdit = async (updatedSlope: Slope) => { + try { + await slopeManageAPI.updateSlope(updatedSlope); + // 수정 성공 후 데이터 갱신 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); + setSelectedRow(null); + closeEditModal(); + } catch (error) { + console.error('수정 실패:', error); + } + }; + + // 다운로드 mutation 설정 + const { mutate: downloadExcel, isPending: isDownloading } = useMutation({ + mutationFn: (params: { + searchQuery?: string; + city?: string; + county?: string; + }) => slopeManageAPI.downloadExcel(params), + + onSuccess: () => { + showNotification('엑셀 파일 다운로드가 완료되었습니다.', { + severity: 'success', + }); + }, + + onError: (error: any) => { + const errorMessage = + error.response?.data?.message || '다운로드에 실패했습니다.'; + showNotification(errorMessage, { + severity: 'error', + autoHideDuration: 6000, + }); + console.error('다운로드 실패:', error); + }, + }); + + return ( + + {/* 모달 */} + + + + downloadExcel({ + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + county: selectedRegion?.county, + }) + } + isDownloading={isDownloading} + totalCount={totalCount} + /> + + + + + ); +}; + +export default SteepSlopeDup; + +//전체 컨테이너너 +const Container = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; diff --git a/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeEmpty.tsx b/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeEmpty.tsx new file mode 100644 index 0000000..ee0c770 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeEmpty.tsx @@ -0,0 +1,344 @@ +import React, { useRef, useCallback, useEffect, useState } from 'react'; +import { + useReactTable, + getCoreRowModel, + ColumnResizeMode, + FilterFn, + RowSelectionState, +} from '@tanstack/react-table'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { rankItem } from '@tanstack/match-sorter-utils'; +import { + useInfiniteQuery, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; +import { Slope } from '../../../../apis/slopeMap'; +import styled from 'styled-components'; + +import { slopeManageAPI } from '../../../../apis/slopeManage'; +import { useNotificationStore } from '../../../../hooks/notificationStore'; +import { getSlopeColumns } from '../components/table/coloums'; +import { useSteepSlopeEmptyStore } from '../../../../stores/steepSlopeStore'; + +import TableToolbar from '../components/table/TableToolbar'; +import DataTable from '../components/table/DataTable'; +import TableAction from '../components/table/TableAction'; +import TableModals from '../components/table/TableModals'; + +const FETCH_SIZE = 50; +declare module '@tanstack/react-table' { + interface FilterFns { + fuzzy: FilterFn; + } +} + +const SteepSlopeEmpty = () => { + const queryClient = useQueryClient(); + + // Zustand 스토어에서 상태 및 액션 가져오기 + const { + columnVisibility, + setColumnVisibility, + columnSizing, + setColumnSizing, + totalCount, + setTotalCount, + searchQuery, + setSearchQuery, + inputValue, + setInputValue, + selectedRegion, + setSelectedRegion, + grade, + + isModalOpen, + closeModal, + isRegionModalOpen, + + closeRegionModal, + isDeleteModalOpen, + openDeleteModal, + closeDeleteModal, + isEditModalOpen, + openEditModal, + closeEditModal, + + selectedRow, + setSelectedRow, + resetFilters, + setSelectedRows, + selectedRows, + } = useSteepSlopeEmptyStore(); + + // 테이블 컨테이너 ref는 훅 내에서 직접 생성 + const tableContainerRef = useRef(null); + + // 알림 함수 가져오기 + const showNotification = useNotificationStore( + (state) => state.showNotification + ); + + //데이터 조회 쿼리 + const { + data, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + refetch, + } = useInfiniteQuery({ + queryKey: ['slopes', searchQuery, selectedRegion, grade], + queryFn: async ({ pageParam = 0 }) => { + const response = await slopeManageAPI.findOutlierEmpty({ + page: pageParam, + pageSize: FETCH_SIZE, + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + grade: grade, + county: selectedRegion?.county, + }); + return response; + }, + getNextPageParam: (lastPage) => + lastPage?.meta?.hasMore ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 0, + }); + + //flatData를 통해 페이지 계산 + const flatData = React.useMemo(() => { + return data?.pages.flatMap((page) => page.data) ?? []; + }, [data]); + + // 검색어나 지역 필터 변경시 데이터 리페치 + useEffect(() => { + refetch(); + }, [searchQuery, selectedRegion, refetch]); + + //데이터 전체 수 + useEffect(() => { + if (data?.pages[0]?.meta?.totalCount) + setTotalCount(data.pages[0].meta.totalCount); + else setTotalCount(0); + }, [data, setTotalCount]); + //데이터 열 선언 + const columns = React.useMemo(() => getSlopeColumns(), []); + + //필터 선언 + const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ itemRank }); + return itemRank.passed; + }; + // 행 선택 상태 추가 + const [rowSelection, setRowSelection] = useState({}); + + //테이블 선언 + const table = useReactTable({ + data: flatData, + columns, + state: { + columnVisibility, + columnSizing, + rowSelection, + }, + onColumnVisibilityChange: setColumnVisibility, + onColumnSizingChange: setColumnSizing, + columnResizeMode: 'onChange' as ColumnResizeMode, + enableColumnResizing: true, + onRowSelectionChange: setRowSelection, + enableRowSelection: true, + getCoreRowModel: getCoreRowModel(), + defaultColumn: { + minSize: 80, + maxSize: 400, + }, + filterFns: { + fuzzy: fuzzyFilter, + }, + globalFilterFn: 'fuzzy', + }); + + useEffect(() => { + // rowSelection 상태에서 선택된 행 추출 + const selectedRowsArray = Object.keys(rowSelection) + .filter((key) => rowSelection[key]) + .map((key) => flatData[parseInt(key)]); + + // 선택된 행 정보 업데이트 + setSelectedRows(selectedRowsArray); + setSelectedRow(selectedRowsArray.length > 0 ? selectedRowsArray[0] : null); + }, [rowSelection, flatData, setSelectedRow, setSelectedRows]); + + // 스크롤 이벤트 핸들러(무한스크롤 기능) + const handleScroll = useCallback(() => { + if (!tableContainerRef.current || !hasNextPage || isFetching) return; + + const { scrollTop, scrollHeight, clientHeight } = tableContainerRef.current; + if (scrollHeight - scrollTop - clientHeight < 300) { + fetchNextPage(); + } + }, [fetchNextPage, hasNextPage, isFetching]); + + const { rows } = table.getRowModel(); + + //보이는 행만 보일 수 있도록 가상화 + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => 40, + overscan: 10, + }); + + const handleRegionSelect = (city: string, county: string) => { + if (county === '모두') { + console.log(`${city}`); + } else { + console.log(`${city} ${county} `); + } + setSelectedRegion({ city, county }); + }; + + const handleDelete = async () => { + try { + // 선택된 행들이 있는지 확인 (단일 선택 또는 다중 선택) + const rowsToDelete = + selectedRows.length > 0 + ? selectedRows + : selectedRow + ? [selectedRow] + : []; + + if (rowsToDelete.length > 0) { + const idsToDelete = rowsToDelete.map((row) => row._id); // 선택된 모든 항목의 ID 추출 + await slopeManageAPI.deleteSlope(idsToDelete); // API 호출로 삭제 처리 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); // 삭제 성공 후 데이터 갱신 + + // rowSelection 상태를 사용하는 경우 + if (setRowSelection) setRowSelection({}); + // 선택된 행 상태 초기화 + if (setSelectedRows) setSelectedRows([]); + setSelectedRow(null); + + closeDeleteModal(); // 모달 닫기 + + if (showNotification) { + showNotification(`${idsToDelete.length}개 항목이 삭제되었습니다.`, { + severity: 'success', + }); + } + } + } catch (error) { + console.error('삭제 실패:', error); + // 실패 알림 (알림 기능이 있는 경우) + if (showNotification) { + showNotification('삭제 중 오류가 발생했습니다.', { + severity: 'error', + }); + } + } + }; + const handleEdit = async (updatedSlope: Slope) => { + try { + await slopeManageAPI.updateSlope(updatedSlope); + // 수정 성공 후 데이터 갱신 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); + setSelectedRow(null); + closeEditModal(); + } catch (error) { + console.error('수정 실패:', error); + } + }; + + // 다운로드 mutation 설정 + const { mutate: downloadExcel, isPending: isDownloading } = useMutation({ + mutationFn: (params: { + searchQuery?: string; + city?: string; + county?: string; + }) => slopeManageAPI.downloadExcel(params), + + onSuccess: () => { + showNotification('엑셀 파일 다운로드가 완료되었습니다.', { + severity: 'success', + }); + }, + + onError: (error: any) => { + const errorMessage = + error.response?.data?.message || '다운로드에 실패했습니다.'; + showNotification(errorMessage, { + severity: 'error', + autoHideDuration: 6000, + }); + console.error('다운로드 실패:', error); + }, + }); + + return ( + + {/* 모달 */} + + + + downloadExcel({ + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + county: selectedRegion?.county, + }) + } + isDownloading={isDownloading} + totalCount={totalCount} + /> + + + + + ); +}; + +export default SteepSlopeEmpty; + +//전체 컨테이너너 +const Container = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; diff --git a/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeLocation.tsx b/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeLocation.tsx new file mode 100644 index 0000000..5f43cda --- /dev/null +++ b/src/pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeLocation.tsx @@ -0,0 +1,344 @@ +import React, { useRef, useCallback, useEffect, useState } from 'react'; +import { + useReactTable, + getCoreRowModel, + ColumnResizeMode, + FilterFn, + RowSelectionState, +} from '@tanstack/react-table'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { rankItem } from '@tanstack/match-sorter-utils'; +import { + useInfiniteQuery, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; +import { Slope } from '../../../../apis/slopeMap'; +import styled from 'styled-components'; + +import { slopeManageAPI } from '../../../../apis/slopeManage'; +import { useNotificationStore } from '../../../../hooks/notificationStore'; +import { getSlopeColumns } from '../components/table/coloums'; +import { useSteepSlopeLocationStore } from '../../../../stores/steepSlopeStore'; + +import TableToolbar from '../components/table/TableToolbar'; +import DataTable from '../components/table/DataTable'; +import TableAction from '../components/table/TableAction'; +import TableModals from '../components/table/TableModals'; + +const FETCH_SIZE = 50; +declare module '@tanstack/react-table' { + interface FilterFns { + fuzzy: FilterFn; + } +} + +const SteepSlopeLocation = () => { + const queryClient = useQueryClient(); + + // Zustand 스토어에서 상태 및 액션 가져오기 + const { + columnVisibility, + setColumnVisibility, + columnSizing, + setColumnSizing, + totalCount, + setTotalCount, + searchQuery, + setSearchQuery, + inputValue, + setInputValue, + selectedRegion, + setSelectedRegion, + grade, + + isModalOpen, + closeModal, + isRegionModalOpen, + + closeRegionModal, + isDeleteModalOpen, + openDeleteModal, + closeDeleteModal, + isEditModalOpen, + openEditModal, + closeEditModal, + + selectedRow, + setSelectedRow, + resetFilters, + setSelectedRows, + selectedRows, + } = useSteepSlopeLocationStore(); + + // 테이블 컨테이너 ref는 훅 내에서 직접 생성 + const tableContainerRef = useRef(null); + + // 알림 함수 가져오기 + const showNotification = useNotificationStore( + (state) => state.showNotification + ); + + //데이터 조회 쿼리 + const { + data, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + refetch, + } = useInfiniteQuery({ + queryKey: ['slopes', searchQuery, selectedRegion, grade], + queryFn: async ({ pageParam = 0 }) => { + const response = await slopeManageAPI.findOutlierEmpty({ + page: pageParam, + pageSize: FETCH_SIZE, + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + grade: grade, + county: selectedRegion?.county, + }); + return response; + }, + getNextPageParam: (lastPage) => + lastPage?.meta?.hasMore ? lastPage.meta.currentPage + 1 : undefined, + initialPageParam: 0, + }); + + //flatData를 통해 페이지 계산 + const flatData = React.useMemo(() => { + return data?.pages.flatMap((page) => page.data) ?? []; + }, [data]); + + // 검색어나 지역 필터 변경시 데이터 리페치 + useEffect(() => { + refetch(); + }, [searchQuery, selectedRegion, refetch]); + + //데이터 전체 수 + useEffect(() => { + if (data?.pages[0]?.meta?.totalCount) + setTotalCount(data.pages[0].meta.totalCount); + else setTotalCount(0); + }, [data, setTotalCount]); + //데이터 열 선언 + const columns = React.useMemo(() => getSlopeColumns(), []); + + //필터 선언 + const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ itemRank }); + return itemRank.passed; + }; + // 행 선택 상태 추가 + const [rowSelection, setRowSelection] = useState({}); + + //테이블 선언 + const table = useReactTable({ + data: flatData, + columns, + state: { + columnVisibility, + columnSizing, + rowSelection, + }, + onColumnVisibilityChange: setColumnVisibility, + onColumnSizingChange: setColumnSizing, + columnResizeMode: 'onChange' as ColumnResizeMode, + enableColumnResizing: true, + onRowSelectionChange: setRowSelection, + enableRowSelection: true, + getCoreRowModel: getCoreRowModel(), + defaultColumn: { + minSize: 80, + maxSize: 400, + }, + filterFns: { + fuzzy: fuzzyFilter, + }, + globalFilterFn: 'fuzzy', + }); + + useEffect(() => { + // rowSelection 상태에서 선택된 행 추출 + const selectedRowsArray = Object.keys(rowSelection) + .filter((key) => rowSelection[key]) + .map((key) => flatData[parseInt(key)]); + + // 선택된 행 정보 업데이트 + setSelectedRows(selectedRowsArray); + setSelectedRow(selectedRowsArray.length > 0 ? selectedRowsArray[0] : null); + }, [rowSelection, flatData, setSelectedRow, setSelectedRows]); + + // 스크롤 이벤트 핸들러(무한스크롤 기능) + const handleScroll = useCallback(() => { + if (!tableContainerRef.current || !hasNextPage || isFetching) return; + + const { scrollTop, scrollHeight, clientHeight } = tableContainerRef.current; + if (scrollHeight - scrollTop - clientHeight < 300) { + fetchNextPage(); + } + }, [fetchNextPage, hasNextPage, isFetching]); + + const { rows } = table.getRowModel(); + + //보이는 행만 보일 수 있도록 가상화 + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => 40, + overscan: 10, + }); + + const handleRegionSelect = (city: string, county: string) => { + if (county === '모두') { + console.log(`${city}`); + } else { + console.log(`${city} ${county} `); + } + setSelectedRegion({ city, county }); + }; + + const handleDelete = async () => { + try { + // 선택된 행들이 있는지 확인 (단일 선택 또는 다중 선택) + const rowsToDelete = + selectedRows.length > 0 + ? selectedRows + : selectedRow + ? [selectedRow] + : []; + + if (rowsToDelete.length > 0) { + const idsToDelete = rowsToDelete.map((row) => row._id); // 선택된 모든 항목의 ID 추출 + await slopeManageAPI.deleteSlope(idsToDelete); // API 호출로 삭제 처리 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); // 삭제 성공 후 데이터 갱신 + + // rowSelection 상태를 사용하는 경우 + if (setRowSelection) setRowSelection({}); + // 선택된 행 상태 초기화 + if (setSelectedRows) setSelectedRows([]); + setSelectedRow(null); + + closeDeleteModal(); // 모달 닫기 + + if (showNotification) { + showNotification(`${idsToDelete.length}개 항목이 삭제되었습니다.`, { + severity: 'success', + }); + } + } + } catch (error) { + console.error('삭제 실패:', error); + // 실패 알림 (알림 기능이 있는 경우) + if (showNotification) { + showNotification('삭제 중 오류가 발생했습니다.', { + severity: 'error', + }); + } + } + }; + const handleEdit = async (updatedSlope: Slope) => { + try { + await slopeManageAPI.updateSlope(updatedSlope); + // 수정 성공 후 데이터 갱신 + await queryClient.invalidateQueries({ queryKey: ['slopes'] }); + setSelectedRow(null); + closeEditModal(); + } catch (error) { + console.error('수정 실패:', error); + } + }; + + // 다운로드 mutation 설정 + const { mutate: downloadExcel, isPending: isDownloading } = useMutation({ + mutationFn: (params: { + searchQuery?: string; + city?: string; + county?: string; + }) => slopeManageAPI.downloadExcel(params), + + onSuccess: () => { + showNotification('엑셀 파일 다운로드가 완료되었습니다.', { + severity: 'success', + }); + }, + + onError: (error: any) => { + const errorMessage = + error.response?.data?.message || '다운로드에 실패했습니다.'; + showNotification(errorMessage, { + severity: 'error', + autoHideDuration: 6000, + }); + console.error('다운로드 실패:', error); + }, + }); + + return ( + + {/* 모달 */} + + + + downloadExcel({ + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + county: selectedRegion?.county, + }) + } + isDownloading={isDownloading} + totalCount={totalCount} + /> + + + + + ); +}; + +export default SteepSlopeLocation; + +//전체 컨테이너너 +const Container = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; diff --git a/src/pages/ManagePage/StepSlope/components/OutlierDup.tsx b/src/pages/ManagePage/StepSlope/components/OutlierDup.tsx deleted file mode 100644 index c843c6c..0000000 --- a/src/pages/ManagePage/StepSlope/components/OutlierDup.tsx +++ /dev/null @@ -1,933 +0,0 @@ -import React, { - useState, - useMemo, - useRef, - useCallback, - useEffect, -} from 'react'; -import { - useReactTable, - getCoreRowModel, - VisibilityState, - createColumnHelper, - ColumnResizeMode, - ColumnSizingState, -} from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { FilterFn } from '@tanstack/react-table'; -import { rankItem } from '@tanstack/match-sorter-utils'; -import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; -import { Slope } from '../../../../apis/slopeMap'; -import styled from 'styled-components'; - -import { slopeManageAPI } from '../../../../apis/slopeManage'; -import FilterModal from '../components/ColumnFilterModal'; -import Title from '../../components/Title'; -import LoadingMessage from '../../components/LoadingMessage'; -import RegionFilterModal from '../components/RegionFilterModal'; - -import DeleteConfirmModal from '../components/DeleteModal'; -import EditModal from '../components/EditModal'; - -import TuneRoundedIcon from '@mui/icons-material/TuneRounded'; -import CachedRoundedIcon from '@mui/icons-material/CachedRounded'; -import TravelExploreRoundedIcon from '@mui/icons-material/TravelExploreRounded'; -import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; - -const FETCH_SIZE = 50; -declare module '@tanstack/react-table' { - interface FilterFns { - fuzzy: FilterFn; - } -} - -//useInfinity를 위한 데이터 조회 -const fetchSlopeData = async (pageParam = 0) => { - try { - const dupData = await slopeManageAPI.findOutlier(); - const allData = dupData.dup; - const start = pageParam * FETCH_SIZE; - const slicedData = allData.slice(start, start + FETCH_SIZE); - - return { - data: slicedData, - meta: { - totalCount: allData.length, // 전체 데이터 개수 - hasMore: start + FETCH_SIZE < allData.length, - }, - }; - } catch (error) { - console.error('급경사지 데이터 조회 오류:', error); - throw error; - } -}; - -//기본 표시할 열 항목 필터 설정 -const StepSlopeDup = () => { - const queryClient = useQueryClient(); - - const [columnVisibility, setColumnVisibility] = useState({ - startLatDegree: false, - startLatMinute: false, - startLatSecond: false, - startLongDegree: false, - startLongMinute: false, - startLongSecond: false, - endLatDegree: false, - endLatMinute: false, - endLatSecond: false, - endLongDegree: false, - endLongMinute: false, - endLongSecond: false, - }); - const [columnSizing, setColumnSizing] = useState({}); // 표 열 변수 - const tableContainerRef = useRef(null); //표 변수 - const columnHelper = createColumnHelper(); //열 선언 변수 - const [totalCount, setTotalCount] = useState(0); //전체 데이터 - const [searchQuery, setSearchQuery] = useState(''); //검색어 - const [selectedRegion, setSelectedRegion] = useState<{ - city: string; - county: string; - } | null>(null); //지역 검색 - const [inputValue, setInputValue] = useState(''); //검색입력 창 값 - - // 필터링 함수 - const filterData = ( - data: Slope[], - query: string, - region: { city: string; county: string } | null - ) => { - let filteredData = data; - - // 지역 필터 적용 - if (region) { - filteredData = filteredData.filter((slope) => { - if (region.county === '모두') { - return slope.location.province === region.city; - } - return ( - slope.location.province === region.city && - slope.location.city.indexOf(region.county) >= 0 - ); - }); - } - - // 검색어 필터 적용 - if (query) { - const searchLower = query.toLowerCase(); - filteredData = filteredData.filter((slope) => { - const searchableFields = [ - slope.managementNo, // 관리번호 - slope.name, // 급경사지명 - slope.management?.organization, // 시행청명 - slope.management?.authority, // 관리주체구분코드 - slope.management?.department, // 소관부서명 - slope.location?.province, // 시도 - slope.location?.city, // 시군구 - slope.location?.district, // 읍면동 - slope.location?.address, // 상세주소 - slope.location?.roadAddress, // 도로명상세주소 - slope.disaster?.riskType, // 재해위험도평가종류코드 - slope.disaster?.riskLevel, // 재해위험도평가등급코드 - ]; - - return searchableFields.some( - (field) => field && String(field).toLowerCase().includes(searchLower) - ); - }); - } - - return filteredData; - }; - - //데이터 조회 api - const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = - useInfiniteQuery({ - queryKey: ['slopes-dup'], - queryFn: ({ pageParam = 0 }) => fetchSlopeData(pageParam), - getNextPageParam: (lastPage, allPages) => { - const filteredData = filterData( - lastPage.data, - searchQuery, - selectedRegion - ); - return lastPage.meta.hasMore && filteredData.length >= FETCH_SIZE - ? allPages.length - : undefined; - }, - initialPageParam: 0, - }); - - //flatData를 통해 페이지 다시 계산 - const flatData = useMemo(() => { - const allData = data?.pages.flatMap((page) => page.data) ?? []; - return filterData(allData, searchQuery, selectedRegion); - }, [data, searchQuery, selectedRegion]); - - //데이터 전체 수 - useEffect(() => { - if (data?.pages[0]?.meta.totalCount) { - setTotalCount(data.pages[0].meta.totalCount); - } - }, [data]); - - //데이터 열 선언 - const columns = useMemo( - () => [ - columnHelper.accessor( - (_row, index) => { - const pageIndex = Math.floor(index / FETCH_SIZE); - return pageIndex * FETCH_SIZE + index + 1; - }, - { - id: 'index', - header: '번호', - size: 60, - } - ), - columnHelper.accessor('managementNo', { - header: '관리번호', - size: 120, - }), - columnHelper.accessor('name', { - header: '급경사지명', - size: 150, - }), - columnHelper.accessor((row) => row.management.organization || '', { - id: 'organization', - header: '시행청명', - size: 120, - }), - columnHelper.accessor((row) => row.management.authority || '', { - id: 'authority', - header: '관리주체구분코드', - size: 150, - }), - columnHelper.accessor((row) => row.management.department || '', { - id: 'department', - header: '소관부서명', - size: 150, - }), - columnHelper.accessor((row) => row.location.province, { - id: 'province', - header: '시도', - size: 100, - }), - columnHelper.accessor((row) => row.location.city, { - id: 'city', - header: '시군구', - size: 120, - }), - columnHelper.accessor((row) => row.location.district, { - id: 'district', - header: '읍면동', - size: 120, - }), - columnHelper.accessor((row) => row.location.address || '', { - id: 'address', - header: '상세주소', - size: 200, - }), - columnHelper.accessor((row) => row.location.roadAddress || '', { - id: 'roadAddress', - header: '도로명상세주소', - size: 120, - }), - columnHelper.accessor((row) => row.location.mountainAddress || '', { - id: 'mountainAddress', - header: '산주소여부', - size: 90, - }), - columnHelper.accessor((row) => row.location.mainLotNumber || '', { - id: 'mainLotNumber', - header: '주지번', - size: 60, - }), - columnHelper.accessor((row) => row.location.subLotNumber || '', { - id: 'subLotNumber', - header: '부지번', - size: 60, - }), - columnHelper.accessor( - (row) => { - const start = row.location.coordinates.start; - return start - ? `${start.coordinates[1].toFixed( - 6 - )}°N, ${start.coordinates[0].toFixed(6)}°E` - : ''; - }, - { - id: 'coordinates.start', - header: '시점위경도', - size: 150, - } - ), - - columnHelper.accessor( - (row) => { - const end = row.location.coordinates.end; - return end - ? `${end.coordinates[1].toFixed(6)}°N, ${end.coordinates[0].toFixed( - 6 - )}°E` - : ''; - }, - { - id: 'coordinates.end', - header: '종점위경도', - size: 150, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLatDegree || '', - { - id: 'startLatDegree', - header: 'GIS좌표시점위도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLatMinute || '', - { - id: 'startLatMinute', - header: 'GIS좌표시점위도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLatSecond || '', - { - id: 'startLatSecond', - header: 'GIS좌표시점위도초', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLongDegree || '', - { - id: 'startLongDegree', - header: 'GIS좌표시점경경도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLongMinute || '', - { - id: 'startLongMinute', - header: 'GIS좌표시점경경도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLongSecond || '', - { - id: 'startLongSecond', - header: 'GIS좌표시점경경도초', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLatDegree || '', - { - id: 'endLatDegree', - header: 'GIS좌표종점점위도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLatMinute || '', - { - id: 'endLatMinute', - header: 'GIS좌표종점점위도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLatSecond || '', - { - id: 'endLatSecond', - header: 'GIS좌표종점위도초', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLongDegree || '', - { - id: 'endLongDegree', - header: 'GIS좌표종점점위도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLongMinute || '', - { - id: 'endLongMinute', - header: 'GIS좌표종점점위도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLongSecond || '', - { - id: 'endLongSecond', - header: 'GIS좌표종점위도초', - size: 60, - } - ), - columnHelper.accessor((row) => row.inspections?.serialNumber ?? '', { - id: 'inspections_serialNumber', - header: '안전점검일련번호', - size: 130, - }), - columnHelper.accessor((row) => row.inspections?.date ?? '', { - id: 'inspectionDate', - header: '안전점검일자', - size: 120, - }), - columnHelper.accessor((row) => row.inspections?.result ?? '', { - id: 'inspectionResult', - header: '안전점검결과코드', - size: 130, - }), - columnHelper.accessor((row) => row.disaster?.serialNumber ?? '', { - id: 'serialNumber', - header: '재해위험도평가일련번호', - size: 180, - }), - columnHelper.accessor((row) => row.disaster?.riskDate ?? '', { - id: 'riskDate', - header: '재해위험도평가일자', - size: 170, - }), - columnHelper.accessor((row) => row.disaster?.riskLevel ?? '', { - id: 'riskLevel', - header: '재해위험도평가등급코드', - size: 180, - }), - columnHelper.accessor((row) => row.disaster?.riskScore ?? '', { - id: 'riskScore', - header: '재해위험도평가등급코드', - size: 180, - }), - columnHelper.accessor((row) => row.disaster?.riskType ?? '', { - id: 'riskType', - header: '재해위험도평가종류코드', - size: 180, - }), - columnHelper.accessor((row) => row.collapseRisk?.districtNo || '', { - id: 'districtNo', - header: '붕괴위험지구번호', - size: 130, - }), - columnHelper.accessor((row) => row.collapseRisk?.districtName || '', { - id: 'districtName', - header: '붕괴위험지구명', - size: 130, - }), - columnHelper.accessor( - (row) => (row.collapseRisk.designated ? '지정' : '미지정'), - { - id: 'riskDesignation', - header: '붕괴위험지구지정여부', - size: 160, - } - ), - columnHelper.accessor( - (row) => { - if (!row.collapseRisk?.designationDate) return ''; - return row.collapseRisk.designationDate; - }, - { - id: 'designationDate', - header: '붕괴위험지구지정일자', - size: 160, - } - ), - - columnHelper.accessor((row) => row.maintenanceProject?.type || '', { - id: 'maintenanceProject', - header: '정비사업유형코드', - size: 130, - }), - columnHelper.accessor((row) => row.maintenanceProject?.year || '', { - id: 'maintenanceYear', - header: '정비사업년도', - size: 120, - }), - ], - [] - ); - - // 스크롤 이벤트 핸들러 - const handleScroll = useCallback(() => { - const element = tableContainerRef.current; - if (!element || !hasNextPage || isFetching) return; - - const { scrollTop, scrollHeight, clientHeight } = element; - if (scrollHeight - scrollTop - clientHeight < 300) { - fetchNextPage(); - } - }, [fetchNextPage, hasNextPage, isFetching]); - const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { - const itemRank = rankItem(row.getValue(columnId), value); - addMeta({ itemRank }); - return itemRank.passed; - }; - - const table = useReactTable({ - data: flatData, - columns, - state: { - columnVisibility, - columnSizing, - }, - onColumnVisibilityChange: setColumnVisibility, - onColumnSizingChange: setColumnSizing, - columnResizeMode: 'onChange' as ColumnResizeMode, - enableColumnResizing: true, - getCoreRowModel: getCoreRowModel(), - defaultColumn: { - minSize: 80, - maxSize: 400, - }, - filterFns: { - fuzzy: fuzzyFilter, - }, - globalFilterFn: 'fuzzy', - }); - - const { rows } = table.getRowModel(); - - const rowVirtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => tableContainerRef.current, - estimateSize: () => 40, - overscan: 10, - }); - - const paddingTop = rowVirtualizer.getVirtualItems()[0]?.start || 0; - const paddingBottom = - rowVirtualizer.getTotalSize() - - (paddingTop + rowVirtualizer.getVirtualItems().length * 40); - - //필터 모달 관련 state함수 - const [isModalOpen, setIsModalOpen] = useState(false); - const onCloseModal = () => { - setIsModalOpen(false); - }; - - //지역 선택 모달 함수 - const [isRegionModalOpen, setIsRegionModalOpen] = useState(false); - const onCloseRegionModal = () => { - setIsRegionModalOpen(false); - }; - const handleRegionSelect = (city: string, county: string) => { - if (county === '모두') { - console.log(`${city}`); - } else { - console.log(`${city} ${county} `); - } - setSelectedRegion({ city, county }); - }; - - // 필터 초기화 함수 - const handleReset = () => { - setSearchQuery(''); - setSelectedRegion(null); - setColumnVisibility({ - startLatDegree: false, - startLatMinute: false, - startLatSecond: false, - startLongDegree: false, - startLongMinute: false, - startLongSecond: false, - endLatDegree: false, - endLatMinute: false, - endLatSecond: false, - endLongDegree: false, - endLongMinute: false, - endLongSecond: false, - }); - }; - - const [selectedRow, setSelectedRow] = useState(null); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - - const handleDelete = async () => { - try { - if (selectedRow) { - await slopeManageAPI.deleteSlope([selectedRow.managementNo]); - // 삭제 성공 후 데이터 갱신 - await queryClient.invalidateQueries({ queryKey: ['slopes'] }); - setSelectedRow(null); - setIsDeleteModalOpen(false); - } - } catch (error) { - console.error('삭제 실패:', error); - } - }; - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - - const handleEdit = async (updatedSlope: Slope) => { - try { - await slopeManageAPI.updateSlope(updatedSlope); - // 수정 성공 후 데이터 갱신 - await queryClient.invalidateQueries({ queryKey: ['slopes'] }); - setSelectedRow(null); - setIsEditModalOpen(false); - } catch (error) { - console.error('수정 실패:', error); - } - }; - return ( - - {/* 모달 */} - - - setIsDeleteModalOpen(false)} - onConfirm={handleDelete} - selectedRow={selectedRow} - /> - setIsEditModalOpen(false)} - onSubmit={handleEdit} - selectedRow={selectedRow} - /> - - {/* 헤더 */} - - - - { - setIsModalOpen(true); - }} - > - - -

표시할 열 항목 설정

-
- setIsRegionModalOpen(true)}> - - {selectedRegion - ? `${selectedRegion.city} ${ - selectedRegion.county === '모두' ? '' : selectedRegion.county - }` - : '지역선택'} - - - -

초기화

-
- - - - setSearchQuery(inputValue)} /> - ) => { - setInputValue(e.target.value); - }} - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - setSearchQuery(inputValue); - } - }} - /> - - -
-
- - 총 {totalCount}개 - - - {/* 테이블 */} - -
- - - {table.getHeaderGroups()[0].headers.map((header) => ( - - {header.column.columnDef.header as string} - - - ))} - - - - {paddingTop > 0 && ( - - - )} - {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const row = rows[virtualRow.index]; - return ( - { - if (selectedRow === row.original) setSelectedRow(null); - else setSelectedRow(row.original); - }} - $selected={ - selectedRow?.managementNo === row.original.managementNo - } - > - {row.getVisibleCells().map((cell) => ( - - {cell.getValue() as string} - - ))} - - ); - })} - {paddingBottom > 0 && ( - - - )} - -
-
-
-
- {(isFetchingNextPage || !data) && ( - - )} - {/* 하단 버튼 컨테이너 */} - {selectedRow && ( - - { - setIsEditModalOpen(true); - }} - > - 수정 - - { - setIsDeleteModalOpen(true); - }} - className="delete" - > - 삭제 - - - )} - - ); -}; - -export default StepSlopeDup; -//전체 컨테이너너 -const Container = styled.div` - width: 100%; - height: 50%; - display: flex; - flex-direction: column; -`; -//헤더 -const HeaderContainer = styled.div` - width: 100%; - height: 8%; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 20px; -`; -const HeaderWrapper = styled.div` - display: flex; - align-items: center; - gap: 10px; -`; - -const FilterButton = styled.button` - height: 34px; - display: flex; - align-items: center; - justify-content: flex-start; - gap: 8px; - padding: 0 8px; - background-color: white; - border: 1px solid #e5e7eb; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease-in-out; - - &:hover { - background-color: #f9fafb; - border-color: #d1d5db; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - } - - &:active { - background-color: #f3f4f6; - transform: scale(1.06); - } -`; - -//검색바 -const SearchWrapper = styled.div` - display: flex; - gap: 12px; -`; - -const SearchInput = styled.div` - position: relative; - - input { - width: 288px; - padding: 8px 16px 8px 40px; - border: 1px solid #e0e0e0; - border-radius: 8px; - } -`; -const SearchIcon = styled(SearchRoundedIcon)` - position: absolute; - width: 30px; - left: 8px; - top: 50%; - transform: translateY(-50%); - color: #bdbdbd; - cursor: pointer; -`; - -const TableSubInfo = styled.div` - width: 100%; - display: flex; - justify-content: flex-end; - padding: 0 30px; - min-height: 30px; - align-items: center; -`; -const TotalCount = styled.div` - font-size: ${({ theme }) => theme.fonts.sizes.ms}; - color: #374151; -`; - -//테이블블 -const TableContainer = styled.div` - height: 85%; - overflow: auto; - position: relative; -`; - -const Table = styled.table` - width: 100%; - border-collapse: collapse; - table-layout: fixed; -`; - -const TableHeader = styled.thead` - position: sticky; - top: 0; - /* background-color: #f9fafb; */ - background-color: ${({ theme }) => theme.colors.grey[100]}; -`; - -const HeaderCell = styled.th<{ width?: number }>` - border-bottom: 1px solid ${({ theme }) => theme.colors.grey[300]}; - border-right: 2px solid ${({ theme }) => theme.colors.grey[300]}; - padding: 0.5rem; - text-align: left; - position: relative; - width: ${(props) => props.width}px; -`; - -const ResizeHandle = styled.div` - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 5px; - cursor: col-resize; - background-color: #d1d5db; - opacity: 0; - transition: opacity 0.2s; - - &:hover { - opacity: 1; - } -`; - -const TableRow = styled.tr<{ $selected?: boolean }>` - cursor: pointer; - background-color: ${(props) => (props.$selected ? '#f0f7ff' : 'transparent')}; - - &:hover { - background-color: ${(props) => (props.$selected ? '#f0f7ff' : '#f9fafb')}; - } -`; - -const TableCell = styled.td<{ width?: number }>` - border-bottom: 1px solid #e5e7eb; - padding: 0.5rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: ${(props) => props.width}px; -`; - -//추가 수정 삭제 관련 css -const BottomButtonContainer = styled.div` - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 16px; - background-color: white; - border-top: 1px solid #e5e7eb; - display: flex; - justify-content: flex-end; - gap: 8px; - z-index: 10; -`; - -const ActionButton = styled.button` - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - background-color: #24478f; - color: white; - border: none; - - &:hover { - opacity: 0.9; - } - - &.delete { - background-color: #dc2626; - } -`; diff --git a/src/pages/ManagePage/StepSlope/components/OutlierEmpty.tsx b/src/pages/ManagePage/StepSlope/components/OutlierEmpty.tsx deleted file mode 100644 index f0e3ee3..0000000 --- a/src/pages/ManagePage/StepSlope/components/OutlierEmpty.tsx +++ /dev/null @@ -1,932 +0,0 @@ -import React, { - useState, - useMemo, - useRef, - useCallback, - useEffect, -} from 'react'; -import { - useReactTable, - getCoreRowModel, - VisibilityState, - createColumnHelper, - ColumnResizeMode, - ColumnSizingState, -} from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { FilterFn } from '@tanstack/react-table'; -import { rankItem } from '@tanstack/match-sorter-utils'; -import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; -import { Slope } from '../../../../apis/slopeMap'; -import styled from 'styled-components'; - -import { slopeManageAPI } from '../../../../apis/slopeManage'; -import FilterModal from '../components/ColumnFilterModal'; -import Title from '../../components/Title'; -import LoadingMessage from '../../components/LoadingMessage'; -import RegionFilterModal from '../components/RegionFilterModal'; - -import DeleteConfirmModal from '../components/DeleteModal'; -import EditModal from '../components/EditModal'; - -import TuneRoundedIcon from '@mui/icons-material/TuneRounded'; -import CachedRoundedIcon from '@mui/icons-material/CachedRounded'; -import TravelExploreRoundedIcon from '@mui/icons-material/TravelExploreRounded'; -import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; - -const FETCH_SIZE = 50; -declare module '@tanstack/react-table' { - interface FilterFns { - fuzzy: FilterFn; - } -} - -//useInfinity를 위한 데이터 조회 -const fetchSlopeData = async (pageParam = 0) => { - try { - const emptyData = await slopeManageAPI.findOutlier(); - const allData = emptyData.empty; - const start = pageParam * FETCH_SIZE; - const slicedData = allData.slice(start, start + FETCH_SIZE); - - return { - data: slicedData, - meta: { - totalCount: allData.length, // 전체 데이터 개수 - hasMore: start + FETCH_SIZE < allData.length, - }, - }; - } catch (error) { - console.error('급경사지 데이터 조회 오류:', error); - throw error; - } -}; - -//기본 표시할 열 항목 필터 설정 -const SteepSlopeEmpty = () => { - const queryClient = useQueryClient(); - - const [columnVisibility, setColumnVisibility] = useState({ - startLatDegree: false, - startLatMinute: false, - startLatSecond: false, - startLongDegree: false, - startLongMinute: false, - startLongSecond: false, - endLatDegree: false, - endLatMinute: false, - endLatSecond: false, - endLongDegree: false, - endLongMinute: false, - endLongSecond: false, - }); - const [columnSizing, setColumnSizing] = useState({}); // 표 열 변수 - const tableContainerRef = useRef(null); //표 변수 - const columnHelper = createColumnHelper(); //열 선언 변수 - const [totalCount, setTotalCount] = useState(0); //전체 데이터 - const [searchQuery, setSearchQuery] = useState(''); //검색어 - const [selectedRegion, setSelectedRegion] = useState<{ - city: string; - county: string; - } | null>(null); //지역 검색 - const [inputValue, setInputValue] = useState(''); //검색입력 창 값 - - // 필터링 함수 - const filterData = ( - data: Slope[], - query: string, - region: { city: string; county: string } | null - ) => { - let filteredData = data; - - // 지역 필터 적용 - if (region) { - filteredData = filteredData.filter((slope) => { - if (region.county === '모두') { - return slope.location.province === region.city; - } - return ( - slope.location.province === region.city && - slope.location.city.indexOf(region.county) >= 0 - ); - }); - } - - // 검색어 필터 적용 - if (query) { - const searchLower = query.toLowerCase(); - filteredData = filteredData.filter((slope) => { - const searchableFields = [ - slope.managementNo, // 관리번호 - slope.name, // 급경사지명 - slope.management?.organization, // 시행청명 - slope.management?.authority, // 관리주체구분코드 - slope.management?.department, // 소관부서명 - slope.location?.province, // 시도 - slope.location?.city, // 시군구 - slope.location?.district, // 읍면동 - slope.location?.address, // 상세주소 - slope.location?.roadAddress, // 도로명상세주소 - slope.disaster?.riskType, // 재해위험도평가종류코드 - slope.disaster?.riskLevel, // 재해위험도평가등급코드 - ]; - - return searchableFields.some( - (field) => field && String(field).toLowerCase().includes(searchLower) - ); - }); - } - - return filteredData; - }; - - //데이터 조회 api - const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = - useInfiniteQuery({ - queryKey: ['slopes-empty'], - queryFn: ({ pageParam = 0 }) => fetchSlopeData(pageParam), - getNextPageParam: (lastPage, allPages) => { - const filteredData = filterData( - lastPage.data, - searchQuery, - selectedRegion - ); - return lastPage.meta.hasMore && filteredData.length >= FETCH_SIZE - ? allPages.length - : undefined; - }, - initialPageParam: 0, - }); - - //flatData를 통해 페이지 다시 계산 - const flatData = useMemo(() => { - const allData = data?.pages.flatMap((page) => page.data) ?? []; - return filterData(allData, searchQuery, selectedRegion); - }, [data, searchQuery, selectedRegion]); - - //데이터 전체 수 - useEffect(() => { - if (data?.pages[0]?.meta.totalCount) { - setTotalCount(data.pages[0].meta.totalCount); - } - }, [data]); - - //데이터 열 선언 - const columns = useMemo( - () => [ - columnHelper.accessor( - (_row, index) => { - const pageIndex = Math.floor(index / FETCH_SIZE); - return pageIndex * FETCH_SIZE + index + 1; - }, - { - id: 'index', - header: '번호', - size: 60, - } - ), - columnHelper.accessor('managementNo', { - header: '관리번호', - size: 120, - }), - columnHelper.accessor('name', { - header: '급경사지명', - size: 150, - }), - columnHelper.accessor((row) => row.management.organization || '', { - id: 'organization', - header: '시행청명', - size: 120, - }), - columnHelper.accessor((row) => row.management.authority || '', { - id: 'authority', - header: '관리주체구분코드', - size: 150, - }), - columnHelper.accessor((row) => row.management.department || '', { - id: 'department', - header: '소관부서명', - size: 150, - }), - columnHelper.accessor((row) => row.location.province, { - id: 'province', - header: '시도', - size: 100, - }), - columnHelper.accessor((row) => row.location.city, { - id: 'city', - header: '시군구', - size: 120, - }), - columnHelper.accessor((row) => row.location.district, { - id: 'district', - header: '읍면동', - size: 120, - }), - columnHelper.accessor((row) => row.location.address || '', { - id: 'address', - header: '상세주소', - size: 200, - }), - columnHelper.accessor((row) => row.location.roadAddress || '', { - id: 'roadAddress', - header: '도로명상세주소', - size: 120, - }), - columnHelper.accessor((row) => row.location.mountainAddress || '', { - id: 'mountainAddress', - header: '산주소여부', - size: 90, - }), - columnHelper.accessor((row) => row.location.mainLotNumber || '', { - id: 'mainLotNumber', - header: '주지번', - size: 60, - }), - columnHelper.accessor((row) => row.location.subLotNumber || '', { - id: 'subLotNumber', - header: '부지번', - size: 60, - }), - columnHelper.accessor( - (row) => { - const start = row.location.coordinates.start; - return start - ? `${start.coordinates[1].toFixed( - 6 - )}°N, ${start.coordinates[0].toFixed(6)}°E` - : ''; - }, - { - id: 'coordinates.start', - header: '시점위경도', - size: 150, - } - ), - - columnHelper.accessor( - (row) => { - const end = row.location.coordinates.end; - return end - ? `${end.coordinates[1].toFixed(6)}°N, ${end.coordinates[0].toFixed( - 6 - )}°E` - : ''; - }, - { - id: 'coordinates.end', - header: '종점위경도', - size: 150, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLatDegree || '', - { - id: 'startLatDegree', - header: 'GIS좌표시점위도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLatMinute || '', - { - id: 'startLatMinute', - header: 'GIS좌표시점위도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLatSecond || '', - { - id: 'startLatSecond', - header: 'GIS좌표시점위도초', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLongDegree || '', - { - id: 'startLongDegree', - header: 'GIS좌표시점경경도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLongMinute || '', - { - id: 'startLongMinute', - header: 'GIS좌표시점경경도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.start.startLongSecond || '', - { - id: 'startLongSecond', - header: 'GIS좌표시점경경도초', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLatDegree || '', - { - id: 'endLatDegree', - header: 'GIS좌표종점점위도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLatMinute || '', - { - id: 'endLatMinute', - header: 'GIS좌표종점점위도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLatSecond || '', - { - id: 'endLatSecond', - header: 'GIS좌표종점위도초', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLongDegree || '', - { - id: 'endLongDegree', - header: 'GIS좌표종점점위도도', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLongMinute || '', - { - id: 'endLongMinute', - header: 'GIS좌표종점점위도분', - size: 60, - } - ), - columnHelper.accessor( - (row) => row.location.coordinates.end.endLongSecond || '', - { - id: 'endLongSecond', - header: 'GIS좌표종점위도초', - size: 60, - } - ), - columnHelper.accessor((row) => row.inspections?.serialNumber ?? '', { - id: 'inspections_serialNumber', - header: '안전점검일련번호', - size: 130, - }), - columnHelper.accessor((row) => row.inspections?.date ?? '', { - id: 'inspectionDate', - header: '안전점검일자', - size: 120, - }), - columnHelper.accessor((row) => row.inspections?.result ?? '', { - id: 'inspectionResult', - header: '안전점검결과코드', - size: 130, - }), - columnHelper.accessor((row) => row.disaster?.serialNumber ?? '', { - id: 'serialNumber', - header: '재해위험도평가일련번호', - size: 180, - }), - columnHelper.accessor((row) => row.disaster?.riskDate ?? '', { - id: 'riskDate', - header: '재해위험도평가일자', - size: 170, - }), - columnHelper.accessor((row) => row.disaster?.riskLevel ?? '', { - id: 'riskLevel', - header: '재해위험도평가등급코드', - size: 180, - }), - columnHelper.accessor((row) => row.disaster?.riskScore ?? '', { - id: 'riskScore', - header: '재해위험도평가등급코드', - size: 180, - }), - columnHelper.accessor((row) => row.disaster?.riskType ?? '', { - id: 'riskType', - header: '재해위험도평가종류코드', - size: 180, - }), - columnHelper.accessor((row) => row.collapseRisk?.districtNo || '', { - id: 'districtNo', - header: '붕괴위험지구번호', - size: 130, - }), - columnHelper.accessor((row) => row.collapseRisk?.districtName || '', { - id: 'districtName', - header: '붕괴위험지구명', - size: 130, - }), - columnHelper.accessor( - (row) => (row.collapseRisk.designated ? '지정' : '미지정'), - { - id: 'riskDesignation', - header: '붕괴위험지구지정여부', - size: 160, - } - ), - columnHelper.accessor( - (row) => { - if (!row.collapseRisk?.designationDate) return ''; - return row.collapseRisk.designationDate; - }, - { - id: 'designationDate', - header: '붕괴위험지구지정일자', - size: 160, - } - ), - - columnHelper.accessor((row) => row.maintenanceProject?.type || '', { - id: 'maintenanceProject', - header: '정비사업유형코드', - size: 130, - }), - columnHelper.accessor((row) => row.maintenanceProject?.year || '', { - id: 'maintenanceYear', - header: '정비사업년도', - size: 120, - }), - ], - [] - ); - // 스크롤 이벤트 핸들러 - const handleScroll = useCallback(() => { - const element = tableContainerRef.current; - if (!element || !hasNextPage || isFetching) return; - - const { scrollTop, scrollHeight, clientHeight } = element; - if (scrollHeight - scrollTop - clientHeight < 300) { - fetchNextPage(); - } - }, [fetchNextPage, hasNextPage, isFetching]); - const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { - const itemRank = rankItem(row.getValue(columnId), value); - addMeta({ itemRank }); - return itemRank.passed; - }; - - const table = useReactTable({ - data: flatData, - columns, - state: { - columnVisibility, - columnSizing, - }, - onColumnVisibilityChange: setColumnVisibility, - onColumnSizingChange: setColumnSizing, - columnResizeMode: 'onChange' as ColumnResizeMode, - enableColumnResizing: true, - getCoreRowModel: getCoreRowModel(), - defaultColumn: { - minSize: 80, - maxSize: 400, - }, - filterFns: { - fuzzy: fuzzyFilter, - }, - globalFilterFn: 'fuzzy', - }); - - const { rows } = table.getRowModel(); - - const rowVirtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => tableContainerRef.current, - estimateSize: () => 40, - overscan: 10, - }); - - const paddingTop = rowVirtualizer.getVirtualItems()[0]?.start || 0; - const paddingBottom = - rowVirtualizer.getTotalSize() - - (paddingTop + rowVirtualizer.getVirtualItems().length * 40); - - //필터 모달 관련 state함수 - const [isModalOpen, setIsModalOpen] = useState(false); - const onCloseModal = () => { - setIsModalOpen(false); - }; - - //지역 선택 모달 함수 - const [isRegionModalOpen, setIsRegionModalOpen] = useState(false); - const onCloseRegionModal = () => { - setIsRegionModalOpen(false); - }; - const handleRegionSelect = (city: string, county: string) => { - if (county === '모두') { - console.log(`${city}`); - } else { - console.log(`${city} ${county} `); - } - setSelectedRegion({ city, county }); - }; - - // 필터 초기화 함수 - const handleReset = () => { - setSearchQuery(''); - setSelectedRegion(null); - setColumnVisibility({ - startLatDegree: false, - startLatMinute: false, - startLatSecond: false, - startLongDegree: false, - startLongMinute: false, - startLongSecond: false, - endLatDegree: false, - endLatMinute: false, - endLatSecond: false, - endLongDegree: false, - endLongMinute: false, - endLongSecond: false, - }); - }; - - const [selectedRow, setSelectedRow] = useState(null); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - - const handleDelete = async () => { - try { - if (selectedRow) { - await slopeManageAPI.deleteSlope([selectedRow.managementNo]); - // 삭제 성공 후 데이터 갱신 - await queryClient.invalidateQueries({ queryKey: ['slopes'] }); - setSelectedRow(null); - setIsDeleteModalOpen(false); - } - } catch (error) { - console.error('삭제 실패:', error); - } - }; - - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - - const handleEdit = async (updatedSlope: Slope) => { - try { - await slopeManageAPI.updateSlope(updatedSlope); - // 수정 성공 후 데이터 갱신 - await queryClient.invalidateQueries({ queryKey: ['slopes'] }); - setSelectedRow(null); - setIsEditModalOpen(false); - } catch (error) { - console.error('수정 실패:', error); - } - }; - return ( - - {/* 모달 */} - - - setIsDeleteModalOpen(false)} - onConfirm={handleDelete} - selectedRow={selectedRow} - /> - setIsEditModalOpen(false)} - onSubmit={handleEdit} - selectedRow={selectedRow} - /> - - {/* 헤더 */} - - - - { - setIsModalOpen(true); - }} - > - - -

표시할 열 항목 설정

-
- setIsRegionModalOpen(true)}> - - {selectedRegion - ? `${selectedRegion.city} ${ - selectedRegion.county === '모두' ? '' : selectedRegion.county - }` - : '지역선택'} - - - -

초기화

-
- - - - setSearchQuery(inputValue)} /> - ) => { - setInputValue(e.target.value); - }} - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - setSearchQuery(inputValue); - } - }} - /> - - -
-
- - 총 {totalCount}개 - - - {/* 테이블 */} - - - - - {table.getHeaderGroups()[0].headers.map((header) => ( - - {header.column.columnDef.header as string} - - - ))} - - - - {paddingTop > 0 && ( - - - )} - {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const row = rows[virtualRow.index]; - return ( - { - if (selectedRow === row.original) setSelectedRow(null); - else setSelectedRow(row.original); - }} - $selected={ - selectedRow?.managementNo === row.original.managementNo - } - > - {row.getVisibleCells().map((cell) => ( - - {cell.getValue() as string} - - ))} - - ); - })} - {paddingBottom > 0 && ( - - - )} - -
-
-
-
- {(isFetchingNextPage || !data) && ( - - )} - {/* 하단 버튼 컨테이너 */} - {selectedRow && ( - - { - setIsEditModalOpen(true); - }} - > - 수정 - - { - setIsDeleteModalOpen(true); - }} - className="delete" - > - 삭제 - - - )} -
- ); -}; - -export default SteepSlopeEmpty; -//전체 컨테이너너 -const Container = styled.div` - width: 100%; - height: 50%; - display: flex; - flex-direction: column; -`; -//헤더 -const HeaderContainer = styled.div` - width: 100%; - height: 8%; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 20px; -`; -const HeaderWrapper = styled.div` - display: flex; - align-items: center; - gap: 10px; -`; - -const FilterButton = styled.button` - height: 34px; - display: flex; - align-items: center; - justify-content: flex-start; - gap: 8px; - padding: 0 8px; - background-color: white; - border: 1px solid #e5e7eb; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease-in-out; - - &:hover { - background-color: #f9fafb; - border-color: #d1d5db; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - } - - &:active { - background-color: #f3f4f6; - transform: scale(1.06); - } -`; - -//검색바 -const SearchWrapper = styled.div` - display: flex; - gap: 12px; -`; - -const SearchInput = styled.div` - position: relative; - - input { - width: 288px; - padding: 8px 16px 8px 40px; - border: 1px solid #e0e0e0; - border-radius: 8px; - } -`; -const SearchIcon = styled(SearchRoundedIcon)` - position: absolute; - width: 30px; - left: 8px; - top: 50%; - transform: translateY(-50%); - color: #bdbdbd; - cursor: pointer; -`; - -const TableSubInfo = styled.div` - width: 100%; - display: flex; - justify-content: flex-end; - padding: 0 30px; - min-height: 30px; - align-items: center; -`; -const TotalCount = styled.div` - font-size: ${({ theme }) => theme.fonts.sizes.ms}; - color: #374151; -`; - -//테이블블 -const TableContainer = styled.div` - height: 85%; - overflow: auto; - position: relative; -`; - -const Table = styled.table` - width: 100%; - border-collapse: collapse; - table-layout: fixed; -`; - -const TableHeader = styled.thead` - position: sticky; - top: 0; - /* background-color: #f9fafb; */ - background-color: ${({ theme }) => theme.colors.grey[100]}; -`; - -const HeaderCell = styled.th<{ width?: number }>` - border-bottom: 1px solid ${({ theme }) => theme.colors.grey[300]}; - border-right: 2px solid ${({ theme }) => theme.colors.grey[300]}; - padding: 0.5rem; - text-align: left; - position: relative; - width: ${(props) => props.width}px; -`; - -const ResizeHandle = styled.div` - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 5px; - cursor: col-resize; - background-color: #d1d5db; - opacity: 0; - transition: opacity 0.2s; - - &:hover { - opacity: 1; - } -`; - -const TableRow = styled.tr<{ $selected?: boolean }>` - cursor: pointer; - background-color: ${(props) => (props.$selected ? '#f0f7ff' : 'transparent')}; - - &:hover { - background-color: ${(props) => (props.$selected ? '#f0f7ff' : '#f9fafb')}; - } -`; - -const TableCell = styled.td<{ width?: number }>` - border-bottom: 1px solid #e5e7eb; - padding: 0.5rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: ${(props) => props.width}px; -`; - -//추가 수정 삭제 관련 css -const BottomButtonContainer = styled.div` - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 16px; - background-color: white; - border-top: 1px solid #e5e7eb; - display: flex; - justify-content: flex-end; - gap: 8px; - z-index: 10; -`; - -const ActionButton = styled.button` - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - background-color: #24478f; - color: white; - border: none; - - &:hover { - opacity: 0.9; - } - - &.delete { - background-color: #dc2626; - } -`; diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index dc29da9..a21ff09 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -101,7 +101,7 @@ const SteepSlopeLookUp = () => { return response; }, getNextPageParam: (lastPage) => - lastPage.meta.hasMore ? lastPage.meta.currentPage + 1 : undefined, + lastPage?.meta?.hasMore ? lastPage.meta.currentPage + 1 : undefined, initialPageParam: 0, }); @@ -117,9 +117,9 @@ const SteepSlopeLookUp = () => { //데이터 전체 수 useEffect(() => { - if (data?.pages[0]?.meta.totalCount) { + if (data?.pages[0]?.meta?.totalCount) setTotalCount(data.pages[0].meta.totalCount); - } + else setTotalCount(0); }, [data, setTotalCount]); //데이터 열 선언 @@ -159,7 +159,9 @@ const SteepSlopeLookUp = () => { }, globalFilterFn: 'fuzzy', }); - + useEffect(() => { + console.log('API 응답:', data); + }, [data]); useEffect(() => { // rowSelection 상태에서 선택된 행 추출 const selectedRowsArray = Object.keys(rowSelection) @@ -313,6 +315,7 @@ const SteepSlopeLookUp = () => { isDownloading={isDownloading} totalCount={totalCount} /> + { - return ( - - - - - ); -}; - -export default SteepSlopeOutlier; -const Container = styled.div` - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - gap: 20px; - padding-top: 20px; -`; diff --git a/src/pages/ManagePage/components/SideComponents.tsx b/src/pages/ManagePage/components/SideComponents.tsx index 41816c8..88a07e7 100644 --- a/src/pages/ManagePage/components/SideComponents.tsx +++ b/src/pages/ManagePage/components/SideComponents.tsx @@ -6,7 +6,7 @@ const SideComponents: FC = ({ selectPage, ChooseIndex, }) => { - const [toggle, setToggle] = useState<[boolean, boolean]>([false, false]); + const [toggle, setToggle] = useState([false, false, false]); return ( @@ -21,7 +21,7 @@ const SideComponents: FC = ({ */} { - setToggle([!toggle[0], toggle[1]]); + setToggle([!toggle[0], toggle[1], toggle[2]]); }} > 급경사지 관리 @@ -43,45 +43,67 @@ const SideComponents: FC = ({ > 급경사지 추가 - ChooseIndex(3)} - > - - 급경사지 이상값 찾기 - - )} { - setToggle([toggle[0], !toggle[1]]); + setToggle([toggle[0], !toggle[1], toggle[2]]); }} > - 회원관리 + 급경사지 이상값 {toggle[1] && ( <> + ChooseIndex(3)} + > + 빈값 찾기 + ChooseIndex(4)} > - 회원조회 및 승인 + 중복값 찾기 - ChooseIndex(5)} > - 회원수정 및 삭제 + 위치이상 + */} + + )} + { + setToggle([toggle[0], toggle[1], !toggle[2]]); + }} + > + 회원관리 + + + {toggle[2] && ( + <> + ChooseIndex(6)} + > + 회원조회 및 승인 + + ChooseIndex(7)} + > + 회원수정 및 삭제 )} ChooseIndex(6)} + $isSelect={selectPage[8]} + onClick={() => ChooseIndex(8)} > - 지도 + 지도 diff --git a/src/pages/ManagePage/interface.ts b/src/pages/ManagePage/interface.ts index d356838..8f92b1c 100644 --- a/src/pages/ManagePage/interface.ts +++ b/src/pages/ManagePage/interface.ts @@ -6,6 +6,8 @@ export type SelectPageState = [ boolean, boolean, boolean, + boolean, + boolean, boolean ]; // 목차 선택 페이지 관련 타입 diff --git a/src/stores/steepSlopeStore.ts b/src/stores/steepSlopeStore.ts index 394d18c..a251229 100644 --- a/src/stores/steepSlopeStore.ts +++ b/src/stores/steepSlopeStore.ts @@ -3,3 +3,6 @@ import { Slope } from '../apis/slopeMap'; // 급경사지 스토어 생성 - 팩토리 함수 사용 export const useSteepSlopeStore = createTableStore(); +export const useSteepSlopeDupStore = createTableStore(); +export const useSteepSlopeEmptyStore = createTableStore(); +export const useSteepSlopeLocationStore = createTableStore();