Skip to content

Commit

Permalink
[FE] Feat/#386 지도, 핀 이미지에 s3 적용 (#409)
Browse files Browse the repository at this point in the history
* feat: 지도 추가 때 필요한 이미지 s3 적용

* feat: pin 생성에서 이미지 s3 적용

* feat: 핀 디테일 페이지에서 핀 사진 추가 구현

* refator: prettier 적용

* refactor: 잘못된 경로 수정

* refactor: 필요없는 console.log 삭제

* refactor: event handler method명 컨벤션에 맞게 수정

* refactor: 변경이 필요한 컴포넌트, 변수 이름 수정

* refactor: alt 및 error 처리 시 알림 메시지 사용자가 이해하기 쉽도록 수정

* refactor: 인라인 스타일 태그 styled component로 수정

* refactor: 지도 만들 때 기본 이미지 설정 해주도록 수정

* fix: 핀 이미지 추가할 때 무한 렌더링 에러 해결

* refactor: type 이름 컨벤션에 맞게 수정

* refactor: 필요없는 type 삭제

* refactor: postApi와 postFormApi 합치기 및 필요없는 코드 삭제

* fix: 핀 추가할 때 이미지 여러개 등록하면 하나만 등록되는 에러 해결

* refactor: 지도, 핀 추가 시 이미지 필수 아니도록 설정
  • Loading branch information
GC-Park authored Sep 19, 2023
1 parent 592eba0 commit c82e138
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 72 deletions.
10 changes: 0 additions & 10 deletions frontend/src/apis/getApi.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
// const API_URL =
// process.env.NODE_ENV === 'production'
// ? process.env.REACT_APP_API_DEFAULT_PROD
// : process.env.REACT_APP_API_DEFAULT_DEV;

import { DEFAULT_PROD_URL } from '../constants';

const API_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: DEFAULT_PROD_URL;

interface Headers {
'content-type': string;
[key: string]: string;
Expand Down
34 changes: 26 additions & 8 deletions frontend/src/apis/postApi.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
// const API_URL =
// process.env.NODE_ENV === 'production'
// ? process.env.REACT_APP_API_DEFAULT_PROD
// : process.env.REACT_APP_API_DEFAULT_DEV;

import { DEFAULT_PROD_URL } from '../constants';
import { ContentTypeType } from '../types/Api';

const API_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : DEFAULT_PROD_URL

interface Headers {
'content-type': string;
[key: string]: string;
}

interface HeadersForm {
[key: string]: string;
}

export const postApi = async (
url: string,
payload: {},
payload: {} | FormData,
contentType?: ContentTypeType,
) => {
const apiUrl = `${DEFAULT_PROD_URL + url}`;
const userToken = localStorage.getItem('userToken');

if (payload instanceof FormData) {
const headers: HeadersForm = {};

if (userToken) {
headers['Authorization'] = `Bearer ${userToken}`;
}

const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: payload,
});

if (response.status >= 400) {
throw new Error('[SERVER] POST 요청에 실패했습니다.');
}

return response;
}

const headers: Headers = {
'content-type': 'application/json',
};
Expand Down
8 changes: 0 additions & 8 deletions frontend/src/apis/putApi.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
// const API_URL =
// process.env.NODE_ENV === 'production'
// ? process.env.REACT_APP_API_DEFAULT_PROD
// : process.env.REACT_APP_API_DEFAULT_DEV;

import { DEFAULT_PROD_URL } from '../constants';
import { ContentTypeType } from '../types/Api';

const API_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : DEFAULT_PROD_URL

