diff --git a/src/apis/slopeManage.tsx b/src/apis/slopeManage.tsx index dcc8703..5ece429 100644 --- a/src/apis/slopeManage.tsx +++ b/src/apis/slopeManage.tsx @@ -5,6 +5,7 @@ interface FetchSlopeParams { pageSize: number; searchQuery?: string; city?: string; + grade?: string; county?: string; } diff --git a/src/apis/slopeMap.tsx b/src/apis/slopeMap.tsx index 4dc21ac..8d628a7 100644 --- a/src/apis/slopeMap.tsx +++ b/src/apis/slopeMap.tsx @@ -66,6 +66,24 @@ export interface Slope { historyNumber: string; inspectionDate: string; }; + priority: { + usage: string; // 비탈면용도 + slopeNature: string; // 자연/인공 구분 + slopeType: string; // 비탈면유형 + slopeStructure: string; // 비탈면구조 + maxVerticalHeight: string; // 최고수직고 (단위: m) + longitudinalLength: string; // 종단길이 (단위: m) + averageSlope: string; // 평균경사 (단위: 도) + images: [ + { + // 이미지 + url: string; // url + createdAt: Date; // 이미지 생성날짜 + } + ]; + Score: string; //점수 + grade: string; // 등급 + }; createdAt: Date; _id: string; } diff --git a/src/components/NotificationProvider.tsx b/src/components/NotificationProvider.tsx index 88a4066..632c6fb 100644 --- a/src/components/NotificationProvider.tsx +++ b/src/components/NotificationProvider.tsx @@ -32,8 +32,14 @@ export const NotificationProvider = () => { autoHideDuration={autoHideDuration} onClose={handleClose} anchorOrigin={{ vertical: 'top', horizontal: 'right' }} + sx={{}} > - + {message} diff --git a/src/hooks/notificationStore.ts b/src/hooks/notificationStore.ts index b88ecce..24c860e 100644 --- a/src/hooks/notificationStore.ts +++ b/src/hooks/notificationStore.ts @@ -20,7 +20,7 @@ export const useNotificationStore = create((set) => ({ isOpen: false, message: '', severity: 'info', - autoHideDuration: 4000, + autoHideDuration: 3000, showNotification: (message, options = {}) => set({ @@ -30,7 +30,7 @@ export const useNotificationStore = create((set) => ({ autoHideDuration: options.autoHideDuration !== undefined ? options.autoHideDuration - : 4000, + : 3000, }), hideNotification: () => set({ isOpen: false }), diff --git a/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx b/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx index 5ca311f..84941e0 100644 --- a/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx +++ b/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx @@ -1,6 +1,39 @@ -import styled from 'styled-components'; import { Slope } from '../../../../apis/slopeMap'; -import { useState } from 'react'; +import SlopeForm from './SlopeForm'; +import { slopeManageAPI } from '../../../../apis/slopeManage'; + +interface AddSlopeProps { + isOpen: boolean; + onClose: () => void; +} + +const AddSlope = ({ isOpen, onClose }: AddSlopeProps) => { + const onSubmit = async (newSlopeData: Slope) => { + try { + await slopeManageAPI.createSlope(newSlopeData); + onClose(); + } catch (error) { + console.error('파일 업로드 오류:', error); + } + }; + + if (!isOpen) return null; + return ( + { + onSubmit(data); + }} + submitButtonText="저장" + /> + ); +}; + +export default AddSlope; + const createDefaultSlope = (): Slope => { const today = new Date(); @@ -70,908 +103,24 @@ const createDefaultSlope = (): Slope => { historyNumber: '', inspectionDate: '', }, + priority: { + usage: '', + slopeNature: '', + slopeType: '', + slopeStructure: '', + maxVerticalHeight: '', + longitudinalLength: '', + averageSlope: '', + images: [ + { + url: '', + createdAt: today, + }, + ], + Score: '', + grade: '', + }, createdAt: today, _id: '', }; }; - -interface AddSlopeProps { - onSubmit: (updatedSlope: Slope) => void; -} - -const AddSlope = ({ onSubmit }: AddSlopeProps) => { - const [newSlopeData, setNewSlopeData] = useState(createDefaultSlope()); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - console.log('버튼은 누름'); - onSubmit(newSlopeData); - //초기화 - // setNewSlopeData(createDefaultSlope()); - }; - - return ( - -
- - -
- 기본 정보 - - - - setNewSlopeData({ - ...newSlopeData, - managementNo: e.target.value, - }) - } - /> - - - - - setNewSlopeData({ ...newSlopeData, name: e.target.value }) - } - /> - -
- -
- 관리 정보 - - - - setNewSlopeData({ - ...newSlopeData, - management: { - ...newSlopeData.management, - organization: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - management: { - ...newSlopeData.management, - authority: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - management: { - ...newSlopeData.management, - department: e.target.value, - }, - }) - } - /> - -
- -
- 위치 정보 - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - province: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - city: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - district: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - address: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - roadAddress: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - mountainAddress: e.target.value, - }, - }) - } - /> - - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - mainLotNumber: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - subLotNumber: e.target.value, - }, - }) - } - /> - - -
-
-
- - -
- GIS 정보 - GIS좌표시점 - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - start: { - ...newSlopeData.location.coordinates.start, - startLatDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - start: { - ...newSlopeData.location.coordinates.start, - startLatMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - start: { - ...newSlopeData.location.coordinates.start, - startLatSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - start: { - ...newSlopeData.location.coordinates.start, - startLongDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - start: { - ...newSlopeData.location.coordinates.start, - startLongMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - start: { - ...newSlopeData.location.coordinates.start, - startLongSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - GIS좌표종점 - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - end: { - ...newSlopeData.location.coordinates.end, - endLatDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - end: { - ...newSlopeData.location.coordinates.end, - endLatMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - end: { - ...newSlopeData.location.coordinates.end, - endLatSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - end: { - ...newSlopeData.location.coordinates.end, - endLongDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - end: { - ...newSlopeData.location.coordinates.end, - endLongMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - location: { - ...newSlopeData.location, - coordinates: { - ...newSlopeData.location.coordinates, - end: { - ...newSlopeData.location.coordinates.end, - endLongSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - -
-
- 안전 - - - - setNewSlopeData({ - ...newSlopeData, - inspections: { - ...newSlopeData.inspections, - serialNumber: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - inspections: { - ...newSlopeData.inspections, - date: new Date(e.target.value), - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - inspections: { - ...newSlopeData.inspections, - result: e.target.value, - }, - }) - } - /> - -
-
- 재해위험도 - - - - setNewSlopeData({ - ...newSlopeData, - disaster: { - ...newSlopeData.disaster, - serialNumber: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - disaster: { - ...newSlopeData.disaster, - riskDate: new Date(e.target.value), - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - disaster: { - ...newSlopeData.disaster, - riskLevel: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - disaster: { - ...newSlopeData.disaster, - riskScore: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - disaster: { - ...newSlopeData.disaster, - riskType: e.target.value, - }, - }) - } - /> - -
-
- 붕괴위험지구 - - - - setNewSlopeData({ - ...newSlopeData, - collapseRisk: { - ...newSlopeData.collapseRisk, - districtNo: e.target.value, - }, - }) - } - /> - - - - - - setNewSlopeData({ - ...newSlopeData, - collapseRisk: { - ...newSlopeData.collapseRisk, - districtName: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - collapseRisk: { - ...newSlopeData.collapseRisk, - designated: e.target.checked, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - collapseRisk: { - ...newSlopeData.collapseRisk, - designationDate: new Date(e.target.value), - }, - }) - } - /> - -
-
- 정비사업 - - - - setNewSlopeData({ - ...newSlopeData, - maintenanceProject: { - ...newSlopeData.maintenanceProject, - type: e.target.value, - }, - }) - } - /> - - - - - setNewSlopeData({ - ...newSlopeData, - maintenanceProject: { - ...newSlopeData.maintenanceProject, - year: e.target.value, - }, - }) - } - /> - -
-
-
-
- - - 추가하기 - - -
- ); -}; -export default AddSlope; - -const BaseContainer = styled.div` - position: relative; - display: flex; - flex-direction: column; - width: 100%; - height: 85%; -`; - -const ContentContainer = styled.div` - background: white; - border-radius: 8px; - width: 100%; - display: flex; - flex-direction: column; -`; - -const Form = styled.form` - display: flex; - flex: 1; - overflow-y: auto; - position: relative; -`; - -const ScrollContainer = styled.div` - flex: 1; - overflow-y: auto; - padding: 24px; -`; - -const Section = styled.div` - margin-bottom: 12px; - padding-bottom: 12px; - &:last-child { - margin-bottom: 0; - } - border-bottom: 1px solid ${({ theme }) => theme.colors.grey[400]}; -`; - -const SectionTitle = styled.h3` - font-size: 16px; - color: ${({ theme }) => theme.colors.grey[900]}; - margin: 0 0 16px 0; -`; -const SubSectionTitle = styled.div` - font-size: 14px; - color: ${({ theme }) => theme.colors.grey[900]}; - margin: 20px 0 16px 0; - font-weight: bold; -`; -const FormGroup = styled.div` - margin-bottom: 16px; -`; - -const Label = styled.label` - display: block; - margin-bottom: 8px; - font-size: 14px; - color: ${({ theme }) => theme.colors.grey[700]}; -`; - -const Input = styled.input` - width: 100%; - padding: 8px 12px; - border: 1px solid ${({ theme }) => theme.colors.grey[300]}; - border-radius: 6px; - font-size: 14px; - - &:focus { - outline: none; - border-color: #24478f; - box-shadow: 0 0 0 2px rgba(36, 71, 143, 0.1); - } -`; - -const ButtonGroup = styled.div` - top: -90px; - right: 0; - position: absolute; - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 16px 24px; -`; - -const Button = styled.button` - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -`; - -const SubmitButton = styled(Button)` - background: #24478f; - color: white; - border: none; - - &:hover { - opacity: 0.9; - } -`; - -const SubSection = styled.div` - display: flex; - justify-content: space-between; - gap: 25px; -`; diff --git a/src/pages/ManagePage/StepSlope/components/EditModal.tsx b/src/pages/ManagePage/StepSlope/components/EditModal.tsx index 3fe829a..de2a4c2 100644 --- a/src/pages/ManagePage/StepSlope/components/EditModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/EditModal.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { Slope } from '../../../../apis/slopeMap'; import { useState, useEffect } from 'react'; +import SlopeForm from './SlopeForm'; interface EditModalProps { isOpen: boolean; @@ -23,936 +24,23 @@ const EditModal = ({ } }, [selectedRow]); - if (!editedData) return null; + if (!isOpen || !editedData) return null; - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (editedData) { - onSubmit(editedData); - } + const handleSubmit = (data: Slope) => { + onSubmit(data); onClose(); }; return ( - - - -

급경사지 정보 수정

- × -
-
- -
- 기본 정보 - - - - setEditedData({ - ...editedData, - managementNo: e.target.value, - }) - } - /> - - - - - setEditedData({ ...editedData, name: e.target.value }) - } - /> - -
- -
- 관리 정보 - - - - setEditedData({ - ...editedData, - management: { - ...editedData.management, - organization: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - management: { - ...editedData.management, - authority: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - management: { - ...editedData.management, - department: e.target.value, - }, - }) - } - /> - -
- -
- 위치 정보 - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - province: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - city: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - district: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - address: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - roadAddress: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - mountainAddress: e.target.value, - }, - }) - } - /> - - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - mainLotNumber: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - subLotNumber: e.target.value, - }, - }) - } - /> - - -
-
- GIS 정보 - GIS좌표시점 - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - start: { - ...editedData.location.coordinates.start, - startLatDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - start: { - ...editedData.location.coordinates.start, - startLatMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - start: { - ...editedData.location.coordinates.start, - startLatSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - start: { - ...editedData.location.coordinates.start, - startLongDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - start: { - ...editedData.location.coordinates.start, - startLongMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - start: { - ...editedData.location.coordinates.start, - startLongSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - GIS좌표종점 - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - end: { - ...editedData.location.coordinates.end, - endLatDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - end: { - ...editedData.location.coordinates.end, - endLatMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - end: { - ...editedData.location.coordinates.end, - endLatSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - end: { - ...editedData.location.coordinates.end, - endLongDegree: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - end: { - ...editedData.location.coordinates.end, - endLongMinute: Number(e.target.value), - }, - }, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - location: { - ...editedData.location, - coordinates: { - ...editedData.location.coordinates, - end: { - ...editedData.location.coordinates.end, - endLongSecond: Number(e.target.value), - }, - }, - }, - }) - } - /> - - -
-
- 안전 - - - - setEditedData({ - ...editedData, - inspections: { - ...editedData.inspections, - serialNumber: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - inspections: { - ...editedData.inspections, - date: new Date(e.target.value), - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - inspections: { - ...editedData.inspections, - result: e.target.value, - }, - }) - } - /> - -
-
- 재해위험도 - - - - setEditedData({ - ...editedData, - disaster: { - ...editedData.disaster, - serialNumber: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - disaster: { - ...editedData.disaster, - riskDate: new Date(e.target.value), - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - disaster: { - ...editedData.disaster, - riskLevel: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - disaster: { - ...editedData.disaster, - riskScore: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - disaster: { - ...editedData.disaster, - riskType: e.target.value, - }, - }) - } - /> - -
-
- 붕괴위험지구 - - - - setEditedData({ - ...editedData, - collapseRisk: { - ...editedData.collapseRisk, - districtNo: e.target.value, - }, - }) - } - /> - - - - - - setEditedData({ - ...editedData, - collapseRisk: { - ...editedData.collapseRisk, - districtName: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - collapseRisk: { - ...editedData.collapseRisk, - designated: e.target.checked, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - collapseRisk: { - ...editedData.collapseRisk, - designationDate: new Date(e.target.value), - }, - }) - } - /> - -
-
- 정비사업 - - - - setEditedData({ - ...editedData, - maintenanceProject: { - ...editedData.maintenanceProject, - type: e.target.value, - }, - }) - } - /> - - - - - setEditedData({ - ...editedData, - maintenanceProject: { - ...editedData.maintenanceProject, - year: e.target.value, - }, - }) - } - /> - -
-
- - - 저장 - - 취소 - - -
-
-
+ ); }; -const ModalOverlay = styled.div<{ $isOpen: boolean }>` - display: ${(props) => (props.$isOpen ? 'flex' : 'none')}; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; - align-items: center; - z-index: 1000; -`; - -const ModalContent = styled.div` - background: white; - border-radius: 8px; - width: 100%; - max-width: 600px; - max-height: 90vh; - display: flex; - flex-direction: column; -`; - -const ModalHeader = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 24px; - border-bottom: 1px solid ${({ theme }) => theme.colors.grey[200]}; - - h2 { - margin: 0; - font-size: 20px; - color: ${({ theme }) => theme.colors.grey[900]}; - } -`; - -const Form = styled.form` - display: flex; - flex-direction: column; - flex: 1; - overflow-y: auto; -`; - -const ScrollContainer = styled.div` - flex: 1; - overflow-y: auto; - padding: 24px; -`; - -const Section = styled.div` - margin-bottom: 12px; - padding-bottom: 12px; - &:last-child { - margin-bottom: 0; - } - border-bottom: 1px solid ${({ theme }) => theme.colors.grey[400]}; -`; - -const SectionTitle = styled.h3` - font-size: 16px; - color: ${({ theme }) => theme.colors.grey[900]}; - margin: 0 0 16px 0; -`; -const SubSectionTitle = styled.div` - font-size: 14px; - color: ${({ theme }) => theme.colors.grey[900]}; - margin: 20px 0 16px 0; - font-weight: bold; -`; -const FormGroup = styled.div` - margin-bottom: 16px; -`; - -const Label = styled.label` - display: block; - margin-bottom: 8px; - font-size: 14px; - color: ${({ theme }) => theme.colors.grey[700]}; -`; - -const Input = styled.input` - width: 100%; - padding: 8px 12px; - border: 1px solid ${({ theme }) => theme.colors.grey[300]}; - border-radius: 6px; - font-size: 14px; - - &:focus { - outline: none; - border-color: #24478f; - box-shadow: 0 0 0 2px rgba(36, 71, 143, 0.1); - } -`; - -const ButtonGroup = styled.div` - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 16px 24px; - border-top: 1px solid ${({ theme }) => theme.colors.grey[200]}; -`; - -const Button = styled.button` - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -`; - -const SubmitButton = styled(Button)` - background: #24478f; - color: white; - border: none; - - &:hover { - opacity: 0.9; - } -`; - -const CancelButton = styled(Button)` - background: white; - color: ${({ theme }) => theme.colors.grey[700]}; - border: 1px solid ${({ theme }) => theme.colors.grey[300]}; - - &:hover { - background: ${({ theme }) => theme.colors.grey[50]}; - } -`; - -const CloseButton = styled.button` - background: none; - border: none; - font-size: 24px; - cursor: pointer; - padding: 0; - color: ${({ theme }) => theme.colors.grey[500]}; - - &:hover { - color: ${({ theme }) => theme.colors.grey[700]}; - } -`; - -const SubSection = styled.div` - display: flex; - justify-content: space-between; - gap: 25px; -`; export default EditModal; diff --git a/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx b/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx new file mode 100644 index 0000000..8d9bb17 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/SlopeForm.tsx @@ -0,0 +1,1084 @@ +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; +} + +const SlopeForm = ({ + titleText, + initialData, + isOpen, + onClose, + onSubmit, + submitButtonText, +}: SlopeFormProps) => { + const [formData, setFormData] = useState(initialData); + + useEffect(() => { + setFormData(initialData); + }, [initialData]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(formData); + }; + + return ( + + + +

{titleText}

+ × +
+
+ +
+ 기본 정보 + + + + setFormData({ + ...formData, + managementNo: e.target.value, + }) + } + /> + + + + + setFormData({ ...formData, name: e.target.value }) + } + /> + +
+ +
+ 관리 정보 + + + + setFormData({ + ...formData, + management: { + ...formData.management, + organization: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + management: { + ...formData.management, + authority: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + management: { + ...formData.management, + department: e.target.value, + }, + }) + } + /> + +
+ +
+ 위치 정보 + + + + setFormData({ + ...formData, + location: { + ...formData.location, + province: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + city: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + district: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + address: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + roadAddress: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + mountainAddress: e.target.value, + }, + }) + } + /> + + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + mainLotNumber: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + subLotNumber: e.target.value, + }, + }) + } + /> + + +
+
+ GIS 정보 + GIS좌표시점 + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + start: { + ...formData.location.coordinates.start, + startLatDegree: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + start: { + ...formData.location.coordinates.start, + startLatMinute: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + start: { + ...formData.location.coordinates.start, + startLatSecond: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + start: { + ...formData.location.coordinates.start, + startLongDegree: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + start: { + ...formData.location.coordinates.start, + startLongMinute: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + start: { + ...formData.location.coordinates.start, + startLongSecond: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + GIS좌표종점 + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + end: { + ...formData.location.coordinates.end, + endLatDegree: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + end: { + ...formData.location.coordinates.end, + endLatMinute: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + end: { + ...formData.location.coordinates.end, + endLatSecond: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + end: { + ...formData.location.coordinates.end, + endLongDegree: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + end: { + ...formData.location.coordinates.end, + endLongMinute: Number(e.target.value), + }, + }, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + location: { + ...formData.location, + coordinates: { + ...formData.location.coordinates, + end: { + ...formData.location.coordinates.end, + endLongSecond: Number(e.target.value), + }, + }, + }, + }) + } + /> + + +
+
+ 안전 + + + + setFormData({ + ...formData, + inspections: { + ...formData.inspections, + serialNumber: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + inspections: { + ...formData.inspections, + date: new Date(e.target.value), + }, + }) + } + /> + + + + + setFormData({ + ...formData, + inspections: { + ...formData.inspections, + result: e.target.value, + }, + }) + } + /> + +
+
+ 재해위험도 + + + + setFormData({ + ...formData, + disaster: { + ...formData.disaster, + serialNumber: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + disaster: { + ...formData.disaster, + riskDate: new Date(e.target.value), + }, + }) + } + /> + + + + + setFormData({ + ...formData, + disaster: { + ...formData.disaster, + riskLevel: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + disaster: { + ...formData.disaster, + riskScore: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + disaster: { + ...formData.disaster, + riskType: e.target.value, + }, + }) + } + /> + +
+
+ 붕괴위험지구 + + + + setFormData({ + ...formData, + collapseRisk: { + ...formData.collapseRisk, + districtNo: e.target.value, + }, + }) + } + /> + + + + + + setFormData({ + ...formData, + collapseRisk: { + ...formData.collapseRisk, + districtName: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + collapseRisk: { + ...formData.collapseRisk, + designated: e.target.checked, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + collapseRisk: { + ...formData.collapseRisk, + designationDate: new Date(e.target.value), + }, + }) + } + /> + +
+
+ 정비사업 + + + + setFormData({ + ...formData, + maintenanceProject: { + ...formData.maintenanceProject, + type: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + maintenanceProject: { + ...formData.maintenanceProject, + year: e.target.value, + }, + }) + } + /> + +
+
+ 급경사지 정보 + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + usage: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + slopeNature: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + slopeStructure: e.target.value, + }, + }) + } + /> + + 급경사지 수치 + + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + maxVerticalHeight: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + longitudinalLength: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + averageSlope: e.target.value, + }, + }) + } + /> + + + 경사지 등급 + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + Score: e.target.value, + }, + }) + } + /> + + + + + setFormData({ + ...formData, + priority: { + ...formData.priority, + grade: e.target.value, + }, + }) + } + /> + + +
+
+ + + {submitButtonText} + + 취소 + + +
+
+
+ ); +}; + +export default SlopeForm; + +const ModalOverlay = styled.div<{ $isOpen: boolean }>` + display: ${(props) => (props.$isOpen ? 'flex' : 'none')}; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; + z-index: 1000; +`; + +const ModalContent = styled.div` + background: white; + border-radius: 8px; + width: 100%; + max-width: 600px; + max-height: 90vh; + display: flex; + flex-direction: column; +`; +const ModalHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid ${({ theme }) => theme.colors.grey[200]}; + + h2 { + margin: 0; + font-size: 20px; + color: ${({ theme }) => theme.colors.grey[900]}; + } +`; +const Form = styled.form` + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +`; + +const ScrollContainer = styled.div` + flex: 1; + overflow-y: auto; + padding: 24px; +`; + +const Section = styled.div` + margin-bottom: 12px; + padding-bottom: 12px; + &:last-child { + margin-bottom: 0; + } + border-bottom: 1px solid ${({ theme }) => theme.colors.grey[400]}; +`; + +const SectionTitle = styled.h3` + font-size: 16px; + color: ${({ theme }) => theme.colors.grey[900]}; + margin: 0 0 16px 0; +`; +const SubSectionTitle = styled.div` + font-size: 14px; + color: ${({ theme }) => theme.colors.grey[900]}; + margin: 20px 0 16px 0; + font-weight: bold; +`; +const FormGroup = styled.div` + margin-bottom: 16px; +`; + +const Label = styled.label` + display: block; + margin-bottom: 8px; + font-size: 14px; + color: ${({ theme }) => theme.colors.grey[700]}; +`; + +const Input = styled.input` + width: 100%; + padding: 8px 12px; + border: 1px solid ${({ theme }) => theme.colors.grey[300]}; + border-radius: 6px; + font-size: 14px; + + &:focus { + outline: none; + border-color: #24478f; + box-shadow: 0 0 0 2px rgba(36, 71, 143, 0.1); + } +`; + +const ButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px 24px; + border-top: 1px solid ${({ theme }) => theme.colors.grey[200]}; +`; +const Button = styled.button` + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +`; + +const SubmitButton = styled(Button)` + background: #24478f; + color: white; + border: none; + + &:hover { + opacity: 0.9; + } +`; + +const SubSection = styled.div` + display: flex; + justify-content: space-between; + gap: 25px; +`; + +const CancelButton = styled(Button)` + background: white; + color: ${({ theme }) => theme.colors.grey[700]}; + border: 1px solid ${({ theme }) => theme.colors.grey[300]}; + + &:hover { + background: ${({ theme }) => theme.colors.grey[50]}; + } +`; +const CloseButton = styled.button` + background: none; + border: none; + font-size: 24px; + cursor: pointer; + padding: 0; + color: ${({ theme }) => theme.colors.grey[500]}; + + &:hover { + color: ${({ theme }) => theme.colors.grey[700]}; + } +`; diff --git a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx new file mode 100644 index 0000000..b509695 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx @@ -0,0 +1,172 @@ +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; +} + +const DataTable: React.FC = ({ + tableContainerRef, + handleScroll, + table, + rows, + rowVirtualizer, + selectedRow, + setSelectedRow, +}) => { + const paddingTop = rowVirtualizer.getVirtualItems()[0]?.start || 0; + const paddingBottom = + rowVirtualizer.getTotalSize() - + (paddingTop + rowVirtualizer.getVirtualItems().length * 40); + + return ( + + + + + {table.getHeaderGroups()[0].headers.map((header) => ( + + {header.column.columnDef.header as string} + + + ))} + + + + {paddingTop > 0 && ( + + + )} + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow.index]; + return ( + { + setSelectedRow( + selectedRow?.managementNo === row.original.managementNo + ? null + : row.original + ); + }} + $selected={ + selectedRow?.managementNo === row.original.managementNo + } + > + {row.getVisibleCells().map((cell) => ( + + {cell.getValue() as string} + + ))} + + ); + })} + {paddingBottom > 0 && ( + + + )} + +
+
+
+
+ ); +}; + +export default DataTable; + +// 스타일 컴포넌트 +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: ${({ theme }) => theme.colors.grey[100]}; +`; + +interface HeaderCellProps { + width?: number; +} + +const HeaderCell = styled.th` + 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; + } +`; + +interface TableRowProps { + $selected?: boolean; +} + +const TableRow = styled.tr` + cursor: pointer; + background-color: ${(props) => (props.$selected ? '#f0f7ff' : 'transparent')}; + + &:hover { + background-color: ${(props) => (props.$selected ? '#f0f7ff' : '#f9fafb')}; + } +`; + +interface TableCellProps { + width?: number; +} + +const TableCell = styled.td` + border-bottom: 1px solid #e5e7eb; + padding: 0.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: ${(props) => props.width}px; +`; diff --git a/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx b/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx new file mode 100644 index 0000000..6d848b9 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/TableAction.tsx @@ -0,0 +1,65 @@ +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; + openEditModal: () => void; + openDeleteModal: () => void; +} +const TableAction: React.FC = ({ + isLoading, + selectedRow, + openEditModal, + openDeleteModal, +}) => { + return ( + <> + {isLoading && } + {/* 하단 버튼 컨테이너 */} + {selectedRow && ( + + 수정 + + 삭제 + + + )} + + ); +}; + +export default TableAction; +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/table/TableModals.tsx b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx new file mode 100644 index 0000000..1a4e4e1 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx @@ -0,0 +1,64 @@ +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; + isEditModalOpen: boolean; + closeEditModal: () => void; + handleEdit: (updatedSlope: Slope) => void; +} + +const TableModals: React.FC = ({ + isModalOpen, + closeModal, + table, + isRegionModalOpen, + closeRegionModal, + handleRegionSelect, + isDeleteModalOpen, + closeDeleteModal, + handleDelete, + selectedRow, + isEditModalOpen, + closeEditModal, + handleEdit, +}) => { + return ( + <> + + + + + + ); +}; + +export default TableModals; diff --git a/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx b/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx new file mode 100644 index 0000000..428b5a2 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/TableToolbar.tsx @@ -0,0 +1,241 @@ +import React from 'react'; +import styled from 'styled-components'; +import Title from '../../../components/Title'; +import TuneRoundedIcon from '@mui/icons-material/TuneRounded'; +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 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; + setGrade: (value: string) => void; + selectedRegion: Region | null; + resetFilters: () => void; + downloadExcel: () => void; + isDownloading: boolean; + totalCount: number; +} + +const TableToolbar: React.FC = ({ + title, + setSearchQuery, + inputValue, + setInputValue, + selectedRegion, + resetFilters, + downloadExcel, + isDownloading, + totalCount, + setGrade, +}) => { + const { openModal, openRegionModal } = useSteepSlopeStore(); + + return ( + <> + + + + + +

