feat: Add image upload feature

This commit is contained in:
jhynsoo 2023-11-13 19:52:04 +09:00
parent abac444027
commit 4f9d9269f5
15 changed files with 260 additions and 111 deletions

View File

@ -31,6 +31,7 @@ export const URL = {
myPage: '/user/mypage/', myPage: '/user/mypage/',
myPost: '/mypost/', myPost: '/mypost/',
password: '/user/password/', password: '/user/password/',
deleteAccount: '/user/deleteMessage/',
user: '/user/', user: '/user/',
brand: '/brand/', brand: '/brand/',
product: '/product/', product: '/product/',

View File

@ -5,7 +5,13 @@ import { ThemeProvider } from 'styled-components';
import theme from './styles/Themes.styles'; import theme from './styles/Themes.styles';
import { OverlayProvider } from '@toss/use-overlay'; import { OverlayProvider } from '@toss/use-overlay';
const queryClient = new QueryClient(); const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
});
const App = () => { const App = () => {
return ( return (

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 960 960" width="40"><path d="M308.283-93.667q-22.758 0-38.854-16.095-16.096-16.096-16.096-38.823v-662.83q0-22.727 16.096-38.823 16.096-16.095 38.854-16.095h343.434q22.758 0 38.854 16.095 16.096 16.096 16.096 38.823v662.83q0 22.727-16.096 38.823-16.096 16.095-38.854 16.095H308.283ZM276.5-204.333h407v-551.334h-407v551.334Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 960 960" width="40"><style>path{fill:#333;}</style><path d="M308.283-93.667q-22.758 0-38.854-16.095-16.096-16.096-16.096-38.823v-662.83q0-22.727 16.096-38.823 16.096-16.095 38.854-16.095h343.434q22.758 0 38.854 16.095 16.096 16.096 16.096 38.823v662.83q0 22.727-16.096 38.823-16.096 16.095-38.854 16.095H308.283ZM276.5-204.333h407v-551.334h-407v551.334Z"/></svg>

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 431 B

View File

@ -1,6 +1,6 @@
import * as S from '../../styles/CheckBox.styles'; import * as S from '../../styles/CheckBox.styles';
function CheckBox({ name, id, text, checked, setChecked }) { function CheckBox({ name, id, text, checked, setChecked, readOnly = false }) {
return ( return (
<S.CheckBox htmlFor={id}> <S.CheckBox htmlFor={id}>
<S.CheckInput <S.CheckInput
@ -8,7 +8,8 @@ function CheckBox({ name, id, text, checked, setChecked }) {
name={name} name={name}
id={id} id={id}
checked={checked} checked={checked}
onChange={() => setChecked(!checked)} disabled={readOnly}
onChange={(e) => setChecked(e.target.checked)}
/> />
<S.Info> <S.Info>
<S.Name>{name}</S.Name> <S.Name>{name}</S.Name>

View File

@ -0,0 +1,76 @@
import { useCallback } from 'react';
import * as S from '../../styles/ItemIssues.styles';
import CheckBox from '../CheckBox';
function ItemIssues({ readOnly, itemIssues, setItemIssues }) {
const setChecked = useCallback(
(key) =>
readOnly
? () => {}
: (value) => setItemIssues((prev) => ({ ...prev, [key]: value })),
[readOnly, setItemIssues]
);
return (
<S.Grid>
<CheckBox
id="화면 깨짐"
name="화면 깨짐"
text="화면이 깨져있어요"
checked={itemIssues.display}
setChecked={setChecked('display')}
readOnly={readOnly}
/>
<CheckBox
id="뒷면/옆면 파손"
name="뒷면/옆면 파손"
text="화면이 아닌 부분에 파손이 있어요"
checked={itemIssues.frame}
setChecked={setChecked('frame')}
readOnly={readOnly}
/>
<CheckBox
id="버튼 고장"
name="버튼 고장"
text="고장난 버튼이 있어요"
checked={itemIssues.button}
setChecked={setChecked('button')}
readOnly={readOnly}
/>
<CheckBox
id="생체 인식 고장"
name="생체 인식 고장"
text="지문이나 얼굴인식이 작동하지 않아요"
checked={itemIssues.biometric}
setChecked={setChecked('biometric')}
readOnly={readOnly}
/>
<CheckBox
id="카메라 고장"
name="카메라 고장"
text="카메라가 작동하지 않아요"
checked={itemIssues.camera}
setChecked={setChecked('camera')}
readOnly={readOnly}
/>
<CheckBox
id="스피커 고장"
name="스피커 고장"
text="스피커가 작동하지 않아요"
checked={itemIssues.speaker}
setChecked={setChecked('speaker')}
readOnly={readOnly}
/>
<CheckBox
id="기타 고장"
name="기타 고장"
text="그 밖에 고장난 부분이 있어요"
checked={itemIssues.others}
setChecked={setChecked('others')}
readOnly={readOnly}
/>
</S.Grid>
);
}
export default ItemIssues;

View File

@ -1,5 +1,5 @@
import { API, URL } from '../../API'; import { API, URL } from '../../API';
import { useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
const getPosts = async ({ my, page }) => { const getPosts = async ({ my, page }) => {
const pageString = page ? `?page=${page}` : ''; const pageString = page ? `?page=${page}` : '';
@ -30,3 +30,17 @@ export const usePost = (id) => {
return data; return data;
}; };
const addPost = async (data) => {
const { data: res } = await API.post(URL.post, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return res;
};
export const useAddPost = (formData) => {
const data = useMutation(() => addPost(formData));
return data;
};

View File

@ -14,6 +14,8 @@ import Button from '../../components/Button';
import { useOverlay } from '@toss/use-overlay'; import { useOverlay } from '@toss/use-overlay';
import { Suspense } from 'react'; import { Suspense } from 'react';
import CustomDialog from '../../components/CustomDialog'; import CustomDialog from '../../components/CustomDialog';
import ItemIssues from '../../components/ItemIssues';
import Spacer from '../../components/Spacer';
const PostDetail = () => { const PostDetail = () => {
const overlay = useOverlay(); const overlay = useOverlay();
@ -45,12 +47,17 @@ const PostDetail = () => {
<PanelLayout> <PanelLayout>
<LeftPanel> <LeftPanel>
<S.Text>{data.text}</S.Text> <S.Text>{data.text}</S.Text>
<ItemIssues
itemIssues={data.item_issues}
readOnly
/>
</LeftPanel> </LeftPanel>
<RightPanel> <RightPanel>
<SubTitle>제품 사진</SubTitle> <SubTitle>제품 사진</SubTitle>
<ImageViewer images={data?.images?.map((image) => image.image)} /> <ImageViewer images={data?.images?.map((image) => image.image)} />
</RightPanel> </RightPanel>
</PanelLayout> </PanelLayout>
<Spacer height={48} />
<ButtonArea> <ButtonArea>
<S.Price>가격 {data.price.toLocaleString()}</S.Price> <S.Price>가격 {data.price.toLocaleString()}</S.Price>
<Button onClick={openOverlay}>최근 가격 보기</Button> <Button onClick={openOverlay}>최근 가격 보기</Button>

View File

@ -0,0 +1,32 @@
import Button from '../../components/Button';
import ButtonArea from '../../components/ButtonArea';
import ItemIssues from '../../components/ItemIssues';
import * as S from '../../styles/WriteSteps.styles';
function ItemIssuesStep({
gotoNextStep,
gotoPrevStep,
itemIssues,
setItemIssues,
}) {
return (
<S.Step>
<S.Title>휴대폰 상태에 대해 알려주세요</S.Title>
<ItemIssues
itemIssues={itemIssues}
setItemIssues={setItemIssues}
/>
<ButtonArea>
<Button
secondary
onClick={gotoPrevStep}
>
이전
</Button>
<Button onClick={gotoNextStep}>다음</Button>
</ButtonArea>
</S.Step>
);
}
export default ItemIssuesStep;

View File

@ -1,3 +1,4 @@
import { useState } from 'react';
import Button from '../../components/Button'; import Button from '../../components/Button';
import ButtonArea from '../../components/ButtonArea'; import ButtonArea from '../../components/ButtonArea';
import ImageInput from '../../components/ImageInput'; import ImageInput from '../../components/ImageInput';
@ -12,8 +13,11 @@ function PhotoStep({ gotoNextStep, gotoPrevStep, photos, setPhotos }) {
Array.from(files).forEach((file) => Array.from(files).forEach((file) =>
newPhotos.push(URL.createObjectURL(file)) newPhotos.push(URL.createObjectURL(file))
); );
setPhotos(newPhotos); setPhotos(files);
setPreviewPhotos(newPhotos);
}; };
const [previewPhotos, setPreviewPhotos] = useState([]);
return ( return (
<S.Step> <S.Step>
<S.Title>휴대폰 사진을 올려주세요</S.Title> <S.Title>휴대폰 사진을 올려주세요</S.Title>
@ -23,7 +27,7 @@ function PhotoStep({ gotoNextStep, gotoPrevStep, photos, setPhotos }) {
onChange={handleChange} onChange={handleChange}
/> />
<Spacer height={32} /> <Spacer height={32} />
{photos.length > 0 && <ImageViewer images={photos} />} {photos.length > 0 && <ImageViewer images={previewPhotos} />}
<ButtonArea> <ButtonArea>
<Button <Button
secondary secondary

View File

@ -0,0 +1,50 @@
import { useEffect } from 'react';
import { getCookie } from '../../API';
import { useAddPost } from '../../hooks/network/post';
import * as S from '../../styles/WriteSteps.styles';
import { useNavigate } from 'react-router-dom';
import { Center } from '../../styles/Center.styles';
function PostCreateStep({ formData }) {
const navigate = useNavigate();
const csrfToken = getCookie('csrftoken');
const formDataWithCSRF = new FormData();
formDataWithCSRF.append('product', formData.product);
formDataWithCSRF.append('price', formData.price);
formDataWithCSRF.append('text', formData.text);
formDataWithCSRF.append('csrfmiddlewaretoken', csrfToken);
Object.keys(formData.item_issues).forEach((itemIssueKey) => {
formDataWithCSRF.append(
`item_issues.${itemIssueKey}`,
formData.item_issues[itemIssueKey]
);
});
Array.from(formData.photos).forEach((photo) => {
formDataWithCSRF.append('photos', photo);
});
const { mutate, isSuccess } = useAddPost(formDataWithCSRF);
useEffect(() => {
mutate();
}, [mutate]);
useEffect(() => {
if (isSuccess) {
setTimeout(() => {
navigate('/', { replace: true });
}, 3000);
}
}, [isSuccess, navigate]);
return (
<S.Step>
<Center>
<S.Title>작성한 판매글을 올렸어요</S.Title>
</Center>
</S.Step>
);
}
export default PostCreateStep;

View File

@ -9,14 +9,14 @@ import * as S from '../../styles/WriteSteps.styles';
function PriceStep({ modelId, gotoNextStep, gotoPrevStep, price, setPrice }) { function PriceStep({ modelId, gotoNextStep, gotoPrevStep, price, setPrice }) {
return ( return (
<S.Step> <S.Step>
<S.Title>얼마에 팔고 싶나요?</S.Title> <S.Title>판매할 가격을 입력해 주세요</S.Title>
<CurrencyInput <CurrencyInput
value={price} value={price}
setValue={setPrice} setValue={setPrice}
autofocus autofocus
/> />
<Spacer height={20} /> <Spacer height={20} />
<SubTitle>최근 판매 가격</SubTitle> <SubTitle>최근 판매 가격다음과 같아요</SubTitle>
<ProductPriceGraph id={modelId} /> <ProductPriceGraph id={modelId} />
<ButtonArea> <ButtonArea>
<Button <Button

View File

@ -1,88 +0,0 @@
import Button from '../../components/Button';
import ButtonArea from '../../components/ButtonArea';
import CheckBox from '../../components/CheckBox';
import * as S from '../../styles/WriteSteps.styles';
function StatusStep({ gotoNextStep, gotoPrevStep, status, setStatus }) {
return (
<S.Step>
<S.Title>휴대폰 상태에 대해 알려주세요</S.Title>
<S.Grid>
<CheckBox
id="화면 깨짐"
name="화면 깨짐"
text="화면이 깨져있어요"
value={status.display}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, display: value }))
}
/>
<CheckBox
id="뒷면/옆면 파손"
name="뒷면/옆면 파손"
text="화면이 아닌 부분에 파손이 있어요"
value={status.frame}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, frame: value }))
}
/>
<CheckBox
id="버튼 고장"
name="버튼 고장"
text="고장난 버튼이 있어요"
value={status.button}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, button: value }))
}
/>
<CheckBox
id="생체 인식 고장"
name="생체 인식 고장"
text="지문이나 얼굴인식이 작동하지 않아요"
value={status.biometrics}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, biometrics: value }))
}
/>
<CheckBox
id="카메라 고장"
name="카메라 고장"
text="카메라가 작동하지 않아요"
value={status.camera}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, camera: value }))
}
/>
<CheckBox
id="스피커 고장"
name="스피커 고장"
text="스피커가 작동하지 않아요"
value={status.speaker}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, speaker: value }))
}
/>
<CheckBox
id="기타 고장"
name="기타 고장"
text="그 밖에 고장난 부분이 있어요"
value={status.others}
setChecked={(value) =>
setStatus((prev) => ({ ...prev, others: value }))
}
/>
</S.Grid>
<ButtonArea>
<Button
secondary
onClick={gotoPrevStep}
>
이전
</Button>
<Button onClick={gotoNextStep}>다음</Button>
</ButtonArea>
</S.Step>
);
}
export default StatusStep;

