From e3998364be6527004a59466085886ac5aa96690c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EA=B2=BD?= Date: Tue, 27 Feb 2024 16:34:07 +0900 Subject: [PATCH] =?UTF-8?q?faet=20#18=20=EC=83=81=ED=92=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 3 +- src/common.ts | 10 + src/components/PreviewMap.tsx | 2 +- src/components/StyledInputText.tsx | 2 + src/components/index.d.ts | 1 + .../GoodsModify/components/ModifyCategory.tsx | 28 ++ .../GoodsModify/components/ModifyHeader.tsx | 37 ++ .../GoodsModify/components/PostCodeModify.tsx | 86 ++++ .../Goods/components/GoodsModify/index.tsx | 435 +++++++++++++++++- src/pages/Goods/components/IntroCategory.tsx | 2 +- src/pages/Goods/index.d.ts | 15 +- src/pages/Map/index.tsx | 1 - .../Deadline/components/calendarDate.tsx | 27 +- .../Register/components/Deadline/index.tsx | 12 +- .../components/Place/components/Selector.tsx | 2 +- src/pages/Register/components/Place/index.tsx | 27 +- 16 files changed, 650 insertions(+), 40 deletions(-) create mode 100644 src/pages/Goods/components/GoodsModify/components/ModifyCategory.tsx create mode 100644 src/pages/Goods/components/GoodsModify/components/ModifyHeader.tsx create mode 100644 src/pages/Goods/components/GoodsModify/components/PostCodeModify.tsx diff --git a/src/Router.tsx b/src/Router.tsx index 0a86347..ba0281d 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -39,6 +39,7 @@ import MyJoinList from './pages/Mypage/components/MyJoinList'; import GoodsDetail from './pages/Goods/components/GoodsDetail'; import { AuthType } from './types/data'; +import GoodsModify from './pages/Goods/components/GoodsModify'; export default function Router() { const [auth] = useRecoilState(authState); @@ -109,7 +110,7 @@ export default function Router() { element: , children: [{ path: 'submitted', element: }], }, - { path: 'modify', element: 'modify' }, + { path: 'modify', element: }, { path: 'notice', element: , diff --git a/src/common.ts b/src/common.ts index 1f936d3..8043271 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,3 +1,5 @@ +import { CategoryGroupCode } from './types/register'; + export const primaryYello = '#FFAE39'; export const primaryJade = '#8CDDE2'; export const primaryPurple = '#E4CCFF'; @@ -62,3 +64,11 @@ export const prohibition = { '만약 특정 항목이나 물품에 대한 의문이 있을 경우, 담당자에게 문의해 주시기 바랍니다.', description4: '이용해 주셔서 감사합니다.', }; + +export const PlaceCodes: CategoryGroupCode[] = [ + 'MT1', + 'CS2', + 'SW8', + 'BK9', + 'PO3', +]; diff --git a/src/components/PreviewMap.tsx b/src/components/PreviewMap.tsx index e15747a..3bc23a0 100644 --- a/src/components/PreviewMap.tsx +++ b/src/components/PreviewMap.tsx @@ -12,7 +12,7 @@ export default function PreviewMap({ position?: PositionType; }) { return ( -
+
{position?.lat ? ( // ); diff --git a/src/components/index.d.ts b/src/components/index.d.ts index e92ff08..3860384 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -12,6 +12,7 @@ export type StyledInputTextType = { onChange: (e: React.ChangeEvent) => void; placeholder: string; height?: numger; + disabled?: boolean; }; export type StyledCurrencyInputType = { diff --git a/src/pages/Goods/components/GoodsModify/components/ModifyCategory.tsx b/src/pages/Goods/components/GoodsModify/components/ModifyCategory.tsx new file mode 100644 index 0000000..8cb2791 --- /dev/null +++ b/src/pages/Goods/components/GoodsModify/components/ModifyCategory.tsx @@ -0,0 +1,28 @@ +import category from '@src/pages/Map/components/BottomSheetComponent/data'; + +export default function ModifyCategory({ + idx, + btn, + onClick, +}: { + idx: number; + btn?: JSX.Element; + onClick?: () => void; +}) { + const { icon, name } = category.filter((item) => item.idx === idx)[0]; + + return ( + + {name} + {name} {btn} + + ); +} + +ModifyCategory.defaultProps = { + btn: undefined, + onClick: undefined, +}; diff --git a/src/pages/Goods/components/GoodsModify/components/ModifyHeader.tsx b/src/pages/Goods/components/GoodsModify/components/ModifyHeader.tsx new file mode 100644 index 0000000..f399d66 --- /dev/null +++ b/src/pages/Goods/components/GoodsModify/components/ModifyHeader.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import ChevronLeft from '@src/asset/icon/chevronLeft.svg'; +import { useNavigate } from 'react-router-dom'; +import { GoodsModifyType } from '@src/pages/Goods/index.d'; + +export default function ModifyHeader({ + curr, + prev, + setIsConfirm, +}: { + curr: GoodsModifyType; + prev: GoodsModifyType; + setIsConfirm: React.Dispatch>; +}) { + const navigate = useNavigate(); + + const handleGoingBack = () => { + if (JSON.stringify(curr) === JSON.stringify(prev)) { + // console.log('변동사항 없음'); + navigate(-1); + } else { + // console.log('변동사항 있음'); + setIsConfirm(true); + } + }; + return ( +
+ +
+ ); +} diff --git a/src/pages/Goods/components/GoodsModify/components/PostCodeModify.tsx b/src/pages/Goods/components/GoodsModify/components/PostCodeModify.tsx new file mode 100644 index 0000000..5914d28 --- /dev/null +++ b/src/pages/Goods/components/GoodsModify/components/PostCodeModify.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; +import DaumPostcodeEmbed, { Address } from 'react-daum-postcode'; +import CloseIcon from '@src/asset/icon/CloseIcon.svg'; +import { GoodsModifyType } from '@src/pages/Goods/index.d'; + +function PostCodeModify({ + address, + setAddress, + setModify, +}: { + address: string; + setAddress: React.Dispatch>; + setModify: React.Dispatch>; +}) { + const [open, setOpen] = useState(false); + const { kakao } = window; + + const handleComplete = (param: Address) => { + const geocoder = new kakao.maps.services.Geocoder(); + let fullAddress = param.address; + let extraAddress = ''; + + if (param.addressType === 'R') { + if (param.bname !== '') { + extraAddress += param.bname; + } + if (param.buildingName !== '') { + extraAddress += + extraAddress !== '' ? `, ${param.buildingName}` : param.buildingName; + } + fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; + } + + geocoder.addressSearch(fullAddress, (result, status) => { + if (status === kakao.maps.services.Status.OK) { + setAddress(fullAddress); + setModify((prev) => ({ + ...prev, + coordinate: { + latitude: Number(result[0].y), + longitude: Number(result[0].x), + }, + })); + } + }); + + setOpen(false); + }; + + return ( +
+ + {open && ( +
+
+
setOpen(false)}> + 닫기 +
+
+ +
+ )} +
+ ); +} + +export default PostCodeModify; diff --git a/src/pages/Goods/components/GoodsModify/index.tsx b/src/pages/Goods/components/GoodsModify/index.tsx index 701dc97..3550dc7 100644 --- a/src/pages/Goods/components/GoodsModify/index.tsx +++ b/src/pages/Goods/components/GoodsModify/index.tsx @@ -1,5 +1,436 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { useMutation } from 'react-query'; +import RegistMarker from '@src/asset/icon/RegistMarker.svg'; +import camera from '@src/asset/icon/mdi_camera.svg'; +import close from '@src/asset/icon/CloseIcon.svg'; +import Loading from '@src/pages/Map/components/Loading'; +import StyledCountInput from '@src/components/StyledCountInput'; +import StyledCurrencyInput from '@src/components/StyledCurrencyInput'; +import StyledTextarea from '@src/components/StyledTextarea'; +import InputWithNum from '@src/components/InputWithNum'; +import getOpenGraph from '@src/api/og'; +import OpenGraphViewer from '@src/components/OpenGraphViewer'; +import StyledInputText from '@src/components/StyledInputText'; +import CalendarDate from '@src/pages/Register/components/Deadline/components/calendarDate'; +import Selector from '@src/pages/Register/components/Place/components/Selector'; +import { NearPlacesType } from '@src/types/register'; +import { PositionType } from '@src/types/map'; +import { getPlaces } from '@src/api/register'; +import PreviewMap from '@src/components/PreviewMap'; +import { PlaceCodes } from '@src/common'; +import { getCurrentLocation } from '@src/utils'; +import category from '@src/pages/Map/components/BottomSheetComponent/data'; +import PostCodeModify from './components/PostCodeModify'; +import { GoodsContextType, GoodsModifyType } from '../../index.d'; +import ModifyHeader from './components/ModifyHeader'; +import ModifyCategory from './components/ModifyCategory'; + +const initModifyData: GoodsModifyType = { + images: [], + imagesInput: [], + categoryId: -1, + goodsName: '', + introduction: '', + goodsPrice: 0, + deliveryFee: 0, + goodsLimitCount: 0, + goodsLimitTime: new Date(), + coordinate: { longitude: 0, latitude: 0 }, +}; export default function GoodsModify() { - return
GoodsModify
; + const navigate = useNavigate(); + const { goods, isSuccess } = useOutletContext(); + const [modify, setModify] = useState(initModifyData); + const [prevData, setPrevData] = useState(initModifyData); + // 위치 관련 states + // -2: default, -1: 직접 입력 + const [index, setIndex] = useState(-2); + const [places, setPlaces] = useState([]); + const [position, setPosition] = useState(undefined); + const [address, setAddress] = useState(''); + + const [isConfirm, setIsConfirm] = useState(false); + + const encodeFileToBase64 = (fileBlob: File) => { + // 이미지 선택 취소 시 예외처리 + if (fileBlob === undefined) return null; + const reader = new FileReader(); + reader.readAsDataURL(fileBlob); + + return new Promise(() => { + // onLoad에서 실행 + reader.onload = () => { + const prevImages = modify.images; + const prevImageInput = modify.imagesInput; + prevImages.push(reader.result as string); + prevImageInput.push(fileBlob); + setModify((prev) => ({ + ...prev, + images: prevImages, + imagesInput: prevImageInput, + })); + }; + }); + }; + + // OpenGraph API 사용 + const { mutate: getOG, data: openGraph } = useMutation( + 'introOpenGraph', + () => getOpenGraph(goods.goodsPageDto.link), + { + retryDelay: 500, + } + ); + + // KAKAO API로 검색 + const { mutate: getKakaoPlaces, isLoading } = useMutation({ + mutationFn: getPlaces, + onSuccess: (res) => { + res.forEach((item) => { + setPlaces((prev) => { + // 중복 방지 + const isExist = prev.find( + (place) => place.place_name === item.place_name + ); + if (!isExist) return [...prev, item]; + return prev; + }); + return item; + }); + }, + }); + + // 초기 위치 검색 + useEffect(() => { + getCurrentLocation(setPosition); + }, []); + + // 등록된 코드를 통해 주변 위치 검색 + useEffect(() => { + if (position) + PlaceCodes.forEach((CODE) => + getKakaoPlaces({ position, CAT_CODE: CODE }) + ); + }, [position]); + + // 주변 위치를 거래 장소로 지정했을 시 + useEffect(() => { + if (index >= 0) { + setAddress(places[index].place_name); + setModify((prev) => ({ + ...prev, + coordinate: { + latitude: places[index].y, + longitude: places[index].x, + }, + })); + } else if (index === -1) { + setAddress(''); + setModify((prev) => ({ + ...prev, + coordinate: { latitude: 0, longitude: 0 }, + })); + } + }, [index]); + + // 기존값을 수정할 값에 반영 + useEffect(() => { + if (isSuccess) { + getOG(); + const newData = { + images: goods.goodsPageDto.goodsImagesList.map( + (item) => item.goodsImgUrl + ), + imagesInput: [], + categoryId: goods.goodsPageDto.categoryId, + goodsName: goods.goodsPageDto.goodsName, + introduction: goods.goodsPageDto.introduction, + goodsPrice: goods.goodsPageDto.goodsPrice, + deliveryFee: goods.goodsPageDto.deliveryFee, + goodsLimitCount: goods.goodsPageDto.goodsLimitCount, + goodsLimitTime: new Date(goods.goodsPageDto.goodsLimitTime), + coordinate: goods.goodsPageDto.coordinate, + }; + setModify(newData); + setPrevData(newData); + } + }, [isSuccess]); + + if (!isSuccess) + return ( +
+ +
+ ); + + return ( +
+ {/* header */} + + + {/* content */} +
+
+
상품 소개 이미지
+
+ + {modify.images.map((item, idx) => ( +
+ + 상품 이미지 +
+ ))} +
+
+ +
+
카테고리
+
+ {modify.categoryId === initModifyData.categoryId || !isSuccess ? ( +
+ {category.map((item) => ( + + setModify((prev) => ({ + ...prev, + categoryId: item.idx, + })) + } + /> + ))} +
+ ) : ( +
+
+ setModify((prev) => ({ + ...prev, + categoryId: initModifyData.categoryId, + })) + } + > + 카테고리 변경 +
+ + setModify((prev) => ({ + ...prev, + categoryId: initModifyData.categoryId, + })) + } + > + close +
+ } + /> +
+ )} +
+
+ +
+
폼 제목
+ + setModify((prev) => ({ ...prev, goodsName: e.target.value })) + } + maxLength={30} + /> +
+ +
+
상품 링크 (수정불가)
+ undefined} + placeholder="" + disabled + /> + +
+ +
+
폼 내용
+ + setModify((prev) => ({ ...prev, introduction: e.target.value })) + } + placeholder="" + height={128} + /> +
+ +
+
상품 구매가
+ + setModify((prev) => ({ + ...prev, + goodsPrice: Number(value), + })) + } + /> +
+ +
+
배송비 (추가 배송비 포함)
+ + setModify((prev) => ({ + ...prev, + deliveryFee: Number(value), + })) + } + /> +
+ +
+
목표 공동 구매 수량
+ + setModify((prev) => ({ + ...prev, + goodsLimitCount: Number(value), + })) + } + /> +
+ +
+
공동 구매 종료 시점
+ + setModify((prev) => ({ ...prev, goodsLimitTime: newDate })) + } + /> +
+ +
+
픽업 위치 지정
+
+ +
+
+ {index === -1 && ( +
+ +
+ )} + +
+ = 0 + ? { + lat: places[index].y, + lng: places[index].x, + } + : { + lat: modify.coordinate.latitude, + lng: modify.coordinate.longitude, + } + } + markerImg={RegistMarker} + /> +
+
+
+ +
+ +
+
+ + {isConfirm && ( +
setIsConfirm(false)} + > +
+
+
+ 변경 사항을 취소 하시겠어요? +
+
+ 수정하신 내용이 반영되지 않아요. +
+
+
+
navigate(-1)} + > + 수정 취소 +
+
setIsConfirm(false)} + > + 계속 편집 +
+
+
+
+ )} +
+ ); } diff --git a/src/pages/Goods/components/IntroCategory.tsx b/src/pages/Goods/components/IntroCategory.tsx index 16ad2d8..76cffc5 100644 --- a/src/pages/Goods/components/IntroCategory.tsx +++ b/src/pages/Goods/components/IntroCategory.tsx @@ -1,4 +1,4 @@ -import category from '../../Map/components/BottomSheetComponent/data'; +import category from '@src/pages/Map/components/BottomSheetComponent/data'; export default function IntroCategory({ idx }: { idx: number }) { const { icon, name } = category.filter((item) => item.idx === idx)[0]; diff --git a/src/pages/Goods/index.d.ts b/src/pages/Goods/index.d.ts index 4818182..957ea5f 100644 --- a/src/pages/Goods/index.d.ts +++ b/src/pages/Goods/index.d.ts @@ -1,4 +1,4 @@ -import { GoodsDetailDTO } from '@src/types/goods'; +import { Address, GoodsDetailDTO } from '@src/types/goods'; export type GoodsContextType = { goods: GoodsDetailDTO; @@ -8,3 +8,16 @@ export type PostCommentStateType = { value: string; targetCommentId: number; }; + +export type GoodsModifyType = { + images: string[]; + imagesInput: File[]; + categoryId: number; + goodsName: string; + introduction: string; + goodsPrice: number; + deliveryFee: number; + goodsLimitCount: number; + goodsLimitTime: Date; + coordinate: Address; +}; diff --git a/src/pages/Map/index.tsx b/src/pages/Map/index.tsx index 9316b96..07de911 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -158,7 +158,6 @@ export default function Map() { // 마커 등록 useEffect(() => { if (markerList) initMap(markerList); - console.log(markerList); }, [markerList]); // 주소 변환 diff --git a/src/pages/Register/components/Deadline/components/calendarDate.tsx b/src/pages/Register/components/Deadline/components/calendarDate.tsx index ee652ae..50dbe6c 100644 --- a/src/pages/Register/components/Deadline/components/calendarDate.tsx +++ b/src/pages/Register/components/Deadline/components/calendarDate.tsx @@ -1,28 +1,23 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Calendar } from 'react-date-range'; import locale from 'date-fns/locale/ko'; import 'react-date-range/dist/styles.css'; import 'react-date-range/dist/theme/default.css'; -import { RegisterDataType } from '@src/types/register'; import { primaryBlue } from '../../../../../common'; function CalendarDate({ - data, - setData, + date, + onChange, }: { - data: RegisterDataType; - setData: React.Dispatch>; + date: Date; + onChange: (date: Date) => void; }) { const [open, setOpen] = useState(false); - const handleDateChange = (newDate: Date) => { - setData((prev) => ({ - ...prev, - goodsDto: { ...prev.goodsDto, goodsLimitTime: newDate }, - })); - setOpen(false); // 날짜 선택 후 모달 닫기 - }; - const date = data.goodsDto.goodsLimitTime; + useEffect(() => { + setOpen(false); + }, [date]); + return ( <>