표시할 열 항목 설정

+
+ + + {selectedRegion + ? `${selectedRegion.city} ${ + selectedRegion.county === '모두' ? '' : selectedRegion.county + }` + : '지역선택'} + + + +

초기화

+
+ + +

{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}

+
+ + + + setSearchQuery(inputValue)} /> + ) => { + setInputValue(e.target.value); + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + setSearchQuery(inputValue); + } + }} + /> + + +
+
+ + 총 {totalCount}개 + + + ); +}; + +export default TableToolbar; + +//헤더 +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; +`; +const TotalCount = styled.div` + font-size: ${({ theme }) => theme.fonts.sizes.ms}; + color: #374151; + margin-bottom: 8px; +`; + +const GradeButton = () => { + const [anchorEl, setAnchorEl] = React.useState(null); + const [selectedGrade, setSelectedGrade] = React.useState('선택안함'); + const open = Boolean(anchorEl); + const setGrade = useSteepSlopeStore((state) => state.setGrade); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleGradeSelect = (grade: string) => { + setSelectedGrade(grade); + setGrade(grade); + handleClose(); + }; + + return ( + <> + 등급: {selectedGrade} + + handleGradeSelect('선택안함')}> + 선택안함 + + handleGradeSelect('A')}>A + handleGradeSelect('B')}>B + handleGradeSelect('C')}>C + handleGradeSelect('D')}>D + handleGradeSelect('F')}>F + + + ); +}; diff --git a/src/pages/ManagePage/StepSlope/components/table/coloums.ts b/src/pages/ManagePage/StepSlope/components/table/coloums.ts new file mode 100644 index 0000000..fb6078a --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/coloums.ts @@ -0,0 +1,364 @@ +import { createColumnHelper } from '@tanstack/react-table'; +import { Slope } from '../../../../../apis/slopeMap'; + +const columnHelper = createColumnHelper(); + +export const getSlopeColumns = () => [ + columnHelper.accessor( + (_row, index) => { + return 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, + }), + columnHelper.accessor((row) => row.priority?.usage || '', { + id: 'usage', + header: '비탈면용도', + size: 120, + }), + columnHelper.accessor((row) => row.priority?.slopeNature || '', { + id: 'slopeNature', + header: '자연/인공', + size: 100, + }), + columnHelper.accessor((row) => row.priority?.slopeType || '', { + id: 'slopeType', + header: '비탈면유형', + size: 120, + }), + columnHelper.accessor((row) => row.priority?.slopeStructure || '', { + id: 'slopeStructure', + header: '비탈면구조', + size: 120, + }), + columnHelper.accessor((row) => row.priority?.maxVerticalHeight || '', { + id: 'maxVerticalHeight', + header: '최고수직고', + size: 100, + }), + columnHelper.accessor((row) => row.priority?.longitudinalLength || '', { + id: 'longitudinalLength', + header: '종단길이', + size: 100, + }), + columnHelper.accessor((row) => row.priority?.averageSlope || '', { + id: 'averageSlope', + header: '평균경사', + size: 100, + }), + columnHelper.accessor((row) => row.priority?.Score || '', { + id: 'Score', + header: '점수', + size: 80, + }), + columnHelper.accessor((row) => row.priority?.grade || '', { + id: 'grade', + header: '등급', + size: 80, + }), +]; + +// 기본 표시할 열 항목 필터 설정 +export const getDefaultColumnVisibility = () => ({ + roadAddress: false, + mountainAddress: false, + mainLotNumber: false, + subLotNumber: false, + startLatDegree: false, + startLatMinute: false, + startLatSecond: false, + startLongDegree: false, + startLongMinute: false, + startLongSecond: false, + endLatDegree: false, + endLatMinute: false, + endLatSecond: false, + endLongDegree: false, + endLongMinute: false, + endLongSecond: false, + inspections_serialNumber: false, + inspectionDate: false, + inspectionResult: false, + serialNumber: false, + riskDate: false, + riskLevel: false, + riskScore: false, + riskType: false, + districtNo: false, + districtName: false, + riskDesignation: false, + designationDate: false, + maintenanceProject: false, + maintenanceYear: false, +}); diff --git a/src/pages/ManagePage/StepSlope/components/table/store/steepSlopeStore.ts b/src/pages/ManagePage/StepSlope/components/table/store/steepSlopeStore.ts new file mode 100644 index 0000000..254876e --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/store/steepSlopeStore.ts @@ -0,0 +1,5 @@ +import { createTableStore } from './tableStore'; +import { Slope } from '../../../../../../apis/slopeMap'; + +// 급경사지 스토어 생성 - 팩토리 함수 사용 +export const useSteepSlopeStore = createTableStore(); diff --git a/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts b/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts new file mode 100644 index 0000000..b874e04 --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/table/store/tableStore.ts @@ -0,0 +1,122 @@ +import { create } from 'zustand'; +import { + VisibilityState, + ColumnSizingState, + OnChangeFn, +} from '@tanstack/react-table'; +import { getDefaultColumnVisibility } from '..//coloums'; + +// 제네릭 타입 T는 각 테이블에서 사용할 데이터 타입입니다 (Slope, SlopeOutlier 등) +export interface TableState { + // 테이블 상태 + columnVisibility: VisibilityState; + columnSizing: ColumnSizingState; + totalCount: number; + searchQuery: string; + inputValue: string; + selectedRegion: { city: string; county: string } | null; + grade: string; + + // 모달 상태 + isModalOpen: boolean; + isRegionModalOpen: boolean; + isDeleteModalOpen: boolean; + isEditModalOpen: boolean; + + // 선택된 행 + selectedRow: T | null; + + // 액션 - 타입 수정됨 + setColumnVisibility: OnChangeFn; + setColumnSizing: OnChangeFn; + setTotalCount: (count: number) => void; + setSearchQuery: (query: string) => void; + setInputValue: (value: string) => void; + setSelectedRegion: (region: { city: string; county: string } | null) => void; + setGrade: (value: string) => void; + openModal: () => void; + closeModal: () => void; + openRegionModal: () => void; + closeRegionModal: () => void; + openDeleteModal: () => void; + closeDeleteModal: () => void; + openEditModal: () => void; + closeEditModal: () => void; + + setSelectedRow: (row: T | null) => void; + resetFilters: () => void; +} + +// 테이블 스토어 팩토리 함수 +export function createTableStore(initialState: Partial> = {}) { + return create>((set) => ({ + // 기본 상태 + columnVisibility: getDefaultColumnVisibility(), + columnSizing: {}, + totalCount: 0, + searchQuery: '', + inputValue: '', + selectedRegion: null, + grade: '선택안함', + isModalOpen: false, + isRegionModalOpen: false, + isDeleteModalOpen: false, + isEditModalOpen: false, + + selectedRow: null, + + // 액션 메서드들 - TanStack Table 호환 타입으로 수정 + setColumnVisibility: (updaterOrValue) => { + // OnChangeFn 타입 호환을 위한 함수 구현 + if (typeof updaterOrValue === 'function') { + set((state) => ({ + columnVisibility: updaterOrValue(state.columnVisibility), + })); + } else { + set({ columnVisibility: updaterOrValue }); + } + }, + + setColumnSizing: (updaterOrValue) => { + // OnChangeFn 타입 호환을 위한 함수 구현 + if (typeof updaterOrValue === 'function') { + set((state) => ({ + columnSizing: updaterOrValue(state.columnSizing), + })); + } else { + set({ columnSizing: updaterOrValue }); + } + }, + + setTotalCount: (count) => set({ totalCount: count }), + setSearchQuery: (query) => set({ searchQuery: query }), + setInputValue: (value) => set({ inputValue: value }), + setSelectedRegion: (region) => set({ selectedRegion: region }), + setGrade: (grade) => set({ grade: grade }), + + openModal: () => set({ isModalOpen: true }), + closeModal: () => set({ isModalOpen: false }), + openRegionModal: () => set({ isRegionModalOpen: true }), + closeRegionModal: () => set({ isRegionModalOpen: false }), + openDeleteModal: () => set({ isDeleteModalOpen: true }), + closeDeleteModal: () => set({ isDeleteModalOpen: false }), + openEditModal: () => set({ isEditModalOpen: true }), + closeEditModal: () => set({ isEditModalOpen: false }), + + setSelectedRow: (row) => set({ selectedRow: row }), + + resetFilters: () => + set({ + searchQuery: '', + inputValue: '', + selectedRegion: null, + columnVisibility: getDefaultColumnVisibility(), + }), + + // 초기 상태 오버라이드 + ...initialState, + })); +} + +// 특정 타입의 테이블 스토어 생성 예시 +// export const useSlopeOutlierStore = createTableStore(); diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx index a6eb2df..70313c5 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx @@ -2,10 +2,9 @@ import React, { useState, useRef, DragEvent, ChangeEvent } from 'react'; import styled from 'styled-components'; import Title from '../../components/Title'; import { slopeManageAPI } from '../../../../apis/slopeManage'; -import AddSlope from '../components/AddSlopeContainer'; -import { Slope } from '../../../../apis/slopeMap'; import CloudUploadRoundedIcon from '@mui/icons-material/CloudUploadRounded'; import { useNotificationStore } from '../../../../hooks/notificationStore'; +import AddSlope from '../components/AddSlopeContainer'; interface FileInputContainerProps { $isDragActive?: boolean; $hasFile?: boolean; @@ -109,64 +108,63 @@ const SteepSlopeAdd: React.FC = () => { setIsUploading(false); } }; - const handleAdd = async (newSlopeData: Slope) => { - try { - await slopeManageAPI.createSlope(newSlopeData); - } catch (error) { - console.error('파일 업로드 오류:', error); - } - }; + + const [isOpen, setIsOpen] = useState(false); + return ( - - - - - - ) => { - event.preventDefault(); - setIsDragActive(true); - }} - onDragLeave={handleDragEnd} - onDrop={handleDrop} - > - + <> + setIsOpen(false)} /> + + + + setIsOpen(!isOpen)}> + 직접 추가 + + + + ) => { + event.preventDefault(); + setIsDragActive(true); + }} + onDragLeave={handleDragEnd} + onDrop={handleDrop} + > + - {uploadedFile ? ( - - {fileName} - {(uploadedFile.size / 1024).toFixed(2)} KB - - - {isUploading ? '업로드 중...' : '업로드'} - - 취소 - - - ) : ( - - - -

