From dfebc78acefc725c4d96352c72c40af41e817271 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Fri, 30 May 2025 18:32:06 +0900 Subject: [PATCH 01/16] =?UTF-8?q?fix:=20=EC=97=B4=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSlope/components/table/coloums.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/table/coloums.ts b/src/pages/ManagePage/StepSlope/components/table/coloums.ts index ccfe3e1..19d81a1 100644 --- a/src/pages/ManagePage/StepSlope/components/table/coloums.ts +++ b/src/pages/ManagePage/StepSlope/components/table/coloums.ts @@ -31,18 +31,18 @@ export const getSlopeColumns = () => [ onChange: handleToggleRow, // 함수 자체를 전달, 호출 결과 아님 }); }, - size: 50, - }), - columnHelper.accessor( - (_row, index) => { - return index + 1; - }, - { - id: 'index', - header: '번호', - size: 60, - } - ), + size: 40, + }), + // columnHelper.accessor( + // (_row, index) => { + // return index + 1; + // }, + // { + // id: 'index', + // header: '번호', + // size: 40, + // } + // ), columnHelper.accessor('managementNo', { header: '관리번호', size: 120, @@ -51,6 +51,14 @@ export const getSlopeColumns = () => [ header: '급경사지명', size: 150, }), + columnHelper.accessor( + (row) => row.slopeInspectionHistory.historyNumber || '', + { + id: 'historyNumber', + header: 'SMC번호', + size: 120, + } + ), columnHelper.accessor((row) => row.management.organization || '', { id: 'organization', header: '시행청명', From 8d95065703835a80fa39e98862cf82dfcd28fe1f Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 21:00:13 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EC=9C=84=EC=B9=98=EB=8F=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/slopeManage.tsx | 16 + .../StepSlope/components/ImgsModal.tsx | 800 ++++++++++++++++++ .../StepSlope/components/table/DataTable.tsx | 12 + .../components/table/TableModals.tsx | 8 + .../StepSlope/pages/SteepSlopeLookUp.tsx | 7 + src/pages/ManagePage/interface.ts | 3 + src/stores/tableStore.ts | 6 + 7 files changed, 852 insertions(+) create mode 100644 src/pages/ManagePage/StepSlope/components/ImgsModal.tsx diff --git a/src/apis/slopeManage.tsx b/src/apis/slopeManage.tsx index 6f72cf0..e7e54cc 100644 --- a/src/apis/slopeManage.tsx +++ b/src/apis/slopeManage.tsx @@ -87,4 +87,20 @@ export const slopeManageAPI = { console.log(' 경사지 이상값 조회', response.data); return response.data; }, + updateAllImg: async ({ formData, historyNumber }: UpdateAllImg) => { + const response = await api.put( + `/slopes/${historyNumber}/images`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + return response.data; + }, }; +export interface UpdateAllImg { + formData: FormData; + historyNumber: string; +} diff --git a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx new file mode 100644 index 0000000..d205e0f --- /dev/null +++ b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx @@ -0,0 +1,800 @@ +import styled from 'styled-components'; +import { Slope } from '../../../../apis/slopeMap'; +import { useEffect, useState, useRef } from 'react'; +import { slopeManageAPI } from '../../../../apis/slopeManage'; +import { useNotificationStore } from '../../../../hooks/notificationStore'; + +interface ImgsModalProps { + isOpen: boolean; + onClose: () => void; + selectedRow: Slope | null; +} + +interface ImageData { + id: string; + file: File; + url: string; + name: string; + isNew: boolean; // 새로 추가된 이미지인지 여부 +} + +interface ImageCategory { + key: string; + name: string; +} + +// 이미지 상태 타입 +type ImageAction = 'none' | 'add' | 'delete' | 'update'; + +interface ImageState { + data: ImageData | null; + action: ImageAction; // 이 이미지에 대한 액션 +} + +const imageCategories: ImageCategory[] = [ + { key: 'position', name: '위치도' }, + { key: 'start', name: '시점' }, + { key: 'end', name: '종점' }, + { key: 'overview', name: '전경' }, +]; + +const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => { + const [selectedData, setSelectedData] = useState(null); + const [categoryImages, setCategoryImages] = useState< + Record + >({ + position: { data: null, action: 'none' }, + start: { data: null, action: 'none' }, + end: { data: null, action: 'none' }, + overview: { data: null, action: 'none' }, + }); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [categoryToDelete, setCategoryToDelete] = useState(null); + const [deleteConfirmText, setDeleteConfirmText] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const fileInputRefs = useRef>({}); + + // 알림 함수 + const showNotification = useNotificationStore( + (state) => state.showNotification + ); + + // 초기 이미지 로드 (기존 서버의 이미지가 있다면) + useEffect(() => { + if (selectedRow) { + setSelectedData(selectedRow); + + // 기존 이미지 데이터가 있다면 로드 (예시 - 실제 데이터 구조에 맞게 수정) + // 실제로는 서버에서 기존 이미지를 조회해야 할 수도 있음 + const initialImages: Record = { + position: { data: null, action: 'none' }, + start: { data: null, action: 'none' }, + end: { data: null, action: 'none' }, + overview: { data: null, action: 'none' }, + }; + + setCategoryImages(initialImages); + } + }, [selectedRow]); + + // hidden input들을 생성하여 각 카테고리별 파일 업로드 처리 + const createFileInputs = () => { + return imageCategories.map((category) => ( + (fileInputRefs.current[category.key] = el)} + type="file" + accept="image/*" + onChange={handleFileUpload(category.key)} + style={{ display: 'none' }} + /> + )); + }; + + // 특정 카테고리에 이미지 추가/수정 + const addImageToCategory = (category: string, file: File) => { + const newImage: ImageData = { + id: Date.now().toString() + Math.random().toString(36).substr(2, 9), + file, + url: URL.createObjectURL(file), + name: file.name, + isNew: true, + }; + + // 기존 이미지가 있다면 URL 해제 + const existingImageState = categoryImages[category]; + if (existingImageState.data) { + URL.revokeObjectURL(existingImageState.data.url); + } + + setCategoryImages((prev) => ({ + ...prev, + [category]: { + data: newImage, + action: existingImageState.data ? '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 handleSave = async () => { + if (!selectedData?.slopeInspectionHistory?.historyNumber) { + showNotification('관리번호가 없습니다.', { severity: 'error' }); + return; + } + + setIsSaving(true); + + try { + 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, + }); + + showNotification('이미지가 성공적으로 저장되었습니다.', { + severity: 'success', + }); + onClose(); + } catch (error) { + console.error('이미지 저장 실패:', error); + showNotification('이미지 저장 중 오류가 발생했습니다.', { + severity: 'error', + }); + } finally { + setIsSaving(false); + } + }; + + // 붙여넣기 이벤트 핸들러 + 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); + setDeleteModalOpen(true); + setDeleteConfirmText(''); + }; + + // 이미지 삭제 실행 + const handleDeleteConfirm = () => { + if (categoryToDelete && deleteConfirmText === '삭제') { + const existingImageState = categoryImages[categoryToDelete]; + + // URL 해제 + if (existingImageState.data) { + URL.revokeObjectURL(existingImageState.data.url); + } + + setCategoryImages((prev) => ({ + ...prev, + [categoryToDelete]: { + data: null, + action: existingImageState.data?.isNew ? 'none' : 'delete', + }, + })); + + setDeleteModalOpen(false); + setCategoryToDelete(null); + setDeleteConfirmText(''); + } + }; + + // 삭제 모달 닫기 + const closeDeleteModal = () => { + setDeleteModalOpen(false); + setCategoryToDelete(null); + setDeleteConfirmText(''); + }; + + // 컴포넌트 언마운트 시 URL 해제 + useEffect(() => { + return () => { + Object.values(categoryImages).forEach((imageState) => { + if (imageState.data) { + URL.revokeObjectURL(imageState.data.url); + } + }); + }; + }, []); + + // 변경사항이 있는지 확인 + const hasChanges = Object.values(categoryImages).some( + (imageState) => imageState.action !== 'none' + ); + + return ( + <> + + + + +

급경사지 이미지 관리

+ × +
+
+ + + {selectedData && ( + + + 급경사지명: + {selectedData.name || '정보 없음'} + + + 관리번호: + {selectedData.managementNo} + + + 주소: + + {selectedData?.location?.province || ''} + {selectedData?.location?.city || ''} + {selectedData?.location?.district || ''} + {selectedData?.location?.address || ''} + {selectedData?.location?.mainLotNumber + ? selectedData?.location?.subLotNumber + ? ` ${selectedData?.location?.mainLotNumber}-${selectedData?.location?.subLotNumber}` + : ` ${selectedData?.location?.mainLotNumber}` + : ''} + + + + )} + + + + {imageCategories.map((category) => { + const imageState = categoryImages[category.key]; + const image = imageState.data; + + return ( + + + + {category.name} + {imageState.action !== 'none' && ( + + {imageState.action === 'add' && ' (추가)'} + {imageState.action === 'update' && ' (수정)'} + {imageState.action === 'delete' && ' (삭제 예정)'} + + )} + + + + + + 파일을 마우스로 끌어 오세요 +
+ CTRL+C/CTRL+V +
+
+ + {image && imageState.action !== 'delete' ? ( + <> + + openDeleteModal(category.key)} + > + × + + + ) : ( + + 급경사지 등록대상 +
+ 사진({category.name}) + {imageState.action === 'delete' &&
} + {imageState.action === 'delete' && '삭제 예정'} +
+ )} +
+
+
+ ); + })} +
+
+
+ + + + + {isSaving ? '저장 중...' : '저장'} + + 취소 + + + + {/* Hidden file inputs */} + {createFileInputs()} +
+
+ + {/* 삭제 확인 모달 */} + + + +

