Skip to content

Commit

Permalink
[FE] Feature/#550 토픽 이미지 수정 및 핀 이미지 삭제 기능 (#583)
Browse files Browse the repository at this point in the history
* feat: 핀 이미지 삭제 기능 구현

* feat: 토픽 사진 수정 기능 예비

* refactor: 핀 이미지 삭제 시 재렌더링 안되던 문제 해결

* refactor: 핀 수정 기능 명세 연결 및 구현

* refactor: 지도 사진 수정할 때 사진만 수정되도록 변경

* refactor: 함수 이름 변경 및 지도 사진 수정 타이틀 작성

* refactor: 지도 수정 버튼 위치 이동
  • Loading branch information
GC-Park authored Oct 13, 2023
1 parent df8f0cc commit ff18a09
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 24 deletions.
23 changes: 22 additions & 1 deletion frontend/src/apis/putApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/assets/remove_image_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 64 additions & 21 deletions frontend/src/components/PinImageContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FilmList>
{images.map(
(image, index) =>
index < 3 && (
<ImageWrapper>
<Image
key={image.id}
height="100px"
width="100px"
src={image.imageUrl}
$errorDefaultSrc={NOT_FOUND_IMAGE}
/>
</ImageWrapper>
),
)}
</FilmList>
<>
<FilmList>
{images.map(
(image, index) =>
index < 3 && (
<ImageWrapper>
<Image
key={image.id}
height="100px"
width="100px"
src={image.imageUrl}
$errorDefaultSrc={NOT_FOUND_IMAGE}
/>
<RemoveImageIconWrapper
onClick={() => onRemovePinImage(image.id)}
>
<RemoveImageButton />
</RemoveImageIconWrapper>
</ImageWrapper>
),
)}
</FilmList>
</>
);
}
};

const FilmList = styled.ul`
width: 330px;
Expand All @@ -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;
110 changes: 109 additions & 1 deletion frontend/src/components/TopicInfo/UpdatedTopicInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,6 +56,8 @@ function UpdatedTopicInfo({
const [isPrivate, setIsPrivate] = useState(false); // 혼자 볼 지도 : 같이 볼 지도
const [isAllPermissioned, setIsAllPermissioned] = useState(true); // 모두 : 지정 인원
const [authorizedMemberIds, setAuthorizedMemberIds] = useState<number[]>([]);
const [changedImages, setChangedImages] = useState<string | null>(null);
const { compressImage } = useCompressImage();

const updateTopicInfo = async () => {
try {
Expand Down Expand Up @@ -123,8 +130,60 @@ function UpdatedTopicInfo({
);
}, []);

const onTopicImageFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
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 (
<Wrapper>
<ImageWrapper>
<Text color="black" $fontSize="default" $fontWeight="normal">
지도 사진
</Text>
<Text color="gray" $fontSize="small" $fontWeight="normal">
지도를 대표하는 사진을 변경할 수 있습니다.
</Text>
<Space size={0} />
<TopicImage
height="168px"
width="100%"
src={changedImages ? changedImages : image}
alt="사진 이미지"
$objectFit="cover"
onError={(e: React.SyntheticEvent<HTMLImageElement, Event>) => {
e.currentTarget.src = DEFAULT_TOPIC_IMAGE;
}}
/>
<ImageInputLabel htmlFor="file">수정</ImageInputLabel>
<ImageInputButton
id="file"
type="file"
name="image"
onChange={onTopicImageFileChange}
/>
</ImageWrapper>

<Space size={5} />

<InputContainer
tagType="input"
containerTitle="지도 이름"
Expand Down Expand Up @@ -185,6 +244,55 @@ function UpdatedTopicInfo({
);
}

const Wrapper = styled.section``;
const Wrapper = styled.article``;

const ImageWrapper = styled.div`
position: relative;
`;

const ImageInputLabel = styled.label`
width: 60px;
height: 35px;
margin-bottom: 10px;
padding: 10px 10px;
color: ${({ theme }) => 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.');
}
2 changes: 1 addition & 1 deletion frontend/src/pages/PinDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ function PinDetail({
onChange={onPinImageFileChange}
/>

<PinImageContainer images={pin.images} />
<PinImageContainer images={pin.images} getPinData={getPinData} />

<Space size={6} />

Expand Down

0 comments on commit ff18a09

Please sign in to comment.