-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue164 리뷰 이미지 업로드 기능 추가 #165
The head ref may contain hidden characters: "issue164-\uB9AC\uBDF0-\uC774\uBBF8\uC9C0-\uC5C5\uB85C\uB4DC-\uAE30\uB2A5-\uCD94\uAC00"
Changes from 15 commits
001df53
4a9790b
626d982
a2ae69c
bab0593
7f128d4
4f10130
be7755b
b3caf5c
66790c1
3a4c4ae
9c9058c
13b822d
00323ab
54b1c28
155b5a1
27af139
9613370
08fbe37
77f41e5
a300b20
d587de3
0cca335
c97d818
9ca5c8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { AxiosResponse } from "axios"; | ||
|
||
import { ACCESS_TOKEN, ENDPOINTS } from "constants/api"; | ||
|
||
import axiosInstance from "api/axiosInstance"; | ||
|
||
interface ImageUploadResponse { | ||
imageUrl: string; | ||
} | ||
|
||
const sendImageUploadPostRequest = async (imageFile: File) => { | ||
const accessToken = window.sessionStorage.getItem(ACCESS_TOKEN); | ||
|
||
if (!accessToken) { | ||
window.sessionStorage.removeItem(ACCESS_TOKEN); | ||
window.alert("다시 로그인 해주세요"); | ||
window.location.reload(); | ||
throw new Error("엑세스토큰이 유효하지 않습니다"); | ||
} | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const response: AxiosResponse<ImageUploadResponse> = await axiosInstance.post( | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ENDPOINTS.IMAGE_UPLOAD, | ||
imageFile, | ||
{ | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"Content-Type": "multipart/form-data", | ||
}, | ||
} | ||
); | ||
|
||
return response.data; | ||
}; | ||
|
||
export default sendImageUploadPostRequest; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import styled, { css } from "styled-components"; | ||
|
||
export const Container = styled.div` | ||
width: 100%; | ||
display: flex; | ||
flex-direction: column; | ||
gap: ${({ theme }) => theme.spacer.spacing2}; | ||
`; | ||
|
||
export const InputWrapper = styled.div` | ||
display: flex; | ||
align-items: flex-start; | ||
gap: ${({ theme }) => theme.spacer.spacing2}; | ||
`; | ||
|
||
export const Input = styled.input` | ||
display: none; | ||
`; | ||
|
||
export const UploadedImageWrapper = styled.div` | ||
position: relative; | ||
height: 10rem; | ||
`; | ||
|
||
export const UploadedImage = styled.img` | ||
width: 10rem; | ||
height: 10rem; | ||
object-fit: cover; | ||
border-radius: ${({ theme }) => theme.borderRadius.small}; | ||
`; | ||
|
||
export const DeleteButton = styled.button` | ||
position: absolute; | ||
right: 0; | ||
width: 3.6rem; | ||
height: 3.6rem; | ||
border-radius: 0; | ||
background-color: ${({ theme }) => theme.color.black}; | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
border: none; | ||
border-top-right-radius: ${({ theme }) => theme.borderRadius.small}; | ||
outline: 0; | ||
transition: all 0.2s ease-in; | ||
|
||
&:hover { | ||
background-color: ${({ theme }) => theme.color.gray800}; | ||
} | ||
|
||
& svg { | ||
width: 12px; | ||
height: 12px; | ||
|
||
& > path { | ||
stroke: ${({ theme }) => theme.color.white}; | ||
} | ||
} | ||
`; | ||
|
||
export const uploadButtonStyle = css` | ||
height: 10rem; | ||
|
||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
gap: ${({ theme }) => theme.spacer.spacing2}; | ||
|
||
font-weight: normal; | ||
|
||
transition-property: background-color, border, outline !important; | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
&.uploaded { | ||
display: none; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 왜 여기에선 props를 활용하지 않고 className을 사용했나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Button 컴포넌트를 만들어 둔 common 컴포넌트를 사용한 다음에 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음 이 부분은 생각보다 스타일을 주는 방식이 너무 섞여있는거일 수도 있겠네요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스타일 적용하는 방식을 줄이는 게 좋을 것 같아요! 블링이 말한 함수 활용도 좋네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이런 방법이 있었네요! |
||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Button } from "../Button/Button.style"; | ||
import { Label } from "../Label/Label.style"; | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import * as S from "./ImageUploadInput.style"; | ||
import { useRef } from "react"; | ||
|
||
import { CloseIcon, ImageIcon } from "asset"; | ||
|
||
interface ImageUploadInputProps { | ||
label?: string; | ||
imageUrl: string | null; | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||
onRemove: () => void; | ||
} | ||
function ImageUploadInput({ | ||
label, | ||
imageUrl, | ||
onChange, | ||
onRemove, | ||
}: ImageUploadInputProps) { | ||
const inputRef = useRef<HTMLInputElement | null>(null); | ||
|
||
const handleUploadButton = () => { | ||
if (!inputRef.current) return; | ||
|
||
inputRef.current.click(); | ||
}; | ||
|
||
return ( | ||
<S.Container> | ||
{label && <Label id="image-upload">{label}</Label>} | ||
<S.InputWrapper> | ||
<Button | ||
css={S.uploadButtonStyle} | ||
type="button" | ||
className={imageUrl ? "uploaded" : ""} | ||
onClick={handleUploadButton} | ||
> | ||
<ImageIcon /> | ||
{!imageUrl && " 이미지를 업로드해 주세요"} | ||
</Button> | ||
<S.Input | ||
type="file" | ||
accept="image/*" | ||
id="image-upload" | ||
name="image-upload" | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ref={inputRef} | ||
onChange={onChange} | ||
/> | ||
{imageUrl && ( | ||
<S.UploadedImageWrapper> | ||
<S.UploadedImage src={imageUrl} alt="리뷰 이미지" /> | ||
ashleysyheo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<S.DeleteButton | ||
type="button" | ||
aria-label="이미지 삭제" | ||
onClick={onRemove} | ||
> | ||
<CloseIcon /> | ||
</S.DeleteButton> | ||
</S.UploadedImageWrapper> | ||
)} | ||
</S.InputWrapper> | ||
</S.Container> | ||
); | ||
} | ||
export default ImageUploadInput; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
디렉토리명 > image보다 더 구체적으로 정하면 어떨까요?
review 디렉토리에 넣지 않고 image를 새로 만든 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미지 api 자체를 재사용할 수 있게 만들기 위해서 리뷰를 보낼 때 이미지 파일 데이터를 보내서 imageUrl을 생성하는 것이 아닌 별도의 api를 만들었다고 백엔드한테 들었습니다. 그런 의미에서 추후에 이 api가 리뷰가 아닌 다른 상황에서 사용될 것을 생각하면 images를 만드는 것이 좀 더 어울리지 않나 싶어서 그렇게 했습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하 그러면 한 api로 모든 종류의 이미지 업로드를 다루는건가요?? 아니면 나중에 /image/review 식으로 세부적으로 나뉘나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 이해하기로는 이 images api로 모든 종류의 이미지 업로드를 다 다룬다고 알고 있습니다! 그래서 백엔드 쪽에서도 리뷰 폼을 제출할 때 이미지 파일 데이터를 보내는 것이 아니라, 일차적으로 이미지 url을 요청한 다음에 그 url을 리뷰 보낼 때 함께 보내도록 만든 것 같아요.