이미지 삭제

+ × +
+ + 이미지를 삭제하시려면 입력창에 삭제라고 입력해 + 주세요. + + + setDeleteConfirmText(e.target.value)} + /> + + + + 확인 + + 취소 + +
+
+ + ); +}; + +export default ImgsModal; +// 기본 모달 스타일 +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: 1000px; + max-height: 90vh; + overflow-y: auto; + outline: none; + display: flex; + flex-direction: column; + padding: 0; +`; + +const ModalHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + h2 { + margin: 0; + font-size: 20px; + color: #111827; + } +`; + +const CloseButton = styled.button` + background: none; + border: none; + font-size: 24px; + cursor: pointer; + padding: 0; + color: #6b7280; + + &:hover { + color: #111827; + } +`; + +// 정보 섹션 스타일 +const InfoSection = styled.div` + margin-bottom: 24px; + padding: 16px; + background-color: #f9fafb; + border-radius: 8px; +`; + +const InfoItem = styled.div` + display: flex; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } +`; + +const InfoLabel = styled.span` + font-weight: 600; + color: #374151; + width: 90px; + padding-right: 10px; +`; + +const InfoValue = styled.span` + color: #111827; +`; + +const AddressWrapper = styled(InfoItem)` + align-items: flex-start; +`; + +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; + color: ${(props) => { + switch (props.$action) { + case 'add': + return '#059669'; + case 'update': + return '#d97706'; + case 'delete': + return '#dc2626'; + default: + return '#6b7280'; + } + }}; +`; +const ImageCategoriesContainer = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + max-height: 500px; + padding-right: 8px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: #888; + border-radius: 3px; + } +`; + +const ImageCategory = styled.div` + border: 1px solid #e5e7eb; + border-radius: 8px; + background: white; +`; + +const CategoryHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background-color: #f9fafb; + border-bottom: 1px solid #e5e7eb; +`; + +const CategoryTitle = styled.span` + font-weight: 600; + color: #374151; +`; + +const CategoryContent = styled.div` + display: flex; + padding: 12px; + gap: 12px; + height: 150px; +`; + +const UploadPlaceholder = styled.div` + flex: 1; + border: 2px dashed #d1d5db; + border-radius: 6px; + padding: 20px; + text-align: center; + cursor: pointer; + background: #fafafa; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + &:hover { + border-color: #9ca3af; + } +`; + +const PlaceholderText = styled.div` + color: #6b7280; + font-size: 16px; + line-height: 1.4; +`; + +const ImagePreviewArea = styled.div` + flex: 1; + position: relative; + border: 1px solid #e5e7eb; + border-radius: 6px; + height: 80px; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const PreviewPlaceholder = styled.div` + color: #dc2626; + font-size: 12px; + text-align: center; + line-height: 1.3; + font-weight: 500; +`; + +const CategoryImage = styled.img` + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 6px; +`; + +const DeleteImageButton = styled.button` + position: absolute; + top: 4px; + right: 4px; + background: rgba(0, 0, 0, 0.7); + color: white; + border: none; + border-radius: 50%; + width: 20px; + height: 20px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: rgba(0, 0, 0, 0.9); + } +`; + +// 삭제 확인 모달 스타일 +const DeleteModalOverlay = 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: 1100; +`; + +const DeleteModalContent = styled.div` + background: white; + padding: 24px; + border-radius: 8px; + width: 100%; + max-width: 400px; +`; + +const DeleteModalHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + + h3 { + margin: 0; + font-size: 18px; + color: #111827; + } +`; + +const DeleteMessage = styled.p` + margin-bottom: 20px; + line-height: 1.5; + color: #374151; +`; + +const BoldText = styled.span` + font-weight: 600; + color: #dc2626; +`; + +const DeleteInput = styled.div` + margin-bottom: 16px; + + input { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + + &::placeholder { + color: #9ca3af; + } + } +`; + +const DeleteButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 24px; +`; + +const Button = styled.button` + padding: 8px 16px; + border-radius: 4px; + border: none; + cursor: pointer; + + &:disabled { + background-color: #9ca3af; + cursor: not-allowed; + } +`; + +const ConfirmButton = styled(Button)` + background: #24478f; + color: white; +`; + +const CancelButton = styled(Button)` + background: #e5e7eb; + color: #374151; + + &:hover { + background: #d1d5db; + } +`; + +const FixedHeader = styled.div` + position: sticky; + top: 0; + background: white; + z-index: 10; + border-bottom: 1px solid #e5e7eb; + padding: 12px 12px; +`; + +const ScrollableContent = styled.div` + flex: 1; + overflow-y: auto; + padding: 28px 18px; +`; + +const FixedFooter = styled.div` + position: sticky; + bottom: 0; + background: white; + border-top: 1px solid #e5e7eb; + padding: 16px 16px; + z-index: 10; +`; + +const FooterButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 12px; +`; + +const SaveButton = styled.button` + background: #24478f; + color: white; + border: none; + border-radius: 6px; + padding: 10px 20px; + cursor: pointer; + font-weight: 500; + + &:hover { + background: #1d3660; + } +`; diff --git a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx index b61baa7..4c7e18f 100644 --- a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx @@ -10,6 +10,7 @@ const DataTable: React.FC = ({ rowVirtualizer, selectedRow, setSelectedRow, + openImgsModal, }) => { const paddingTop = rowVirtualizer.getVirtualItems()[0]?.start || 0; const paddingBottom = @@ -81,6 +82,17 @@ const DataTable: React.FC = ({ ); } }} + onDoubleClick={(e) => { + // 체크박스 열이 아닌 부분을 더블클릭했을 때만 모달 열기 + if ( + !(e.target as HTMLElement).closest('input[type="checkbox"]') + ) { + // 더블클릭한 행을 선택 상태로 설정 + setSelectedRow(row.original); + // ImgsModal 열기 + openImgsModal(); + } + }} $selected={ selectedRow?.managementNo === row.original.managementNo || row.getIsSelected?.() || diff --git a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx index ba37f3c..aee0e5c 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx @@ -4,6 +4,7 @@ import DeleteConfirmModal from '../DeleteModal'; import EditModal from '../EditModal'; import RegionFilterModal from '../RegionFilterModal'; import { TableModalProps } from '../../../interface'; +import ImgsModal from '../ImgsModal'; const TableModals: React.FC = ({ isModalOpen, @@ -20,6 +21,8 @@ const TableModals: React.FC = ({ isEditModalOpen, closeEditModal, handleEdit, + isImgsModalOpen, + closeImgsModal, }) => { return ( <> @@ -42,6 +45,11 @@ const TableModals: React.FC = ({ onSubmit={handleEdit} selectedRow={selectedRow} /> + ); }; diff --git a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx index a21ff09..2717c5e 100644 --- a/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx +++ b/src/pages/ManagePage/StepSlope/pages/SteepSlopeLookUp.tsx @@ -69,6 +69,10 @@ const SteepSlopeLookUp = () => { resetFilters, setSelectedRows, selectedRows, + + isImgsModalOpen, + openImgsModal, + closeImgsModal, } = useSteepSlopeStore(); // 테이블 컨테이너 ref는 훅 내에서 직접 생성 @@ -296,6 +300,8 @@ const SteepSlopeLookUp = () => { isEditModalOpen={isEditModalOpen} closeEditModal={closeEditModal} handleEdit={handleEdit} + isImgsModalOpen={isImgsModalOpen} + closeImgsModal={closeImgsModal} /> { rowVirtualizer={rowVirtualizer} selectedRow={selectedRow} setSelectedRow={setSelectedRow} + openImgsModal={openImgsModal} /> ; selectedRow: Slope | null; setSelectedRow: (row: Slope | null) => void; + openImgsModal: () => void; } export interface TableActionProps { @@ -88,6 +89,8 @@ export interface TableModalProps { isEditModalOpen: boolean; closeEditModal: () => void; handleEdit: (updatedSlope: Slope) => void; + isImgsModalOpen: boolean; + closeImgsModal: () => void; } export interface TableToolbarProps { diff --git a/src/stores/tableStore.ts b/src/stores/tableStore.ts index 10e7e83..e36c25e 100644 --- a/src/stores/tableStore.ts +++ b/src/stores/tableStore.ts @@ -26,6 +26,7 @@ export interface TableState { isRegionModalOpen: boolean; isDeleteModalOpen: boolean; isEditModalOpen: boolean; + isImgsModalOpen: boolean; // 선택된 행 - 단일 선택에서 다중 선택으로 변경 selectedRow: T | null; // 기존 호환성을 위해 유지 @@ -52,6 +53,8 @@ export interface TableState { closeDeleteModal: () => void; openEditModal: () => void; closeEditModal: () => void; + openImgsModal: () => void; + closeImgsModal: () => void; setSelectedRow: (row: T | null) => void; resetFilters: () => void; @@ -76,6 +79,7 @@ export function createTableStore(initialState: Partial> = {}) { isRegionModalOpen: false, isDeleteModalOpen: false, isEditModalOpen: false, + isImgsModalOpen: false, selectedRow: null, selectedRows: [], // 다중 선택 배열 초기화 @@ -130,6 +134,8 @@ export function createTableStore(initialState: Partial> = {}) { closeDeleteModal: () => set({ isDeleteModalOpen: false }), openEditModal: () => set({ isEditModalOpen: true }), closeEditModal: () => set({ isEditModalOpen: false }), + openImgsModal: () => set({ isImgsModalOpen: true }), + closeImgsModal: () => set({ isImgsModalOpen: false }), setSelectedRow: (row) => set({ selectedRow: row }), From 72e2bdaa0b7b7d8054925f98eda49cf807f029de Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 21:28:00 +0900 Subject: [PATCH 03/16] =?UTF-8?q?fix:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B3=80=EB=8F=99=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?slope=20=ED=83=80=EC=9E=85=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/slopeMap.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/apis/slopeMap.tsx b/src/apis/slopeMap.tsx index 8d628a7..488fa66 100644 --- a/src/apis/slopeMap.tsx +++ b/src/apis/slopeMap.tsx @@ -1,5 +1,9 @@ import { api } from './api'; - +interface SlopeImg { + // 이미지 + url: string; // url + createdAt: Date; // 이미지 생성날짜 +} export interface Slope { managementNo: string; name: string; @@ -74,13 +78,12 @@ export interface Slope { maxVerticalHeight: string; // 최고수직고 (단위: m) longitudinalLength: string; // 종단길이 (단위: m) averageSlope: string; // 평균경사 (단위: 도) - images: [ - { - // 이미지 - url: string; // url - createdAt: Date; // 이미지 생성날짜 - } - ]; + images: { + position?: SlopeImg; + start?: SlopeImg; + overview?: SlopeImg; + end?: SlopeImg; + }; Score: string; //점수 grade: string; // 등급 }; From 103281f833d4b8c2be8c1272af88a5162c619931 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 21:28:14 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=A0=81=EC=9A=A9=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSlope/components/ImgsModal.tsx | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx index d205e0f..13067ae 100644 --- a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx @@ -3,6 +3,7 @@ import { Slope } from '../../../../apis/slopeMap'; import { useEffect, useState, useRef } from 'react'; import { slopeManageAPI } from '../../../../apis/slopeManage'; import { useNotificationStore } from '../../../../hooks/notificationStore'; +import { useQueryClient } from '@tanstack/react-query'; interface ImgsModalProps { isOpen: boolean; @@ -29,6 +30,7 @@ type ImageAction = 'none' | 'add' | 'delete' | 'update'; interface ImageState { data: ImageData | null; action: ImageAction; // 이 이미지에 대한 액션 + existingUrl?: string; } const imageCategories: ImageCategory[] = [ @@ -39,6 +41,7 @@ const imageCategories: ImageCategory[] = [ ]; const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => { + const queryClient = useQueryClient(); const [selectedData, setSelectedData] = useState(null); const [categoryImages, setCategoryImages] = useState< Record @@ -64,19 +67,34 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => { if (selectedRow) { setSelectedData(selectedRow); - // 기존 이미지 데이터가 있다면 로드 (예시 - 실제 데이터 구조에 맞게 수정) - // 실제로는 서버에서 기존 이미지를 조회해야 할 수도 있음 const initialImages: Record = { - position: { data: null, action: 'none' }, - start: { data: null, action: 'none' }, - end: { data: null, action: 'none' }, - overview: { data: null, action: 'none' }, + position: { + data: null, // File 객체가 아니므로 null + action: 'none', + existingUrl: + selectedData?.priority?.images?.position?.url || undefined, + }, + start: { + data: null, + action: 'none', + existingUrl: selectedData?.priority?.images?.start?.url || undefined, + }, + end: { + data: null, + action: 'none', + existingUrl: selectedData?.priority?.images?.end?.url || undefined, + }, + overview: { + data: null, + action: 'none', + existingUrl: + selectedData?.priority?.images?.overview?.url || undefined, + }, }; setCategoryImages(initialImages); } }, [selectedRow]); - // hidden input들을 생성하여 각 카테고리별 파일 업로드 처리 const createFileInputs = () => { return imageCategories.map((category) => ( @@ -177,6 +195,9 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => { formData, historyNumber: selectedData.slopeInspectionHistory.historyNumber, }); + await queryClient.invalidateQueries({ + queryKey: ['slopes'], + }); showNotification('이미지가 성공적으로 저장되었습니다.', { severity: 'success', @@ -353,9 +374,13 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => { - {image && imageState.action !== 'delete' ? ( + {(image || imageState.existingUrl) && + imageState.action !== 'delete' ? ( <> - + openDeleteModal(category.key)} > From 9206813c15f0e5ae8353010a7ea4cc39f87ed853 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 21:55:55 +0900 Subject: [PATCH 05/16] =?UTF-8?q?chore:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ManagePage/StepSlope/components/ImgsModal.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx index 13067ae..d002840 100644 --- a/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx +++ b/src/pages/ManagePage/StepSlope/components/ImgsModal.tsx @@ -389,8 +389,6 @@ const ImgsModal = ({ isOpen, onClose, selectedRow }: ImgsModalProps) => { ) : ( - 급경사지 등록대상 -
사진({category.name}) {imageState.action === 'delete' &&
} {imageState.action === 'delete' && '삭제 예정'} @@ -615,7 +613,7 @@ const CategoryContent = styled.div` `; const UploadPlaceholder = styled.div` - flex: 1; + flex: 3; border: 2px dashed #d1d5db; border-radius: 6px; padding: 20px; @@ -638,7 +636,7 @@ const PlaceholderText = styled.div` `; const ImagePreviewArea = styled.div` - flex: 1; + flex: 2; position: relative; border: 1px solid #e5e7eb; border-radius: 6px; From fdae53c4ad71c54bd8b6bf2788ff9a3c057e04f7 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 21:56:13 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=97=B4=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSlope/components/table/coloums.ts | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/table/coloums.ts b/src/pages/ManagePage/StepSlope/components/table/coloums.ts index 19d81a1..95cfce8 100644 --- a/src/pages/ManagePage/StepSlope/components/table/coloums.ts +++ b/src/pages/ManagePage/StepSlope/components/table/coloums.ts @@ -33,16 +33,6 @@ export const getSlopeColumns = () => [ }, size: 40, }), - // columnHelper.accessor( - // (_row, index) => { - // return index + 1; - // }, - // { - // id: 'index', - // header: '번호', - // size: 40, - // } - // ), columnHelper.accessor('managementNo', { header: '관리번호', size: 120, @@ -59,6 +49,35 @@ export const getSlopeColumns = () => [ size: 120, } ), + columnHelper.accessor( + (row) => (row.priority?.images?.position?.url ? 'O' : ''), + { + id: 'images.position', + header: '위치도', + size: 40, + } + ), + columnHelper.accessor( + (row) => (row.priority?.images?.start?.url ? 'O' : ''), + { + id: 'images.start', + header: '시점', + size: 40, + } + ), + columnHelper.accessor((row) => (row.priority?.images?.end?.url ? 'O' : ''), { + id: 'images.end', + header: '종점', + size: 40, + }), + columnHelper.accessor( + (row) => (row.priority?.images?.overview?.url ? 'O' : ''), + { + id: 'images.overview', + header: '전경', + size: 40, + } + ), columnHelper.accessor((row) => row.management.organization || '', { id: 'organization', header: '시행청명', From b331b5bb64afd4ba4d4b7c7372dfdb0cdbbbf3b8 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 22:07:52 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=EB=A7=88=EC=BB=A4=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=20=ED=81=AC=EA=B8=B0=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MapPage/components/map/MapComponent.tsx | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/src/pages/MapPage/components/map/MapComponent.tsx b/src/pages/MapPage/components/map/MapComponent.tsx index 78fb64e..f757cee 100644 --- a/src/pages/MapPage/components/map/MapComponent.tsx +++ b/src/pages/MapPage/components/map/MapComponent.tsx @@ -145,6 +145,7 @@ const MapComponent = () => { if (!userLocation) { return
지도를 로드 중입니다...
; } + return ( { : grade === 'D' ? DmarkerIcon : EmarkerIcon; + const selectedIcon = { + content: ` +
+ ${ + allTextShow || selectedMarkerId === index + ? `
+
+ ${item.name} +
+
` + : '' + } + marker +
+ `, + anchor: new navermaps.Point(24, 24), // 48px의 중심점 + }; + // 일반 마커 아이콘 설정 + const normalIcon = { + content: ` +
+ ${ + allTextShow + ? `
+
+ ${item.name} +
+
` + : '' + } + marker +
+ `, + anchor: new navermaps.Point(18, 18), // 36px의 중심점 + }; return ( { item.location.coordinates.start.coordinates[0] ) } - icon={{ - content: ` -
- ${ - selectedMarkerId === index || allTextShow - ? `
-
- ${item.name} -
-
` - : '' - } - marker -
- `, - anchor: new navermaps.Point(18, 18), - }} + icon={ + selectedMarkerId === index ? selectedIcon : normalIcon + } onClick={() => { chooseSelectItem(item, index); }} From 79c185a36ee3120635e9d1d436a16697858a409c Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 23:25:21 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20=EB=86=92=EC=9D=B4=EB=B3=80?= =?UTF-8?q?=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/components/InfoTable.tsx | 133 ++++++++++++++++++++- src/stores/mapStore.ts | 8 +- 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx index 60a53b4..2e111a9 100644 --- a/src/pages/MapPage/components/InfoTable.tsx +++ b/src/pages/MapPage/components/InfoTable.tsx @@ -3,9 +3,10 @@ import { InfotableProps } from '../interface'; import { useMapStore } from '../../../stores/mapStore'; const InfoTable = ({ selectItem }: InfotableProps) => { - const { setSelectedMarkerId } = useMapStore(); + const { setSelectedMarkerId, setBottomSheetHeight } = useMapStore(); const onCloseInfo = () => { setSelectedMarkerId(null); + setBottomSheetHeight(200); }; if (!selectItem) return null; const grade = selectItem.priority?.grade?.includes('A') @@ -34,6 +35,40 @@ const InfoTable = ({ selectItem }: InfotableProps) => { × + + + {selectItem.priority?.images?.position?.url ? ( + 위치도 + ) : ( + 이미지 없음 + )} + 위치도 + + + {selectItem.priority?.images?.start?.url ? ( + 시점 + ) : ( + 이미지 없음 + )} + 시점 + + + {selectItem.priority?.images?.end?.url ? ( + 종점 + ) : ( + 이미지 없음 + )} + 종점 + + + {selectItem.priority?.images?.overview?.url ? ( + 전경 + ) : ( + 이미지 없음 + )} + 전경 + + @@ -128,6 +163,8 @@ const Title = styled.div` font-size: 22px; color: ${({ theme }) => theme.colors.primary}; font-weight: 600; + flex-shrink: 0; + white-space: nowrap; `; const UpperAddressValue = styled.div` @@ -208,3 +245,97 @@ const CloseButton = styled.button` const Line = styled.div` border-bottom: 1px dashed ${({ theme }) => theme.colors.grey[200]}; `; + +// const ViewImgSection = styled.div` +// display: grid; +// grid-template-columns: repeat(4, 1fr); +// gap: 10px; +// @media ${({ theme }) => theme.device.mobile} { +// grid-template-columns: repeat(3, 1fr); +// grid-template-areas: +// 'position position position' +// 'start end overview'; + +// & > div:nth-child(1) { +// grid-area: position; +// } +// & > div:nth-child(2) { +// grid-area: start; +// } +// & > div:nth-child(3) { +// grid-area: end; +// } +// & > div:nth-child(4) { +// grid-area: overview; +// } +// } +// `; +// const ImgContainer = styled.div` +// display: flex; +// flex-direction: column; +// `; +// const Img = styled.img` +// width: 100%; +// height: 100%; +// `; +// const ImgTag = styled.div` +// font-size: ${({ theme }) => theme.fonts.sizes.ms}; +// font-weight: ${({ theme }) => theme.fonts.weights.bold}; +// `; +const ViewImgSection = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + padding: 12px 12px; + @media ${({ theme }) => theme.device.mobile} { + grid-template-columns: repeat(3, 1fr); + grid-template-areas: + 'position position position' + 'start end overview'; + + & > div:nth-child(1) { + grid-area: position; + } + & > div:nth-child(2) { + grid-area: start; + } + & > div:nth-child(3) { + grid-area: end; + } + & > div:nth-child(4) { + grid-area: overview; + } + } +`; + +const ImgContainer = styled.div` + display: flex; + flex-direction: column; + gap: 5px; +`; + +const Img = styled.img` + width: 100%; + aspect-ratio: 1; /* 정사각형 비율 유지 */ + object-fit: cover; + border-radius: 8px; +`; + +const ImgTag = styled.div` + font-size: ${({ theme }) => theme.fonts.sizes.ms}; + font-weight: ${({ theme }) => theme.fonts.weights.bold}; + text-align: center; + color: ${({ theme }) => theme.colors.grey[600]}; +`; +const NoImagePlaceholder = styled.div` + width: 100%; + aspect-ratio: 1; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.colors.grey[200]}; + background-color: ${({ theme }) => theme.colors.grey[100]}; + display: flex; + align-items: center; + justify-content: center; + color: ${({ theme }) => theme.colors.grey[500]}; + font-size: ${({ theme }) => theme.fonts.sizes.sm}; +`; diff --git a/src/stores/mapStore.ts b/src/stores/mapStore.ts index 7a19f5f..4117ad8 100644 --- a/src/stores/mapStore.ts +++ b/src/stores/mapStore.ts @@ -129,7 +129,13 @@ export const useMapStore = create((set, get) => ({ const coordinates = item.location.coordinates.start.coordinates; mapInstance.panTo(new naver.maps.LatLng(coordinates[1], coordinates[0])); - set({ selectedMarkerId: selectedMarkerId === index ? null : index }); + // 화면 높이의 75%로 계산 + const targetHeight = window.innerHeight * 0.75; + + set({ + selectedMarkerId: selectedMarkerId === index ? null : index, + bottomSheetHeight: selectedMarkerId === index ? 200 : targetHeight, + }); } }, From 11b47f4b2ce5f8af1d780fcf7194521f4d5abd71 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sat, 31 May 2025 23:34:15 +0900 Subject: [PATCH 09/16] =?UTF-8?q?chore:=20=EC=83=81=EC=84=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=ED=83=AD=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/components/InfoTable.tsx | 38 +--------------------- src/styles/theme.ts | 1 + 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx index 2e111a9..25859da 100644 --- a/src/pages/MapPage/components/InfoTable.tsx +++ b/src/pages/MapPage/components/InfoTable.tsx @@ -246,42 +246,6 @@ const Line = styled.div` border-bottom: 1px dashed ${({ theme }) => theme.colors.grey[200]}; `; -// const ViewImgSection = styled.div` -// display: grid; -// grid-template-columns: repeat(4, 1fr); -// gap: 10px; -// @media ${({ theme }) => theme.device.mobile} { -// grid-template-columns: repeat(3, 1fr); -// grid-template-areas: -// 'position position position' -// 'start end overview'; - -// & > div:nth-child(1) { -// grid-area: position; -// } -// & > div:nth-child(2) { -// grid-area: start; -// } -// & > div:nth-child(3) { -// grid-area: end; -// } -// & > div:nth-child(4) { -// grid-area: overview; -// } -// } -// `; -// const ImgContainer = styled.div` -// display: flex; -// flex-direction: column; -// `; -// const Img = styled.img` -// width: 100%; -// height: 100%; -// `; -// const ImgTag = styled.div` -// font-size: ${({ theme }) => theme.fonts.sizes.ms}; -// font-weight: ${({ theme }) => theme.fonts.weights.bold}; -// `; const ViewImgSection = styled.div` display: grid; grid-template-columns: repeat(4, 1fr); @@ -325,7 +289,7 @@ const ImgTag = styled.div` font-size: ${({ theme }) => theme.fonts.sizes.ms}; font-weight: ${({ theme }) => theme.fonts.weights.bold}; text-align: center; - color: ${({ theme }) => theme.colors.grey[600]}; + color: ${({ theme }) => theme.colors.grey[800]}; `; const NoImagePlaceholder = styled.div` width: 100%; diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 74b532a..3535c81 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -12,6 +12,7 @@ export const theme = { 500: '#9e9e9e', 600: '#666666', 700: '#333333', + 800: '#252525', }, error: '#CD1A1A', grade: { From 4294877963d5775e9e6523749c97a85e3cd12ceb Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 00:31:33 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20throttled=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20height=EB=B3=80=EB=8F=99=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/BottomSheet.tsx | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/pages/MapPage/BottomSheet.tsx b/src/pages/MapPage/BottomSheet.tsx index 62fa62f..7d79ac1 100644 --- a/src/pages/MapPage/BottomSheet.tsx +++ b/src/pages/MapPage/BottomSheet.tsx @@ -1,4 +1,4 @@ -import { useRef, TouchEvent } from 'react'; +import { useRef, TouchEvent, useMemo } from 'react'; import styled from 'styled-components'; import InfoTable from './components/InfoTable'; import ListContainer from './components/ListContainer'; @@ -24,9 +24,23 @@ const BottomSheet = () => { selectedMarkerId !== null ? slopeData[selectedMarkerId] : null; const startY = useRef(0); - const currentHeight = useRef(200); //현재 높이 - const isDragging = useRef(false); //드래그 상태 - const scrollWrapperRef = useRef(null); //스크롤 Wrapper 위치 + const currentHeight = useRef(200); + const isDragging = useRef(false); + const scrollWrapperRef = useRef(null); + const rafId = useRef(null); // RAF ID 저장용 + + // throttled setHeight를 useMemo로 생성 + const throttledSetHeight = useMemo(() => { + return (newHeight: number) => { + // 이미 예약된 업데이트가 있으면 스킵 + if (rafId.current) return; + + rafId.current = requestAnimationFrame(() => { + setHeight(newHeight); + rafId.current = null; // RAF 완료 후 초기화 + }); + }; + }, [setHeight]); //데스크 용 bottomsheet 높이 변동 관련 함수 const handleMouseMove = useRef((e: globalThis.MouseEvent) => { @@ -35,7 +49,7 @@ const BottomSheet = () => { const diff = startY.current - e.clientY; let newHeight = currentHeight.current + diff; newHeight = Math.max(100, Math.min(window.innerHeight * 0.8, newHeight)); - setHeight(newHeight); + throttledSetHeight(newHeight); }).current; const handleMouseUp = useRef(() => { @@ -65,7 +79,7 @@ const BottomSheet = () => { const diff = startY.current - e.touches[0].clientY; let newHeight = currentHeight.current + diff; newHeight = Math.max(100, Math.min(window.innerHeight * 0.8, newHeight)); - setHeight(newHeight); + throttledSetHeight(newHeight); }; const handleTouchStart = (e: TouchEvent) => { From 76e752754d0a89b4e7578cadd280fcbe26e2a44d Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 00:49:13 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20=EB=B2=84=ED=8A=BC=20z-index=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/components/ButtonGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/MapPage/components/ButtonGroup.tsx b/src/pages/MapPage/components/ButtonGroup.tsx index c2e6f0e..a9fba21 100644 --- a/src/pages/MapPage/components/ButtonGroup.tsx +++ b/src/pages/MapPage/components/ButtonGroup.tsx @@ -49,7 +49,7 @@ const Container = styled.div` display: flex; flex-direction: column; gap: 10px; - z-index: 1000; + z-index: 100; `; // 버튼 기본 스타일을 공통으로 분리 From bd9ad0edb76a9ca8dde8e97e24761402cd6d60f5 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 01:45:38 +0900 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20=EB=93=B1=EA=B8=89=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EB=93=B1=EA=B8=89?= =?UTF-8?q?=EB=B3=84=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MapPage/BottomSheet.tsx | 1 + src/pages/MapPage/components/ButtonGroup.tsx | 112 +++++++++++++++++++ src/stores/mapStore.ts | 109 +++++++++++++++--- src/styles/theme.ts | 2 +- 4 files changed, 206 insertions(+), 18 deletions(-) diff --git a/src/pages/MapPage/BottomSheet.tsx b/src/pages/MapPage/BottomSheet.tsx index 7d79ac1..cd32f09 100644 --- a/src/pages/MapPage/BottomSheet.tsx +++ b/src/pages/MapPage/BottomSheet.tsx @@ -214,6 +214,7 @@ const BaseContainer = styled.div<{ $isDragging?: boolean; height: number }>` position: absolute; bottom: 0; left: 0; + z-index: 102; `; const ScrollWrapper = styled.div` diff --git a/src/pages/MapPage/components/ButtonGroup.tsx b/src/pages/MapPage/components/ButtonGroup.tsx index a9fba21..927f88f 100644 --- a/src/pages/MapPage/components/ButtonGroup.tsx +++ b/src/pages/MapPage/components/ButtonGroup.tsx @@ -1,6 +1,9 @@ import styled from 'styled-components'; +import { useState } from 'react'; import { useMapStore, MapTypeId } from '../../../stores/mapStore'; import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; const ButtonGroup = () => { const { @@ -9,6 +12,12 @@ const ButtonGroup = () => { moveToMyLocation, mapTypeId, setMapTypeId, + + isGradeDrawerOpen, + selectedGrade, + grades, + handleGradeSelect, + handleGradeButtonClick, } = useMapStore(); return ( @@ -36,6 +45,39 @@ const ButtonGroup = () => { + + + + + {selectedGrade || '등급'} + {isGradeDrawerOpen ? : } + + + + + {grades.map((grade) => ( + handleGradeSelect(grade)} + > + {grade} + + ))} + {selectedGrade && ( + handleGradeSelect('전체')} + > + 전체 + + )} + + ); }; @@ -102,3 +144,73 @@ const LocationButton = styled(BaseButton)` background-color: ${({ theme }) => theme.colors.grey[200]}; } `; + +// 등급 컨테이너 +const GradeContainer = styled.div` + position: relative; +`; + +// 등급 버튼 +const GradeButton = styled(BaseButton)<{ + $isSelect: boolean; + $isOpen: boolean; +}>` + height: 30px; + width: 100%; + font-weight: ${({ theme }) => theme.fonts.weights.bold}; + font-size: ${({ theme }) => theme.fonts.sizes.ms}; + background-color: ${({ $isSelect, theme }) => + $isSelect ? theme.colors.primaryDark : '#fff'}; + color: ${({ $isSelect, theme }) => + !$isSelect ? theme.colors.primaryDark : '#fff'}; + border-radius: ${({ $isOpen }) => ($isOpen ? '8px 8px 0 0' : '8px')}; +`; + +const GradeButtonContent = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + svg { + width: 16px; + height: 16px; + } +`; + +// 등급 드로어 +const GradeDrawer = styled.div<{ $isOpen: boolean }>` + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: #fff; + border-radius: 0 0 8px 8px; + box-shadow: ${({ theme }) => theme.shadows.sm}; + overflow: hidden; + max-height: ${({ $isOpen }) => ($isOpen ? '200px' : '0')}; + transition: max-height 0.3s ease-in-out; + z-index: 1; +`; + +// 등급 옵션 +const GradeOption = styled.button<{ $isSelected: boolean }>` + width: 100%; + padding: 8px 12px; + border: none; + background-color: ${({ $isSelected, theme }) => + $isSelected ? theme.colors.primaryLight : '#fff'}; + color: ${({ $isSelected, theme }) => + $isSelected ? theme.colors.primaryDark : '#333'}; + font-size: ${({ theme }) => theme.fonts.sizes.ms}; + cursor: pointer; + transition: background-color 0.15s ease-in-out; + + &:hover { + background-color: ${({ theme }) => theme.colors.grey[100]}; + } + + &:not(:last-child) { + border-bottom: 1px solid ${({ theme }) => theme.colors.grey[200]}; + } +`; diff --git a/src/stores/mapStore.ts b/src/stores/mapStore.ts index 4117ad8..f5686e8 100644 --- a/src/stores/mapStore.ts +++ b/src/stores/mapStore.ts @@ -15,22 +15,31 @@ export interface MapState { allTextShow: boolean; userLocation: naver.maps.LatLng | null; slopeData: Slope[]; + originalSlopeData: Slope[]; // 필터링 전 원본 데이터 searchMod: boolean; bottomSheetHeight: number; mapInstance: naver.maps.Map | null; mapTypeId: MapTypeId; - isMapReady: boolean; // 추가: 맵이 완전히 로드되었는지 확인하는 플래그 + isMapReady: boolean; + + grades: string[]; + selectedGrade: string | null; + isGradeDrawerOpen: boolean; // 액션 setSelectedMarkerId: (id: number | null) => void; setAllTextShow: (show: boolean) => void; setUserLocation: (location: naver.maps.LatLng | null) => void; setSlopeData: (data: Slope[]) => void; + setOriginalSlopeData: (data: Slope[]) => void; // 원본 데이터 설정 setSearchMod: (mod: boolean) => void; setBottomSheetHeight: (height: number) => void; setMapInstance: (map: naver.maps.Map | null) => void; setMapTypeId: (typeId: MapTypeId) => void; - setIsMapReady: (isReady: boolean) => void; // 추가: 맵 준비 상태 설정 + setIsMapReady: (isReady: boolean) => void; + + setSelectedGrade: (value: string | null) => void; + setIsGradeDrawerOpen: (value: boolean) => void; // 비즈니스 로직 fetchSlopes: () => Promise; @@ -38,6 +47,10 @@ export interface MapState { chooseSelectItem: (item: Slope, index: number) => void; moveToMyLocation: () => void; closeInfo: () => void; + + handleGradeSelect: (grade: string) => void; + handleGradeButtonClick: () => void; + applyGradeFilter: () => void; // 등급 필터 적용 함수 } export const useMapStore = create((set, get) => ({ @@ -46,27 +59,49 @@ export const useMapStore = create((set, get) => ({ allTextShow: false, userLocation: null, slopeData: [], + originalSlopeData: [], // 원본 데이터 초기화 searchMod: false, bottomSheetHeight: 200, mapInstance: null, mapTypeId: MapTypeId.NORMAL, - isMapReady: false, // 초기값: 맵 미준비 상태 + isMapReady: false, + isGradeDrawerOpen: false, + selectedGrade: null, + grades: ['A', 'B', 'C', 'D', 'E'], // 액션 setSelectedMarkerId: (id) => set({ selectedMarkerId: id }), setAllTextShow: (show) => set({ allTextShow: show }), setUserLocation: (location) => set({ userLocation: location }), setSlopeData: (data) => set({ slopeData: data }), + setOriginalSlopeData: (data) => set({ originalSlopeData: data }), // 추가 setSearchMod: (mod) => set({ searchMod: mod }), setBottomSheetHeight: (height) => set({ bottomSheetHeight: height }), setMapInstance: (map) => set({ mapInstance: map }), setMapTypeId: (typeId) => set({ mapTypeId: typeId }), setIsMapReady: (isReady) => set({ isMapReady: isReady }), + setIsGradeDrawerOpen: (value) => set({ isGradeDrawerOpen: value }), + setSelectedGrade: (value) => set({ selectedGrade: value }), + + // 등급 필터 적용 함수 추가 + applyGradeFilter: () => { + const { originalSlopeData, selectedGrade } = get(); + + if (!selectedGrade || selectedGrade === '전체') { + // 등급이 선택되지 않았거나 '전체'인 경우 원본 데이터 표시 + set({ slopeData: originalSlopeData }); + } else { + // 선택된 등급에 맞는 데이터만 필터링 + const filteredData = originalSlopeData.filter( + (slope) => slope.priority?.grade === selectedGrade + ); + set({ slopeData: filteredData }); + } + }, // 비즈니스 로직 fetchSlopes: async () => { - const { userLocation, isMapReady } = get(); - // 맵이 준비되지 않았거나 위치 정보가 없으면 중단 + const { userLocation, isMapReady, applyGradeFilter } = get(); if (!isMapReady || !userLocation?.lat() || !userLocation?.lng()) return; try { @@ -74,16 +109,32 @@ export const useMapStore = create((set, get) => ({ userLocation.lat(), userLocation.lng() ); - set({ slopeData: data || [] }); + + // 수정: 원본 데이터와 표시 데이터 모두 설정 + set({ + originalSlopeData: data || [], + slopeData: data || [], + }); + + // 등급 필터 적용 + applyGradeFilter(); } catch (error) { console.error('Error fetching slopes:', error); - set({ slopeData: [] }); + set({ + slopeData: [], + originalSlopeData: [], + }); } }, handleSearch: async (searchValue) => { - const { fetchSlopes, userLocation, mapInstance, isMapReady } = get(); - // 맵이 준비되지 않았으면 중단 + const { + fetchSlopes, + userLocation, + mapInstance, + isMapReady, + applyGradeFilter, + } = get(); if (!isMapReady) return; if (searchValue === '') { @@ -102,34 +153,40 @@ export const useMapStore = create((set, get) => ({ userLocation.lat(), userLocation.lng() ); - set({ slopeData: data || [] }); + + // 수정: 검색 결과도 원본 데이터로 저장하고 필터 적용 + set({ + originalSlopeData: data || [], + slopeData: data || [], + }); + + // 등급 필터 적용 + applyGradeFilter(); if (mapInstance && data && data.length > 0) { const coordinates = data[0].location.coordinates.start.coordinates; mapInstance.panTo( new naver.maps.LatLng(coordinates[1], coordinates[0]) ); - mapInstance.panTo( - new naver.maps.LatLng(coordinates[1], coordinates[0]) - ); } } catch (error) { console.error('Error search slopes:', error); - set({ slopeData: [] }); + set({ + slopeData: [], + originalSlopeData: [], + }); } }, chooseSelectItem: (item, index) => { const { mapInstance, selectedMarkerId, isMapReady } = get(); - // 맵이 준비되지 않았으면 중단 if (!isMapReady || !mapInstance) return; if (item) { const coordinates = item.location.coordinates.start.coordinates; mapInstance.panTo(new naver.maps.LatLng(coordinates[1], coordinates[0])); - // 화면 높이의 75%로 계산 const targetHeight = window.innerHeight * 0.75; set({ @@ -142,7 +199,6 @@ export const useMapStore = create((set, get) => ({ moveToMyLocation: () => { const { mapInstance, userLocation, fetchSlopes, isMapReady } = get(); - // 맵이 준비되지 않았으면 중단 if (!isMapReady || !mapInstance || !userLocation) return; mapInstance.setZoom(15); @@ -157,4 +213,23 @@ export const useMapStore = create((set, get) => ({ closeInfo: () => { set({ selectedMarkerId: null }); }, + + handleGradeSelect: (grade: string) => { + const { setSelectedGrade, setIsGradeDrawerOpen, applyGradeFilter } = get(); + + const selectedGradeValue = grade === '전체' ? null : grade; + + setSelectedGrade(selectedGradeValue); + setIsGradeDrawerOpen(false); + + // 등급 필터 적용 + applyGradeFilter(); + + console.log('Selected grade:', grade); + }, + + handleGradeButtonClick: () => { + const { isGradeDrawerOpen, setIsGradeDrawerOpen } = get(); + setIsGradeDrawerOpen(!isGradeDrawerOpen); + }, })); diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 3535c81..daea9d5 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -50,7 +50,7 @@ export const theme = { lg: '0 8px 16px rgba(0, 0, 0, 0.1)', }, device: { - mobile: `screen and (min-width: 355px) and (max-width:767px)`, + mobile: `screen and (max-width:767px)`, tablet: `screen and (min-width:768px) and (max-width:1023px)`, laptop: `screen and (min-width: 1024px)`, }, From 7cb6c8dd79b38a19023cd2cb2f0fc450091e9afe Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 01:46:00 +0900 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20no-unusedvar=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 7b77760..df3033b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,8 +24,6 @@ export default tseslint.config( { allowConstantExport: true }, ], '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'off', - 'no-unused-vars': 'off', 'react/prop-types': 'off', }, } From df8efb663c33993b3539cd14a976c445a39abd3f Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 01:46:16 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C=20?= =?UTF-8?q?console.log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 56 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + vite.config.ts | 9 ++++++++ 3 files changed, 66 insertions(+) diff --git a/package-lock.json b/package-lock.json index cfa0918..e370eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.14.0", + "terser": "^5.40.0", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", "vite": "^6.0.5" @@ -1101,6 +1102,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -2578,6 +2590,13 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2675,6 +2694,13 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4526,6 +4552,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stack-generator": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", @@ -4700,6 +4737,25 @@ "react": ">=17.0" } }, + "node_modules/terser": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz", + "integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/throttle-debounce": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", diff --git a/package.json b/package.json index 412d05b..eaf7658 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.14.0", + "terser": "^5.40.0", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", "vite": "^6.0.5" diff --git a/vite.config.ts b/vite.config.ts index 6da11b5..911d31a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react-swc'; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + build: { + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + }, + }, + }, }); From 63b593c0aa6744dd1ecd351e8daa5ea9ade24c5f Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 01:51:22 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20unusedVar=EB=B0=8F=20slope?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSlope/components/AddSlopeContainer.tsx | 12 ++++++------ src/pages/MapPage/components/ButtonGroup.tsx | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx b/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx index 1c37c70..0c1f863 100644 --- a/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx +++ b/src/pages/ManagePage/StepSlope/components/AddSlopeContainer.tsx @@ -107,12 +107,12 @@ const createDefaultSlope = (): Slope => { maxVerticalHeight: '', longitudinalLength: '', averageSlope: '', - images: [ - { - url: '', - createdAt: today, - }, - ], + images: { + position: { url: '', createdAt: today }, + start: { url: '', createdAt: today }, + overview: { url: '', createdAt: today }, + end: { url: '', createdAt: today }, + }, Score: '', grade: '', }, diff --git a/src/pages/MapPage/components/ButtonGroup.tsx b/src/pages/MapPage/components/ButtonGroup.tsx index 927f88f..9bebdb1 100644 --- a/src/pages/MapPage/components/ButtonGroup.tsx +++ b/src/pages/MapPage/components/ButtonGroup.tsx @@ -1,5 +1,4 @@ import styled from 'styled-components'; -import { useState } from 'react'; import { useMapStore, MapTypeId } from '../../../stores/mapStore'; import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; From d04d7e7643cacc6bb908bb0d1bc98ff4ca6a6df1 Mon Sep 17 00:00:00 2001 From: KimDoHyun Date: Sun, 1 Jun 2025 01:57:58 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20=EC=9D=B4=EC=83=81=EA=B0=92=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=EC=97=90=EC=84=9C=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=AA=A8=EB=8B=AC=20X=20=3D>=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EB=B6=80=20=EB=A0=8C=EB=8D=94=EB=A7=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSlope/components/table/DataTable.tsx | 4 +++- .../StepSlope/components/table/TableModals.tsx | 12 +++++++----- src/pages/ManagePage/interface.ts | 6 +++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx index 4c7e18f..c965554 100644 --- a/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/DataTable.tsx @@ -90,7 +90,9 @@ const DataTable: React.FC = ({ // 더블클릭한 행을 선택 상태로 설정 setSelectedRow(row.original); // ImgsModal 열기 - openImgsModal(); + if (openImgsModal) { + openImgsModal(); + } } }} $selected={ diff --git a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx index aee0e5c..a9da085 100644 --- a/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx +++ b/src/pages/ManagePage/StepSlope/components/table/TableModals.tsx @@ -45,11 +45,13 @@ const TableModals: React.FC = ({ onSubmit={handleEdit} selectedRow={selectedRow} /> - + {isImgsModalOpen !== undefined && closeImgsModal && ( + + )} ); }; diff --git a/src/pages/ManagePage/interface.ts b/src/pages/ManagePage/interface.ts index 184d458..db666ad 100644 --- a/src/pages/ManagePage/interface.ts +++ b/src/pages/ManagePage/interface.ts @@ -55,7 +55,7 @@ export interface DataTableProps { rowVirtualizer: Virtualizer; selectedRow: Slope | null; setSelectedRow: (row: Slope | null) => void; - openImgsModal: () => void; + openImgsModal?: () => void; } export interface TableActionProps { @@ -89,8 +89,8 @@ export interface TableModalProps { isEditModalOpen: boolean; closeEditModal: () => void; handleEdit: (updatedSlope: Slope) => void; - isImgsModalOpen: boolean; - closeImgsModal: () => void; + isImgsModalOpen?: boolean; + closeImgsModal?: () => void; } export interface TableToolbarProps {