diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 67bbe85..ceebe33 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -11,3 +11,7 @@ ex) feat(17): pull request template 작성(확인 후 지워주세요)
- 구현되었는지 설명해주세요
+
+## 관련 이슈
+
+Closes #
diff --git a/src/components/ImgViewerModal.tsx b/src/components/ImgViewerModal.tsx
new file mode 100644
index 0000000..01d5279
--- /dev/null
+++ b/src/components/ImgViewerModal.tsx
@@ -0,0 +1,268 @@
+import { useImgViewerStore } from '../stores/imgViewerStore';
+import styled from 'styled-components';
+import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded';
+import ArrowBackIosRoundedIcon from '@mui/icons-material/ArrowBackIosRounded';
+
+const ImgViewerModal = () => {
+ const {
+ isOpen,
+ onClose,
+ currentImageType,
+ currentImageUrl,
+ getPrevImageType,
+ getNextImageType,
+ getImagePosition,
+ canGoPrev,
+ canGoNext,
+ openImage,
+ images,
+ } = useImgViewerStore();
+
+ if (!isOpen || !currentImageType || !images) return null;
+
+ const { current, total } = getImagePosition();
+ const prevEnabled = canGoPrev();
+ const nextEnabled = canGoNext();
+
+ // 이미지 타입별 한국어 이름
+ const getImageTypeName = (type: string) => {
+ switch (type) {
+ case 'position':
+ return '위치도';
+ case 'start':
+ return '시점';
+ case 'end':
+ return '종점';
+ case 'overview':
+ return '전경';
+ default:
+ return '';
+ }
+ };
+
+ const handlePrev = () => {
+ const prevType = getPrevImageType();
+ if (prevType && images[prevType]?.url) {
+ openImage(images[prevType].url, prevType);
+ }
+ };
+
+ const handleNext = () => {
+ const nextType = getNextImageType();
+ if (nextType && images[nextType]?.url) {
+ openImage(images[nextType].url, nextType);
+ }
+ };
+
+ // 키보드 이벤트 처리
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'ArrowLeft' && prevEnabled) {
+ handlePrev();
+ } else if (e.key === 'ArrowRight' && nextEnabled) {
+ handleNext();
+ } else if (e.key === 'Escape') {
+ onClose();
+ }
+ };
+
+ return (
+
+ e.stopPropagation()}>
+
+
+ {getImageTypeName(currentImageType)} ({current}/{total})
+
+ ✕
+
+
+
+ {/* 이미지 */}
+ {
+ console.error('이미지 로드 실패:', currentImageUrl);
+ }}
+ />
+
+ {/* 왼쪽 화살표 - MUI 아이콘 사용 */}
+
+
+
+
+ {/* 오른쪽 화살표 - MUI 아이콘 사용 */}
+
+
+
+
+
+
+ );
+};
+
+export default ImgViewerModal;
+
+const ModalOverlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.85);
+ z-index: 2000;
+ display: flex;
+ flex-direction: column;
+ outline: none;
+`;
+
+const ModalContainer = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+`;
+
+const ModalHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 20px;
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 20;
+ pointer-events: none;
+
+ /* 헤더 요소들만 클릭 가능하게 */
+ > * {
+ pointer-events: auto;
+ }
+`;
+
+const HeaderTitle = styled.h1`
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0;
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
+`;
+
+const CloseButton = styled.button`
+ background: rgba(0, 0, 0, 0.6);
+ border: none;
+ font-size: 28px;
+ cursor: pointer;
+ padding: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ border-radius: 50%;
+ width: 44px;
+ height: 44px;
+ backdrop-filter: blur(10px);
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.8);
+ transform: scale(1.1);
+ }
+`;
+
+const ImageContainer = styled.div`
+ position: relative;
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+`;
+
+const MainImage = styled.img`
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ user-select: none;
+ pointer-events: none;
+`;
+
+const ArrowButton = styled.button<{
+ $position: 'left' | 'right';
+ $enabled: boolean;
+}>`
+ position: absolute;
+ ${({ $position }) => $position}: 30px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(0, 0, 0, 0.7);
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ color: ${({ $enabled }) => ($enabled ? 'white' : 'rgba(255, 255, 255, 0.3)')};
+ cursor: ${({ $enabled }) => ($enabled ? 'pointer' : 'not-allowed')};
+ padding: 20px;
+ border-radius: 50%;
+ backdrop-filter: blur(20px);
+ transition: all 0.3s ease;
+ z-index: 10;
+
+ /* MUI 아이콘을 위한 스타일 조정 */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 80px;
+
+ &:hover {
+ background: ${({ $enabled }) =>
+ $enabled ? 'rgba(0, 0, 0, 0.9)' : 'rgba(0, 0, 0, 0.7)'};
+ border-color: ${({ $enabled }) =>
+ $enabled ? 'rgba(255, 255, 255, 0.6)' : 'rgba(255, 255, 255, 0.3)'};
+ transform: translateY(-50%)
+ ${({ $enabled }) => ($enabled ? 'scale(1.1)' : 'scale(1)')};
+ box-shadow: ${({ $enabled }) =>
+ $enabled ? '0 8px 32px rgba(0, 0, 0, 0.5)' : 'none'};
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+
+ /* MUI 아이콘 스타일 조정 */
+ .MuiSvgIcon-root {
+ color: inherit;
+ }
+
+ @media (max-width: 768px) {
+ ${({ $position }) => $position}: 15px;
+ width: 60px;
+ height: 60px;
+ padding: 15px;
+
+ .MuiSvgIcon-root {
+ font-size: 36px !important;
+ }
+ }
+
+ @media (max-width: 480px) {
+ ${({ $position }) => $position}: 10px;
+ width: 50px;
+ height: 50px;
+ padding: 12px;
+
+ .MuiSvgIcon-root {
+ font-size: 28px !important;
+ }
+ }
+`;
diff --git a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx
index d002840..a6c2f75 100644
--- a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx
+++ b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx
@@ -4,6 +4,11 @@ import { useEffect, useState, useRef } from 'react';
import { slopeManageAPI } from '../../../../apis/slopeManage';
import { useNotificationStore } from '../../../../hooks/notificationStore';
import { useQueryClient } from '@tanstack/react-query';
+import {
+ ImageType,
+ useImgViewerStore,
+} from '../../../../stores/imgViewerStore';
+import ImgViewerModal from '../../../../components/ImgViewerModal';
interface ImgsModalProps {
isOpen: boolean;
@@ -16,7 +21,7 @@ interface ImageData {
file: File;
url: string;
name: string;
- isNew: boolean; // 새로 추가된 이미지인지 여부
+ isNew: boolean;
}
interface ImageCategory {
@@ -24,12 +29,11 @@ interface ImageCategory {
name: string;
}
-// 이미지 상태 타입
type ImageAction = 'none' | 'add' | 'delete' | 'update';
interface ImageState {
data: ImageData | null;
- action: ImageAction; // 이 이미지에 대한 액션
+ action: ImageAction;
existingUrl?: string;
}
@@ -55,46 +59,47 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
const [categoryToDelete, setCategoryToDelete] = useState(null);
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [isSaving, setIsSaving] = useState(false);
+ const [focusedCategory, setFocusedCategory] = useState(null); // 포커스된 카테고리 추적
const fileInputRefs = useRef>({});
- // 알림 함수
const showNotification = useNotificationStore(
(state) => state.showNotification
);
- // 초기 이미지 로드 (기존 서버의 이미지가 있다면)
+ // 초기 이미지 로드
useEffect(() => {
if (selectedRow) {
setSelectedData(selectedRow);
const initialImages: Record = {
position: {
- data: null, // File 객체가 아니므로 null
+ data: null,
action: 'none',
existingUrl:
- selectedData?.priority?.images?.position?.url || undefined,
+ selectedRow?.priority?.images?.position?.url || undefined,
},
start: {
data: null,
action: 'none',
- existingUrl: selectedData?.priority?.images?.start?.url || undefined,
+ existingUrl: selectedRow?.priority?.images?.start?.url || undefined,
},
end: {
data: null,
action: 'none',
- existingUrl: selectedData?.priority?.images?.end?.url || undefined,
+ existingUrl: selectedRow?.priority?.images?.end?.url || undefined,
},
overview: {
data: null,
action: 'none',
existingUrl:
- selectedData?.priority?.images?.overview?.url || undefined,
+ selectedRow?.priority?.images?.overview?.url || undefined,
},
};
setCategoryImages(initialImages);
}
}, [selectedRow]);
+
// hidden input들을 생성하여 각 카테고리별 파일 업로드 처리
const createFileInputs = () => {
return imageCategories.map((category) => (
@@ -119,7 +124,6 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
isNew: true,
};
- // 기존 이미지가 있다면 URL 해제
const existingImageState = categoryImages[category];
if (existingImageState.data) {
URL.revokeObjectURL(existingImageState.data.url);
@@ -129,25 +133,78 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
...prev,
[category]: {
data: newImage,
- action: existingImageState.data ? 'update' : 'add',
+ action: existingImageState.existingUrl ? 'update' : 'add',
},
}));
};
- // 파일 업로드 핸들러
const handleFileUpload =
(category: string) => (e: React.ChangeEvent) => {
const files = e.target.files;
if (files && files[0]) {
addImageToCategory(category, files[0]);
}
- // input 값 초기화
if (fileInputRefs.current[category]) {
fileInputRefs.current[category]!.value = '';
}
};
- // 저장 함수 - FormData 생성 및 API 호출
+ // 수정된 붙여넣기 이벤트 핸들러
+ const handlePaste = (e: React.ClipboardEvent) => {
+ const items = e.clipboardData?.items;
+ if (items) {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.type.indexOf('image') !== -1) {
+ const file = item.getAsFile();
+ if (file) {
+ let targetCategory = focusedCategory;
+
+ // 포커스된 카테고리가 없거나 이미 이미지가 있는 경우, 첫 번째 빈 카테고리 찾기
+ if (!targetCategory || categoryImages[targetCategory]?.data) {
+ const emptyCategory = imageCategories.find(
+ (cat) =>
+ !categoryImages[cat.key].data &&
+ categoryImages[cat.key].action !== 'delete'
+ );
+ targetCategory = emptyCategory?.key || null;
+ }
+
+ if (targetCategory) {
+ addImageToCategory(targetCategory, file);
+ // 붙여넣기 후 포커스 해제
+ setFocusedCategory(null);
+ break;
+ } else {
+ showNotification('모든 카테고리에 이미지가 있습니다.', {
+ severity: 'warning',
+ });
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // 카테고리 클릭 핸들러 추가
+ const handleCategoryClick = (category: string) => {
+ setFocusedCategory(category);
+ };
+
+ // 드래그 앤 드롭 핸들러
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ };
+
+ const handleDrop = (category: string) => (e: React.DragEvent) => {
+ e.preventDefault();
+ const files = e.dataTransfer.files;
+ if (files && files[0]) {
+ addImageToCategory(category, files[0]);
+ }
+ };
+
+ // 저장 함수
const handleSave = async () => {
if (!selectedData?.slopeInspectionHistory?.historyNumber) {
showNotification('관리번호가 없습니다.', { severity: 'error' });
@@ -160,37 +217,29 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
const formData = new FormData();
const deletePositions: string[] = [];
- // 각 카테고리별로 처리
Object.entries(categoryImages).forEach(([category, imageState]) => {
const { data, action } = imageState;
switch (action) {
case 'add':
case 'update':
- // 새 이미지 추가 또는 기존 이미지 수정
if (data && data.file) {
formData.append(category, data.file);
}
break;
-
case 'delete':
- // 삭제할 카테고리 추가
deletePositions.push(category);
break;
-
case 'none':
default:
- // 변경사항 없음
break;
}
});
- // 삭제할 포지션들 추가
deletePositions.forEach((position) => {
formData.append('deletePositions', position);
});
- // API 호출
await slopeManageAPI.updateAllImg({
formData,
historyNumber: selectedData.slopeInspectionHistory.historyNumber,
@@ -213,42 +262,6 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
}
};
- // 붙여넣기 이벤트 핸들러
- const handlePaste = (e: React.ClipboardEvent) => {
- const items = e.clipboardData?.items;
- if (items) {
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- if (item.type.indexOf('image') !== -1) {
- const file = item.getAsFile();
- if (file) {
- // 첫 번째 빈 카테고리에 추가
- const emptyCategory = imageCategories.find(
- (cat) => !categoryImages[cat.key].data
- );
- if (emptyCategory) {
- addImageToCategory(emptyCategory.key, file);
- break;
- }
- }
- }
- }
- }
- };
-
- // 드래그 앤 드롭 핸들러
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- };
-
- const handleDrop = (category: string) => (e: React.DragEvent) => {
- e.preventDefault();
- const files = e.dataTransfer.files;
- if (files && files[0]) {
- addImageToCategory(category, files[0]);
- }
- };
-
// 이미지 삭제 확인 모달 열기
const openDeleteModal = (category: string) => {
setCategoryToDelete(category);
@@ -261,7 +274,6 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
if (categoryToDelete && deleteConfirmText === '삭제') {
const existingImageState = categoryImages[categoryToDelete];
- // URL 해제
if (existingImageState.data) {
URL.revokeObjectURL(existingImageState.data.url);
}
@@ -270,7 +282,7 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
...prev,
[categoryToDelete]: {
data: null,
- action: existingImageState.data?.isNew ? 'none' : 'delete',
+ action: existingImageState.existingUrl ? 'delete' : 'none',
},
}));
@@ -280,7 +292,6 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
}
};
- // 삭제 모달 닫기
const closeDeleteModal = () => {
setDeleteModalOpen(false);
setCategoryToDelete(null);
@@ -298,13 +309,54 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
};
}, []);
- // 변경사항이 있는지 확인
const hasChanges = Object.values(categoryImages).some(
(imageState) => imageState.action !== 'none'
);
+ const { setImageData, openImage } = useImgViewerStore();
+
+ const convertSlopeImagesToStoreFormat = (slopeImages: any) => {
+ return {
+ position: {
+ url: slopeImages?.position?.url,
+ createdAt: slopeImages?.position?.createdAt,
+ },
+ start: {
+ url: slopeImages?.start?.url,
+ createdAt: slopeImages?.start?.createdAt,
+ },
+ end: {
+ url: slopeImages?.end?.url,
+ createdAt: slopeImages?.end?.createdAt,
+ },
+ overview: {
+ url: slopeImages?.overview?.url,
+ createdAt: slopeImages?.overview?.createdAt,
+ },
+ };
+ };
+
+ useEffect(() => {
+ if (selectedData?.priority?.images) {
+ const convertedImages = convertSlopeImagesToStoreFormat(
+ selectedData.priority.images
+ );
+ setImageData(convertedImages);
+ }
+ }, [selectedData, setImageData]);
+
+ const handleImageClick = (category: string) => {
+ const imageState = categoryImages[category];
+ const imageUrl = imageState.existingUrl || imageState.data?.url;
+
+ if (imageUrl) {
+ openImage(imageUrl, category as ImageType);
+ }
+ };
+
return (
<>
+ {/* 이미지 확대 모달*/}
@@ -347,12 +399,20 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
{imageCategories.map((category) => {
const imageState = categoryImages[category.key];
const image = imageState.data;
+ const isFocused = focusedCategory === category.key;
return (
-
+ handleCategoryClick(category.key)}
+ >
{category.name}
+ {isFocused && (
+ (선택됨)
+ )}
{imageState.action !== 'none' && (
{imageState.action === 'add' && ' (추가)'}
@@ -366,11 +426,16 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
+ fileInputRefs.current[category.key]?.click()
+ }
>
파일을 마우스로 끌어 오세요
- CTRL+C/CTRL+V
+ {isFocused
+ ? 'CTRL+V로 붙여넣기 가능'
+ : '클릭하여 선택 후 CTRL+V'}
@@ -380,9 +445,16 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
{
+ e.stopPropagation();
+ handleImageClick(category.key);
+ }}
/>
openDeleteModal(category.key)}
+ onClick={(e) => {
+ e.stopPropagation();
+ openDeleteModal(category.key);
+ }}
>
×
@@ -415,12 +487,9 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
- {/* Hidden file inputs */}
{createFileInputs()}
-
- {/* 삭제 확인 모달 */}
@@ -455,7 +524,8 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => {
};
export default ImgsModal;
-// 기본 모달 스타일
+
+// 스타일 컴포넌트들
const ModalOverlay = styled.div<{ $isOpen: boolean }>`
display: ${(props) => (props.$isOpen ? 'flex' : 'none')};
position: fixed;
@@ -507,7 +577,6 @@ const CloseButton = styled.button`
}
`;
-// 정보 섹션 스타일
const InfoSection = styled.div`
margin-bottom: 24px;
padding: 16px;
@@ -543,10 +612,10 @@ const AddressValue = styled(InfoValue)`
line-height: 1.4;
`;
-// 우측 이미지 섹션
const ImagesSection = styled.div`
flex: 1;
`;
+
const ActionIndicator = styled.span<{ $action: ImageAction }>`
font-size: 12px;
font-weight: 500;
@@ -563,6 +632,14 @@ const ActionIndicator = styled.span<{ $action: ImageAction }>`
}
}};
`;
+
+// 포커스 인디케이터 추가
+const FocusIndicator = styled.span`
+ font-size: 12px;
+ font-weight: 500;
+ color: #2563eb;
+`;
+
const ImageCategoriesContainer = styled.div`
display: flex;
flex-direction: column;
@@ -585,10 +662,17 @@ const ImageCategoriesContainer = styled.div`
}
`;
-const ImageCategory = styled.div`
- border: 1px solid #e5e7eb;
+// 포커스 스타일 추가
+const ImageCategory = styled.div<{ $isFocused?: boolean }>`
+ border: 1px solid ${(props) => (props.$isFocused ? '#2563eb' : '#e5e7eb')};
border-radius: 8px;
- background: white;
+ background: ${(props) => (props.$isFocused ? '#eff6ff' : 'white')};
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: #9ca3af;
+ }
`;
const CategoryHeader = styled.div`
@@ -684,7 +768,6 @@ const DeleteImageButton = styled.button`
}
`;
-// 삭제 확인 모달 스타일
const DeleteModalOverlay = styled.div<{ $isOpen: boolean }>`
display: ${(props) => (props.$isOpen ? 'flex' : 'none')};
position: fixed;
diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx
index 25859da..8379db3 100644
--- a/src/pages/MapPage/components/InfoTable.tsx
+++ b/src/pages/MapPage/components/InfoTable.tsx
@@ -1,14 +1,59 @@
import styled from 'styled-components';
import { InfotableProps } from '../interface';
import { useMapStore } from '../../../stores/mapStore';
+import { useImgViewerStore } from '../../../stores/imgViewerStore';
+import { useEffect } from 'react';
+import ImgViewerModal from '../../../components/ImgViewerModal';
const InfoTable = ({ selectItem }: InfotableProps) => {
const { setSelectedMarkerId, setBottomSheetHeight } = useMapStore();
+ const { setImageData, openImage } = useImgViewerStore();
+
const onCloseInfo = () => {
setSelectedMarkerId(null);
setBottomSheetHeight(200);
};
+
+ // 컴포넌트 진입 시 이미지 데이터 설정
+ const convertSlopeImagesToStoreFormat = (slopeImages: any) => {
+ return {
+ position: {
+ url: slopeImages?.position?.url,
+ createdAt: slopeImages?.position?.createdAt,
+ },
+ start: {
+ url: slopeImages?.start?.url,
+ createdAt: slopeImages?.start?.createdAt,
+ },
+ end: {
+ url: slopeImages?.end?.url,
+ createdAt: slopeImages?.end?.createdAt,
+ },
+ overview: {
+ url: slopeImages?.overview?.url,
+ createdAt: slopeImages?.overview?.createdAt,
+ },
+ };
+ };
+
+ useEffect(() => {
+ if (selectItem?.priority?.images) {
+ const convertedImages = convertSlopeImagesToStoreFormat(
+ selectItem.priority.images
+ );
+ setImageData(convertedImages);
+ }
+ }, [selectItem, setImageData]);
+
+ // 이미지 클릭 핸들러
+ const handleImageClick = (imageType: string, imageUrl?: string) => {
+ if (imageUrl) {
+ openImage(imageUrl, imageType as any);
+ }
+ };
+
if (!selectItem) return null;
+
const grade = selectItem.priority?.grade?.includes('A')
? 'A'
: selectItem.priority?.grade?.includes('B')
@@ -20,123 +65,159 @@ const InfoTable = ({ selectItem }: InfotableProps) => {
: 'E';
return (
-
-
-
- {selectItem?.name || ''}
-
- {selectItem?.location?.province || ''}
- {selectItem?.location?.city || ''}
- {selectItem?.location?.district || ''}
- {selectItem?.location?.address || ''}
- {selectItem?.location?.mountainAddress === 'Y' ? '(산)' : ''}
-
-
-
- ×
-
-
-
- {selectItem.priority?.images?.position?.url ? (
-
- ) : (
- 이미지 없음
- )}
- 위치도
-
-
- {selectItem.priority?.images?.start?.url ? (
-
- ) : (
- 이미지 없음
- )}
- 시점
-
-
- {selectItem.priority?.images?.end?.url ? (
-
- ) : (
- 이미지 없음
- )}
- 종점
-
-
- {selectItem.priority?.images?.overview?.url ? (
-
- ) : (
- 이미지 없음
- )}
- 전경
-
-
-
-
-
- {selectItem?.inspections?.serialNumber || ''}
-
-
-
-
-
-
+ <>
+ {/* 이미지 확대 모달*/}
+
+
+
+ {selectItem?.name || ''}
+
{selectItem?.location?.province || ''}
{selectItem?.location?.city || ''}
{selectItem?.location?.district || ''}
{selectItem?.location?.address || ''}
+ {selectItem?.location?.mountainAddress === 'Y' ? '(산)' : ''}
+
+
- {selectItem?.location?.mainLotNumber
- ? selectItem?.location?.subLotNumber
- ? ` ${selectItem?.location?.mainLotNumber}-${selectItem?.location?.subLotNumber}`
- : ` ${selectItem?.location?.mainLotNumber}`
- : ''}
-
-
- {selectItem?.location?.roadAddress
- ? `(${selectItem?.location?.roadAddress})`
- : ''}
-
-
-
-
-
-
- {selectItem.priority.maxVerticalHeight}
-
-
-
- {selectItem.priority.longitudinalLength}
-
-
-
- {selectItem.priority.averageSlope}
-
+ ×
+
+
+
+ {selectItem.priority?.images?.position?.url ? (
+
+ handleImageClick(
+ 'position',
+ selectItem.priority.images.position?.url
+ )
+ }
+ />
+ ) : (
+ 이미지 없음
+ )}
+ 위치도
+
+
+ {selectItem.priority?.images?.start?.url ? (
+
+ handleImageClick(
+ 'start',
+ selectItem.priority.images.start?.url
+ )
+ }
+ />
+ ) : (
+ 이미지 없음
+ )}
+ 시점
+
+
+ {selectItem.priority?.images?.end?.url ? (
+
+ handleImageClick('end', selectItem.priority.images.end?.url)
+ }
+ />
+ ) : (
+ 이미지 없음
+ )}
+ 종점
+
+
+ {selectItem.priority?.images?.overview?.url ? (
+
+ handleImageClick(
+ 'overview',
+ selectItem.priority.images.overview?.url
+ )
+ }
+ />
+ ) : (
+ 이미지 없음
+ )}
+ 전경
+
+
+
+
+
+ {selectItem?.inspections?.serialNumber || ''}
+
-
-
- {selectItem.priority.Score}
-
-
-
- {grade}
-
+
+
+
+
+ {selectItem?.location?.province || ''}
+ {selectItem?.location?.city || ''}
+ {selectItem?.location?.district || ''}
+ {selectItem?.location?.address || ''}
-
- {selectItem?.priority?.usage && (
+ {selectItem?.location?.mainLotNumber
+ ? selectItem?.location?.subLotNumber
+ ? ` ${selectItem?.location?.mainLotNumber}-${selectItem?.location?.subLotNumber}`
+ : ` ${selectItem?.location?.mainLotNumber}`
+ : ''}
+
+
+ {selectItem?.location?.roadAddress
+ ? `(${selectItem?.location?.roadAddress})`
+ : ''}
+
+
+
+
+
+
+ {selectItem.priority.maxVerticalHeight}
+
-
- {selectItem.priority.usage}
+
+ {selectItem.priority.longitudinalLength}
- )}
-
-
- {selectItem.priority.slopeNature}
-
-
-
- {selectItem.priority.slopeType}
-
-
-
+
+
+ {selectItem.priority.averageSlope}
+
+
+
+
+ {selectItem.priority.Score}
+
+
+
+ {grade}
+
+
+
+ {selectItem?.priority?.usage && (
+
+
+ {selectItem.priority.usage}
+
+ )}
+
+
+ {selectItem.priority.slopeNature}
+
+
+
+ {selectItem.priority.slopeType}
+
+
+
+ >
);
};
@@ -197,6 +278,9 @@ const Value = styled.div`
font-size: ${({ theme }) => theme.fonts.sizes.ms};
color: ${({ theme }) => theme.colors.grey[700]};
font-weight: ${({ theme }) => theme.fonts.weights.medium};
+ user-select: text;
+ -webkit-user-select: text;
+ -moz-user-select: text;
`;
const AddressWrapper = styled(InfoRow)`
@@ -280,11 +364,16 @@ const ImgContainer = styled.div`
const Img = styled.img`
width: 100%;
- aspect-ratio: 1; /* 정사각형 비율 유지 */
+ aspect-ratio: 1;
object-fit: cover;
border-radius: 8px;
-`;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+ &:hover {
+ transform: scale(1.02); /* 추가: 호버 시 살짝 확대 */
+ }
+`;
const ImgTag = styled.div`
font-size: ${({ theme }) => theme.fonts.sizes.ms};
font-weight: ${({ theme }) => theme.fonts.weights.bold};
diff --git a/src/stores/imgViewerStore.ts b/src/stores/imgViewerStore.ts
new file mode 100644
index 0000000..0eb930d
--- /dev/null
+++ b/src/stores/imgViewerStore.ts
@@ -0,0 +1,127 @@
+import { create } from 'zustand';
+
+export type ImageType = 'position' | 'start' | 'end' | 'overview';
+
+interface ImageData {
+ url?: string;
+ createdAt?: string;
+}
+
+interface ImgViewerStore {
+ isOpen: boolean;
+ currentImageUrl: string | null;
+ currentImageType: ImageType | null;
+ images: Record | null; // 이미지 데이터 저장
+
+ setIsOpen: (isOpen: boolean) => void;
+ onClose: () => void;
+ openImage: (imageUrl: string, imageType: ImageType) => void;
+
+ // 이미지 데이터 설정 (컴포넌트 진입 시)
+ setImageData: (images: Record) => void;
+
+ // 존재하는 이미지들만 순서대로 배열로 반환
+ getAvailableImages: () => ImageType[];
+
+ // 현재 이미지의 인덱스 반환 (0부터 시작)
+ getCurrentIndex: () => number;
+
+ // 이전 이미지 타입 반환 (없으면 null)
+ getPrevImageType: () => ImageType | null;
+
+ // 다음 이미지 타입 반환 (없으면 null)
+ getNextImageType: () => ImageType | null;
+
+ // 현재 몇 번째 / 전체 몇 개 정보
+ getImagePosition: () => { current: number; total: number };
+
+ // 이전/다음 버튼 활성화 여부
+ canGoPrev: () => boolean;
+ canGoNext: () => boolean;
+}
+
+export const useImgViewerStore = create((set, get) => ({
+ isOpen: false,
+ currentImageUrl: null,
+ currentImageType: null,
+ images: null,
+
+ setIsOpen: (isOpen: boolean) => set({ isOpen: isOpen }),
+
+ onClose: () => {
+ set({
+ isOpen: false,
+ currentImageUrl: null,
+ currentImageType: null,
+ });
+ },
+
+ openImage: (imageUrl: string, imageType: ImageType) => {
+ set({
+ isOpen: true,
+ currentImageUrl: imageUrl,
+ currentImageType: imageType,
+ });
+ },
+
+ setImageData: (images: Record) => {
+ set({ images });
+ },
+
+ getAvailableImages: () => {
+ const { images } = get();
+ if (!images) return [];
+
+ const imageOrder: ImageType[] = ['position', 'start', 'end', 'overview'];
+ return imageOrder.filter((type) => images[type]?.url);
+ },
+
+ getCurrentIndex: () => {
+ const { currentImageType, getAvailableImages } = get();
+ if (!currentImageType) return -1;
+
+ const availableImages = getAvailableImages();
+ return availableImages.indexOf(currentImageType);
+ },
+
+ getPrevImageType: () => {
+ const { getCurrentIndex, getAvailableImages } = get();
+ const availableImages = getAvailableImages();
+ const currentIndex = getCurrentIndex();
+
+ if (currentIndex <= 0) return null;
+ return availableImages[currentIndex - 1];
+ },
+
+ getNextImageType: () => {
+ const { getCurrentIndex, getAvailableImages } = get();
+ const availableImages = getAvailableImages();
+ const currentIndex = getCurrentIndex();
+
+ if (currentIndex >= availableImages.length - 1) return null;
+ return availableImages[currentIndex + 1];
+ },
+
+ getImagePosition: () => {
+ const { getCurrentIndex, getAvailableImages } = get();
+ const availableImages = getAvailableImages();
+ const currentIndex = getCurrentIndex();
+
+ return {
+ current: currentIndex + 1, // 1부터 시작
+ total: availableImages.length,
+ };
+ },
+
+ canGoPrev: () => {
+ const { getCurrentIndex } = get();
+ return getCurrentIndex() > 0;
+ },
+
+ canGoNext: () => {
+ const { getCurrentIndex, getAvailableImages } = get();
+ const availableImages = getAvailableImages();
+ const currentIndex = getCurrentIndex();
+ return currentIndex < availableImages.length - 1;
+ },
+}));