feat: Add image upload feature
This commit is contained in:
parent
abac444027
commit
4f9d9269f5
@ -31,6 +31,7 @@ export const URL = {
|
||||
myPage: '/user/mypage/',
|
||||
myPost: '/mypost/',
|
||||
password: '/user/password/',
|
||||
deleteAccount: '/user/deleteMessage/',
|
||||
user: '/user/',
|
||||
brand: '/brand/',
|
||||
product: '/product/',
|
||||
|
@ -5,7 +5,13 @@ import { ThemeProvider } from 'styled-components';
|
||||
import theme from './styles/Themes.styles';
|
||||
import { OverlayProvider } from '@toss/use-overlay';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
suspense: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
|
@ -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 |
@ -1,6 +1,6 @@
|
||||
import * as S from '../../styles/CheckBox.styles';
|
||||
|
||||
function CheckBox({ name, id, text, checked, setChecked }) {
|
||||
function CheckBox({ name, id, text, checked, setChecked, readOnly = false }) {
|
||||
return (
|
||||
<S.CheckBox htmlFor={id}>
|
||||
<S.CheckInput
|
||||
@ -8,7 +8,8 @@ function CheckBox({ name, id, text, checked, setChecked }) {
|
||||
name={name}
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={() => setChecked(!checked)}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => setChecked(e.target.checked)}
|
||||
/>
|
||||
<S.Info>
|
||||
<S.Name>{name}</S.Name>
|
||||
|
76
src/components/ItemIssues/index.jsx
Normal file
76
src/components/ItemIssues/index.jsx
Normal 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;
|
@ -1,5 +1,5 @@
|
||||
import { API, URL } from '../../API';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
const getPosts = async ({ my, page }) => {
|
||||
const pageString = page ? `?page=${page}` : '';
|
||||
@ -30,3 +30,17 @@ export const usePost = (id) => {
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -14,6 +14,8 @@ import Button from '../../components/Button';
|
||||
import { useOverlay } from '@toss/use-overlay';
|
||||
import { Suspense } from 'react';
|
||||
import CustomDialog from '../../components/CustomDialog';
|
||||
import ItemIssues from '../../components/ItemIssues';
|
||||
import Spacer from '../../components/Spacer';
|
||||
|
||||
const PostDetail = () => {
|
||||
const overlay = useOverlay();
|
||||
@ -45,12 +47,17 @@ const PostDetail = () => {
|
||||
<PanelLayout>
|
||||
<LeftPanel>
|
||||
<S.Text>{data.text}</S.Text>
|
||||
<ItemIssues
|
||||
itemIssues={data.item_issues}
|
||||
readOnly
|
||||
/>
|
||||
</LeftPanel>
|
||||
<RightPanel>
|
||||
<SubTitle>제품 사진</SubTitle>
|
||||
<ImageViewer images={data?.images?.map((image) => image.image)} />
|
||||
</RightPanel>
|
||||
</PanelLayout>
|
||||
<Spacer height={48} />
|
||||
<ButtonArea>
|
||||
<S.Price>가격 {data.price.toLocaleString()}원</S.Price>
|
||||
<Button onClick={openOverlay}>최근 가격 보기</Button>
|
||||
|
32
src/pages/WriteSteps/ItemIssuesStep.jsx
Normal file
32
src/pages/WriteSteps/ItemIssuesStep.jsx
Normal 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;
|
@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import Button from '../../components/Button';
|
||||
import ButtonArea from '../../components/ButtonArea';
|
||||
import ImageInput from '../../components/ImageInput';
|
||||
@ -12,8 +13,11 @@ function PhotoStep({ gotoNextStep, gotoPrevStep, photos, setPhotos }) {
|
||||
Array.from(files).forEach((file) =>
|
||||
newPhotos.push(URL.createObjectURL(file))
|
||||
);
|
||||
setPhotos(newPhotos);
|
||||
setPhotos(files);
|
||||
setPreviewPhotos(newPhotos);
|
||||
};
|
||||
const [previewPhotos, setPreviewPhotos] = useState([]);
|
||||
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>휴대폰 사진을 올려주세요</S.Title>
|
||||
@ -23,7 +27,7 @@ function PhotoStep({ gotoNextStep, gotoPrevStep, photos, setPhotos }) {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Spacer height={32} />
|
||||
{photos.length > 0 && <ImageViewer images={photos} />}
|
||||
{photos.length > 0 && <ImageViewer images={previewPhotos} />}
|
||||
<ButtonArea>
|
||||
<Button
|
||||
secondary
|
||||
|
50
src/pages/WriteSteps/PostCreateStep.jsx
Normal file
50
src/pages/WriteSteps/PostCreateStep.jsx
Normal 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;
|
@ -9,14 +9,14 @@ import * as S from '../../styles/WriteSteps.styles';
|
||||
function PriceStep({ modelId, gotoNextStep, gotoPrevStep, price, setPrice }) {
|
||||
return (
|
||||
<S.Step>
|
||||
<S.Title>얼마에 팔고 싶나요?</S.Title>
|
||||
<S.Title>판매할 가격을 입력해 주세요</S.Title>
|
||||
<CurrencyInput
|
||||
value={price}
|
||||
setValue={setPrice}
|
||||
autofocus
|
||||
/>
|
||||
<Spacer height={20} />
|
||||
<SubTitle>최근 판매 가격</SubTitle>
|
||||
<SubTitle>최근 판매 가격은 다음과 같아요</SubTitle>
|
||||
<ProductPriceGraph id={modelId} />
|
||||
<ButtonArea>
|
||||
<Button
|
||||
|
@ -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;
|
14
src/pages/WriteSteps/WaitingCreation.jsx
Normal file
14
src/pages/WriteSteps/WaitingCreation.jsx
Normal 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;
|
@ -1,18 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { Suspense, useState } from 'react';
|
||||
import ProductSelectStep from './ProductSelectStep';
|
||||
import StatusStep from './StatusStep';
|
||||
import ItemIssuesStep from './ItemIssuesStep';
|
||||
import PhotoStep from './PhotoStep';
|
||||
import PriceStep from './PriceStep';
|
||||
import TextStep from './TextStep';
|
||||
import PostCreateStep from './PostCreateStep';
|
||||
import WaitingCreation from './WaitingCreation';
|
||||
|
||||
function WriteSteps() {
|
||||
const [formData, setFormData] = useState({
|
||||
product: '',
|
||||
status: {
|
||||
item_issues: {
|
||||
display: false,
|
||||
frame: false,
|
||||
button: false,
|
||||
biometrics: false,
|
||||
biometric: false,
|
||||
camera: false,
|
||||
speaker: false,
|
||||
others: false,
|
||||
@ -21,12 +23,26 @@ function WriteSteps() {
|
||||
photos: [],
|
||||
text: '',
|
||||
});
|
||||
const STEP_INFO = ['product', 'status', 'price', 'photo', 'text', 'step6'];
|
||||
const STEP_INFO = [
|
||||
'product',
|
||||
'itemIssues',
|
||||
'price',
|
||||
'photo',
|
||||
'text',
|
||||
'postCreate',
|
||||
];
|
||||
const setProduct = (product) => {
|
||||
setFormData((prev) => ({ ...prev, product }));
|
||||
};
|
||||
const setStatus = (status) => {
|
||||
setFormData((prev) => ({ ...prev, status }));
|
||||
const setItemIssues = (itemIssues) => {
|
||||
if (typeof itemIssues === 'function') {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
item_issues: itemIssues(prev.item_issues),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setFormData((prev) => ({ ...prev, itemIssues }));
|
||||
};
|
||||
const setPrice = (price) => {
|
||||
setFormData((prev) => ({ ...prev, price }));
|
||||
@ -43,15 +59,15 @@ function WriteSteps() {
|
||||
<>
|
||||
{step === 'product' && (
|
||||
<ProductSelectStep
|
||||
gotoNextStep={() => setStep('status')}
|
||||
gotoNextStep={() => setStep('itemIssues')}
|
||||
product={formData.product}
|
||||
setProduct={setProduct}
|
||||
/>
|
||||
)}
|
||||
{step === 'status' && (
|
||||
<StatusStep
|
||||
status={formData.status}
|
||||
setStatus={setStatus}
|
||||
{step === 'itemIssues' && (
|
||||
<ItemIssuesStep
|
||||
itemIssues={formData.item_issues}
|
||||
setItemIssues={setItemIssues}
|
||||
gotoNextStep={() => setStep('price')}
|
||||
gotoPrevStep={() => setStep('product')}
|
||||
/>
|
||||
@ -62,7 +78,7 @@ function WriteSteps() {
|
||||
price={formData.price}
|
||||
setPrice={setPrice}
|
||||
gotoNextStep={() => setStep('photo')}
|
||||
gotoPrevStep={() => setStep('status')}
|
||||
gotoPrevStep={() => setStep('itemIssues')}
|
||||
/>
|
||||
)}
|
||||
{step === 'photo' && (
|
||||
@ -77,10 +93,15 @@ function WriteSteps() {
|
||||
<TextStep
|
||||
text={formData.text}
|
||||
setText={setText}
|
||||
gotoNextStep={() => setStep('step6')}
|
||||
gotoNextStep={() => setStep('postCreate')}
|
||||
gotoPrevStep={() => setStep('photo')}
|
||||
/>
|
||||
)}
|
||||
{step === 'postCreate' && (
|
||||
<Suspense fallback={<WaitingCreation />}>
|
||||
<PostCreateStep formData={formData} />
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
11
src/styles/ItemIssues.styles.js
Normal file
11
src/styles/ItemIssues.styles.js
Normal 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);
|
||||
}
|
||||
`;
|
Loading…
Reference in New Issue
Block a user