diff --git a/.github/workflows/front-build.yaml b/.github/workflows/front-build.yaml
index 17c6acd..a560863 100644
--- a/.github/workflows/front-build.yaml
+++ b/.github/workflows/front-build.yaml
@@ -38,6 +38,7 @@ jobs:
run: |
echo "VITE_NAVER_MAP_ID=${{ secrets.VITE_NAVER_MAP_ID }}" >> .env
echo "VITE_SERVER_ADDRESS=${{ secrets.VITE_SERVER_ADDRESS }}" >> .env
+ echo "VITE_GA_TRACKING_ID=${{ secrets.VITE_GA_TRACKING_ID }}" >> .env
- name: react build
run: npm run build
diff --git a/package-lock.json b/package-lock.json
index e370eaa..1470208 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-ga4": "^2.1.0",
"react-naver-maps": "^0.1.3",
"react-router-dom": "^7.1.1",
"react-table": "^7.8.0",
@@ -4185,6 +4186,12 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-ga4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
+ "integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==",
+ "license": "MIT"
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/package.json b/package.json
index eaf7658..30b45af 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-ga4": "^2.1.0",
"react-naver-maps": "^0.1.3",
"react-router-dom": "^7.1.1",
"react-table": "^7.8.0",
diff --git a/src/App.tsx b/src/App.tsx
index 0863e18..e7c1f95 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import GlobalStyle from './styles/globalStyle';
import './styles/font.css';
import { NotificationProvider } from './components/NotificationProvider';
+import { NavermapsProvider } from 'react-naver-maps';
+import { BrowserRouter } from 'react-router-dom';
+
function App() {
const queryClient = new QueryClient();
return (
@@ -12,7 +15,13 @@ function App() {
-
+
+
+
+
+
diff --git a/src/Router.tsx b/src/Router.tsx
index ef2e222..d16290a 100644
--- a/src/Router.tsx
+++ b/src/Router.tsx
@@ -1,5 +1,6 @@
-import { NavermapsProvider } from 'react-naver-maps';
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import { Routes, Route, useLocation } from 'react-router-dom';
+import { useEffect } from 'react';
+import ReactGA from 'react-ga4';
import LoginPage from './pages/LoginPage/LoginPage';
import MapPage from './pages/MapPage/MapPage';
import ManagePage from './pages/ManagePage/ManagePage';
@@ -11,33 +12,46 @@ import Home from './pages/ManagePage/Home';
import SteepSlopeDup from './pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeDup';
import SteepSlopeEmpty from './pages/ManagePage/StepSlope/StepSlopeOutlier/SteepSlopeEmpty';
const Router = () => {
+ const location = useLocation();
+
+ useEffect(() => {
+ ReactGA.initialize(import.meta.env.VITE_GA_TRACKING_ID, {
+ gtagOptions: {
+ debug_mode: import.meta.env.DEV,
+ },
+ });
+ }, []);
+
+ useEffect(() => {
+ ReactGA.send({
+ hitType: 'pageview',
+ page: location.pathname + location.search,
+ title: document.title,
+ });
+ }, [location]);
return (
-
-
-
- } />
- } />
- }>
- } />
- } />
-
- } />
- } />
-
-
- } />
- } />
- } />
-
-
- } />
- } />
-
- } />
-
-
-
-
+
+ } />
+ } />
+ }>
+ } />
+ } />
+
+ } />
+ } />
+
+
+ } />
+ } />
+ } />
+
+
+ } />
+ } />
+
+ } />
+
+
);
};
export default Router;
diff --git a/src/apis/slopeManage.tsx b/src/apis/slopeManage.tsx
index e7e54cc..6ca9da7 100644
--- a/src/apis/slopeManage.tsx
+++ b/src/apis/slopeManage.tsx
@@ -99,6 +99,11 @@ export const slopeManageAPI = {
);
return response.data;
},
+ restoreImg: async () => {
+ const response = await api.post('/slopes/restore');
+ console.log(response);
+ return response.data;
+ },
};
export interface UpdateAllImg {
formData: FormData;
diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx
index b499146..b4f7c5a 100644
--- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx
+++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeAdd.tsx
@@ -1,8 +1,313 @@
+// import React, { useState, useRef, DragEvent, ChangeEvent } from 'react';
+// import styled from 'styled-components';
+// import Title from '../../components/Title';
+// import { slopeManageAPI } from '../../../../apis/slopeManage';
+// import CloudUploadRoundedIcon from '@mui/icons-material/CloudUploadRounded';
+// import { useNotificationStore } from '../../../../hooks/notificationStore';
+// import AddSlope from '../components/AddSlopeContainer';
+// import { FileInputContainerProps } from '../../interface';
+
+// const SteepSlopeAdd: React.FC = () => {
+// const [isDragActive, setIsDragActive] = useState(false);
+// const [uploadedFile, setUploadedFile] = useState(null);
+// const [fileName, setFileName] = useState('');
+// const [isUploading, setIsUploading] = useState(false);
+// const fileInputRef = useRef(null);
+// //전역 알림
+// const showNotification = useNotificationStore(
+// (state) => state.showNotification
+// );
+// // 드래그 시작 핸들러
+// const handleDragStart = (event: DragEvent): void => {
+// event.preventDefault();
+// setIsDragActive(true);
+// };
+
+// // 드래그 종료 핸들러
+// const handleDragEnd = (event: DragEvent): void => {
+// event.preventDefault();
+// setIsDragActive(false);
+// };
+
+// // 파일 드롭 핸들러
+// const handleDrop = (event: DragEvent): void => {
+// event.preventDefault();
+// setIsDragActive(false);
+
+// const files = event.dataTransfer.files;
+// if (files && files.length > 0) {
+// handleFile(files[0]);
+// }
+// };
+
+// // 파일 선택 핸들러
+// const handleFileSelect = (event: ChangeEvent): void => {
+// const files = event.target.files;
+// if (files && files.length > 0) {
+// handleFile(files[0]);
+// }
+// };
+
+// // 파일 처리 함수
+// const handleFile = (file: File): void => {
+// setUploadedFile(file);
+// setFileName(file.name);
+// console.log('선택된 파일:', file);
+// };
+
+// // 파일 입력창 클릭
+// const handleContainerClick = (): void => {
+// if (fileInputRef.current) {
+// fileInputRef.current.click();
+// }
+// };
+
+// // 업로드 취소
+// const handleCancelUpload = (e: React.MouseEvent): void => {
+// e.stopPropagation(); // 이벤트 버블링 방지
+// setUploadedFile(null);
+// setFileName('');
+// if (fileInputRef.current) {
+// fileInputRef.current.value = '';
+// }
+// };
+
+// // 파일 업로드 실행
+// const handleUpload = async (
+// e: React.MouseEvent
+// ): Promise => {
+// e.stopPropagation(); // 이벤트 버블링 방지
+// if (!uploadedFile) return;
+
+// setIsUploading(true);
+
+// try {
+// console.log('파일 업로드 시작:', uploadedFile);
+// const formData = new FormData();
+// formData.append('file', uploadedFile);
+// const data = await slopeManageAPI.uploadExcelSlope(formData);
+// showNotification(`${data.message}\n${data.count}건 추가되었습니다.`, {
+// severity: 'success',
+// });
+// // 업로드 성공 후 상태 초기화
+// setUploadedFile(null);
+// setFileName('');
+// if (fileInputRef.current) {
+// fileInputRef.current.value = '';
+// }
+// } catch (error) {
+// showNotification('파일 업로드 오류', {
+// severity: 'error',
+// });
+// console.error('파일 업로드 오류:', error);
+// } finally {
+// setIsUploading(false);
+// }
+// };
+
+// const [isOpen, setIsOpen] = useState(false);
+
+// return (
+// <>
+// setIsOpen(false)} />
+//
+//
+//
+// setIsOpen(!isOpen)}>
+// 직접 추가
+//
+//
+//
+// ) => {
+// event.preventDefault();
+// setIsDragActive(true);
+// }}
+// onDragLeave={handleDragEnd}
+// onDrop={handleDrop}
+// >
+//
+
+// {uploadedFile ? (
+//
+// {fileName}
+// {(uploadedFile.size / 1024).toFixed(2)} KB
+//
+//
+// {isUploading ? '업로드 중...' : '업로드'}
+//
+// 취소
+//
+//
+// ) : (
+//
+//
+//
+// 클릭 혹은 파일을 이곳에 드롭하세요.
+//
+//
+// )}
+//
+//
+//
+// >
+// );
+// };
+
+// export default SteepSlopeAdd;
+
+// const Container = styled.div`
+// width: 100%;
+// height: 100%;
+// display: flex;
+// flex-direction: column;
+// gap: 30px;
+// padding-top: 20px;
+// `;
+
+// const HeaderContainer = styled.div`
+// width: 100%;
+// height: 8%;
+// display: flex;
+// align-items: center;
+// justify-content: space-between;
+// padding: 0 20px;
+// margin-bottom: 16px;
+// `;
+// const InputContainerWrapper = styled.div`
+// padding: 0 20px;
+// `;
+// const FileInputContainer = styled.div`
+// width: 100%;
+// display: flex;
+// flex-direction: column;
+// min-height: 250px;
+// height: 250px;
+// border-radius: 15px;
+// border: 3px dashed
+// ${({ theme, $isDragActive, $hasFile }) =>
+// $isDragActive
+// ? '#24478f'
+// : $hasFile
+// ? theme.colors.grey[300]
+// : theme.colors.grey[200]};
+// justify-content: center;
+// align-items: center;
+// transition: all 0.3s ease;
+// background-color: ${({ $isDragActive }) =>
+// $isDragActive ? 'rgba(51, 102, 255, 0.05)' : 'transparent'};
+// `;
+
+// // 파일이 없을 때 표시되는 영역
+// const UploadArea = styled.div`
+// display: flex;
+// flex-direction: column;
+// justify-content: center;
+// align-items: center;
+// width: 100%;
+// height: 100%;
+// cursor: pointer;
+// &:hover {
+// background-color: rgba(0, 0, 0, 0.02);
+// }
+// `;
+
+// const UploadText = styled.div`
+// margin-top: 15px;
+// color: ${({ theme }) => theme.colors.grey[600]};
+// `;
+
+// const FileInfo = styled.div`
+// display: flex;
+// flex-direction: column;
+// align-items: center;
+// justify-content: center;
+// width: 100%;
+// height: 100%;
+// `;
+
+// const FileName = styled.p`
+// font-size: 16px;
+// font-weight: 500;
+// margin-bottom: 8px;
+// `;
+
+// const FileSize = styled.p`
+// font-size: 14px;
+// color: ${({ theme }) => theme.colors.grey[600]};
+// margin-bottom: 15px;
+// `;
+
+// const ButtonContainer = styled.div`
+// display: flex;
+// gap: 10px;
+// `;
+
+// const UploadButton = styled.button`
+// padding: 8px 16px;
+// background-color: ${({ theme }) => theme.colors.primary};
+// color: white;
+// border: none;
+// border-radius: 4px;
+// cursor: pointer;
+// transition: all 0.2s ease;
+
+// &:hover {
+// background-color: ${({ theme }) => theme.colors.primary}DD;
+// }
+
+// &:disabled {
+// background-color: ${({ theme }) => theme.colors.grey[400]};
+// cursor: not-allowed;
+// }
+// `;
+
+// const CancelButton = styled.button`
+// padding: 8px 16px;
+// background-color: transparent;
+// border: 1px solid ${({ theme }) => theme.colors.grey[300]};
+// border-radius: 4px;
+// cursor: pointer;
+// transition: all 0.2s ease;
+
+// &:hover {
+// 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;
+// }
+// `;
import React, { useState, useRef, DragEvent, ChangeEvent } from 'react';
import styled from 'styled-components';
import Title from '../../components/Title';
import { slopeManageAPI } from '../../../../apis/slopeManage';
import CloudUploadRoundedIcon from '@mui/icons-material/CloudUploadRounded';
+import RestoreIcon from '@mui/icons-material/Restore';
import { useNotificationStore } from '../../../../hooks/notificationStore';
import AddSlope from '../components/AddSlopeContainer';
import { FileInputContainerProps } from '../../interface';
@@ -12,11 +317,14 @@ const SteepSlopeAdd: React.FC = () => {
const [uploadedFile, setUploadedFile] = useState(null);
const [fileName, setFileName] = useState('');
const [isUploading, setIsUploading] = useState(false);
+ const [isRestoring, setIsRestoring] = useState(false);
const fileInputRef = useRef(null);
- //전역 알림
+
+ // 전역 알림
const showNotification = useNotificationStore(
(state) => state.showNotification
);
+
// 드래그 시작 핸들러
const handleDragStart = (event: DragEvent): void => {
event.preventDefault();
@@ -105,6 +413,28 @@ const SteepSlopeAdd: React.FC = () => {
}
};
+ // 이미지 백업 복구 실행
+ const handleImageRestore = async (): Promise => {
+ setIsRestoring(true);
+
+ try {
+ const response = await slopeManageAPI.restoreImg();
+ showNotification(
+ `이미지 복구 완료\n(복구된 이미지: ${response.summary.restoredImages}개 / 이미지와 연결된 급경사지: ${response.summary.restoredSlopes}개)`,
+ {
+ severity: 'success',
+ }
+ );
+ } catch (error) {
+ showNotification('이미지 복구 실패', {
+ severity: 'error',
+ });
+ console.error('이미지 복구 오류:', error);
+ } finally {
+ setIsRestoring(false);
+ }
+ };
+
const [isOpen, setIsOpen] = useState(false);
return (
@@ -117,48 +447,90 @@ const SteepSlopeAdd: React.FC = () => {
직접 추가
-
- ) => {
- event.preventDefault();
- setIsDragActive(true);
- }}
- onDragLeave={handleDragEnd}
- onDrop={handleDrop}
- >
-
-
- {uploadedFile ? (
-
- {fileName}
- {(uploadedFile.size / 1024).toFixed(2)} KB
-
-
- {isUploading ? '업로드 중...' : '업로드'}
-
- 취소
-
-
- ) : (
-
-
+ {/* Excel 업로드 섹션 */}
+
+ Excel 데이터 업로드
+ ) => {
+ event.preventDefault();
+ setIsDragActive(true);
+ }}
+ onDragLeave={handleDragEnd}
+ onDrop={handleDrop}
+ >
+
+
+ {uploadedFile ? (
+
+ {fileName}
+
+ {(uploadedFile.size / 1024).toFixed(2)} KB
+
+
+
+ {isUploading ? '업로드 중...' : '업로드'}
+
+
+ 취소
+
+
+
+ ) : (
+
+
+
+ 클릭 혹은 파일을 이곳에 드롭하세요.
+
+
+ )}
+
+
+
+ {/* 이미지 백업 복구 섹션 */}
+
+ 이미지 백업 연결
+
+
+
-
- 클릭 혹은 파일을 이곳에 드롭하세요.
-
-
- )}
-
-
+
+
+ 이미지 복구
+
+ 급경사지 데이터가 초기화 된 경우
+
+ 기존에 등록했던 이미지를 복원합니다
+
+
+
+ {isRestoring ? '복구 중...' : '복구 실행'}
+
+
+ {isRestoring && (
+
+
+
+ )}
+
+
+
>
);
@@ -182,11 +554,35 @@ const HeaderContainer = styled.div`
align-items: center;
justify-content: space-between;
padding: 0 20px;
- margin-bottom: 16px;
`;
-const InputContainerWrapper = styled.div`
+
+const ContentGrid = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 48px;
padding: 0 20px;
`;
+
+const UploadSection = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const BackupSection = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const SectionTitle = styled.h2`
+ font-size: 18px;
+ font-weight: 600;
+ color: ${({ theme }) => theme.colors.grey[800] || '#495057'};
+ margin-bottom: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+`;
+
const FileInputContainer = styled.div`
width: 100%;
display: flex;
@@ -205,7 +601,134 @@ const FileInputContainer = styled.div`
align-items: center;
transition: all 0.3s ease;
background-color: ${({ $isDragActive }) =>
- $isDragActive ? 'rgba(51, 102, 255, 0.05)' : 'transparent'};
+ $isDragActive ? 'rgba(36, 71, 143, 0.05)' : 'transparent'};
+`;
+
+const BackupRestoreCard = styled.div<{ $isRestoring: boolean }>`
+ background: white;
+ border: 2px solid ${({ theme }) => theme.colors.grey[200] || '#e9ecef'};
+ border-radius: 15px;
+ padding: 24px;
+ text-align: center;
+ transition: all 0.3s ease;
+ min-height: 250px;
+ height: 250px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ border-color: #24478f;
+ box-shadow: 0 4px 12px rgba(36, 71, 143, 0.1);
+ }
+
+ ${({ $isRestoring }) =>
+ $isRestoring &&
+ `
+ border-color: #24478f;
+ box-shadow: 0 4px 12px rgba(36, 71, 143, 0.1);
+ `}
+`;
+
+const BackupIconLarge = styled.div<{ $isRestoring: boolean }>`
+ width: 64px;
+ height: 64px;
+ margin: 0 auto 16px;
+ padding: 16px;
+ background: linear-gradient(135deg, #24478f, #3b82f6);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ ${({ $isRestoring }) =>
+ $isRestoring &&
+ `
+ animation: pulse 1.5s infinite;
+ `}
+
+ @keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ }
+`;
+
+const BackupTitle = styled.h3`
+ font-size: 20px;
+ font-weight: 600;
+ color: ${({ theme }) => theme.colors.grey[900] || '#212529'};
+ margin-bottom: 8px;
+`;
+
+const BackupDescription = styled.p`
+ color: ${({ theme }) => theme.colors.grey[600] || '#6c757d'};
+ margin-bottom: 20px;
+ line-height: 1.5;
+ font-size: 14px;
+`;
+
+const RestoreButton = styled.button<{ $isRestoring: boolean }>`
+ background: linear-gradient(135deg, #24478f, #3b82f6);
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 16px;
+ width: 100%;
+ max-width: 200px;
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(36, 71, 143, 0.3);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+ }
+`;
+
+const ProgressContainer = styled.div`
+ background: ${({ theme }) => theme.colors.grey[200] || '#e9ecef'};
+ border-radius: 10px;
+ overflow: hidden;
+ height: 6px;
+ margin-top: 16px;
+ width: 100%;
+ max-width: 200px;
+`;
+
+const ProgressBar = styled.div`
+ height: 100%;
+ background: linear-gradient(90deg, #24478f, #3b82f6);
+ width: 0%;
+ animation: progress 2s ease-in-out infinite;
+
+ @keyframes progress {
+ 0% {
+ width: 0%;
+ }
+ 50% {
+ width: 70%;
+ }
+ 100% {
+ width: 100%;
+ }
+ }
`;
// 파일이 없을 때 표시되는 영역
@@ -288,7 +811,6 @@ const CancelButton = styled.button`
const Button = styled.button`
padding: 8px 16px;
border-radius: 6px;
- font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
`;
@@ -297,7 +819,8 @@ const SubmitButton = styled(Button)`
background: #24478f;
color: white;
border: none;
-
+ font-size: ${({ theme }) => theme.fonts.sizes.mm};
+ font-weight: ${({ theme }) => theme.fonts.weights.bold};
&:hover {
opacity: 0.9;
}
diff --git a/src/pages/ManagePage/components/SideComponents.tsx b/src/pages/ManagePage/components/SideComponents.tsx
index 88a07e7..e28fbea 100644
--- a/src/pages/ManagePage/components/SideComponents.tsx
+++ b/src/pages/ManagePage/components/SideComponents.tsx
@@ -41,7 +41,9 @@ const SideComponents: FC = ({
$isSelect={selectPage[2]}
onClick={() => ChooseIndex(2)}
>
- 급경사지 추가
+
+ 급경사지 추가/복구
+
>
)}