interface Headers {
'content-type': string;
[key: string]: string;
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/components/PinImageContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import styled from 'styled-components';
import { ImageProps } from '../../types/Pin';
import Image from '../common/Image';

interface PinImageContainerProps {
images: ImageProps[];
}

const PinImageContainer = ({ images }: PinImageContainerProps) => {
return (
<>
<FilmList>
{images.map((image, index) => (
<ImageWrapper>
<Image
key={index}
height="100px"
width="100px"
src={image.imageUrl}
/>
</ImageWrapper>
))}
</FilmList>
</>
);
};

const FilmList = styled.ul`
width: 330px;
display: flex;
flex-direction: row;
overflow: hidden;
`;

const ImageWrapper = styled.li`
margin-right: 10px;
`;

export default PinImageContainer;
2 changes: 1 addition & 1 deletion frontend/src/components/TopicCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const TopicCard = ({
height="138px"
width="138px"
src={image}
alt="토픽 이미지"
alt="사진 이미지"
$objectFit="cover"
onError={(e: SyntheticEvent<HTMLImageElement, Event>) => {
e.currentTarget.src = DEFAULT_TOPIC_IMAGE;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/TopicInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const TopicInfo = ({
height="168px"
width="100%"
src={topicImage}
alt="토픽 이미지"
alt="사진 이미지"
$objectFit="cover"
onError={(e: React.SyntheticEvent<HTMLImageElement, Event>) => {
e.currentTarget.src = DEFAULT_TOPIC_IMAGE;
Expand Down
104 changes: 98 additions & 6 deletions frontend/src/pages/NewPin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FormEvent, useContext, useEffect, useState } from 'react';
import { getApi } from '../apis/getApi';
import { TopicCardProps } from '../types/Topic';
import useNavigator from '../hooks/useNavigator';
import { NewPinFormProps } from '../types/tmap';
import { NewPinFormProps } from '../types/FormValues';
import useFormValues from '../hooks/useFormValues';
import { MarkerContext } from '../context/MarkerContext';
import { CoordinatesContext } from '../context/CoordinatesContext';
Expand All @@ -35,6 +35,7 @@ const NewPin = () => {
const { navbarHighlights: _ } = useSetNavbarHighlight('addMapOrPin');
const [topic, setTopic] = useState<any>(null);
const [selectedTopic, setSelectedTopic] = useState<any>(null);
const [showedImages, setShowedImages] = useState<string[]>([]);
const { clickedMarker } = useContext(MarkerContext);
const { clickedCoordinate, setClickedCoordinate } =
useContext(CoordinatesContext);
Expand All @@ -49,6 +50,8 @@ const NewPin = () => {
const { width } = useSetLayoutWidth(SIDEBAR);
const { openModal, closeModal } = useContext(ModalContext);

const [formImages, setFormImages] = useState<File[]>([]);

const goToBack = () => {
routePage(-1);
};
Expand All @@ -57,6 +60,8 @@ const NewPin = () => {
let postTopicId = topic?.id;
let postName = formValues.name;

const formData = new FormData();

if (!topic) {
//토픽이 없으면 selectedTopic을 통해 토픽을 생성한다.
postTopicId = selectedTopic?.topicId;
Expand All @@ -66,16 +71,26 @@ const NewPin = () => {
postTopicId = selectedTopic.topicId;
}

await postApi('/pins', {
formImages.forEach((file) => {
formData.append('images', file);
});

const objectData = {
topicId: postTopicId,
name: postName,
address: clickedCoordinate.address,
description: formValues.description,
latitude: clickedCoordinate.latitude,
longitude: clickedCoordinate.longitude,
legalDongCode: '',
images: [],
});
};

const data = JSON.stringify(objectData);
const jsonBlob = new Blob([data], { type: 'application/json' });

formData.append('request', jsonBlob);

await postApi('/pins', formData);
};

const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
Expand All @@ -102,8 +117,8 @@ const NewPin = () => {
}

setClickedCoordinate({
latitude: '',
longitude: '',
latitude: 0,
longitude: 0,
address: '',
});

Expand Down Expand Up @@ -162,6 +177,36 @@ const NewPin = () => {
});
};

const onPinImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const imageLists = event.target.files;
let imageUrlLists = [...showedImages];

if (!imageLists) {
showToast(
'error',
'추가하신 이미지를 찾을 수 없습니다. 다시 선택해 주세요.',
);
return;
}

for (let i = 0; i < imageLists.length; i++) {
const currentImageUrl = URL.createObjectURL(imageLists[i]);
imageUrlLists.push(currentImageUrl);
}

if (imageUrlLists.length > 8) {
showToast(
'info',
'이미지 개수는 최대 8개까지만 선택 가능합니다. 다시 선택해 주세요.',
);
imageUrlLists = imageUrlLists.slice(0, 8);
return;
}

setFormImages([...formImages, ...imageLists]);
setShowedImages(imageUrlLists);
};

useEffect(() => {
const getTopicId = async () => {
if (topicId && topicId.split(',').length === 1) {
Expand Down Expand Up @@ -221,6 +266,32 @@ const NewPin = () => {

<Space size={5} />

<Text color="black" $fontSize="default" $fontWeight="normal">
사진 선택
</Text>
<Space size={0} />
<Flex>
<ImageInputLabel htmlFor="file">파일찾기</ImageInputLabel>
<ImageInputButton
id="file"
type="file"
name="images"
onChange={onPinImageChange}
multiple
/>
</Flex>
<Space size={0} />
<Flex $flexDirection="row" $flexWrap="wrap">
{showedImages.map((image, id) => (
<div key={id}>
<ShowImage src={image} alt={`${image}-${id}`} />
<Space size={0} />
</div>
))}
</Flex>

<Space size={5} />

<InputContainer
tagType="input"
containerTitle="장소 이름"
Expand Down Expand Up @@ -337,4 +408,25 @@ const ModalContentsWrapper = styled.div`
overflow: scroll;
`;

const ImageInputLabel = styled.label`
height: 40px;
margin-left: 10px;
padding: 10px 10px;
color: ${({ theme }) => theme.color.black};
background-color: ${({ theme }) => theme.color.lightGray};
font-size: ${({ theme }) => theme.fontSize.extraSmall};
cursor: pointer;
`;

const ShowImage = styled.img`
width: 80px;
height: 80px;
`;

const ImageInputButton = styled.input`
display: none;
`;

export default NewPin;
Loading

0 comments on commit c82e138

Please sign in to comment.