From ff18a099b052ec383a0827abc01cb21198f2189e Mon Sep 17 00:00:00 2001 From: ParkGeunCheol <72205402+GC-Park@users.noreply.github.com> Date: Sat, 14 Oct 2023 03:22:43 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20Feature/#550=20=ED=86=A0=ED=94=BD=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20(#583)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 핀 이미지 삭제 기능 구현 * feat: 토픽 사진 수정 기능 예비 * refactor: 핀 이미지 삭제 시 재렌더링 안되던 문제 해결 * refactor: 핀 수정 기능 명세 연결 및 구현 * refactor: 지도 사진 수정할 때 사진만 수정되도록 변경 * refactor: 함수 이름 변경 및 지도 사진 수정 타이틀 작성 * refactor: 지도 수정 버튼 위치 이동 --- frontend/src/apis/putApi.ts | 23 +++- frontend/src/assets/remove_image_icon.svg | 9 ++ .../components/PinImageContainer/index.tsx | 85 ++++++++++---- .../components/TopicInfo/UpdatedTopicInfo.tsx | 110 +++++++++++++++++- frontend/src/pages/PinDetail.tsx | 2 +- 5 files changed, 205 insertions(+), 24 deletions(-) create mode 100644 frontend/src/assets/remove_image_icon.svg diff --git a/frontend/src/apis/putApi.ts b/frontend/src/apis/putApi.ts index bdcf46f3..e84978e7 100644 --- a/frontend/src/apis/putApi.ts +++ b/frontend/src/apis/putApi.ts @@ -4,12 +4,33 @@ import withTokenRefresh from './utils'; export const putApi = async ( url: string, - payload: {}, + payload: {} | FormData, contentType?: ContentTypeType, ) => { const data = await withTokenRefresh(async () => { const apiUrl = `${DEFAULT_PROD_URL + url}`; const userToken = localStorage.getItem('userToken'); + + if (payload instanceof FormData) { + const headers: any = {}; + + if (userToken) { + headers.Authorization = `Bearer ${userToken}`; + } + + const response = await fetch(apiUrl, { + method: 'PUT', + headers, + body: payload, + }); + + if (response.status >= 400) { + throw new Error('[SERVER] POST 요청에 실패했습니다.'); + } + + return response; + } + const headers: any = { 'content-type': 'application/json', }; diff --git a/frontend/src/assets/remove_image_icon.svg b/frontend/src/assets/remove_image_icon.svg new file mode 100644 index 00000000..f4f48825 --- /dev/null +++ b/frontend/src/assets/remove_image_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/components/PinImageContainer/index.tsx b/frontend/src/components/PinImageContainer/index.tsx index 5e25a276..fbc83065 100644 --- a/frontend/src/components/PinImageContainer/index.tsx +++ b/frontend/src/components/PinImageContainer/index.tsx @@ -2,34 +2,62 @@ import styled from 'styled-components'; import { ImageProps } from '../../types/Pin'; import Image from '../common/Image'; +import RemoveImageButton from '../../assets/remove_image_icon.svg'; +import useDelete from '../../apiHooks/useDelete'; +import useToast from '../../hooks/useToast'; interface PinImageContainerProps { images: ImageProps[]; -} + getPinData: () => void; + +}const NOT_FOUND_IMAGE = +'https://dr702blqc4x5d.cloudfront.net/2023-map-be-fine/icon/notFound_image.svg'; -const NOT_FOUND_IMAGE = - 'https://dr702blqc4x5d.cloudfront.net/2023-map-be-fine/icon/notFound_image.svg'; +const PinImageContainer = ({ images, getPinData }: PinImageContainerProps) => { + const { fetchDelete } = useDelete(); + const { showToast } = useToast(); -function PinImageContainer({ images }: PinImageContainerProps) { + const onRemovePinImage = (imageId: number) => { + const isRemoveImage = confirm('해당 이미지를 삭제하시겠습니까?'); + + if (isRemoveImage) { + fetchDelete({ + url: `/pins/images/${imageId}`, + errorMessage: '이미지 제거에 실패했습니다.', + isThrow: true, + onSuccess: () => { + showToast('info', '핀에서 이미지가 삭제 되었습니다.'); + getPinData(); + }, + }); + } + }; return ( - - {images.map( - (image, index) => - index < 3 && ( - - - - ), - )} - + <> + + {images.map( + (image, index) => + index < 3 && ( + + + onRemovePinImage(image.id)} + > + + + + ), + )} + + ); -} +}; const FilmList = styled.ul` width: 330px; @@ -38,7 +66,22 @@ const FilmList = styled.ul` `; const ImageWrapper = styled.li` + position: relative; margin-right: 10px; `; +const RemoveImageIconWrapper = styled.div` + opacity: 0.6; + position: absolute; + right: 1px; + top: 1px; + line-height: 0; + background-color: #ffffff; + cursor: pointer; + + &:hover { + opacity: 1; + } +`; + export default PinImageContainer; diff --git a/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx b/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx index 91b70f0e..930a089a 100644 --- a/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx +++ b/frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx @@ -12,6 +12,11 @@ import Button from '../common/Button'; import Flex from '../common/Flex'; import Space from '../common/Space'; import InputContainer from '../InputContainer'; +import Text from '../common/Text'; +import useCompressImage from '../../hooks/useCompressImage'; +import Image from '../common/Image'; +import { DEFAULT_TOPIC_IMAGE } from '../../constants'; +import { putApi } from '../../apis/putApi'; interface UpdatedTopicInfoProp { id: number; @@ -51,6 +56,8 @@ function UpdatedTopicInfo({ const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도 const [isAllPermissioned, setIsAllPermissioned] = useState(true); // 모두 : 지정 인원 const [authorizedMemberIds, setAuthorizedMemberIds] = useState([]); + const [changedImages, setChangedImages] = useState(null); + const { compressImage } = useCompressImage(); const updateTopicInfo = async () => { try { @@ -123,8 +130,60 @@ function UpdatedTopicInfo({ ); }, []); + const onTopicImageFileChange = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files && event.target.files[0]; + const formData = new FormData(); + + if (!file) { + showToast( + 'error', + '이미지를 선택하지 않았거나 추가하신 이미지를 찾을 수 없습니다. 다시 선택해 주세요.', + ); + return; + } + + const compressedFile = await compressImage(file); + formData.append('image', compressedFile); + + await putApi(`/topics/images/${id}`, formData); + + const updatedImageUrl = URL.createObjectURL(compressedFile); + setChangedImages(updatedImageUrl); + }; + return ( + + + 지도 사진 + + + 지도를 대표하는 사진을 변경할 수 있습니다. + + + ) => { + e.currentTarget.src = DEFAULT_TOPIC_IMAGE; + }} + /> + 수정 + + + + + theme.color.black}; + background-color: ${({ theme }) => theme.color.lightGray}; + + font-size: ${({ theme }) => theme.fontSize.extraSmall}; + font-weight: ${({ theme }) => theme.fontWeight.bold}; + text-align: center; + + border-radius: ${({ theme }) => theme.radius.small}; + cursor: pointer; + + position: absolute; + right: 5px; + bottom: -5px; + + box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px 0px; + + &:hover { + filter: brightness(0.95); + } + + @media (max-width: 372px) { + width: 40px; + height: 30px; + + font-size: 8px; + } +`; + +const ImageInputButton = styled.input` + display: none; +`; + +const TopicImage = styled(Image)` + border-radius: ${({ theme }) => theme.radius.medium}; +`; export default UpdatedTopicInfo; +function compressImage(file: File) { + throw new Error('Function not implemented.'); +} diff --git a/frontend/src/pages/PinDetail.tsx b/frontend/src/pages/PinDetail.tsx index ef5f2179..f4f43ba1 100644 --- a/frontend/src/pages/PinDetail.tsx +++ b/frontend/src/pages/PinDetail.tsx @@ -175,7 +175,7 @@ function PinDetail({ onChange={onPinImageFileChange} /> - +