View File

@ -0,0 +1,14 @@
import { Center } from '../../styles/Center.styles';
import * as S from '../../styles/WriteSteps.styles';
function WaitingCreation() {
return (
<S.Step>
<Center>
<S.Title>작성한 판매글을 올리는 중이에요</S.Title>
</Center>
</S.Step>
);
}
export default WaitingCreation;

View File

@ -1,18 +1,20 @@
import { useState } from 'react'; import { Suspense, useState } from 'react';
import ProductSelectStep from './ProductSelectStep'; import ProductSelectStep from './ProductSelectStep';
import StatusStep from './StatusStep'; import ItemIssuesStep from './ItemIssuesStep';
import PhotoStep from './PhotoStep'; import PhotoStep from './PhotoStep';
import PriceStep from './PriceStep'; import PriceStep from './PriceStep';
import TextStep from './TextStep'; import TextStep from './TextStep';
import PostCreateStep from './PostCreateStep';
import WaitingCreation from './WaitingCreation';
function WriteSteps() { function WriteSteps() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
product: '', product: '',
status: { item_issues: {
display: false, display: false,
frame: false, frame: false,
button: false, button: false,
biometrics: false, biometric: false,
camera: false, camera: false,
speaker: false, speaker: false,
others: false, others: false,
@ -21,12 +23,26 @@ function WriteSteps() {
photos: [], photos: [],
text: '', text: '',
}); });
const STEP_INFO = ['product', 'status', 'price', 'photo', 'text', 'step6']; const STEP_INFO = [
'product',
'itemIssues',
'price',
'photo',
'text',
'postCreate',
];
const setProduct = (product) => { const setProduct = (product) => {
setFormData((prev) => ({ ...prev, product })); setFormData((prev) => ({ ...prev, product }));
}; };
const setStatus = (status) => { const setItemIssues = (itemIssues) => {
setFormData((prev) => ({ ...prev, status })); if (typeof itemIssues === 'function') {
setFormData((prev) => ({
...prev,
item_issues: itemIssues(prev.item_issues),
}));
return;
}
setFormData((prev) => ({ ...prev, itemIssues }));
}; };
const setPrice = (price) => { const setPrice = (price) => {
setFormData((prev) => ({ ...prev, price })); setFormData((prev) => ({ ...prev, price }));
@ -43,15 +59,15 @@ function WriteSteps() {
<> <>
{step === 'product' && ( {step === 'product' && (
<ProductSelectStep <ProductSelectStep
gotoNextStep={() => setStep('status')} gotoNextStep={() => setStep('itemIssues')}
product={formData.product} product={formData.product}
setProduct={setProduct} setProduct={setProduct}
/> />
)} )}
{step === 'status' && ( {step === 'itemIssues' && (
<StatusStep <ItemIssuesStep
status={formData.status} itemIssues={formData.item_issues}
setStatus={setStatus} setItemIssues={setItemIssues}
gotoNextStep={() => setStep('price')} gotoNextStep={() => setStep('price')}
gotoPrevStep={() => setStep('product')} gotoPrevStep={() => setStep('product')}
/> />
@ -62,7 +78,7 @@ function WriteSteps() {
price={formData.price} price={formData.price}
setPrice={setPrice} setPrice={setPrice}
gotoNextStep={() => setStep('photo')} gotoNextStep={() => setStep('photo')}
gotoPrevStep={() => setStep('status')} gotoPrevStep={() => setStep('itemIssues')}
/> />
)} )}
{step === 'photo' && ( {step === 'photo' && (
@ -77,10 +93,15 @@ function WriteSteps() {
<TextStep <TextStep
text={formData.text} text={formData.text}
setText={setText} setText={setText}
gotoNextStep={() => setStep('step6')} gotoNextStep={() => setStep('postCreate')}
gotoPrevStep={() => setStep('photo')} gotoPrevStep={() => setStep('photo')}
/> />
)} )}
{step === 'postCreate' && (
<Suspense fallback={<WaitingCreation />}>
<PostCreateStep formData={formData} />
</Suspense>
)}
</> </>
); );
} }

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
export const Grid = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
@media screen and (max-width: 768px) {
grid-template-columns: repeat(1, 1fr);
}
`;