From 08289db5b22cb5fc6e9ce71b71f6f6953cf97cc9 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 02:39:40 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=EC=83=B5=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/salon/src/App.tsx | 3 + .../components/my/shop/DesignerInfoArea.tsx | 104 +++++++++++ .../src/components/my/shop/ShopImageArea.tsx | 76 ++++++++ .../src/components/my/shop/ShopInfoArea.tsx | 172 ++++++++++++++++++ apps/salon/src/pages/My/Shop.tsx | 131 +++++++++++++ .../src/components/Portfolio/DesignerInfo.tsx | 70 ++++--- packages/utils/src/apis/salon/hooks/my.ts | 19 +- packages/utils/src/apis/salon/my.ts | 10 +- packages/utils/src/apis/types/my.ts | 23 +++ 9 files changed, 577 insertions(+), 31 deletions(-) create mode 100644 apps/salon/src/components/my/shop/DesignerInfoArea.tsx create mode 100644 apps/salon/src/components/my/shop/ShopImageArea.tsx create mode 100644 apps/salon/src/components/my/shop/ShopInfoArea.tsx create mode 100644 apps/salon/src/pages/My/Shop.tsx diff --git a/apps/salon/src/App.tsx b/apps/salon/src/App.tsx index 3f7bad17..09f5cc52 100644 --- a/apps/salon/src/App.tsx +++ b/apps/salon/src/App.tsx @@ -19,6 +19,8 @@ import ReservationPage from '@pages/Quotation/ReservationPage'; import PrivateRoute from '@components/PrivateRoute'; +import MyShopPage from './pages/My/Shop'; + function App() { return ( @@ -47,6 +49,7 @@ function App() { path="/portfolio/:groomerId/:portfolioId" element={} /> + } /> diff --git a/apps/salon/src/components/my/shop/DesignerInfoArea.tsx b/apps/salon/src/components/my/shop/DesignerInfoArea.tsx new file mode 100644 index 00000000..21107425 --- /dev/null +++ b/apps/salon/src/components/my/shop/DesignerInfoArea.tsx @@ -0,0 +1,104 @@ +import { useEffect, useRef } from 'react'; + +import { + DesignerInfo, + Flex, + HeightFitFlex, + Pencil, + Text, + theme, + WidthFitFlex, +} from '@duri-fe/ui'; +import styled from '@emotion/styled'; + +interface DesignerInfoAreaProps { + id: number; + name: string; + age: number; + gender: string; + history: number; + license: string[]; + image: string; + onEdit: boolean; + setOnEdit: React.Dispatch>; +} + +const DesignerInfoArea = ({ + id, + name, + age, + gender, + history, + license, + image, + onEdit, + setOnEdit, +}: DesignerInfoAreaProps) => { + const designerInfoRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + designerInfoRef.current && + !designerInfoRef.current.contains(e.target as Node) + ) { + setOnEdit(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [designerInfoRef]); + + return ( + + 디자이너 소개 + setOnEdit(true)} + > + + {onEdit && ( + + + + 수정하기 + + + )} + + + ); +}; + +const DesignerInfoWrapper = styled(WidthFitFlex)` + position: relative; +`; + +const ShopEditArea = styled(Flex)` + position: absolute; + top: -4px; + left: -4px; + width: calc(100% + 8px); + height: calc(100% + 8px); + background-color: rgba(17, 17, 17, 0.5); +`; + +export default DesignerInfoArea; diff --git a/apps/salon/src/components/my/shop/ShopImageArea.tsx b/apps/salon/src/components/my/shop/ShopImageArea.tsx new file mode 100644 index 00000000..9837f9c6 --- /dev/null +++ b/apps/salon/src/components/my/shop/ShopImageArea.tsx @@ -0,0 +1,76 @@ +import { useEffect, useRef } from 'react'; + +import { Flex, Pencil, Text, theme } from '@duri-fe/ui'; +import styled from '@emotion/styled'; + +interface ShopImageAreaProps { + imageURL: string; + onEdit: boolean; + setOnEdit: React.Dispatch>; +} + +const ShopImageArea = ({ imageURL, onEdit, setOnEdit }: ShopImageAreaProps) => { + const shopImageRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + shopImageRef.current && + !shopImageRef.current.contains(e.target as Node) + ) { + setOnEdit(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [shopImageRef]); + + return ( + setOnEdit(true)}> + {imageURL ? ( + + ) : ( + + + 매장 대표 사진을 등록해주세요. + + + )} + {onEdit && ( + + + + 수정하기 + + + )} + + ); +}; + +const ShopImageWrapper = styled(Flex)` + position: relative; + cursor: pointer; +`; + +const MainImg = styled.img` + width: 100%; + aspect-ratio: 330 / 180; + border-radius: 12px; + object-fit: cover; +`; + +const ShopEditArea = styled(Flex)` + position: absolute; + top: 0; + left: 0; + background-color: rgba(17, 17, 17, 0.5); +`; + +export default ShopImageArea; diff --git a/apps/salon/src/components/my/shop/ShopInfoArea.tsx b/apps/salon/src/components/my/shop/ShopInfoArea.tsx new file mode 100644 index 00000000..2eb6c28d --- /dev/null +++ b/apps/salon/src/components/my/shop/ShopInfoArea.tsx @@ -0,0 +1,172 @@ +import { useEffect, useRef } from 'react'; + +import { + Call, + FilledLocation, + Flex, + HeightFitFlex, + Pencil, + SalonTag, + Star, + Text, + theme, + Time, + WidthFitFlex, +} from '@duri-fe/ui'; +import styled from '@emotion/styled'; + +interface ShopInfoAreaProps { + name: string; + address: string; + phone: string; + openTime: string; + closeTime: string; + tags: string[]; + info: string; + onEdit: boolean; + setOnEdit: React.Dispatch>; +} + +const ShopInfoArea = ({ + name, + address, + phone, + openTime, + closeTime, + tags, + info, + onEdit, + setOnEdit, +}: ShopInfoAreaProps) => { + const shopInfoRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + shopInfoRef.current && + !shopInfoRef.current.contains(e.target as Node) + ) { + setOnEdit(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [shopInfoRef]); + + return ( + + + {name} + + + + + + {/** 주소 */} + + + + {address} + + + + setOnEdit(true)} + > + {/** 전화번호 */} + + + + {phone} + + + + {/** 영업시간 */} + + + + {/** 태그 */} + {tags.length > 0 ? ( + + {tags.map((tag, index) => ( + + ))} + + ) : ( + + + 매장 태그를 추가해주세요. + + + )} + + {/** 한줄소개 */} + + + {info === null ? '매장 한줄소개를 작성해주세요.' : info} + + + + {onEdit && ( + + + + 수정하기 + + + )} + + + ); +}; + +const ShopInfoContainer = styled(Flex)` + position: relative; + cursor: pointer; +`; + +const ShopEditArea = styled(Flex)` + position: absolute; + top: -4px; + left: 0; + height: calc(100% + 4px); + background-color: rgba(17, 17, 17, 0.5); +`; + +const TextLine = styled(Text)` + display: inline; +`; + +const TextHeight = styled(Text)` + line-height: 140%; +`; + +const TagList = styled(WidthFitFlex)` + flex-wrap: wrap; +`; + +export default ShopInfoArea; diff --git a/apps/salon/src/pages/My/Shop.tsx b/apps/salon/src/pages/My/Shop.tsx new file mode 100644 index 00000000..6b34114f --- /dev/null +++ b/apps/salon/src/pages/My/Shop.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { Flex, Header, MobileLayout, SalonNavbar } from '@duri-fe/ui'; +import { UseGetMyShopInfo } from '@duri-fe/utils'; +import styled from '@emotion/styled'; +import DesignerInfoArea from '@salon/components/my/shop/DesignerInfoArea'; +import ShopImageArea from '@salon/components/my/shop/ShopImageArea'; +import ShopInfoArea from '@salon/components/my/shop/ShopInfoArea'; + +const MyShopPage = () => { + const navigate = useNavigate(); + + const [onShopImageEdit, setOnShopImageEdit] = useState(false); + const [onShopInfoEdit, setOnShopInfoEdit] = useState(false); + const [onDesignerInfoEdit, setOnDesignerInfoEdit] = useState(false); + + const { data: myShopInfo } = UseGetMyShopInfo({}); + + const { + groomerProfileDetailResponse: groomerDetail = { + id: 0, + email: '', + phone: '', + name: '', + gender: '', + age: 0, + history: 0, + image: '', + info: '', + license: [], + }, + shopProfileDetailResponse: shopDetail = { + id: 0, + name: '', + address: '', + imageURL: '', + phone: '', + openTime: '', + closeTime: '', + info: '', + kakaoTalk: '', + tags: [], + }, + } = myShopInfo || {}; + + const handleClickBack = () => { + navigate('/my'); + }; + + return ( + +
+ {myShopInfo && ( + <> + + + + {/**매장 정보 */} + + + {/**디자이너 */} + + + {/**리뷰 */} + {/* + + 리뷰 + + {shopRating} + + ({reviewCnt}) + + + {reviewData && reviewData.length > 0 ? ( + reviewData.map((review) => ( + + )) + ) : ( + + 아직 등록된 리뷰가 없습니다. + + )} + */} + + + )} + + + ); +}; + +const ShopInfoContainer = styled(Flex)` + overflow-y: auto; +`; + +export default MyShopPage; diff --git a/packages/ui/src/components/Portfolio/DesignerInfo.tsx b/packages/ui/src/components/Portfolio/DesignerInfo.tsx index 973ac92c..44fc3f1f 100644 --- a/packages/ui/src/components/Portfolio/DesignerInfo.tsx +++ b/packages/ui/src/components/Portfolio/DesignerInfo.tsx @@ -1,7 +1,13 @@ -import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Approve, Flex, ProfileImage, Text, theme } from '@duri-fe/ui'; +import { + Approve, + Flex, + ProfileImage, + Text, + theme, + WidthFitFlex, +} from '@duri-fe/ui'; import styled from '@emotion/styled'; interface DesignerInfoProps { @@ -14,6 +20,7 @@ interface DesignerInfoProps { roles: string[]; imageUrl: string; padding?: string; + isNavigate?: boolean; } export const DesignerInfo = ({ @@ -26,24 +33,28 @@ export const DesignerInfo = ({ roles, imageUrl, padding, + isNavigate = true, }: DesignerInfoProps) => { const navigate = useNavigate(); - const [careerYear, setCareerYear] = useState(0); - const [careerMonth, setCareerMonth] = useState(0); - const moveToPortfolio = () => { - if (version === 'vertical') { + if (version === 'vertical' && isNavigate) { navigate(`/portfolio/${designerId}`); } }; - useEffect(() => { - if (experience !== 0) { - setCareerYear(Math.floor(experience / 12)); - setCareerMonth(experience % 12); + const historyStr = (experience: number) => { + if (experience % 12 === 0) { + return `${experience / 12}년`; + } else { + const month = experience % 12; + const year = (experience - month) / 12; + if (year === 0) { + return `${month}개월`; + } + return `${year}년 ${month}개월`; } - }, [experience]); + }; return ( - - {name ?? '정보없음'} + + {name} {`경력 ${careerYear ?? '-'}년 ${careerMonth ?? '-'}개월, ${age ?? '-'}세, ${gender}`} + >{`경력 ${historyStr(experience)}, ${age}세, ${gender}`} - {roles?.length > 0 && ( - - {roles.map((item, idx) => ( - - - {item} - - - - ))} - - )} - + + {roles.map((item, idx) => ( + + + {item} + + + + ))} + + ); }; -const Container = styled(Flex)<{ +const Container = styled(WidthFitFlex)<{ version: 'vertical' | 'horizontal'; clickable: boolean; }>` diff --git a/packages/utils/src/apis/salon/hooks/my.ts b/packages/utils/src/apis/salon/hooks/my.ts index 129eb5bc..66995cf0 100644 --- a/packages/utils/src/apis/salon/hooks/my.ts +++ b/packages/utils/src/apis/salon/hooks/my.ts @@ -2,11 +2,12 @@ import { useQuery } from '@tanstack/react-query'; import { BaseError } from '../../types'; import { GroomerAndShopProfileResponse } from '../../types/my'; +import { GetMyShopInfoResponse } from '../../types/my'; import { UseQueryProps } from '../../types/tanstack'; -import { getGroomerInfo } from '../my'; +import { getGroomerInfo, getMyShopInfo } from '../my'; type UseGetGroomerInfo = UseQueryProps< -GroomerAndShopProfileResponse['response'], + GroomerAndShopProfileResponse['response'], BaseError > & { groomerId: number; @@ -24,3 +25,17 @@ export const UseGetGroomerInfo = ({ ...options, }); }; + +type UseGetMyShopInfo = UseQueryProps< + GetMyShopInfoResponse['response'], + BaseError +>; + +/** [GET] /groomer/profile 미용사 마이샵 */ +export const UseGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { + return useQuery({ + queryKey: ['getMyShopInfo', ...(queryKey || [])], + queryFn: () => getMyShopInfo(), + ...options, + }); +}; diff --git a/packages/utils/src/apis/salon/my.ts b/packages/utils/src/apis/salon/my.ts index 104dfd7f..e7b6441a 100644 --- a/packages/utils/src/apis/salon/my.ts +++ b/packages/utils/src/apis/salon/my.ts @@ -1,6 +1,7 @@ -import { publicInstance } from '@duri-fe/utils'; +import { publicInstance, salonInstance } from '@duri-fe/utils'; import { GroomerAndShopProfileResponse } from '../types/my'; +import { GetMyShopInfoResponse } from '../types/my'; export const getGroomerInfo = async ({ groomerId, @@ -12,3 +13,10 @@ export const getGroomerInfo = async ({ }); return data.response; }; + +export const getMyShopInfo = async (): Promise< + GetMyShopInfoResponse['response'] +> => { + const response = await salonInstance.get('groomer/profile'); + return response.data.response; +}; diff --git a/packages/utils/src/apis/types/my.ts b/packages/utils/src/apis/types/my.ts index b436ebad..dc4c0d5d 100644 --- a/packages/utils/src/apis/types/my.ts +++ b/packages/utils/src/apis/types/my.ts @@ -149,3 +149,26 @@ export interface GroomerAndShopProfileResponse extends BaseResponse { shopProfileDetail: ShopInfoType; }; } + +/** [GET] /groomer/profile 미용사 마이샵 */ +export interface GetMyShopInfoResponse extends BaseResponse { + response: { + groomerProfileDetailResponse: GroomerInfoType; + reservationCount: number; + noShowCount: number; + shopProfileDetailResponse: ShopDetailType; + }; +} + +interface ShopDetailType { + id: number; + name: string; + address: string; + imageURL: string; + phone: string; + openTime: string; + closeTime: string; + info: string; + kakaoTalk: string; + tags: string[]; +} From 9b16b5b82eebcef610be0f464951bebb3b8e8558 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 03:40:40 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=EC=83=B5=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20api=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ShopImageArea.tsx | 92 +++++++++++++++++-- packages/utils/src/apis/salon/hooks/my.ts | 21 ++++- packages/utils/src/apis/salon/my.ts | 20 +++- packages/utils/src/apis/types/my.ts | 5 + 4 files changed, 124 insertions(+), 14 deletions(-) diff --git a/apps/salon/src/components/my/shop/ShopImageArea.tsx b/apps/salon/src/components/my/shop/ShopImageArea.tsx index 9837f9c6..250d62ae 100644 --- a/apps/salon/src/components/my/shop/ShopImageArea.tsx +++ b/apps/salon/src/components/my/shop/ShopImageArea.tsx @@ -1,6 +1,7 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; -import { Flex, Pencil, Text, theme } from '@duri-fe/ui'; +import { Flex, FrontBtn, Pencil, Text, theme } from '@duri-fe/ui'; +import { UsePutShopImage } from '@duri-fe/utils'; import styled from '@emotion/styled'; interface ShopImageAreaProps { @@ -10,6 +11,9 @@ interface ShopImageAreaProps { } const ShopImageArea = ({ imageURL, onEdit, setOnEdit }: ShopImageAreaProps) => { + const [imageFile, setImageFile] = useState(null); + const [imagePreviewUrl, setImagePreviewUrl] = useState(null); + const { mutateAsync } = UsePutShopImage(); const shopImageRef = useRef(null); useEffect(() => { @@ -27,9 +31,34 @@ const ShopImageArea = ({ imageURL, onEdit, setOnEdit }: ShopImageAreaProps) => { }; }, [shopImageRef]); + const handleImageChange = async (e: React.ChangeEvent) => { + if (e.target.files) { + setImagePreviewUrl(URL.createObjectURL(e.target.files[0])); + setImageFile(e.target.files[0]); + } + }; + + const handleImageUpload = async () => { + const formData = new FormData(); + if (!imageFile) { + setOnEdit(false); + return; + } + formData.append('image', imageFile); + + try { + await mutateAsync(formData); + setOnEdit(false); + } catch (error) { + console.error(error); + } + }; + return ( setOnEdit(true)}> - {imageURL ? ( + {imagePreviewUrl ? ( + + ) : imageURL ? ( ) : ( { )} {onEdit && ( - - - - 수정하기 - - + <> + + + + + 수정하기 + + + + + + { + e.stopPropagation(); + handleImageUpload(); + }} + > + + 수정하기 + + + )} ); @@ -66,6 +123,14 @@ const MainImg = styled.img` object-fit: cover; `; +const ShopEditWrapper = styled(Flex)` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +`; + const ShopEditArea = styled(Flex)` position: absolute; top: 0; @@ -73,4 +138,13 @@ const ShopEditArea = styled(Flex)` background-color: rgba(17, 17, 17, 0.5); `; +const ShopEditInput = styled.input` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; +`; + export default ShopImageArea; diff --git a/packages/utils/src/apis/salon/hooks/my.ts b/packages/utils/src/apis/salon/hooks/my.ts index 66995cf0..8051c1cc 100644 --- a/packages/utils/src/apis/salon/hooks/my.ts +++ b/packages/utils/src/apis/salon/hooks/my.ts @@ -1,11 +1,16 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { BaseError } from '../../types'; -import { GroomerAndShopProfileResponse } from '../../types/my'; -import { GetMyShopInfoResponse } from '../../types/my'; +import { + GetMyShopInfoResponse, + GroomerAndShopProfileResponse, + PutShopImageResponse, +} from '../../types/my'; import { UseQueryProps } from '../../types/tanstack'; import { getGroomerInfo, getMyShopInfo } from '../my'; +import { putShopImage } from './../my'; + type UseGetGroomerInfo = UseQueryProps< GroomerAndShopProfileResponse['response'], BaseError @@ -39,3 +44,13 @@ export const UseGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { ...options, }); }; + +export const UsePutShopImage = () => { + return useMutation({ + mutationFn: (formData: FormData) => putShopImage(formData), + onError: (error) => { + console.error(error); + alert('샵 사진 등록에 실패했습니다.'); + }, + }); +}; diff --git a/packages/utils/src/apis/salon/my.ts b/packages/utils/src/apis/salon/my.ts index e7b6441a..d051f325 100644 --- a/packages/utils/src/apis/salon/my.ts +++ b/packages/utils/src/apis/salon/my.ts @@ -1,7 +1,10 @@ import { publicInstance, salonInstance } from '@duri-fe/utils'; -import { GroomerAndShopProfileResponse } from '../types/my'; -import { GetMyShopInfoResponse } from '../types/my'; +import { + GetMyShopInfoResponse, + GroomerAndShopProfileResponse, + PutShopImageResponse, +} from '../types/my'; export const getGroomerInfo = async ({ groomerId, @@ -14,9 +17,22 @@ export const getGroomerInfo = async ({ return data.response; }; +/** [GET] /groomer/profile 미용사 마이샵 */ export const getMyShopInfo = async (): Promise< GetMyShopInfoResponse['response'] > => { const response = await salonInstance.get('groomer/profile'); return response.data.response; }; + +/** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ +export const putShopImage = async ( + formData: FormData, +): Promise => { + const response = await salonInstance.put('shop/profile/image', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + return response.data.response; +}; diff --git a/packages/utils/src/apis/types/my.ts b/packages/utils/src/apis/types/my.ts index dc4c0d5d..f7cf6bd6 100644 --- a/packages/utils/src/apis/types/my.ts +++ b/packages/utils/src/apis/types/my.ts @@ -172,3 +172,8 @@ interface ShopDetailType { kakaoTalk: string; tags: string[]; } + +/** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ +export interface PutShopImageResponse extends BaseResponse { + response: ShopDetailType; +} From 53fedec90cfaade12b554dba7b7232f46e252b1c Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 03:52:36 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=EC=83=B5=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=88=98=EC=A0=95=20api=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20cs?= =?UTF-8?q?s=20=EC=A0=84=EC=97=AD=EC=9C=BC=EB=A1=9C=20=EC=98=AC=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/salon/src/App.tsx | 3 ++- apps/salon/src/pages/Home/index.tsx | 2 -- packages/utils/src/apis/salon/hooks/my.ts | 23 ++++++++++++++++++++--- packages/utils/src/apis/salon/my.ts | 13 +++++++++++-- packages/utils/src/apis/types/my.ts | 14 ++++++++++++-- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/apps/salon/src/App.tsx b/apps/salon/src/App.tsx index 09f5cc52..d0eb634c 100644 --- a/apps/salon/src/App.tsx +++ b/apps/salon/src/App.tsx @@ -8,6 +8,7 @@ import AuthPage from '@pages/Auth'; import Home from '@pages/Home'; import LoginPage from '@pages/Login'; import MyPage from '@pages/My'; +import MyShopPage from '@pages/My/Shop'; import OnboardingPage from '@pages/Onboarding'; import OnboardingPendingPage from '@pages/Onboarding/Pending'; import StartPage from '@pages/Onboarding/StartPage'; @@ -19,7 +20,7 @@ import ReservationPage from '@pages/Quotation/ReservationPage'; import PrivateRoute from '@components/PrivateRoute'; -import MyShopPage from './pages/My/Shop'; +import 'react-spring-bottom-sheet/dist/style.css'; function App() { return ( diff --git a/apps/salon/src/pages/Home/index.tsx b/apps/salon/src/pages/Home/index.tsx index 7ebb4e40..6ac5586b 100644 --- a/apps/salon/src/pages/Home/index.tsx +++ b/apps/salon/src/pages/Home/index.tsx @@ -36,8 +36,6 @@ import ClosetGrooming from '@components/home/ClosetGrooming'; import DailyScheduleItem from '@components/home/DailyScheduleItem'; import NewRequestItem from '@components/home/NewRequestItem'; -import 'react-spring-bottom-sheet/dist/style.css'; - const completeToggleData = ['노쇼했어요.', '네, 완료했어요!']; const Home = () => { diff --git a/packages/utils/src/apis/salon/hooks/my.ts b/packages/utils/src/apis/salon/hooks/my.ts index 8051c1cc..8a0adc74 100644 --- a/packages/utils/src/apis/salon/hooks/my.ts +++ b/packages/utils/src/apis/salon/hooks/my.ts @@ -4,10 +4,11 @@ import { BaseError } from '../../types'; import { GetMyShopInfoResponse, GroomerAndShopProfileResponse, - PutShopImageResponse, + PutShopInfoRequest, + PutShopInfoResponse, } from '../../types/my'; import { UseQueryProps } from '../../types/tanstack'; -import { getGroomerInfo, getMyShopInfo } from '../my'; +import { getGroomerInfo, getMyShopInfo, putShopInfo } from '../my'; import { putShopImage } from './../my'; @@ -45,8 +46,9 @@ export const UseGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { }); }; +/** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ export const UsePutShopImage = () => { - return useMutation({ + return useMutation({ mutationFn: (formData: FormData) => putShopImage(formData), onError: (error) => { console.error(error); @@ -54,3 +56,18 @@ export const UsePutShopImage = () => { }, }); }; + +/** [PUT] /shop/profile 미용사 마이샵 정보 수정 */ +export const UsePutShopInfo = () => { + return useMutation< + PutShopInfoResponse['response'], + Error, + PutShopInfoRequest + >({ + mutationFn: (request: PutShopInfoRequest) => putShopInfo(request), + onError: (error) => { + console.error(error); + alert('샵 정보 수정에 실패했습니다.'); + }, + }); +}; diff --git a/packages/utils/src/apis/salon/my.ts b/packages/utils/src/apis/salon/my.ts index d051f325..c0ccb006 100644 --- a/packages/utils/src/apis/salon/my.ts +++ b/packages/utils/src/apis/salon/my.ts @@ -3,7 +3,8 @@ import { publicInstance, salonInstance } from '@duri-fe/utils'; import { GetMyShopInfoResponse, GroomerAndShopProfileResponse, - PutShopImageResponse, + PutShopInfoRequest, + PutShopInfoResponse, } from '../types/my'; export const getGroomerInfo = async ({ @@ -28,7 +29,7 @@ export const getMyShopInfo = async (): Promise< /** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ export const putShopImage = async ( formData: FormData, -): Promise => { +): Promise => { const response = await salonInstance.put('shop/profile/image', formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -36,3 +37,11 @@ export const putShopImage = async ( }); return response.data.response; }; + +/** [PUT] /shop/profile 미용사 마이샵 정보 수정 */ +export const putShopInfo = async ( + request: PutShopInfoRequest, +): Promise => { + const response = await salonInstance.put('shop/profile', request); + return response.data.response; +}; diff --git a/packages/utils/src/apis/types/my.ts b/packages/utils/src/apis/types/my.ts index f7cf6bd6..955c2fa3 100644 --- a/packages/utils/src/apis/types/my.ts +++ b/packages/utils/src/apis/types/my.ts @@ -173,7 +173,17 @@ interface ShopDetailType { tags: string[]; } -/** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ -export interface PutShopImageResponse extends BaseResponse { +/** [PUT] /shop/profile, /shop/profile/image 미용사 마이샵 수정 */ +export interface PutShopInfoResponse extends BaseResponse { response: ShopDetailType; } + +/** [PUT] /shop/profile 미용사 마이샵 정보 수정 */ +export interface PutShopInfoRequest { + phone: string; + openTime: string; + closeTime: string; + info: string; + kakaoTalk: string; + tags: string[]; +} From 16236df61dd6f12c2b2bc811bf61eb61f9bf3969 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 04:33:48 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EB=82=B4=EB=B6=80=20=EC=A0=95=EB=B3=B4=EC=88=98?= =?UTF-8?q?=EC=A0=95=EB=9E=80=20=ED=86=A0=EA=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ShopInfoArea.tsx | 168 ++++++++++-------- .../my/shop/ShopInfoBottomSheet.tsx | 68 +++++++ .../src/components/my/shop/ShopInfoItem.tsx | 46 +++++ 3 files changed, 207 insertions(+), 75 deletions(-) create mode 100644 apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx create mode 100644 apps/salon/src/components/my/shop/ShopInfoItem.tsx diff --git a/apps/salon/src/components/my/shop/ShopInfoArea.tsx b/apps/salon/src/components/my/shop/ShopInfoArea.tsx index 2eb6c28d..3e0dca5b 100644 --- a/apps/salon/src/components/my/shop/ShopInfoArea.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoArea.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef } from 'react'; +import { BottomSheet } from 'react-spring-bottom-sheet'; import { Call, @@ -13,8 +14,11 @@ import { Time, WidthFitFlex, } from '@duri-fe/ui'; +import { useBottomSheet } from '@duri-fe/utils'; import styled from '@emotion/styled'; +import ShopInfoBottomSheet from './ShopInfoBottomSheet'; + interface ShopInfoAreaProps { name: string; address: string; @@ -38,6 +42,10 @@ const ShopInfoArea = ({ onEdit, setOnEdit, }: ShopInfoAreaProps) => { + const { openSheet, closeSheet, bottomSheetProps } = useBottomSheet({ + maxHeight: 636, + }); + const shopInfoRef = useRef(null); useEffect(() => { @@ -56,91 +64,101 @@ const ShopInfoArea = ({ }, [shopInfoRef]); return ( - - - {name} - - - - - - {/** 주소 */} - - - - {address} - - - - setOnEdit(true)} - > - {/** 전화번호 */} - - - - {phone} - + <> + + + {name} + + + - {/** 영업시간 */} - - + {onEdit && ( + + + + 수정하기 + + + )} + + + + + + + ); }; diff --git a/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx b/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx new file mode 100644 index 00000000..15762b9d --- /dev/null +++ b/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import { Flex } from '@duri-fe/ui'; + +import ShopInfoItem from './ShopInfoItem'; + +/** 각 항목 열고닫기 토글용 라벨 */ +export interface ToggleOpenState { + phone: boolean; + time: boolean; + tags: boolean; + kakaoTalk: boolean; + info: boolean; +} + +interface ShopInfoBottomSheetProps { + closeSheet: () => void; +} + +const ShopInfoBottomSheet = ({ closeSheet }: ShopInfoBottomSheetProps) => { + const [isOpen, setIsOpen] = useState({ + phone: false, + time: false, + tags: false, + kakaoTalk: false, + info: false, + }); + + return ( + + +
수정고고
+
+ + + + + ㅎㅎ +
+ ); +}; + +export default ShopInfoBottomSheet; diff --git a/apps/salon/src/components/my/shop/ShopInfoItem.tsx b/apps/salon/src/components/my/shop/ShopInfoItem.tsx new file mode 100644 index 00000000..22f4b6d1 --- /dev/null +++ b/apps/salon/src/components/my/shop/ShopInfoItem.tsx @@ -0,0 +1,46 @@ +import { Flex, Text, theme, UnionDown, UnionUp } from '@duri-fe/ui'; + +import { ToggleOpenState } from './ShopInfoBottomSheet'; + +interface ShopInfoItemProps { + title: string; + keyName: keyof ToggleOpenState; + isOpen: boolean; + setIsOpen: React.Dispatch>; + children?: React.ReactNode; +} + +const ShopInfoItem = ({ + title, + keyName, + isOpen, + setIsOpen, + children, +}: ShopInfoItemProps) => { + /** item 열고 닫기 */ + const handleToggle = () => { + setIsOpen((prev) => ({ + ...prev, + [keyName]: !prev[keyName], + })); + }; + + return ( + + + + {title} + + {isOpen ? : } + + {isOpen && children} + + ); +}; + +export default ShopInfoItem; From 63f32dab6c84ec58c77be919d89a420b1a0c5de4 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 05:20:48 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/assets/Call.tsx | 4 ++-- packages/ui/src/assets/LinkIcon.tsx | 15 +++++++++++++++ packages/ui/src/assets/index.tsx | 3 +++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/assets/LinkIcon.tsx diff --git a/packages/ui/src/assets/Call.tsx b/packages/ui/src/assets/Call.tsx index 27353dd1..bd7f475c 100644 --- a/packages/ui/src/assets/Call.tsx +++ b/packages/ui/src/assets/Call.tsx @@ -2,13 +2,13 @@ import * as React from 'react'; const SvgCall = (props: React.SVGProps) => ( ) => ( + + + +); +export default SvgLinkIcon; diff --git a/packages/ui/src/assets/index.tsx b/packages/ui/src/assets/index.tsx index 25c70b4c..08287981 100644 --- a/packages/ui/src/assets/index.tsx +++ b/packages/ui/src/assets/index.tsx @@ -80,6 +80,7 @@ export { default as PaymentSuccess } from './PaymentSuccess'; export { default as FilledHome } from './FilledHome'; export { default as UploadIcon } from './UploadIcon'; export { default as Save } from './Save'; +export { default as LinkIcon } from './LinkIcon'; import Add from './Add'; import AddNew from './AddNew'; @@ -115,6 +116,7 @@ import Help from './Help'; import Hold from './Hold'; import HomeIcon from './HomeIcon'; import Information from './Information'; +import LinkIcon from './LinkIcon'; import List from './List'; import Location from './Location'; import LocationShop from './LocationShop'; @@ -221,6 +223,7 @@ export const icons = { FilledHome, UploadIcon, Save, + LinkIcon, }; export default icons; From 79f3ee2a7f6dc0486e816ec03c9ed1cbf7e05f33 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 05:21:26 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8,=20=EC=98=A4=ED=94=88=EC=B9=B4=ED=86=A1,=20=ED=95=9C?= =?UTF-8?q?=EC=A4=84=EC=86=8C=EA=B0=9C=20=EC=9E=85=EB=A0=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ShopInfoArea.tsx | 12 +- .../my/shop/ShopInfoBottomSheet.tsx | 196 ++++++++++++++---- .../src/components/my/shop/ShopInfoItem.tsx | 2 + apps/salon/src/pages/My/Shop.tsx | 1 + 4 files changed, 173 insertions(+), 38 deletions(-) diff --git a/apps/salon/src/components/my/shop/ShopInfoArea.tsx b/apps/salon/src/components/my/shop/ShopInfoArea.tsx index 3e0dca5b..4d0615a8 100644 --- a/apps/salon/src/components/my/shop/ShopInfoArea.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoArea.tsx @@ -27,6 +27,7 @@ interface ShopInfoAreaProps { closeTime: string; tags: string[]; info: string; + kakaoTalk: string; onEdit: boolean; setOnEdit: React.Dispatch>; } @@ -39,6 +40,7 @@ const ShopInfoArea = ({ closeTime, tags, info, + kakaoTalk, onEdit, setOnEdit, }: ShopInfoAreaProps) => { @@ -156,7 +158,15 @@ const ShopInfoArea = ({ - + ); diff --git a/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx b/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx index 15762b9d..b5ae8993 100644 --- a/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx @@ -1,6 +1,9 @@ import { useState } from 'react'; +import { useForm } from 'react-hook-form'; -import { Flex } from '@duri-fe/ui'; +import { Call, Flex, LinkIcon, TextField, theme } from '@duri-fe/ui'; +import styled from '@emotion/styled'; +import { formatPhoneNumber } from '@salon/utils'; import ShopInfoItem from './ShopInfoItem'; @@ -13,11 +16,47 @@ export interface ToggleOpenState { info: boolean; } +interface PutShopInfoRequest { + phone: string; + openTime: string; + closeTime: string; + info: string; + kakaoTalk: string; + tags: string[]; +} + interface ShopInfoBottomSheetProps { + phone: string; + openTime: string; + closeTime: string; + tags: string[]; + kakaoTalk: string; + info: string; closeSheet: () => void; } -const ShopInfoBottomSheet = ({ closeSheet }: ShopInfoBottomSheetProps) => { +const ShopInfoBottomSheet = ({ + phone, + openTime, + closeTime, + tags, + kakaoTalk, + info, + closeSheet, +}: ShopInfoBottomSheetProps) => { + const { register, setValue, handleSubmit } = useForm({ + mode: 'onChange', + reValidateMode: 'onChange', + defaultValues: { + phone: phone, + openTime: openTime, + closeTime: closeTime, + info: info, + kakaoTalk: kakaoTalk, + tags: tags, + }, + }); + const [isOpen, setIsOpen] = useState({ phone: false, time: false, @@ -26,43 +65,126 @@ const ShopInfoBottomSheet = ({ closeSheet }: ShopInfoBottomSheetProps) => { info: false, }); + const handlePutShopInfo = (data: PutShopInfoRequest) => { + console.log(data); + }; + return ( - - -
수정고고
- - - - - - ㅎㅎ - + + + {/* 매장 정보 수정 */} + + + + { + const formattedValue = formatPhoneNumber(e.target.value); + setValue('phone', formattedValue); + }, + })} + width={175} + placeholder="전화번호를 입력해주세요." + maxLength={13} + /> + + + + {/* 매장 운영시간 수정 */} + + + {openTime} ~ {closeTime} + + + + {/* 매장 태그 수정 */} + + + {tags.map((tag) => ( + {tag} + ))} + + + + {/* 카카오톡 오픈채팅 링크 수정 */} + + + + + + + + {/* 매장 한줄 소개 수정 */} + + + + + + ); }; +const FormWrapper = styled.form` + width: 100%; +`; + +const InputWrapper = styled(Flex)` + background-color: ${theme.palette.White}; + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.1); +`; + +const InputField = styled.input` + width: 100%; +`; export default ShopInfoBottomSheet; diff --git a/apps/salon/src/components/my/shop/ShopInfoItem.tsx b/apps/salon/src/components/my/shop/ShopInfoItem.tsx index 22f4b6d1..b9277fcd 100644 --- a/apps/salon/src/components/my/shop/ShopInfoItem.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoItem.tsx @@ -28,7 +28,9 @@ const ShopInfoItem = ({ return ( diff --git a/apps/salon/src/pages/My/Shop.tsx b/apps/salon/src/pages/My/Shop.tsx index 6b34114f..e7c36fe4 100644 --- a/apps/salon/src/pages/My/Shop.tsx +++ b/apps/salon/src/pages/My/Shop.tsx @@ -74,6 +74,7 @@ const MyShopPage = () => { closeTime={shopDetail.closeTime} tags={shopDetail.tags} info={shopDetail.info} + kakaoTalk={shopDetail.kakaoTalk} onEdit={onShopInfoEdit} setOnEdit={setOnShopInfoEdit} /> From a3b366de42f880bfb61b1244231087252f71ce8f Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 15:40:30 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=EC=88=98=EC=A0=95=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ShopInfoArea.tsx | 8 + .../my/shop/ShopInfoBottomSheet.tsx | 171 ++++++++++++++++-- .../src/components/my/shop/ShopInfoItem.tsx | 4 +- .../src/components/quotation/ReplyForm.tsx | 109 +++++++---- apps/salon/src/pages/My/Shop.tsx | 3 + apps/salon/src/utils/checkContinuousTime.ts | 13 +- apps/salon/src/utils/parseTimeRange.ts | 39 ++++ packages/utils/src/apis/salon/hooks/my.ts | 10 +- packages/utils/src/apis/types/my.ts | 1 + 9 files changed, 303 insertions(+), 55 deletions(-) create mode 100644 apps/salon/src/utils/parseTimeRange.ts diff --git a/apps/salon/src/components/my/shop/ShopInfoArea.tsx b/apps/salon/src/components/my/shop/ShopInfoArea.tsx index 4d0615a8..a1c465d3 100644 --- a/apps/salon/src/components/my/shop/ShopInfoArea.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoArea.tsx @@ -28,6 +28,8 @@ interface ShopInfoAreaProps { tags: string[]; info: string; kakaoTalk: string; + rating: number; + reviewCnt: number; onEdit: boolean; setOnEdit: React.Dispatch>; } @@ -41,6 +43,8 @@ const ShopInfoArea = ({ tags, info, kakaoTalk, + rating, + reviewCnt, onEdit, setOnEdit, }: ShopInfoAreaProps) => { @@ -72,6 +76,9 @@ const ShopInfoArea = ({ {name} + + {rating} ({reviewCnt}) + @@ -88,6 +95,7 @@ const ShopInfoArea = ({ gap={8} ref={shopInfoRef} onClick={() => setOnEdit(true)} + align="flex-start" > {/** 전화번호 */} diff --git a/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx b/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx index b5ae8993..ef47547d 100644 --- a/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoBottomSheet.tsx @@ -1,12 +1,38 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { Call, Flex, LinkIcon, TextField, theme } from '@duri-fe/ui'; +import { + Button, + Call, + Flex, + LinkIcon, + Text, + TextField, + theme, + TimeTable, +} from '@duri-fe/ui'; +import { TimeType, UsePutShopInfo } from '@duri-fe/utils'; import styled from '@emotion/styled'; -import { formatPhoneNumber } from '@salon/utils'; +import { checkContinuousTime, formatPhoneNumber } from '@salon/utils'; +import { getTimeRange, getTimeStr } from '@salon/utils/parseTimeRange'; import ShopInfoItem from './ShopInfoItem'; +const timeList = Array(12) + .fill(0) + .map((_, i) => `${9 + i}:00`); + +const TAG_LIST = [ + '소형견', + '중형견', + '대형견', + '예민한 반려견', + '예민한 피부', + '스트레스 케어', + '훈육미용', + '피어프리', +]; + /** 각 항목 열고닫기 토글용 라벨 */ export interface ToggleOpenState { phone: boolean; @@ -65,8 +91,60 @@ const ShopInfoBottomSheet = ({ info: false, }); - const handlePutShopInfo = (data: PutShopInfoRequest) => { - console.log(data); + const [selectedTimeList, setSelectedTimeList] = useState( + getTimeRange(openTime, closeTime), + ); + + const [selectedTagList, setSelectedTagList] = useState(tags); + + const handleTimeTableSelect = (key: string, value: boolean) => { + const updatedTimeList = { ...selectedTimeList, [key]: value }; + + // 연속적인 시간대인지 검증 + const { isCountinuous } = checkContinuousTime(updatedTimeList, ''); + + if (isCountinuous) { + setSelectedTimeList(updatedTimeList); + } else { + alert('연속된 시간을 선택해주세요.'); + } + }; + + const handleTagSelect = (selectedTag: string) => { + const set = new Set(selectedTagList); + if (typeof selectedTag === 'string') { + if (selectedTagList.includes(selectedTag)) { + set.delete(selectedTag); + console.log([...set]); + setSelectedTagList([...set]); + } else { + set.add(selectedTag); + if (set.size > 3) { + alert('태그는 최대 3개까지 선택 가능합니다.'); + return; + } + console.log([...set]); + setSelectedTagList([...set]); + } + } + }; + + useEffect(() => { + const { startTime, endTime } = getTimeStr(selectedTimeList); + setValue('openTime', startTime); + setValue('closeTime', endTime); + }, [selectedTimeList]); + + useEffect(() => { + setValue('tags', selectedTagList); + }, [selectedTagList]); + + const { mutateAsync } = UsePutShopInfo(); + + const handlePutShopInfo = async (data: PutShopInfoRequest) => { + // console.log(data); + await mutateAsync(data); + closeSheet(); }; return ( @@ -108,9 +186,11 @@ const ShopInfoBottomSheet = ({ isOpen={isOpen.time} setIsOpen={setIsOpen} > - - {openTime} ~ {closeTime} - + {/* 매장 태그 수정 */} @@ -119,12 +199,35 @@ const ShopInfoBottomSheet = ({ keyName="tags" isOpen={isOpen.tags} setIsOpen={setIsOpen} + gap={12} > - - {tags.map((tag) => ( - {tag} + + 최대 3개 선택 가능 + + + {TAG_LIST.map((tag, index) => ( + handleTagSelect(tag)} + bg={ + selectedTagList.includes(tag) + ? theme.palette.Black + : theme.palette.Gray_White + } + fontColor={ + selectedTagList.includes(tag) + ? theme.palette.White + : theme.palette.Black + } + border={`1px solid ${theme.palette.Gray100}`} + height="43px" + typo="Body2" + width="fit-content" + > + {tag} + ))} - + {/* 카카오톡 오픈채팅 링크 수정 */} @@ -153,7 +256,7 @@ const ShopInfoBottomSheet = ({ {/* 매장 한줄 소개 수정 */} - + + + + + 취소 + + + + + + ); @@ -187,4 +307,21 @@ const InputWrapper = styled(Flex)` const InputField = styled.input` width: 100%; `; + +const TagContainer = styled(Flex)` + flex-wrap: wrap; +`; + +const TagButton = styled(Button)` + flex-shrink: 0; +`; + +const CancleButton = styled(Button)` + flex-shrink: 0; +`; + +const ConfirmButton = styled.button` + flex-grow: 1; +`; + export default ShopInfoBottomSheet; diff --git a/apps/salon/src/components/my/shop/ShopInfoItem.tsx b/apps/salon/src/components/my/shop/ShopInfoItem.tsx index b9277fcd..f5897f3a 100644 --- a/apps/salon/src/components/my/shop/ShopInfoItem.tsx +++ b/apps/salon/src/components/my/shop/ShopInfoItem.tsx @@ -8,6 +8,7 @@ interface ShopInfoItemProps { isOpen: boolean; setIsOpen: React.Dispatch>; children?: React.ReactNode; + gap?: number; } const ShopInfoItem = ({ @@ -16,6 +17,7 @@ const ShopInfoItem = ({ isOpen, setIsOpen, children, + gap = 8, }: ShopInfoItemProps) => { /** item 열고 닫기 */ const handleToggle = () => { @@ -30,7 +32,7 @@ const ShopInfoItem = ({ direction="column" align="flex-start" padding="27px 16px" - gap={8} + gap={gap} backgroundColor={theme.palette.Gray_White} borderRadius={12} > diff --git a/apps/salon/src/components/quotation/ReplyForm.tsx b/apps/salon/src/components/quotation/ReplyForm.tsx index 3bb9c8eb..232dacf3 100644 --- a/apps/salon/src/components/quotation/ReplyForm.tsx +++ b/apps/salon/src/components/quotation/ReplyForm.tsx @@ -1,10 +1,17 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; -import { QuotationFormData, TimeType } from "@assets/types/quotation"; -import { Flex, ProfileImage, Text, theme, TimeTableGroomer, WidthFitFlex } from "@duri-fe/ui"; -import { RequestDetailResponse } from "@duri-fe/utils"; -import { defaultTimeList } from "@salon/assets/data/quotation"; -import { checkContinuousTime } from "@salon/utils/checkContinuousTime"; +import { QuotationFormData, TimeType } from '@assets/types/quotation'; +import { + Flex, + ProfileImage, + Text, + theme, + TimeTableGroomer, + WidthFitFlex, +} from '@duri-fe/ui'; +import { RequestDetailResponse } from '@duri-fe/utils'; +import { defaultTimeList } from '@salon/assets/data/quotation'; +import { checkContinuousTime } from '@salon/utils/checkContinuousTime'; const timeList = Array(10) .fill(0) @@ -24,12 +31,10 @@ const ReplyForm = ({ setIsValid, }: ReplyFormProps) => { const requestDay = request.quotationDetails.day; - const [reservationTimeList, setReservationTimeList] = useState(defaultTimeList); + const [reservationTimeList, setReservationTimeList] = + useState(defaultTimeList); - const handleReserve = ( - key: string, - value: boolean, - ) => { + const handleReserve = (key: string, value: boolean) => { setReservationTimeList((prev) => ({ ...prev, [key]: value, @@ -37,7 +42,8 @@ const ReplyForm = ({ }; useEffect(() => { - const { isEmpty, isCountinuous, startDateTime, endDateTime } = checkContinuousTime(reservationTimeList, requestDay); + const { isEmpty, isCountinuous, startDateTime, endDateTime } = + checkContinuousTime(reservationTimeList, requestDay); if (isEmpty) { setIsValid(false); @@ -54,44 +60,85 @@ const ReplyForm = ({ ...prev, startDateTime: startDateTime, endDateTime: endDateTime, - })) + })); setIsValid(true); - }, [reservationTimeList]) + }, [reservationTimeList]); return ( - {/** 미용사 */} - 디자이너 선택 - 담당 디자이너를 선택해주세요. - - + + 디자이너 선택 + + + 담당 디자이너를 선택해주세요. + + + {request?.groomer.name} - 희망 날짜 - 예약자분이 희망하는 예약 날짜에요. - + + 희망 날짜 + + + 예약자분이 희망하는 예약 날짜에요. + + {request?.quotationDetails.day} - 희망 예약 시간 - 예약자분이 희망하는 예약 시간이에요. - + + 희망 예약 시간 + + + 예약자분이 희망하는 예약 시간이에요. + + - 미용 제안 시간 - 희망 예약 시간을 보고 사장님이 미용가능한 시간을 지정해주세요. - + + 미용 제안 시간 + + + 희망 예약 시간을 보고 사장님이 미용가능한 시간을 지정해주세요. + + - ); -} + ); +}; -export default ReplyForm; \ No newline at end of file +export default ReplyForm; diff --git a/apps/salon/src/pages/My/Shop.tsx b/apps/salon/src/pages/My/Shop.tsx index e7c36fe4..9ece4c48 100644 --- a/apps/salon/src/pages/My/Shop.tsx +++ b/apps/salon/src/pages/My/Shop.tsx @@ -40,6 +40,7 @@ const MyShopPage = () => { closeTime: '', info: '', kakaoTalk: '', + rating: 0, tags: [], }, } = myShopInfo || {}; @@ -75,6 +76,8 @@ const MyShopPage = () => { tags={shopDetail.tags} info={shopDetail.info} kakaoTalk={shopDetail.kakaoTalk} + rating={shopDetail.rating} + reviewCnt={0} onEdit={onShopInfoEdit} setOnEdit={setOnShopInfoEdit} /> diff --git a/apps/salon/src/utils/checkContinuousTime.ts b/apps/salon/src/utils/checkContinuousTime.ts index 9debbc3a..21ad4091 100644 --- a/apps/salon/src/utils/checkContinuousTime.ts +++ b/apps/salon/src/utils/checkContinuousTime.ts @@ -1,10 +1,13 @@ -import { TimeType } from "@salon/assets/types/quotation"; +import { TimeType } from '@salon/assets/types/quotation'; /** 연속된 시간인지 확인 */ -export const checkContinuousTime = (reservationTimeList: TimeType, requestDay: string) => { +export const checkContinuousTime = ( + reservationTimeList: TimeType, + requestDay: string, +) => { const selectedTime = Object.keys(reservationTimeList).filter( - (key) => reservationTimeList[key as keyof TimeType] - ) + (key) => reservationTimeList[key as keyof TimeType], + ); if (selectedTime.length === 0) { return { isEmpty: true, startDateTime: '', endDateTime: '' }; @@ -30,4 +33,4 @@ export const checkContinuousTime = (reservationTimeList: TimeType, requestDay: s const endDateTime = `${requestDay}T${end.slice(4).padStart(2, '0')}:59:59Z`; return { isCountinuous, startDateTime, endDateTime }; -} \ No newline at end of file +}; diff --git a/apps/salon/src/utils/parseTimeRange.ts b/apps/salon/src/utils/parseTimeRange.ts new file mode 100644 index 00000000..1c774039 --- /dev/null +++ b/apps/salon/src/utils/parseTimeRange.ts @@ -0,0 +1,39 @@ +import { TimeType } from '@duri-fe/utils'; + +/** hh:mm ~ hh:mm -> TimeType 변경 */ +export const getTimeRange = (startTime: string, endTime: string): TimeType => { + const startHour = parseInt(startTime.split(':')[0]); + const endHour = parseInt(endTime.split(':')[0]); + const timeRange: TimeType = {} as TimeType; + + for (let i = 9; i <= 20; i++) { + const key = `time${i}` as keyof TimeType; + timeRange[key] = i >= startHour && i <= endHour; // 해당 시간대를 선택 상태로 설정 + } + + return timeRange; +}; + +/** TimeType -> hh:mm ~ hh:mm 변경 */ +export const getTimeStr = ( + timeList: TimeType, +): { + startTime: string; + endTime: string; +} => { + const selectedTime = Object.keys(timeList).filter( + (key) => timeList[key as keyof TimeType], + ); + + if (selectedTime.length === 0) { + return { startTime: '', endTime: '' }; + } + + const start = selectedTime[0] || ''; + const end = selectedTime[selectedTime.length - 1] || ''; + + const startTime = `${start.slice(4).padStart(2, '0')}:00`; + const endTime = `${end.slice(4)}:00`; + + return { startTime: startTime, endTime: endTime }; +}; diff --git a/packages/utils/src/apis/salon/hooks/my.ts b/packages/utils/src/apis/salon/hooks/my.ts index 8a0adc74..34e117af 100644 --- a/packages/utils/src/apis/salon/hooks/my.ts +++ b/packages/utils/src/apis/salon/hooks/my.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { BaseError } from '../../types'; import { @@ -48,8 +48,12 @@ export const UseGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { /** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ export const UsePutShopImage = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: (formData: FormData) => putShopImage(formData), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['getMyShopInfo'] }); + }, onError: (error) => { console.error(error); alert('샵 사진 등록에 실패했습니다.'); @@ -59,12 +63,16 @@ export const UsePutShopImage = () => { /** [PUT] /shop/profile 미용사 마이샵 정보 수정 */ export const UsePutShopInfo = () => { + const queryClient = useQueryClient(); return useMutation< PutShopInfoResponse['response'], Error, PutShopInfoRequest >({ mutationFn: (request: PutShopInfoRequest) => putShopInfo(request), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['getMyShopInfo'] }); + }, onError: (error) => { console.error(error); alert('샵 정보 수정에 실패했습니다.'); diff --git a/packages/utils/src/apis/types/my.ts b/packages/utils/src/apis/types/my.ts index 955c2fa3..e4e421bd 100644 --- a/packages/utils/src/apis/types/my.ts +++ b/packages/utils/src/apis/types/my.ts @@ -170,6 +170,7 @@ interface ShopDetailType { closeTime: string; info: string; kakaoTalk: string; + rating: number; tags: string[]; } From 173d9bff19e03656a3a6a205cbbe0c691cf54a82 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 16:03:25 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ReviewPreview.tsx | 51 +++++++++ .../src/components/my/shop/ShopReviewBox.tsx | 100 ++++++++++++++++++ apps/salon/src/pages/My/Shop.tsx | 39 ++----- packages/utils/src/apis/salon/hooks/my.ts | 28 ++++- packages/utils/src/apis/salon/my.ts | 14 ++- 5 files changed, 200 insertions(+), 32 deletions(-) create mode 100644 apps/salon/src/components/my/shop/ReviewPreview.tsx create mode 100644 apps/salon/src/components/my/shop/ShopReviewBox.tsx diff --git a/apps/salon/src/components/my/shop/ReviewPreview.tsx b/apps/salon/src/components/my/shop/ReviewPreview.tsx new file mode 100644 index 00000000..018a11cd --- /dev/null +++ b/apps/salon/src/components/my/shop/ReviewPreview.tsx @@ -0,0 +1,51 @@ +import { + Flex, + HeightFitFlex, + RatingStars, + Text, + WidthFitFlex, +} from '@duri-fe/ui'; +import { ShopReviewType } from '@duri-fe/utils'; + +import { ShopReviewBox } from './ShopReviewBox'; + +interface ReviewPreviewProps { + shopRating: number; + reviewCnt: number; + reviewData: ShopReviewType[]; +} + +const ReviewPreview = ({ + shopRating, + reviewCnt, + reviewData, +}: ReviewPreviewProps) => { + return ( + + + 리뷰 + + {shopRating} + + ({reviewCnt}) + + + {reviewData && reviewData.length > 0 ? ( + reviewData.map((review) => ( + + )) + ) : ( + + 아직 등록된 리뷰가 없습니다. + + )} + + ); +}; + +export default ReviewPreview; diff --git a/apps/salon/src/components/my/shop/ShopReviewBox.tsx b/apps/salon/src/components/my/shop/ShopReviewBox.tsx new file mode 100644 index 00000000..8f8cf078 --- /dev/null +++ b/apps/salon/src/components/my/shop/ShopReviewBox.tsx @@ -0,0 +1,100 @@ +import { + Flex, + HardText, + HeightFitFlex, + Image, + PetInfo, + RatingStars, + Text, + theme, + WidthFitFlex, +} from '@duri-fe/ui'; +import { ShopReviewType } from '@duri-fe/utils'; +import styled from '@emotion/styled'; + +export interface ShopReviewBoxProps { + review: ShopReviewType; +} + +export const ShopReviewBox = (props: ShopReviewBoxProps) => { + const { + review: { + userName, + userImageURL, + rating, + comment, + createdAt, + imgUrl, + petInfo: { + petId, + imageURL: petImage, + name: petName, + age: petAge, + breed: petBreed, + gender: petGender, + weight: petWeight, + neutering: petNeutering, + }, + }, + } = props; + + return ( + + + + + + + {userName} + + + + + + {createdAt} + + + {imgUrl && ( + + + + )} + + {comment} + + + + + + ); +}; + +const ShadowFlex = styled(HeightFitFlex)` + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.1); +`; diff --git a/apps/salon/src/pages/My/Shop.tsx b/apps/salon/src/pages/My/Shop.tsx index 9ece4c48..d5d1d9eb 100644 --- a/apps/salon/src/pages/My/Shop.tsx +++ b/apps/salon/src/pages/My/Shop.tsx @@ -2,9 +2,10 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Flex, Header, MobileLayout, SalonNavbar } from '@duri-fe/ui'; -import { UseGetMyShopInfo } from '@duri-fe/utils'; +import { useGetMyShopInfo, UseGetMyShopReviewList } from '@duri-fe/utils'; import styled from '@emotion/styled'; import DesignerInfoArea from '@salon/components/my/shop/DesignerInfoArea'; +import ReviewPreview from '@salon/components/my/shop/ReviewPreview'; import ShopImageArea from '@salon/components/my/shop/ShopImageArea'; import ShopInfoArea from '@salon/components/my/shop/ShopInfoArea'; @@ -15,7 +16,8 @@ const MyShopPage = () => { const [onShopInfoEdit, setOnShopInfoEdit] = useState(false); const [onDesignerInfoEdit, setOnDesignerInfoEdit] = useState(false); - const { data: myShopInfo } = UseGetMyShopInfo({}); + const { data: myShopInfo } = useGetMyShopInfo({}); + const { data: myShopReviewList } = UseGetMyShopReviewList({}); const { groomerProfileDetailResponse: groomerDetail = { @@ -52,7 +54,7 @@ const MyShopPage = () => { return (
- {myShopInfo && ( + {myShopInfo && myShopReviewList && ( <> { info={shopDetail.info} kakaoTalk={shopDetail.kakaoTalk} rating={shopDetail.rating} - reviewCnt={0} + reviewCnt={myShopReviewList.length} onEdit={onShopInfoEdit} setOnEdit={setOnShopInfoEdit} /> @@ -96,30 +98,11 @@ const MyShopPage = () => { /> {/**리뷰 */} - {/* - - 리뷰 - - {shopRating} - - ({reviewCnt}) - - - {reviewData && reviewData.length > 0 ? ( - reviewData.map((review) => ( - - )) - ) : ( - - 아직 등록된 리뷰가 없습니다. - - )} - */} + )} diff --git a/packages/utils/src/apis/salon/hooks/my.ts b/packages/utils/src/apis/salon/hooks/my.ts index 34e117af..d0aa2103 100644 --- a/packages/utils/src/apis/salon/hooks/my.ts +++ b/packages/utils/src/apis/salon/hooks/my.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { BaseError } from '../../types'; +import { BaseError, ShopReviewListResponse } from '../../types'; import { GetMyShopInfoResponse, GroomerAndShopProfileResponse, @@ -8,7 +8,12 @@ import { PutShopInfoResponse, } from '../../types/my'; import { UseQueryProps } from '../../types/tanstack'; -import { getGroomerInfo, getMyShopInfo, putShopInfo } from '../my'; +import { + getGroomerInfo, + getMyShopInfo, + getMyShopReviewList, + putShopInfo, +} from '../my'; import { putShopImage } from './../my'; @@ -38,7 +43,7 @@ type UseGetMyShopInfo = UseQueryProps< >; /** [GET] /groomer/profile 미용사 마이샵 */ -export const UseGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { +export const useGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { return useQuery({ queryKey: ['getMyShopInfo', ...(queryKey || [])], queryFn: () => getMyShopInfo(), @@ -46,6 +51,23 @@ export const UseGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { }); }; +type UseGetMyShopReviewList = UseQueryProps< + ShopReviewListResponse['response'], + BaseError +>; + +/** [GET] /shop/review 마이샵 리뷰 조회 */ +export const UseGetMyShopReviewList = ({ + queryKey, + options, +}: UseGetMyShopReviewList) => { + return useQuery({ + queryKey: ['getMyShopReviewList', ...(queryKey || [])], + queryFn: () => getMyShopReviewList(), + ...options, + }); +}; + /** [PUT] /shop/profile/image 미용사 마이샵 사진 수정 */ export const UsePutShopImage = () => { const queryClient = useQueryClient(); diff --git a/packages/utils/src/apis/salon/my.ts b/packages/utils/src/apis/salon/my.ts index c0ccb006..63c77e24 100644 --- a/packages/utils/src/apis/salon/my.ts +++ b/packages/utils/src/apis/salon/my.ts @@ -1,4 +1,8 @@ -import { publicInstance, salonInstance } from '@duri-fe/utils'; +import { + publicInstance, + salonInstance, + ShopReviewListResponse, +} from '@duri-fe/utils'; import { GetMyShopInfoResponse, @@ -45,3 +49,11 @@ export const putShopInfo = async ( const response = await salonInstance.put('shop/profile', request); return response.data.response; }; + +/** [GET] /shop/review 마이샵 리뷰 조회 */ +export const getMyShopReviewList = async (): Promise< + ShopReviewListResponse['response'] +> => { + const response = await salonInstance.get('shop/review'); + return response.data.response; +}; From 19b82d90280d76dc075994ad85d55864a1c44ed1 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 16:52:52 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20SalonNavb?= =?UTF-8?q?ar=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/salon/src/App.tsx | 2 + apps/salon/src/pages/My/Review.tsx | 46 +++++++++++++++++++ packages/ui/src/assets/Store.tsx | 26 +++++++---- packages/ui/src/assets/svgs/Store.svg | 11 +++++ packages/ui/src/components/Navbar/NavItem.tsx | 3 +- .../ui/src/components/Navbar/SalonNavbar.tsx | 22 ++++----- 6 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 apps/salon/src/pages/My/Review.tsx create mode 100644 packages/ui/src/assets/svgs/Store.svg diff --git a/apps/salon/src/App.tsx b/apps/salon/src/App.tsx index d0eb634c..19fab514 100644 --- a/apps/salon/src/App.tsx +++ b/apps/salon/src/App.tsx @@ -8,6 +8,7 @@ import AuthPage from '@pages/Auth'; import Home from '@pages/Home'; import LoginPage from '@pages/Login'; import MyPage from '@pages/My'; +import ReviewPage from '@pages/My/Review'; import MyShopPage from '@pages/My/Shop'; import OnboardingPage from '@pages/Onboarding'; import OnboardingPendingPage from '@pages/Onboarding/Pending'; @@ -51,6 +52,7 @@ function App() { element={} /> } /> + } /> diff --git a/apps/salon/src/pages/My/Review.tsx b/apps/salon/src/pages/My/Review.tsx new file mode 100644 index 00000000..13de75d5 --- /dev/null +++ b/apps/salon/src/pages/My/Review.tsx @@ -0,0 +1,46 @@ +import { useNavigate } from 'react-router-dom'; + +import { Header, MobileLayout, SalonNavbar } from '@duri-fe/ui'; + +const ReviewPage = () => { + const navigate = useNavigate(); + const handleClickBackButton = () => { + navigate('/my/review'); + }; + // const handleClickShopButton = (shopId: number) => { + // navigate(`/shop/${shopId}`); + // }; + + // reviewData가 존재하는 경우에만 처리 + // if (!reviewData) { + // return Loading...; // 데이터가 로딩 중일 경우 처리 + // } + + // const { + // userImageURL, + // userName, + // reviewId: reviewDataId, + // rating, + // createdAt, + // shopId, + // shopName, + // comment, + // imgUrl, + // petInfo, + // } = reviewData; + + return ( + +
+ + + + ); +}; + +export default ReviewPage; diff --git a/packages/ui/src/assets/Store.tsx b/packages/ui/src/assets/Store.tsx index f3209599..c86d21c9 100644 --- a/packages/ui/src/assets/Store.tsx +++ b/packages/ui/src/assets/Store.tsx @@ -2,18 +2,24 @@ import * as React from 'react'; const SvgStore = (props: React.SVGProps) => ( - - + + + + + + + + + ); export default SvgStore; diff --git a/packages/ui/src/assets/svgs/Store.svg b/packages/ui/src/assets/svgs/Store.svg new file mode 100644 index 00000000..34aa9e4e --- /dev/null +++ b/packages/ui/src/assets/svgs/Store.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/src/components/Navbar/NavItem.tsx b/packages/ui/src/components/Navbar/NavItem.tsx index 213a28bc..399dbdf0 100644 --- a/packages/ui/src/components/Navbar/NavItem.tsx +++ b/packages/ui/src/components/Navbar/NavItem.tsx @@ -15,7 +15,8 @@ interface NavItemProps { | 'diary' | 'my' | 'portfolio' - | 'timetable'; + | 'timetable' + | 'income'; children: ReactNode; } diff --git a/packages/ui/src/components/Navbar/SalonNavbar.tsx b/packages/ui/src/components/Navbar/SalonNavbar.tsx index caadaaf0..853edf56 100644 --- a/packages/ui/src/components/Navbar/SalonNavbar.tsx +++ b/packages/ui/src/components/Navbar/SalonNavbar.tsx @@ -5,8 +5,8 @@ import { MyIcon, PortfolioIcon, QuotationIcon, + Store, theme, - TimetableIcon, } from '@duri-fe/ui'; import styled from '@emotion/styled'; @@ -41,12 +41,12 @@ export const SalonNavbar = () => { handleNavigate('/timetable')} - iconType="timetable" + isActive={pathname.startsWith('/portfolio')} + text="포트폴리오" + onClick={() => handleNavigate('/portfolio')} + iconType="portfolio" > - + { handleNavigate('/portfolio')} - iconType="portfolio" + isActive={pathname.startsWith('/income')} + text="매출관리" + onClick={() => handleNavigate('/income')} + iconType="income" > - + Date: Wed, 18 Dec 2024 18:03:32 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ReviewUserInfo.tsx | 172 ++++++++++++++++++ apps/salon/src/pages/My/Review.tsx | 113 +++++++++--- packages/utils/src/apis/salon/hooks/my.ts | 4 +- packages/utils/src/apis/salon/my.ts | 4 +- packages/utils/src/apis/types/shop.ts | 16 ++ 5 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 apps/salon/src/components/my/shop/ReviewUserInfo.tsx diff --git a/apps/salon/src/components/my/shop/ReviewUserInfo.tsx b/apps/salon/src/components/my/shop/ReviewUserInfo.tsx new file mode 100644 index 00000000..7e8a19c3 --- /dev/null +++ b/apps/salon/src/components/my/shop/ReviewUserInfo.tsx @@ -0,0 +1,172 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { + Button, + Flex, + Menu, + Modal, + ProfileImage, + RatingStars, + Text, + theme, + WidthFitFlex, +} from '@duri-fe/ui'; +import { useDeleteReview, useModal } from '@duri-fe/utils'; +import styled from '@emotion/styled'; + +interface ReviewUserInfoProps { + reviewId: number; + userImageURL: string; + userName: string; + rating: number; + createdAt: string; +} + +export const ReviewUserInfo = ({ + reviewId, + createdAt, + rating, + userImageURL, + userName, +}: ReviewUserInfoProps) => { + const navigate = useNavigate(); + + const { isOpenModal, toggleModal } = useModal(); + + const [isOpen, setIsOpen] = useState(false); + + const { mutateAsync: deleteReview } = useDeleteReview(() => { + navigate('/my/review', { replace: true }); + }); + + const handleClickMenu = () => { + setIsOpen(!isOpen); + }; + + const handleClickModifyButton = () => { + navigate('/my/review/modify', { state: reviewId }); + }; + + const handleClickDeleteButton = () => { + //삭제 모달 띄우기 + toggleModal(); + }; + + const handleClickDeleteConfirmButton = () => { + deleteReview(reviewId); + }; + + return ( + + {/* 사용자 프로필, 평점 */} + + + + {userName} + + + + + + + {/* 오른쪽 버튼, 작성일자 */} + + + {createdAt} + + + + + + {isOpen && ( + + + 수정하기 + + + 삭제하기 + + + )} + {isOpenModal && ( + + + + + 후기 삭제 후 + + + 복구할 수 없습니다. + + + + + + + + + )} + + ); +}; + +const Wrapper = styled(Flex)` + position: relative; +`; +const MenuCard = styled(Flex)` + position: absolute; + top: 37.4px; + right: 9px; + background-color: ${theme.palette.White}; + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.1); +`; +const SingleLineText = styled(Text)` + word-break: no-wrap; +`; + +const MenuItem = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + cursor: pointer; + padding: 0 10px; // 좌우 여백을 추가하여 텍스트가 너무 붙지 않도록 조정 + &:hover { + background-color: ${theme.palette.Gray_White}; + } +`; diff --git a/apps/salon/src/pages/My/Review.tsx b/apps/salon/src/pages/My/Review.tsx index 13de75d5..1581b8f4 100644 --- a/apps/salon/src/pages/My/Review.tsx +++ b/apps/salon/src/pages/My/Review.tsx @@ -1,33 +1,30 @@ import { useNavigate } from 'react-router-dom'; -import { Header, MobileLayout, SalonNavbar } from '@duri-fe/ui'; +import { + Card, + Flex, + Header, + Image, + MobileLayout, + PetInfo, + SalonNavbar, + Text, +} from '@duri-fe/ui'; +import { UseGetMyShopReviewList } from '@duri-fe/utils'; +import { ReviewUserInfo } from '@salon/components/my/shop/ReviewUserInfo'; const ReviewPage = () => { const navigate = useNavigate(); - const handleClickBackButton = () => { - navigate('/my/review'); - }; - // const handleClickShopButton = (shopId: number) => { - // navigate(`/shop/${shopId}`); - // }; + const { data: reviewData } = UseGetMyShopReviewList({}); // reviewData가 존재하는 경우에만 처리 - // if (!reviewData) { - // return Loading...; // 데이터가 로딩 중일 경우 처리 - // } - - // const { - // userImageURL, - // userName, - // reviewId: reviewDataId, - // rating, - // createdAt, - // shopId, - // shopName, - // comment, - // imgUrl, - // petInfo, - // } = reviewData; + if (!reviewData) { + return Loading...; // 데이터가 로딩 중일 경우 처리 + } + + const handleClickBackButton = () => { + navigate(-1); + }; return ( @@ -37,7 +34,77 @@ const ReviewPage = () => { titleAlign="start" onClickBack={handleClickBackButton} /> + {reviewData && + reviewData.map( + ({ + reviewId, + userImageURL, + userName, + rating, + createdAt, + imgUrl, + comment, + petInfo, + }) => ( + + + {/* 사용자 프로필 + 작성일자 + 버튼 */} + + + + + {/* 리뷰 사진 */} + {imgUrl && ( + 리뷰 사진 + )} + + + {/* 리뷰 텍스트 */} + {comment} + + {/* 펫 정보 */} + {petInfo && ( + + )} + + + ), + )} ); diff --git a/packages/utils/src/apis/salon/hooks/my.ts b/packages/utils/src/apis/salon/hooks/my.ts index d0aa2103..26ff712a 100644 --- a/packages/utils/src/apis/salon/hooks/my.ts +++ b/packages/utils/src/apis/salon/hooks/my.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { BaseError, ShopReviewListResponse } from '../../types'; +import { BaseError, MyShopReviewListResponse } from '../../types'; import { GetMyShopInfoResponse, GroomerAndShopProfileResponse, @@ -52,7 +52,7 @@ export const useGetMyShopInfo = ({ queryKey, options }: UseGetMyShopInfo) => { }; type UseGetMyShopReviewList = UseQueryProps< - ShopReviewListResponse['response'], + MyShopReviewListResponse['response'], BaseError >; diff --git a/packages/utils/src/apis/salon/my.ts b/packages/utils/src/apis/salon/my.ts index 63c77e24..01c3cc0b 100644 --- a/packages/utils/src/apis/salon/my.ts +++ b/packages/utils/src/apis/salon/my.ts @@ -1,7 +1,7 @@ import { + MyShopReviewListResponse, publicInstance, salonInstance, - ShopReviewListResponse, } from '@duri-fe/utils'; import { @@ -52,7 +52,7 @@ export const putShopInfo = async ( /** [GET] /shop/review 마이샵 리뷰 조회 */ export const getMyShopReviewList = async (): Promise< - ShopReviewListResponse['response'] + MyShopReviewListResponse['response'] > => { const response = await salonInstance.get('shop/review'); return response.data.response; diff --git a/packages/utils/src/apis/types/shop.ts b/packages/utils/src/apis/types/shop.ts index 9fffcfec..a330d2bb 100644 --- a/packages/utils/src/apis/types/shop.ts +++ b/packages/utils/src/apis/types/shop.ts @@ -99,3 +99,19 @@ export interface ShopReviewType { export interface ShopReviewListResponse extends BaseResponse { response: ShopReviewType[]; } + +export interface MyShopReviewType { + userId: number; + userName: string; + userImageURL: string; + reviewId: number; + rating: number; + comment: string; + createdAt: string; + imgUrl: string; + petInfo: PetDetail; +} + +export interface MyShopReviewListResponse extends BaseResponse { + response: MyShopReviewType[]; +} From 3e767634b47d377f6070f7a0efbfa5b05f681f29 Mon Sep 17 00:00:00 2001 From: seungboshim Date: Wed, 18 Dec 2024 19:59:52 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=EB=A7=A4=EC=9E=A5=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=20=EB=A9=94=EC=9D=B8=ED=99=94=EB=A9=B4=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/my/shop/ReviewPreview.tsx | 4 +-- .../src/components/my/shop/ShopReviewBox.tsx | 4 +-- apps/salon/src/pages/My/index.tsx | 28 +++++++-------- ....timestamp-1734462252624-4189b1039114a.mjs | 33 ----------------- .../src/components/Portfolio/DesignerInfo.tsx | 35 ++++++++----------- .../components/Portfolio/GroomerPortfolio.tsx | 29 +++++++-------- packages/utils/src/apis/config/instances.ts | 6 ++++ packages/utils/src/apis/types/my.ts | 6 ++-- 8 files changed, 56 insertions(+), 89 deletions(-) delete mode 100644 apps/salon/vite.config.ts.timestamp-1734462252624-4189b1039114a.mjs diff --git a/apps/salon/src/components/my/shop/ReviewPreview.tsx b/apps/salon/src/components/my/shop/ReviewPreview.tsx index 018a11cd..9753a4a9 100644 --- a/apps/salon/src/components/my/shop/ReviewPreview.tsx +++ b/apps/salon/src/components/my/shop/ReviewPreview.tsx @@ -5,14 +5,14 @@ import { Text, WidthFitFlex, } from '@duri-fe/ui'; -import { ShopReviewType } from '@duri-fe/utils'; +import { MyShopReviewType } from '@duri-fe/utils'; import { ShopReviewBox } from './ShopReviewBox'; interface ReviewPreviewProps { shopRating: number; reviewCnt: number; - reviewData: ShopReviewType[]; + reviewData: MyShopReviewType[]; } const ReviewPreview = ({ diff --git a/apps/salon/src/components/my/shop/ShopReviewBox.tsx b/apps/salon/src/components/my/shop/ShopReviewBox.tsx index 8f8cf078..398a310a 100644 --- a/apps/salon/src/components/my/shop/ShopReviewBox.tsx +++ b/apps/salon/src/components/my/shop/ShopReviewBox.tsx @@ -9,11 +9,11 @@ import { theme, WidthFitFlex, } from '@duri-fe/ui'; -import { ShopReviewType } from '@duri-fe/utils'; +import { MyShopReviewType } from '@duri-fe/utils'; import styled from '@emotion/styled'; export interface ShopReviewBoxProps { - review: ShopReviewType; + review: MyShopReviewType; } export const ShopReviewBox = (props: ShopReviewBoxProps) => { diff --git a/apps/salon/src/pages/My/index.tsx b/apps/salon/src/pages/My/index.tsx index e15dc651..a3b6bc89 100644 --- a/apps/salon/src/pages/My/index.tsx +++ b/apps/salon/src/pages/My/index.tsx @@ -11,34 +11,29 @@ import { theme, Write, } from '@duri-fe/ui'; -import { UseGetGroomerInfo } from '@duri-fe/utils'; +import { useGetMyShopInfo } from '@duri-fe/utils'; import { GroomerInfoType, - ShopInfoType, + ShopProfileDetailType, } from '@duri-fe/utils/src/apis/types/my'; import styled from '@emotion/styled'; import { OwnerInfo } from '@salon/components/my/info/OwnerInfo'; import { ShopInfoCard } from '@salon/components/my/info/ShopInfoCard'; import { Status } from '@salon/components/my/info/Status'; -import useGroomerStore from '@salon/stores/groomerStore'; const MyPage = () => { const navigate = useNavigate(); const handleNavigate = (path: string) => navigate(path); - const groomerId = useGroomerStore((state) => state.groomerId); - - const [shopProfile, setShopProfile] = useState(); + const [shopProfile, setShopProfile] = useState(); const [groomerProfile, setGroomerProfile] = useState(); - const { data } = UseGetGroomerInfo({ - groomerId: groomerId, - }); + const { data } = useGetMyShopInfo({}); useEffect(() => { if (data) { - setGroomerProfile(data.groomerProfileDetail); - setShopProfile(data.shopProfileDetail); + setShopProfile(data.shopProfileDetailResponse); + setGroomerProfile(data.groomerProfileDetailResponse); } }, [data]); @@ -60,7 +55,7 @@ const MyPage = () => { margin="0 0 100px 0" justify="flex-start" > - {shopProfile && groomerProfile ? ( + {data && shopProfile && groomerProfile ? ( <> { image={groomerProfile.image} /> - + { gap={5} onClick={() => handleNavigate('/my/income')} > - + 매출관리 { gap={5} onClick={() => handleNavigate('/my/review')} > - + 후기관리 diff --git a/apps/salon/vite.config.ts.timestamp-1734462252624-4189b1039114a.mjs b/apps/salon/vite.config.ts.timestamp-1734462252624-4189b1039114a.mjs deleted file mode 100644 index da2dd990..00000000 --- a/apps/salon/vite.config.ts.timestamp-1734462252624-4189b1039114a.mjs +++ /dev/null @@ -1,33 +0,0 @@ -// vite.config.ts -import react from "file:///Users/binary_ro/Documents/Github/Duri-FE/.yarn/__virtual__/@vitejs-plugin-react-virtual-4e7212b94d/4/.yarn/berry/cache/@vitejs-plugin-react-npm-4.3.4-e5f654de44-10c0.zip/node_modules/@vitejs/plugin-react/dist/index.mjs"; -import { defineConfig } from "file:///Users/binary_ro/Documents/Github/Duri-FE/.yarn/__virtual__/vite-virtual-4c119f49ef/4/.yarn/berry/cache/vite-npm-5.4.11-9da365ef2b-10c0.zip/node_modules/vite/dist/node/index.js"; -var vite_config_default = defineConfig({ - plugins: [react()], - resolve: { - alias: [ - { find: "@salon", replacement: "/src" }, - { find: "@pages", replacement: "/src/pages" }, - { find: "@components", replacement: "/src/components" }, - { find: "@styles", replacement: "/src/styles" }, - { find: "@assets", replacement: "/src/assets" }, - { find: "@mocks", replacement: "/src/mocks" }, - { find: "@utils", replacement: "/src/utils" }, - { find: "@types", replacement: "/src/types" }, - { find: "@hooks", replacement: "/src/hooks" } - ] - }, - server: { - port: 3001, - proxy: { - "/naver-api": { - target: "https://naveropenapi.apigw.ntruss.com", - changeOrigin: true, - rewrite: (path) => path.replace(/^\/naver-api/, "") - } - } - } -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYmluYXJ5X3JvL0RvY3VtZW50cy9HaXRodWIvRHVyaS1GRS9hcHBzL3NhbG9uXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYmluYXJ5X3JvL0RvY3VtZW50cy9HaXRodWIvRHVyaS1GRS9hcHBzL3NhbG9uL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9iaW5hcnlfcm8vRG9jdW1lbnRzL0dpdGh1Yi9EdXJpLUZFL2FwcHMvc2Fsb24vdml0ZS5jb25maWcudHNcIjtpbXBvcnQgcmVhY3QgZnJvbSAnQHZpdGVqcy9wbHVnaW4tcmVhY3QnO1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpXSxcbiAgcmVzb2x2ZToge1xuICAgIGFsaWFzOiBbXG4gICAgICB7IGZpbmQ6ICdAc2Fsb24nLCByZXBsYWNlbWVudDogJy9zcmMnIH0sXG4gICAgICB7IGZpbmQ6ICdAcGFnZXMnLCByZXBsYWNlbWVudDogJy9zcmMvcGFnZXMnIH0sXG4gICAgICB7IGZpbmQ6ICdAY29tcG9uZW50cycsIHJlcGxhY2VtZW50OiAnL3NyYy9jb21wb25lbnRzJyB9LFxuICAgICAgeyBmaW5kOiAnQHN0eWxlcycsIHJlcGxhY2VtZW50OiAnL3NyYy9zdHlsZXMnIH0sXG4gICAgICB7IGZpbmQ6ICdAYXNzZXRzJywgcmVwbGFjZW1lbnQ6ICcvc3JjL2Fzc2V0cycgfSxcbiAgICAgIHsgZmluZDogJ0Btb2NrcycsIHJlcGxhY2VtZW50OiAnL3NyYy9tb2NrcycgfSxcbiAgICAgIHsgZmluZDogJ0B1dGlscycsIHJlcGxhY2VtZW50OiAnL3NyYy91dGlscycgfSxcbiAgICAgIHsgZmluZDogJ0B0eXBlcycsIHJlcGxhY2VtZW50OiAnL3NyYy90eXBlcycgfSxcbiAgICAgIHsgZmluZDogJ0Bob29rcycsIHJlcGxhY2VtZW50OiAnL3NyYy9ob29rcycgfSxcbiAgICBdLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBwb3J0OiAzMDAxLFxuICAgIHByb3h5OiB7XG4gICAgICAnL25hdmVyLWFwaSc6IHtcbiAgICAgICAgdGFyZ2V0OiAnaHR0cHM6Ly9uYXZlcm9wZW5hcGkuYXBpZ3cubnRydXNzLmNvbScsXG4gICAgICAgIGNoYW5nZU9yaWdpbjogdHJ1ZSxcbiAgICAgICAgcmV3cml0ZTogKHBhdGgpID0+IHBhdGgucmVwbGFjZSgvXlxcL25hdmVyLWFwaS8sICcnKSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE4VSxPQUFPLFdBQVc7QUFDaFcsU0FBUyxvQkFBb0I7QUFFN0IsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sQ0FBQztBQUFBLEVBQ2pCLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLEVBQUUsTUFBTSxVQUFVLGFBQWEsT0FBTztBQUFBLE1BQ3RDLEVBQUUsTUFBTSxVQUFVLGFBQWEsYUFBYTtBQUFBLE1BQzVDLEVBQUUsTUFBTSxlQUFlLGFBQWEsa0JBQWtCO0FBQUEsTUFDdEQsRUFBRSxNQUFNLFdBQVcsYUFBYSxjQUFjO0FBQUEsTUFDOUMsRUFBRSxNQUFNLFdBQVcsYUFBYSxjQUFjO0FBQUEsTUFDOUMsRUFBRSxNQUFNLFVBQVUsYUFBYSxhQUFhO0FBQUEsTUFDNUMsRUFBRSxNQUFNLFVBQVUsYUFBYSxhQUFhO0FBQUEsTUFDNUMsRUFBRSxNQUFNLFVBQVUsYUFBYSxhQUFhO0FBQUEsTUFDNUMsRUFBRSxNQUFNLFVBQVUsYUFBYSxhQUFhO0FBQUEsSUFDOUM7QUFBQSxFQUNGO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsSUFDTixPQUFPO0FBQUEsTUFDTCxjQUFjO0FBQUEsUUFDWixRQUFRO0FBQUEsUUFDUixjQUFjO0FBQUEsUUFDZCxTQUFTLENBQUMsU0FBUyxLQUFLLFFBQVEsZ0JBQWdCLEVBQUU7QUFBQSxNQUNwRDtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/packages/ui/src/components/Portfolio/DesignerInfo.tsx b/packages/ui/src/components/Portfolio/DesignerInfo.tsx index 44fc3f1f..38282e9d 100644 --- a/packages/ui/src/components/Portfolio/DesignerInfo.tsx +++ b/packages/ui/src/components/Portfolio/DesignerInfo.tsx @@ -78,30 +78,25 @@ export const DesignerInfo = ({ )} - - {name} - + + {name ?? '정보없음'} {`경력 ${historyStr(experience)}, ${age}세, ${gender}`} - - - {roles.map((item, idx) => ( - - - {item} - - - - ))} - - + {roles.length > 0 && ( + + {roles.map((item, idx) => ( + + + {item} + + + + ))} + + )} + ); }; diff --git a/packages/ui/src/components/Portfolio/GroomerPortfolio.tsx b/packages/ui/src/components/Portfolio/GroomerPortfolio.tsx index 2c88f4fd..ed49130c 100644 --- a/packages/ui/src/components/Portfolio/GroomerPortfolio.tsx +++ b/packages/ui/src/components/Portfolio/GroomerPortfolio.tsx @@ -83,20 +83,21 @@ export const GroomerPortfolio = ({ groomerId }: { groomerId: number }) => { - {groomerProfile && ( - - )} - + + {groomerProfile && ( + + )} + {groomerId && } diff --git a/packages/utils/src/apis/config/instances.ts b/packages/utils/src/apis/config/instances.ts index 7ec5a0af..14139b76 100644 --- a/packages/utils/src/apis/config/instances.ts +++ b/packages/utils/src/apis/config/instances.ts @@ -58,6 +58,12 @@ duriInstance.interceptors.request.use((config) => { return config; }); +AIInstance.interceptors.request.use((config) => { + const token = localStorage.getItem('authorization_user'); + config.headers['authorization_user'] = token ? `Bearer ${token}` : ''; + return config; +}); + salonInstance.interceptors.request.use((config) => { const token = localStorage.getItem('authorization_shop'); config.headers['authorization_shop'] = token ? `Bearer ${token}` : ''; diff --git a/packages/utils/src/apis/types/my.ts b/packages/utils/src/apis/types/my.ts index e4e421bd..ac50a051 100644 --- a/packages/utils/src/apis/types/my.ts +++ b/packages/utils/src/apis/types/my.ts @@ -156,11 +156,11 @@ export interface GetMyShopInfoResponse extends BaseResponse { groomerProfileDetailResponse: GroomerInfoType; reservationCount: number; noShowCount: number; - shopProfileDetailResponse: ShopDetailType; + shopProfileDetailResponse: ShopProfileDetailType; }; } -interface ShopDetailType { +export interface ShopProfileDetailType { id: number; name: string; address: string; @@ -176,7 +176,7 @@ interface ShopDetailType { /** [PUT] /shop/profile, /shop/profile/image 미용사 마이샵 수정 */ export interface PutShopInfoResponse extends BaseResponse { - response: ShopDetailType; + response: ShopProfileDetailType; } /** [PUT] /shop/profile 미용사 마이샵 정보 수정 */