클릭 혹은 파일을 이곳에 드롭하세요.

-
-
- )} -
-
- - - - - -
+ {uploadedFile ? ( + + {fileName} + {(uploadedFile.size / 1024).toFixed(2)} KB + + + {isUploading ? '업로드 중...' : '업로드'} + + 취소 + + + ) : ( + + + +

클릭 혹은 파일을 이곳에 드롭하세요.

+
+
+ )} +
+
+
+ ); }; @@ -180,10 +178,7 @@ const Container = styled.div` gap: 30px; padding-top: 20px; `; -const Line = styled.div` - margin-top: 30px; - border-bottom: 3px solid ${({ theme }) => theme.colors.grey[200]}; -`; + const HeaderContainer = styled.div` width: 100%; height: 8%; @@ -293,3 +288,21 @@ const CancelButton = styled.button` background-color: ${({ theme }) => theme.colors.grey[100]}; } `; + +const Button = styled.button` + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +`; + +const SubmitButton = styled(Button)` + background: #24478f; + color: white; + border: none; + + &:hover { + opacity: 0.9; + } +`; diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index e4a8441..ab1eab9 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -1,20 +1,11 @@ -import React, { - useState, - useMemo, - useRef, - useCallback, - useEffect, -} from 'react'; +import React, { useRef, useCallback, useEffect } from 'react'; import { useReactTable, getCoreRowModel, - VisibilityState, - createColumnHelper, ColumnResizeMode, - ColumnSizingState, + FilterFn, } 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, @@ -25,20 +16,14 @@ 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 DownloadRoundedIcon from '@mui/icons-material/DownloadRounded'; -import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; import { useNotificationStore } from '../../../../hooks/notificationStore'; +import { getSlopeColumns } from '../components/table/coloums'; +import { useSteepSlopeStore } from '../components/table/store/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' { @@ -47,34 +32,50 @@ declare module '@tanstack/react-table' { } } -//기본 표시할 열 항목 필터 설정 const SteepSlopeLookUp = () => { 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 [inputValue, setInputValue] = useState(''); //검색어 - const [selectedRegion, setSelectedRegion] = useState<{ - city: string; - county: string; - } | null>(null); //지역 검색 + // Zustand 스토어에서 상태 및 액션 가져오기 + const { + columnVisibility, + setColumnVisibility, + columnSizing, + setColumnSizing, + totalCount, + setTotalCount, + searchQuery, + setSearchQuery, + inputValue, + setInputValue, + selectedRegion, + setSelectedRegion, + grade, + setGrade, + + isModalOpen, + closeModal, + isRegionModalOpen, + + closeRegionModal, + isDeleteModalOpen, + openDeleteModal, + closeDeleteModal, + isEditModalOpen, + openEditModal, + closeEditModal, + + selectedRow, + setSelectedRow, + resetFilters, + } = useSteepSlopeStore(); + + // 테이블 컨테이너 ref는 훅 내에서 직접 생성 + const tableContainerRef = useRef(null); + + // 알림 함수 가져오기 + const showNotification = useNotificationStore( + (state) => state.showNotification + ); //데이터 조회 쿼리 const { @@ -85,13 +86,14 @@ const SteepSlopeLookUp = () => { isFetchingNextPage, refetch, } = useInfiniteQuery({ - queryKey: ['slopes', searchQuery, selectedRegion], + queryKey: ['slopes', searchQuery, selectedRegion, grade], queryFn: async ({ pageParam = 0 }) => { const response = await slopeManageAPI.batchSlope({ page: pageParam, pageSize: FETCH_SIZE, searchQuery: searchQuery || undefined, city: selectedRegion?.city, + grade: grade, county: selectedRegion?.county, }); return response; @@ -101,8 +103,8 @@ const SteepSlopeLookUp = () => { initialPageParam: 0, }); - //flatData를 통해 페이지 계산 - const flatData = useMemo(() => { + //flatData를 통해 페이지 계산 + const flatData = React.useMemo(() => { return data?.pages.flatMap((page) => page.data) ?? []; }, [data]); @@ -116,294 +118,10 @@ const SteepSlopeLookUp = () => { if (data?.pages[0]?.meta.totalCount) { setTotalCount(data.pages[0].meta.totalCount); } - }, [data]); + }, [data, setTotalCount]); //데이터 열 선언 - const columns = useMemo( - () => [ - columnHelper.accessor( - (_row, index) => { - return 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 columns = React.useMemo(() => getSlopeColumns(), []); //필터 선언 const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { @@ -444,6 +162,7 @@ const SteepSlopeLookUp = () => { fetchNextPage(); } }, [fetchNextPage, hasNextPage, isFetching]); + const { rows } = table.getRowModel(); //보이는 행만 보일 수 있도록 가상화 @@ -454,22 +173,6 @@ const SteepSlopeLookUp = () => { 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}`); @@ -479,33 +182,6 @@ const SteepSlopeLookUp = () => { setSelectedRegion({ city, county }); }; - // 필터 초기화 - const handleReset = () => { - setInputValue(''); - setSearchQuery(''); - setSelectedRegion(null); - refetch(); - 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, - }); - }; - - //삭제 수정을 위한 행 선택 state - const [selectedRow, setSelectedRow] = useState(null); - - //삭제 모달 및 삭제 api - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const handleDelete = async () => { try { if (selectedRow) { @@ -513,33 +189,25 @@ const SteepSlopeLookUp = () => { // 삭제 성공 후 데이터 갱신 await queryClient.invalidateQueries({ queryKey: ['slopes'] }); setSelectedRow(null); - setIsDeleteModalOpen(false); + closeDeleteModal(); } } catch (error) { console.error('삭제 실패:', error); } }; - //수정 모달 및 수정 api - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const handleEdit = async (updatedSlope: Slope) => { try { await slopeManageAPI.updateSlope(updatedSlope); // 수정 성공 후 데이터 갱신 await queryClient.invalidateQueries({ queryKey: ['slopes'] }); setSelectedRow(null); - setIsEditModalOpen(false); + closeEditModal(); } catch (error) { console.error('수정 실패:', error); } }; - // 알림 함수 가져오기 - const showNotification = useNotificationStore( - (state) => state.showNotification - ); - // 다운로드 mutation 설정 const { mutate: downloadExcel, isPending: isDownloading } = useMutation({ mutationFn: (params: { @@ -565,191 +233,65 @@ const SteepSlopeLookUp = () => { }, }); - // 다운로드 버튼 클릭 핸들러 - const handleDownload = () => { - downloadExcel({ - searchQuery: searchQuery || undefined, - city: selectedRegion?.city, - county: selectedRegion?.county, - }); - }; return ( {/* 모달 */} - - - setIsDeleteModalOpen(false)} - onConfirm={handleDelete} + + + + downloadExcel({ + searchQuery: searchQuery || undefined, + city: selectedRegion?.city, + county: selectedRegion?.county, + }) + } + isDownloading={isDownloading} + totalCount={totalCount} /> - setIsEditModalOpen(false)} - onSubmit={handleEdit} + - {/* 헤더 */} - - - - { - setIsModalOpen(true); - }} - > - - -

표시할 열 항목 설정

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

초기화

-
- - -

{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}

-
- - - 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 SteepSlopeLookUp; + //전체 컨테이너너 const Container = styled.div` width: 100%; @@ -757,177 +299,3 @@ const Container = styled.div` 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; -`; -const TotalCount = styled.div` - font-size: ${({ theme }) => theme.fonts.sizes.ms}; - color: #374151; - margin-bottom: 8px; -`; - -//테이블블 -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; - } -`;