-
+
11월 콘테스트 우승자
-
+
{
label="우승 미용사 프로필"
backgroundColor="primary"
size="medium"
+ onClick={() =>
+ navigate(
+ paths.salonProfile.replace(':id', winner.grommerProfileId)
+ )
+ }
/>
@@ -60,10 +91,10 @@ const Home = () => {
우리 동네 추천 반려견 미용사
- {groomers.map((groomer, index) => (
+ {localGroomers.map((groomer, index) => (
))}
@@ -101,10 +132,10 @@ const Home = () => {
전국 인기 반려견 미용사
- {groomers.map((groomer, index) => (
+ {popularGroomers.map((groomer, index) => (
))}
diff --git a/src/pages/NewReview.jsx b/src/pages/NewReview.jsx
index f052932..03c1d91 100644
--- a/src/pages/NewReview.jsx
+++ b/src/pages/NewReview.jsx
@@ -3,9 +3,9 @@ import { Box, Typography } from '@mui/material';
import { useState } from 'react';
import Button from '@components/Common/Button/Button';
import TextArea from '@components/Common/TextArea/TextArea';
-import axios from 'axios';
import { Toaster } from 'react-hot-toast';
import ImageSelector from '@components/Features/ImageSelector';
+import { postReview } from '@/api/review';
const NewReview = () => {
const [data, setData] = useState({
@@ -15,7 +15,7 @@ const NewReview = () => {
});
const MAX_IMAGES = 3;
- const totalStars = 5;
+ const TOTAL_STARS = 5;
const [userRating, setUserRating] = useState(0);
const [hoverRating, setHoverRating] = useState(0);
@@ -26,7 +26,7 @@ const NewReview = () => {
const renderStars = () => {
const stars = [];
- for (let i = 1; i <= totalStars; i++) {
+ for (let i = 1; i <= TOTAL_STARS; i++) {
const isFilled = i <= (hoverRating || userRating);
stars.push(
{
setData((prev) => ({ ...prev, [field]: value }));
};
- const handleSubmit = () => {
- const userId = 2;
- // const accessToken ='';
-
- axios
- .post(`http://localhost:8080/api/reviews/${userId}`, data, {
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- // Authorization: `Bearer ${accessToken}`,
- },
- })
- .then((response) => {
- console.log('Review submitted successfully:', response.data);
- })
- .catch((error) => {
- console.error('Error submitting review:', error);
- });
+ const handleSubmit = async () => {
+ const groomerId = 4; //TODO: 변수로 대체하기
+ await postReview(data, groomerId);
};
return (
@@ -111,16 +96,17 @@ const NewReview = () => {
- setData((prev) => ({ ...prev, imageKey: updatedImages }))
- }
+ onChange={(updatedImages) => {
+ handleChange('imageKey', updatedImages);
+ }}
/>
diff --git a/src/pages/Notification.jsx b/src/pages/Notification.jsx
index d0f5040..b12bed4 100644
--- a/src/pages/Notification.jsx
+++ b/src/pages/Notification.jsx
@@ -1,64 +1,132 @@
import { DetailHeader } from '@components/Common/DetailHeader/DetailHeader';
-import { Box, Typography, Badge } from '@mui/material';
+import { Box, Typography, Badge, Switch } from '@mui/material';
import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded';
import { useNavigate } from 'react-router-dom';
import EmptyContent from '@components/Layout/EmptyContent';
+import { useEffect, useState } from 'react';
+import {
+ deleteAll,
+ getNotification,
+ markAsRead,
+ updateSetting,
+} from '@/api/notification';
+import { Modal } from '@components/Common/Modal/Modal';
+import {
+ handleEnableNotifications,
+ notificationServiceInstance,
+} from '@/utils/NotificationService';
+import useUserStore from '@/store/useUserStore';
+import paths from '@/routes/paths';
const Notification = () => {
const navigate = useNavigate();
+ const [notifications, setNotifications] = useState([]);
+ const { notificationEnabled, setNotificationEnabled } = useUserStore();
- //TODO: API 명세서 업데이트 되면 대상 추가하기
- const notifications = [
- {
- description: '새 견적이 도착했습니다.',
- link: '/quotation/123',
- status: 'NOT_READ',
- },
- {
- description: '리뷰를 작성해 주세요.',
- link: '/newreview',
- status: 'NOT_READ',
- },
- ];
+ useEffect(() => {
+ const getList = async () => {
+ const res = await getNotification();
+ setNotifications(res);
+ };
+
+ getList();
+
+ const handleIncomingNotification = (newNotification) => {
+ setNotifications((prev) => [newNotification, ...prev]);
+ };
+
+ notificationServiceInstance.listenForMessages(handleIncomingNotification);
+
+ return () => {
+ notificationServiceInstance.callbacks =
+ notificationServiceInstance.callbacks.filter(
+ (cb) => cb !== handleIncomingNotification
+ );
+ };
+ }, []);
+
+ const handleDeleteAll = async () => {
+ await deleteAll();
+ setNotifications([]);
+ };
+
+ const handleNotificationChange = async () => {
+ if (await updateSetting(!notificationEnabled))
+ setNotificationEnabled(!notificationEnabled);
+
+ if (!notificationEnabled) {
+ await notificationServiceInstance.registerServiceWorker();
+ await handleEnableNotifications();
+ } else {
+ await notificationServiceInstance.unsubscribeFromNotifications();
+ await notificationServiceInstance.unregisterServiceWorker();
+ }
+ };
return (
- {notifications.length ? (
-
- {notifications.map((notification, index) => (
- navigate(notification.link)}
- >
+
+
+
+ 알림 허용
+
+
+ {notifications.length ? (
+
+
+
+ ) : (
+ <>>
+ )}
+
+
+ {notifications.length ? (
+
+ {notifications.map((notification, index) => (
{
+ if (await markAsRead(notification.id)) {
+ navigate(paths.chat); //TODO: link to chatroom
+ }
+ }}
>
-
-
- {notification.description}
-
-
-
+
+
+ {notification.title}
+
+ {notification.body}
+
+
+
-
- ))}
-
- ) : (
-
- )}
+ ))}
+
+ ) : (
+
+ )}
+
);
};
diff --git a/src/pages/SalonProfile.jsx b/src/pages/SalonProfile.jsx
index 309a9f5..89a83b5 100644
--- a/src/pages/SalonProfile.jsx
+++ b/src/pages/SalonProfile.jsx
@@ -6,47 +6,71 @@ import WorkspacePremiumRoundedIcon from '@mui/icons-material/WorkspacePremiumRou
import ReviewStars from '@components/Features/ReviewStars';
import { useNavigate } from 'react-router-dom';
import Grid from '@mui/material/Grid2';
+import React, { useEffect, useState } from 'react';
+import { groomerPublicProfile } from '@/api/groomerProfile';
+import paths from '@/routes/paths';
const SalonProfile = () => {
const navigate = useNavigate();
+ const [data, setData] = useState({});
+ const [detail, setDetail] = useState({});
- const data = {
- name: '펫살롱 포미',
- experienceYears: '15',
- description:
- '펫살롱 포미는 강아지의 건강과 행복을 우선으로 생각하는 반려견 전문 미용실입니다. 오랜 경력과 다양한 자격을 갖춘 미용사가 고객님의 소중한 반려견에게 맞춤형 미용 서비스를 제공합니다. 피부 상태, 털의 특성, 기질 등을 고려하여 강아지의 스트레스를 최소화하며 편안한 미용 경험을 선사합니다. 기본 미용 외에도 건강 체크와 피부 관리, 맞춤형 스타일링까지 반려견에게 필요한 모든 서비스를 준비해 두었습니다.',
- phone: '010-1111-2222',
- contactHours: '평일 오전 10시 ~ 오후 7시',
- serviceLocation: '서울특별시 성동구, 강남구',
- serviceType: 'ANY',
- services: '목욕, 털 미용, 발톱관리, 피부 미용, 양치, 귀 청소',
- certification: [
- '애견미용사 자격증 1급',
- '반려동물 행동 상담사 2급',
- '반려견 피부 관리사 자격증',
- ],
- businessNumber: '123-45-67890',
- address: '서울특별시 강남구 역삼동 123-45',
- faq: '1. 처음 방문하는데, 제 반려견에게 맞는 서비스는 어떻게 알 수 있나요? \n 반려견의 상태(피부, 털 상태, 성격 등)에 따라 적합한 서비스를 추천드립니다. 방문 시 상담을 통해 최적의 옵션을 안내해 드립니다.\n\n 2. 미용 시간이 얼마나 걸리나요? \n 서비스 내용과 반려견의 크기 및 상태에 따라 다르지만, 보통: \n 소형견: 1.5~2시간 \n 중형견: 2~3시간 \n 대형견: 3시간 이상자세한 시간은 상담 후 알려드립니다.',
- averageReview: 3.5,
- };
+ const defaultImgPath = '/images/default-groomer-profile.png';
+ const imageSrc = data.imageKey ? data.imageKey : defaultImgPath;
+ const imageStyle = data.imageKey
+ ? {
+ borderRadius: '50%',
+ objectFit: 'cover',
+ border: '2px solid',
+ borderColor: '#9747FF',
+ }
+ : {};
+
+ useEffect(() => {
+ const currentPath = window.location.pathname;
+ const id = currentPath.split('/').pop();
+
+ const getGroomerProfile = async () => {
+ const res = await groomerPublicProfile(id);
+ setData(res);
+ setDetail(res.groomerProfileDetailsInfoResponseDto);
+ };
+
+ getGroomerProfile();
+ }, []);
return (
-
+
- {data.name}
+ {data?.name}
-
+ {!isNaN(detail.starScore) && (
+ <>
+
+
+
+
+ >
+ )}
- {data.description}
+ {data?.description}
{
flexDirection="column"
sx={{
cursor: 'pointer',
+ '&:hover': { color: 'secondary.main' },
}}
>
-
+
결제
- 11
+ {detail?.estimateRequestCount}
@@ -102,58 +124,74 @@ const SalonProfile = () => {
flexDirection="column"
sx={{
cursor: 'pointer',
+ '&:hover': { color: 'secondary.main' },
}}
+ onClick={() =>
+ navigate(paths.salonReviews, {
+ state: { profileId: data.profileId },
+ })
+ }
>
-
+
리뷰
- 10
+ {data?.reviewCount || 0}
- {data.experienceYears}년 경력
+ {data?.experience}
📞전화번호:
- {data.phone}
+ {data?.phone}
🧑연락 가능 시간:
- {data.contactHours}
+ {data?.contactHours}
📍서비스 지역:
- {data.serviceLocation}
+
+ {detail?.servicesDistricts?.map((item, index) => (
+
+ {item.city} {item.district}
+
+ ))}
+
🚙서비스 형태:
- {data.serviceType == 'VISIT'
+ {data?.serviceType == 'VISIT'
? '방문'
- : data.serviceType == 'SHOP'
+ : data?.serviceType == 'SHOP'
? '매장'
: '방문, 매장'}
✂제공 서비스:
- {data.services}
+
+ {detail?.servicesOffered?.map((item, index) => (
+
+ {item}
+ {index < detail.servicesOffered.length - 1 && ', '}
+
+ ))}
+
🪪자격증:
- {data.certification.map((cert, index) => {
+ {detail?.certifications?.map((cert, index) => {
return {cert};
})}
💼사업자 번호:
- {data.businessNumber}
+ {data?.businessNumber}
📍가게 위치 정보:
- {data.address}
+ {data?.address}
FAQ
- {data.faq}
+ {data?.faq}
diff --git a/src/pages/SalonReviews.jsx b/src/pages/SalonReviews.jsx
new file mode 100644
index 0000000..ca8812d
--- /dev/null
+++ b/src/pages/SalonReviews.jsx
@@ -0,0 +1,48 @@
+import { myReviews, receivedReviews } from '@/api/review';
+import useUserStore from '@/store/useUserStore';
+import { DetailHeader } from '@components/Common/DetailHeader/DetailHeader';
+import ReviewAccordion from '@components/Features/ReviewAccordion';
+import EmptyContent from '@components/Layout/EmptyContent';
+import { Box, Typography } from '@mui/material';
+import { useState, useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+const SalonReviews = () => {
+ const location = useLocation();
+ const [id, setId] = useState(location.state?.profileId);
+ const [allReviews, setAllReviews] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const getReviews = async () => {
+ const res = await receivedReviews(id);
+ setAllReviews(res);
+ setLoading(false);
+ };
+ getReviews();
+ }, []);
+
+ if (loading) return
LOADING;
+
+ return (
+
+
+
+ {!allReviews.length ? (
+
+ ) : (
+
+ {allReviews?.map((review) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default SalonReviews;
diff --git a/src/pages/Survey/AddDogProfile.jsx b/src/pages/Survey/AddDogProfile.jsx
new file mode 100644
index 0000000..d442f43
--- /dev/null
+++ b/src/pages/Survey/AddDogProfile.jsx
@@ -0,0 +1,119 @@
+import { Container, Box } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+import { SurveyHeader } from '@/components/Common/SurveyHeader/SurveyHeader';
+import Button from '@/components/Common/Button/Button';
+import Step1 from '@/components/Survey/UserSteps/Step1';
+import Step2 from '@/components/Survey/UserSteps/Step2';
+import Step3 from '@/components/Survey/UserSteps/Step3';
+import Step4 from '@/components/Survey/UserSteps/Step4';
+import Step5 from '@/components/Survey/UserSteps/Step5';
+import Step6 from '@/components/Survey/UserSteps/Step6';
+import Step7 from '@/components/Survey/UserSteps/Step7';
+import useSurveyUserStore from '@/store/useSurveyUserStore';
+import { postDogProfile } from '@/api/dogProfile';
+import paths from '@/routes/paths';
+
+const AddDogProfile = () => {
+ const navigate = useNavigate();
+ const { step, setStep, petInfo, characteristics } = useSurveyUserStore();
+
+ const isStepValid = () => {
+ switch (step) {
+ case 1:
+ return petInfo.name.trim() !== '';
+ case 2:
+ return petInfo.ageYear > 0 || petInfo.ageMonth > 0;
+ case 3:
+ return petInfo.weight > 0;
+ case 4:
+ return petInfo.species !== '' && petInfo.species !== null;
+ case 5:
+ return (
+ ['MALE', 'FEMALE'].includes(petInfo.gender) &&
+ ['Y', 'N'].includes(petInfo.neutering)
+ );
+ case 6:
+ return (
+ petInfo.featureIds.length !== 0 ||
+ characteristics.없음 === true ||
+ petInfo.additionalFeature.trim() !== ''
+ );
+ case 7:
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ const handleSaveProfile = async () => {
+ const res = await postDogProfile(petInfo);
+ return res;
+ };
+
+ const handleSubmit = async () => {
+ if (!isStepValid()) return '';
+ if (await handleSaveProfile()) navigate(paths.mypage);
+ };
+
+ const handleNextStep = () => {
+ if (!isStepValid()) return '';
+ setStep(step + 1);
+ };
+
+ const handleBack = () => {
+ if (step > 1) setStep(step - 1);
+ else navigate(-1);
+ };
+
+ return (
+ <>
+
+
+
+ {step === 1 && }
+ {step === 2 && }
+ {step === 3 && }
+ {step === 4 && }
+ {step === 5 && }
+ {step === 6 && }
+ {step === 7 && }
+
+
+ {step === 7 ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+};
+
+export default AddDogProfile;
diff --git a/src/pages/Survey/AddSalonProfile.jsx b/src/pages/Survey/AddSalonProfile.jsx
new file mode 100644
index 0000000..acc2a5c
--- /dev/null
+++ b/src/pages/Survey/AddSalonProfile.jsx
@@ -0,0 +1,124 @@
+import { Box, Container } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+import { SurveyHeader } from '@/components/Common/SurveyHeader/SurveyHeader';
+import Button from '@/components/Common/Button/Button';
+import Step1 from '@/components/Survey/GroomerSteps/Step1';
+import Step2 from '@/components/Survey/GroomerSteps/Step2';
+import Step3 from '@/components/Survey/GroomerSteps/Step3';
+import Step4 from '@/components/Survey/GroomerSteps/Step4';
+import Step5 from '@components/Survey/GroomerSteps/Step5';
+import Step6 from '@components/Survey/GroomerSteps/Step6';
+import Step7 from '@components/Survey/GroomerSteps/Step7';
+import useSurveyGroomerStore from '@/store/useSurveyGroomerStore';
+import {
+ postAddGroomerProfile,
+ postGroomerProfile,
+} from '@/api/groomerProfile';
+import paths from '@/routes/paths';
+
+function AddSalonProfile() {
+ const navigate = useNavigate();
+ const { groomerInfo, businessInfo, step, setStep } = useSurveyGroomerStore();
+
+ const isStepValid = () => {
+ switch (step) {
+ case 1:
+ return groomerInfo.name.trim() !== '';
+ case 2:
+ return groomerInfo.servicesOfferedId.length > 0;
+ case 3:
+ return groomerInfo.phone.trim() !== '';
+ case 4:
+ return groomerInfo.contactHours.trim() !== '';
+ case 5:
+ return groomerInfo.servicesDistrictIds.length > 0;
+ case 6:
+ return groomerInfo.serviceType !== '';
+ case 7:
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ const handleNextStep = async () => {
+ if (!isStepValid()) return '';
+ if (step === 7) {
+ if (await postAddGroomerProfile(businessInfo)) navigate(paths.home);
+ } else setStep(step + 1);
+ };
+
+ const handleBack = () => {
+ if (step > 1) setStep(step - 1);
+ else navigate(-1);
+ };
+
+ const handleSaveProfile = async () => {
+ const res = await postGroomerProfile(groomerInfo);
+ return res;
+ };
+
+ return (
+ <>
+
+
+ {step === 1 && }
+ {step === 2 && }
+ {step === 3 && }
+ {step === 4 && }
+ {step === 5 && }
+ {step === 6 && }
+ {step === 7 && }
+
+ {step === 6 ? (
+
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+}
+
+export default AddSalonProfile;
diff --git a/src/pages/Survey/Survey.jsx b/src/pages/Survey/Survey.jsx
index eaf5015..da21a20 100644
--- a/src/pages/Survey/Survey.jsx
+++ b/src/pages/Survey/Survey.jsx
@@ -8,10 +8,12 @@ import { Selector2 } from '@components/NewRequest/atoms/Selector2';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import { useNavigate } from 'react-router-dom';
import paths from '@/routes/paths';
+import useUserStore from '@/store/useUserStore';
function Survey() {
const [location, setLocation] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
+ const { role, setRole } = useUserStore();
const [id, setId] = useState(null);
const navigate = useNavigate();
@@ -22,11 +24,13 @@ function Survey() {
const handleHairstylistSignup = async () => {
if (!location) return false;
+ setRole('ROLE_SALON');
if (await join('ROLE_SALON', id)) navigate(paths.survey.groomer);
};
const handleUserSignup = async () => {
if (!location) return false;
+ setRole('ROLE_USER');
if (await join('ROLE_USER', id)) navigate(paths.survey.user);
};
diff --git a/src/pages/Survey/SurveyGroomer.jsx b/src/pages/Survey/SurveyGroomer.jsx
index bcaaafd..c764571 100644
--- a/src/pages/Survey/SurveyGroomer.jsx
+++ b/src/pages/Survey/SurveyGroomer.jsx
@@ -10,7 +10,12 @@ import Step5 from '@components/Survey/GroomerSteps/Step5';
import Step6 from '@components/Survey/GroomerSteps/Step6';
import Step7 from '@components/Survey/GroomerSteps/Step7';
import useSurveyGroomerStore from '@/store/useSurveyGroomerStore';
-import { postAddGroomerProfile, postGroomerProfile } from '@/api/profile';
+import {
+ postAddGroomerProfile,
+ postGroomerProfile,
+} from '@/api/groomerProfile';
+import paths from '@/routes/paths';
+import { handleEnableNotifications } from '@/utils/NotificationService';
function SurveyGroomer() {
const navigate = useNavigate();
@@ -40,13 +45,16 @@ function SurveyGroomer() {
const handleNextStep = async () => {
if (!isStepValid()) return '';
if (step === 7) {
- if (await postAddGroomerProfile(businessInfo)) navigate('/home');
+ if (await postAddGroomerProfile(businessInfo)) {
+ handleEnableNotifications();
+ navigate(paths.home);
+ }
} else setStep(step + 1);
};
const handleBack = () => {
if (step > 1) setStep(step - 1);
- else navigate('/survey');
+ else navigate(paths.survey);
};
const handleSaveProfile = async () => {
diff --git a/src/pages/Survey/SurveyUser.jsx b/src/pages/Survey/SurveyUser.jsx
index 687dc56..7581f12 100644
--- a/src/pages/Survey/SurveyUser.jsx
+++ b/src/pages/Survey/SurveyUser.jsx
@@ -11,7 +11,9 @@ import Step5 from '@/components/Survey/UserSteps/Step5';
import Step6 from '@/components/Survey/UserSteps/Step6';
import Step7 from '@/components/Survey/UserSteps/Step7';
import useSurveyUserStore from '@/store/useSurveyUserStore';
-import { postDogProfile } from '@/api/profile';
+import { postDogProfile } from '@/api/dogProfile';
+import paths from '@/routes/paths';
+import { handleEnableNotifications } from '@/utils/NotificationService';
const SurveyUser = () => {
const navigate = useNavigate();
@@ -57,8 +59,11 @@ const SurveyUser = () => {
}
};
- const handleGoHome = async () => {
- if (await handleSaveProfile()) navigate('/home');
+ const completeProfile = async () => {
+ if (await handleSaveProfile()) {
+ handleEnableNotifications();
+ navigate(paths.home);
+ }
};
const handleNextStep = () => {
@@ -68,7 +73,7 @@ const SurveyUser = () => {
const handleBack = () => {
if (step > 1) setStep(step - 1);
- else navigate('/survey');
+ else navigate(paths.survey);
};
return (
@@ -103,14 +108,14 @@ const SurveyUser = () => {
>
{step === 7 ? (
{
const is = 1;
return (
-
+
{is ? : }
diff --git a/src/pages/mypage/DogProfile.jsx b/src/pages/mypage/DogProfile.jsx
index d97f2cc..7840f7f 100644
--- a/src/pages/mypage/DogProfile.jsx
+++ b/src/pages/mypage/DogProfile.jsx
@@ -8,64 +8,53 @@ import { Selector } from '@components/Common/Selector/Selector';
import RadioButton from '@components/Common/RadioButton/RadioButton';
import { breeds } from '@/constants/breeds';
import ProfileSelector from '@components/Features/ProfileSelector';
+import { useEffect } from 'react';
+import { dogProfile, updateDogProfile } from '@/api/dogProfile';
+import { characteristics } from '@/constants/features';
+import { useNavigate } from 'react-router-dom';
const DogProfile = () => {
- const features = [
- '물을 무서워해요',
- '사람을 좋아해요',
- '발을 만지는 걸 싫어해요',
- '없음',
- ];
- const [profileImage, setProfileImage] = useState(null);
- const [etc, setEtc] = useState(false);
- const [data, setData] = useState({
- name: '댕댕이',
- species: '골든 리트리버',
- ageYears: 1,
- ageMonths: 2,
- gender: 'female',
- neutering: 'Y',
- weight: 10,
- additionalFeature: [],
- customFeature: '',
- });
+ const navigate = useNavigate();
+ const [id, setId] = useState(0);
+ const [data, setData] = useState({});
+ const [features, setFeatures] = useState([]);
+ const [additionalFeature, setAdditionalFeature] = useState('');
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const currentPath = window.location.pathname;
+ const id = currentPath.split('/').pop();
+ setId(id);
+
+ const getDogProfile = async () => {
+ const res = await dogProfile(id);
+ setData(res);
+ setFeatures(res.features.map((item) => item.description));
+ setLoading(false);
+ };
+
+ getDogProfile();
+ }, []);
const handleChange = (field, value) => {
setData((prev) => ({ ...prev, [field]: value }));
};
- const handleFeatureChange = (feature) => {
- if (data.additionalFeature.includes(feature)) {
- handleChange(
- 'additionalFeature',
- data.additionalFeature.filter((f) => f !== feature)
- );
- } else if (feature === '없음') {
- handleChange('additionalFeature', ['없음']);
- setEtc(false);
- } else {
- setData((prev) => {
- const updatedFeatures = prev.additionalFeature.includes('없음')
- ? prev.additionalFeature.filter((f) => f !== '없음')
- : [...prev.additionalFeature];
-
- if (!updatedFeatures.includes(feature)) {
- updatedFeatures.push(feature);
- }
-
- return { ...prev, additionalFeature: updatedFeatures };
- });
- }
- };
-
const handleImageChange = (image) => {
- setProfileImage(image);
+ handleChange('profileImage', image);
};
const handleSubmit = () => {
- console.log('Form Submitted:', data);
+ const featureIds = features
+ .filter((feat) => feat !== '기타' && feat !== '없음')
+ .map((feat) => Object.keys(characteristics).indexOf(feat) + 1);
+
+ updateDogProfile(data, id, featureIds, additionalFeature);
+ navigate(-1);
};
+ if (loading) return LOADING;
+
return (
@@ -73,7 +62,7 @@ const DogProfile = () => {
@@ -92,15 +81,15 @@ const DogProfile = () => {
나이
handleChange('ageYears', value)}
- value={data.ageYears}
+ onChange={(value) => handleChange('ageYear', value)}
+ value={data.ageYear}
placeholder={0}
label="년"
/>
handleChange('ageMonths', value)}
- value={data.ageMonths}
+ onChange={(value) => handleChange('ageMonth', value)}
+ value={data.ageMonth}
placeholder={0}
label="개월"
/>
@@ -122,15 +111,15 @@ const DogProfile = () => {
handleChange('gender', 'male')}
+ selected={data.gender === 'MALE'}
+ onChange={() => handleChange('gender', 'MALE')}
/>
handleChange('gender', 'female')}
+ selected={data.gender === 'FEMALE'}
+ onChange={() => handleChange('gender', 'FEMALE')}
/>
@@ -163,35 +152,38 @@ const DogProfile = () => {
특징
- {features.map((feat) => (
- handleFeatureChange(feat)}
- />
+ {Object.entries(characteristics).map(([trait, checked]) => (
+
+ {
+ setFeatures((prevFeatures) => {
+ if (trait === '없음') return ['없음'];
+ if (trait === '기타') setAdditionalFeature('');
+ if (prevFeatures.includes(trait))
+ return prevFeatures.filter((item) => item !== trait);
+
+ return [
+ ...prevFeatures.filter((item) => item !== '없음'),
+ trait,
+ ];
+ });
+ }}
+ />
+ {trait === '기타' && features.includes(trait) && (
+
+ setAdditionalFeature(e.target.value)}
+ />
+
+ )}
+
))}
- {
- setEtc(!etc);
- handleChange(
- 'additionalFeature',
- data.additionalFeature.filter((f) => f !== '없음')
- );
- if (etc) handleChange('customFeature', '');
- }}
- />
- {etc && (
- handleChange('customFeature', e.target.value)}
- />
- )}
{
+ const MAX_IMAGES = 3;
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [reviewId, setReviewId] = useState(null);
+ const [data, setData] = useState(location.state.review);
+
+ useEffect(() => {
+ const currentPath = window.location.pathname;
+ const id = currentPath.split('/').pop();
+ setReviewId(id);
+ }, []);
+
+ const handleChange = (field, value) => {
+ setData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ await updateReview(reviewId, data);
+ navigate(paths.myReviews);
+ };
+
+ return (
+
+
+
+
+
+ {data.groomerName}
+ 에 대한 리뷰를 작성해주세요.
+
+
+
+
+
+ 추가적인 코멘트가 있다면 적어주세요. (선택)
+
+
+
+ );
+};
+
+export default EditReview;
diff --git a/src/pages/mypage/EditSalonProfile.jsx b/src/pages/mypage/EditSalonProfile.jsx
index 2c08b6e..0a5ce12 100644
--- a/src/pages/mypage/EditSalonProfile.jsx
+++ b/src/pages/mypage/EditSalonProfile.jsx
@@ -2,195 +2,231 @@ import { Box, Typography } from '@mui/material';
import { DetailHeader } from '@components/Common/DetailHeader/DetailHeader';
import Button from '@components/Common/Button/Button';
import InputText from '@components/Common/InputText/InputText';
-import { useState } from 'react';
-import NumberPicker from '@components/Common/NumberPicker/NumberPicker';
+import { useState, useEffect } from 'react';
import RadioButton from '@components/Common/RadioButton/RadioButton';
import TextArea from '@components/Common/TextArea/TextArea';
import ServiceRegionForm from '@components/Features/ServiceRegionForm';
import CertificationsForm from '@components/Features/CertificationsForm';
import ProfileSelector from '@components/Features/ProfileSelector';
+import { groomerProfile, updateGroomerProfile } from '@/api/groomerProfile';
+import { services, serviceTypes } from '@/constants/services';
+import { useNavigate } from 'react-router-dom';
const EditSalonProfile = () => {
- const services = [
- '목욕',
- '털 미용',
- '전체 클리핑',
- '부분 가위컷',
- '발톱 정리',
- '피부 미용 (머드팩)',
- '양치',
- '귀 세정',
- ];
+ const navigate = useNavigate();
+ const [data, setData] = useState({});
+ const [loading, setLoading] = useState(true);
const [serviceAreas, setServiceAreas] = useState([]);
- const [profileImage, setProfileImage] = useState(null);
- const [data, setData] = useState({
- name: '홍길동',
- serviceName: '동길이네',
- contactHours: '평일 오전 10시 ~ 오후 7시',
- businessLocation: '서울특별시 강남구 역삼동 123-45',
- phone: '010-1111-2222',
- serviceType: 'ANY',
- services_offered: ['목욕', '털 미용', '발톱 정리'],
- experienceYears: '2',
- experienceMonths: '6',
- businessNumber: '123-45-67890',
- certifications: ['애견미용사 자격증 1급'],
- description:
- '펫살롱 포미는 강아지의 건강과 행복을 우선으로 생각하는 반려견 전문 미용실입니다. 오랜 경력과 다양한 자격을 갖춘 미용사가 고객님의 소중한 반려견에게 맞춤형 미용 서비스를 제공합니다. 피부 상태, 털의 특성, 기질 등을 고려하여 강아지의 스트레스를 최소화하며 편안한 미용 경험을 선사합니다. 기본 미용 외에도 건강 체크와 피부 관리, 맞춤형 스타일링까지 반려견에게 필요한 모든 서비스를 준비해 두었습니다.',
- chatStart:
- '안녕하세요, 펫살롱 포미입니다! 소중한 반려견의 스타일링과 관리를 도와드리겠습니다. 예약이나 상담을 원하시면 말씀해 주세요 😊 반려견의 종, 나이, 성격에 맞춘 세심한 미용을 약속드립니다!',
- address: '서울특별시 강남구 역삼동 123-45',
- faq: 'Question & Answer',
- averageReview: 2.5,
- });
+ const [certifications, setCertifications] = useState([]);
+ const [servicesOffered, setServicesOffered] = useState([]);
+ const [profileImage, setProfileImage] = useState({});
+ const [putData, setPutData] = useState({});
+ const serviceKeys = Object.keys(services);
- const handleChange = (field, value) => {
- setData((prev) => ({ ...prev, [field]: value }));
- };
+ useEffect(() => {
+ const getGroomerProfile = async () => {
+ const res = await groomerProfile();
+ setData(res);
+ setServiceAreas(
+ res.groomerProfileDetailsInfoResponseDto.servicesDistricts
+ );
+ setCertifications(
+ res.groomerProfileDetailsInfoResponseDto.certifications
+ );
+ setServicesOffered(
+ res.groomerProfileDetailsInfoResponseDto.servicesOffered
+ );
+ setProfileImage(res.imageKey);
+ setLoading(false);
+ };
+ getGroomerProfile();
+ }, []);
- const handleSubmit = () => {
- console.log('Form Submitted:', data);
+ useEffect(() => {
+ setPutData({
+ profileId: data.profileId,
+ imageKey: data.imageKey,
+ name: data.name,
+ phone: data.phone,
+ servicesDistrictIds: serviceAreas.map((item) => item.districtId),
+ contactHours: data.contactHours,
+ servicesOfferedId: servicesOffered.map(
+ (key) => serviceKeys.indexOf(key) + 1
+ ),
+ serviceType: data.serviceType,
+ businessNumber: data.businessNumber,
+ address: data.address,
+ experience: data.experience,
+ certifications: certifications,
+ description: data.description,
+ startMessage: data.startMessage,
+ faq: data.faq,
+ });
+ }, [data, certifications, serviceAreas, servicesOffered]);
+
+ const handleChange = (field, value) => {
+ setData((prev) => {
+ return {
+ ...prev,
+ [field]: value,
+ };
+ });
};
const handleImageChange = (image) => {
setProfileImage(image);
+ handleChange('imageKey', image);
+ };
+
+ const handleSubmit = () => {
+ updateGroomerProfile(putData);
+ navigate(-1);
};
return (
-
-
-
- {[
- { name: '서비스 이름', var: 'serviceName' },
- { name: '전화번호', var: 'phone' },
- { name: '연락 가능 시간', var: 'contactHours' },
- ].map((item, index) => (
-
-
- {item.name} *
-
-
- handleChange(item.var, e.target.value)}
- placeholder={item.name}
- />
+ {loading ? (
+ loading
+ ) : (
+
+
+
+ {[
+ { name: '서비스 이름', var: 'name' },
+ { name: '전화번호', var: 'phone' },
+ { name: '연락 가능 시간', var: 'contactHours' },
+ ].map((item, index) => (
+
+
+ {item.name} *
+
+
+ handleChange(item.var, e.target.value)}
+ placeholder={item.name}
+ />
+
+ ))}
+
+
+ 서비스 지역 *
+
+
+
+
+ 서비스 형태 *
+
+ {serviceTypes.map((item, idx) => (
+ handleChange('serviceType', item.value)}
+ />
+ ))}
+
+
+ 제공 서비스 *
+
+ {serviceKeys.map((service, index) => {
+ return (
+
+ {
+ servicesOffered.includes(service)
+ ? setServicesOffered(
+ servicesOffered.filter((s) => s !== service)
+ )
+ : setServicesOffered([...servicesOffered, service]);
+ }}
+ />
+
+
+ );
+ })}
+
+
+ 사업자 번호
+
+
+ handleChange('businessNumber', e.target.value)}
+ placeholder="사업자 번호"
+ />
- ))}
-
-
- 서비스 지역
-
-
-
-
- 제공 서비스 *
-
- {services.map((service, index) => {
- return (
-
- {
- const updatedServices = data.services_offered.includes(
- service
- )
- ? data.services_offered.filter((s) => s !== service) // Remove service if selected
- : [...data.services_offered, service]; // Add service if not selected
- handleChange('services_offered', updatedServices);
- }}
- />
-
-
- );
- })}
-
- 사업자 번호
-
-
+
+ 가게 주소
+
+
+ handleChange('address', e.target.value)}
+ placeholder="가게 주소"
+ />
+
+
+
+ 경력
+
handleChange('businessNumber', e.target.value)}
- placeholder="사업자 번호"
+ onChange={(e) => handleChange('experience', e.target.value)}
+ value={data.experience}
+ placeholder="15년 경력 반려동물 미용사"
+ label="년"
/>
-
-
- 가게 주소
-
-
- handleChange('businessLocation', e.target.value)}
- placeholder="가게 주소"
+
+ 자격증
+
+ {
+ setCertifications(newCerts);
+ }}
/>
-
-
- 경력
-
- handleChange('experienceYears', e.target.value)}
- value={parseInt(data.experienceYears)}
- placeholder={0}
- label="년"
- />
-
- handleChange('experienceMonths', e.target.value)}
- value={parseInt(data.experienceMonths)}
- placeholder={0}
- label="개월"
- />
-
-
- 자격증
-
-
- setData((prev) => ({ ...prev, certifications: newCertifications }))
- }
- />
-
- {[
- { label: '서비스 설명', var: 'description' },
- { label: '채팅 시작 문구', var: 'chatStart' },
- { label: 'FAQ', var: 'faq' },
- ].map((item, index) => (
-
-
- {item.label}
-
-
+ )}
);
};
diff --git a/src/pages/mypage/EditSocialProfile.jsx b/src/pages/mypage/EditSocialProfile.jsx
index b2e8588..8cd5501 100644
--- a/src/pages/mypage/EditSocialProfile.jsx
+++ b/src/pages/mypage/EditSocialProfile.jsx
@@ -1,45 +1,71 @@
-import { Box } from '@mui/material';
+import { Box, Typography } from '@mui/material';
import { DetailHeader } from '@components/Common/DetailHeader/DetailHeader';
import Button from '@components/Common/Button/Button';
import InputText from '@components/Common/InputText/InputText';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import SelectRegion from '@components/NewRequest/modules/SelectRegion';
import SubTitle from '@components/NewRequest/atoms/SubTitle';
+import toast, { Toaster } from 'react-hot-toast';
+import { socialProfile, updateSocialProfile } from '@/api/socialProfile';
+import ProfileSelector from '@components/Features/ProfileSelector';
+import { useNavigate } from 'react-router-dom';
const EditSocialProfile = () => {
+ const navigate = useNavigate();
const [location, setLocation] = useState(null);
- const [data, setData] = useState({
- email: 'hong@gmail.com',
- city: '서울특별시',
- region: '종로구',
- });
+ const [districtId, setDistrictId] = useState(0);
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const getSocialProfile = async () => {
+ const res = await socialProfile();
+ setData(res);
+ setLoading(false);
+ };
+ getSocialProfile();
+ }, []);
const handleChange = (field, value) => {
setData((prev) => ({ ...prev, [field]: value }));
};
+ const handleImageChange = (image) => {
+ handleChange('imageKey', image);
+ };
+
const handleSubmit = () => {
- console.log('Form Submitted:', data);
+ updateSocialProfile(data.imageKey, districtId);
+ navigate(-1);
};
+ if (loading) return LOADING;
+
return (
+
-
+
+
+
-
- handleChange('email', e.target.value)}
- />
+
+
-
-
+
+
diff --git a/src/pages/mypage/MyCoupons.jsx b/src/pages/mypage/MyCoupons.jsx
index 87c28d7..e9ff71b 100644
--- a/src/pages/mypage/MyCoupons.jsx
+++ b/src/pages/mypage/MyCoupons.jsx
@@ -69,11 +69,11 @@ const MyCoupons = () => {
diff --git a/src/pages/mypage/MyReviews.jsx b/src/pages/mypage/MyReviews.jsx
index bd0c4aa..fd9cc16 100644
--- a/src/pages/mypage/MyReviews.jsx
+++ b/src/pages/mypage/MyReviews.jsx
@@ -1,136 +1,54 @@
+import { myReviews, receivedReviews } from '@/api/review';
+import useUserStore from '@/store/useUserStore';
import { DetailHeader } from '@components/Common/DetailHeader/DetailHeader';
-import { ExpandMoreRounded } from '@mui/icons-material';
-import {
- Box,
- Typography,
- Accordion,
- AccordionSummary,
- Button,
-} from '@mui/material';
-import { useState } from 'react';
+import ReviewAccordion from '@components/Features/ReviewAccordion';
+import EmptyContent from '@components/Layout/EmptyContent';
+import { Box, Typography } from '@mui/material';
+import { useState, useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
-const MyReviews = (props) => {
- const review =
- '우리 강아지 뭉치를 여기서 처음 미용했는데, 결과에 정말 만족합니다! 처음 방문했을 때부터 친절하게 상담해주시고, 강아지가 겁먹지 않도록 신경 써주셔서 너무 감사했어요.미용 중간중간 사진도 보내주셔서 어떤 과정을 거치고 있는지 확인할 수 있어서 믿음이 갔습니다. 특히, 털 상태와 피부를 꼼꼼히 체크해 주시면서 관리 팁까지 알려주셔서 정말 전문가라는 생각이 들었어요.';
- const reviewStars = 3.5;
- const totalStars = 5;
- const fullStars = Math.floor(reviewStars);
- const hasHalfStar = reviewStars % 1 != 0;
- const emptyStars = totalStars - fullStars - (hasHalfStar ? 1 : 0);
- const [open, setOpen] = useState(false);
+const MyReviews = () => {
+ const { role } = useUserStore();
+ const location = useLocation();
+ const [profileId, setProfileId] = useState(location.state?.profileId);
+ const [allReviews, setAllReviews] = useState([]);
+ const [loading, setLoading] = useState(true);
- const handleOpen = () => {
- setOpen(!open);
- };
+ useEffect(() => {
+ const getMyReviews = async () => {
+ const res =
+ role == 'ROLE_USER'
+ ? await myReviews()
+ : await receivedReviews(profileId);
+ setAllReviews(res);
+ setLoading(false);
+ };
+ getMyReviews();
+ }, []);
+
+ if (loading) return LOADING;
return (
- {props.role == 'user' ? (
+ {role == 'ROLE_USER' ? (
) : (
)}
-
-
- }
- aria-controls="panel1-content"
- id="panel1-header"
- sx={{ paddingX: 3, paddingTop: 1 }}
- >
-
-
-
-
- 동길이네
- 서울특별시 성동구
-
-
- {props.role == 'user' && (
- <>
- {
- e.stopPropagation();
- }}
- >
- 수정
-
- {
- e.stopPropagation();
- }}
- >
- 삭제
-
- >
- )}
-
-
- {Array(fullStars)
- .fill(0)
- .map((_, index) => (
-
- ))}
- {hasHalfStar && (
-
- )}
- {Array(emptyStars)
- .fill(0)
- .map((_, index) => (
-
- ))}
-
- {review}
-
-
-
-
-
+ {!allReviews.length ? (
+
+ ) : (
+
+ {allReviews?.map((review) => (
+
+ ))}
+
+ )}
);
};
diff --git a/src/pages/mypage/MySalonPage.jsx b/src/pages/mypage/MySalonPage.jsx
index 4017400..92397b0 100644
--- a/src/pages/mypage/MySalonPage.jsx
+++ b/src/pages/mypage/MySalonPage.jsx
@@ -1,194 +1,225 @@
-import { Typography, Box, Button, Divider } from '@mui/material';
+import { Typography, Box, Button, Divider, IconButton } from '@mui/material';
import { Modal } from '@components/Common/Modal/Modal';
import ReviewStars from '../../components/Features/ReviewStars';
import { useNavigate } from 'react-router-dom';
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import Grid from '@mui/material/Grid2';
+import { deleteGroomerProfile, groomerProfile } from '@/api/groomerProfile';
+import ControlPointTwoToneIcon from '@mui/icons-material/ControlPointTwoTone';
+import paths from '@/routes/paths';
const MySalonPage = () => {
const navigate = useNavigate();
- const data = {
- profile_id: 1,
- service_name: '홍길동 헤어샵',
- contact: '010-1111-2222',
- contact_hours: '오전 10시 - 오후 7시',
- region: [
- {
- city: '서울특별시',
- district: '성동구',
- },
- {
- city: '서울특별시',
- district: '강서구',
- },
- ],
- star_score: 4.5,
- estimate_request_count: 4,
- reviewCount: 9,
- store_address: '서울특별시 강남구 역삼동 123-45',
- experience: '15년 경력 반려동물 미용사',
- certifications: ['반려동물 자격증 1급', '반려동물 미용사 자격증'],
- services_offered: ['목욕', '기본 미용', '발톱관리'],
- service_description:
- '고객님의 반려동물을 정성껏 미용해드립니다. 최상의 서비스 제공을 위해 노력하고 있으며, 예약은 필수입니다.',
- start_message: '첫 예약 시 특별할인을 제공합니다.',
- badges: [
- {
- badge_id: 1,
- name: '콘테스트 우승자',
- image: 'image.jpg',
- },
- {
- badge_id: 2,
- name: '자격증 소유자',
- image: 'image.jpg',
- },
- ],
- FAQ: 'Q. 강아지 털 뭉침이 심해도 괜찮나요? \n A. 길동이네는 전문적인 털 미용 관리로 걱정 없습니다. \n Q. 강아지 입질이 심해도 괜찮나요? \n A. 길동이네는 전문적인 미용사로 걱정하지 않으셔도 됩니다.',
+ const [data, setData] = useState({});
+ const [detail, setDetail] = useState({});
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const getGroomerProfile = async () => {
+ const res = await groomerProfile();
+ setData(res);
+ setDetail(res.groomerProfileDetailsInfoResponseDto);
+ setLoading(false);
+ };
+ getGroomerProfile();
+ }, []);
+
+ const handleDeleteProfile = () => {
+ deleteGroomerProfile(data.profileId);
};
const info = [
- { title: '경력', content: data.experience },
- { title: '사업자번호', content: '1234' },
- { title: '가게 위치 정보', content: data.store_address },
- { title: '서비스 설명', content: data.service_description },
- { title: '채팅 시작 문구', content: data.start_message },
+ { title: '경력', content: data?.experience },
+ { title: '사업자번호', content: data?.businessNumber },
+ { title: '가게 위치 정보', content: data?.address },
+ { title: '서비스 설명', content: data?.description },
+ { title: '채팅 시작 문구', content: data?.startMessage },
];
+ const defaultImgPath = '/images/default-groomer-profile.png';
+ const imageSrc = data?.imageKey ? data.imageKey : defaultImgPath;
+ const imageStyle = data?.imageKey
+ ? {
+ borderRadius: '50%',
+ objectFit: 'cover',
+ border: '2px solid',
+ borderColor: '#9747FF',
+ }
+ : {};
+
+ if (loading) return LOADING;
+
return (
미용사 프로필
+ {!data && (
+ navigate(paths.survey.groomerProfile)}>
+
+
+ )}
-
-
- 수정
-
-
-
-
-
-
-
-
-
-
- 서비스 이름:
-
- {data.service_name}
-
-
- 전화번호:
- {data.contact}
-
- 연락 가능 시간:
- {data.contact_hours}
-
- 서비스 지역:
-
- {data.region.map((item, index) => (
-
- {item.city} {item.district}
-
- ))}
-
-
- 제공 서비스:
-
- {data.services_offered.map((item, index) => (
-
- {item}
- {index < data.services_offered.length - 1 && ', '}
-
- ))}
-
-
+
+ {data && (
+
+
+ 수정
+
+
-
+ )}
-
-
-
-
- navigate('/mypage/requesthistory')}
- >
- 견적요청내역
-
- {data.estimate_request_count}
-
-
-
- navigate('/mypage/myreviews')}
- >
- 리뷰
-
- {data.reviewCount}
-
-
-
+ {!data && (
+ 프로필을 등록해주세요.
+ )}
-
- 자격증:
-
- - 애견미용사 자격증 1급
- - 반려동물 행동 상담사 2급
- - 반려견 피부 관리사 자격증
-
-
+ {data && (
+ <>
+
+
+
+
+
+
+ 서비스 이름:
+
+ {data?.name}
+
-
- {info.map((item, index) => (
-
- {index > 2 ? (
- <>
- {item.title}:
- {item.content}
- >
- ) : (
-
-
- {item.title}:
+ 전화번호:
+ {data?.phone}
+
+ 연락 가능 시간:
+ {data?.contactHours}
+
+ 서비스 지역:
+
+ {detail?.servicesDistricts?.map((item, index) => (
+
+ {item.city} {item.district}
+
+ ))}
+
+
+ 제공 서비스:
+
+ {detail?.servicesOffered?.map((item, index) => (
+
+ {item}
+ {index < detail.servicesOffered.length - 1 && ', '}
+
+ ))}
+
+
- {item.content}
- )}
-
- ))}
+
+
+
+
+
+
+ (리뷰 {detail?.reviewCount}개)
+
+
+
+
+
+ navigate(paths.requestHistory)}
+ >
+ 견적요청내역
+
+ {detail?.estimateRequestCount ?? 0}
+
+
+
+
+ navigate(paths.myReviews, {
+ state: { profileId: data?.profileId },
+ })
+ }
+ >
+ 리뷰
+
+ {detail?.reviewCount ?? 0}
+
+
+
+
+ 자격증:
+
+ {detail?.certifications?.map((item) => (
+ - {item}
+ ))}
+
+
+
+ {info.map((item, index) => (
+
+ {index > 2 ? (
+ <>
+ {item.title}:
+ {item.content}
+ >
+ ) : (
+
+
+ {item.title}:
+
+ {item.content}
+
+ )}
+
+ ))}
+
+ >
+ )}
);
diff --git a/src/pages/mypage/MyUserPage.jsx b/src/pages/mypage/MyUserPage.jsx
index b627c30..526a5c8 100644
--- a/src/pages/mypage/MyUserPage.jsx
+++ b/src/pages/mypage/MyUserPage.jsx
@@ -1,41 +1,37 @@
-import { Typography, Box, Divider, IconButton, Button } from '@mui/material';
+import { Typography, Box, Divider, IconButton } from '@mui/material';
import ControlPointTwoToneIcon from '@mui/icons-material/ControlPointTwoTone';
import { useNavigate } from 'react-router-dom';
import { Modal } from '@components/Common/Modal/Modal';
-import React from 'react';
+import React, { useEffect, useState } from 'react';
+import { userProfile } from '@/api/userProfile';
+import paths from '@/routes/paths';
+import { deleteDogProfile } from '@/api/dogProfile';
+import useSurveyUserStore from '@/store/useSurveyUserStore';
const MyUserPage = () => {
const navigate = useNavigate();
+ const [data, setData] = useState(null);
+ const { resetPetInfo } = useSurveyUserStore();
- const userData = {
- dogProfiles: [
- {
- dogProfileId: 1,
- name: '구름이',
- profileImage: 'imageUrl',
- },
- {
- dogProfileId: 2,
- name: '구름이2',
- profileImage: 'imageUrl2',
- },
- ],
- couponCount: 3,
- reviewCount: 2,
- paymentCount: 5,
- };
+ useEffect(() => {
+ const getUserProfile = async () => {
+ const res = await userProfile();
+ setData(res);
+ };
+ getUserProfile();
+ }, []);
const statButton = [
- { label: '쿠폰함', route: '/mypage/coupons', value: userData.couponCount },
+ { label: '쿠폰함', route: paths.coupon, value: data?.couponCount },
{
label: '결제내역',
- route: '/mypage/paymenthistory',
- value: userData.paymentCount,
+ route: paths.paymentHistory,
+ value: data?.paymentCount,
},
{
label: '나의 리뷰',
- route: '/mypage/myreviews',
- value: userData.reviewCount,
+ route: paths.myReviews,
+ value: data?.reviewCount,
},
];
@@ -46,29 +42,57 @@ const MyUserPage = () => {
댕댕이들
- navigate('/mypage/dogprofile')}>
+ {
+ resetPetInfo();
+ navigate(paths.survey.dogProfile);
+ }}
+ >
+
+ {!data?.dogProfiles.length && (
+ 반려견을 등록해주세요.
+ )}
+
- {userData.dogProfiles.map((dog) => {
+ {data?.dogProfiles.map((dog) => {
return (
navigate('/mypage/dogprofile')}
+ onClick={() =>
+ navigate(
+ paths.editDogProfile.replace(':id', dog.dogProfileId)
+ )
+ }
>
-
+
{dog.name}
{
+ await deleteDogProfile(dog.dogProfileId);
+ setData((prevData) => ({
+ ...prevData,
+ dogProfiles: prevData.dogProfiles.filter(
+ (e) => e.dogProfileId !== dog.dogProfileId
+ ),
+ }));
+ }}
/>
);
diff --git a/src/pages/mypage/Mypage.jsx b/src/pages/mypage/Mypage.jsx
index 1d18796..9c063f8 100644
--- a/src/pages/mypage/Mypage.jsx
+++ b/src/pages/mypage/Mypage.jsx
@@ -3,32 +3,59 @@ import { Typography, Box, Button } from '@mui/material';
import MyUserPage from './MyUserPage';
import MySalonPage from './MySalonPage';
import { Modal } from '@components/Common/Modal/Modal';
+import { socialProfile } from '@/api/socialProfile';
+import { useEffect, useState } from 'react';
+import useUserStore from '@/store/useUserStore';
+import { logout, deleteAccount } from '@/api/auth';
-const Mypage = (props) => {
- const userData = {
- role: 'USER',
- name: '이민수',
- email: 'dsdas@gmail.com',
- profileImage: 'imageUrl',
- city: '서울특별시',
- district: '성동구',
+const Mypage = () => {
+ const defaultImgPath = '/images/default-groomer-profile.png';
+ const [data, setData] = useState({});
+ const { role } = useUserStore();
+
+ useEffect(() => {
+ const getSocialProfile = async () => {
+ const res = await socialProfile();
+ setData(res);
+ };
+ getSocialProfile();
+ }, []);
+
+ const imageSrc = data.imageKey ? data.imageKey : defaultImgPath;
+ const imageStyle = data.imageKey
+ ? {
+ borderRadius: '50%',
+ objectFit: 'cover',
+ border: '2px solid',
+ borderColor: '#9747FF',
+ }
+ : {};
+
+ const handleLogout = async () => {
+ try {
+ await logout();
+ window.location.reload();
+ } catch (error) {
+ console.error('로그아웃에 실패했습니다:', error);
+ }
};
return (
-
+
- {userData.name}
+ {data?.name}
- {userData.city} {userData.district} | {userData.email}
+ {data?.city} {data?.district} | {data?.email}
@@ -41,8 +68,8 @@ const Mypage = (props) => {
- {props.role === 'user' ? (
-
+ {role == 'ROLE_USER' ? (
+
) : (
)}
@@ -51,16 +78,18 @@ const Mypage = (props) => {
로그아웃
deleteAccount()}
/>
diff --git a/src/pages/mypage/RequestHistory.jsx b/src/pages/mypage/RequestHistory.jsx
index 36277b2..238f3a9 100644
--- a/src/pages/mypage/RequestHistory.jsx
+++ b/src/pages/mypage/RequestHistory.jsx
@@ -81,15 +81,12 @@ const RequestHistory = () => {
top: -110,
right: -10,
}}
- // onClick={(e) => {
- // e.stopPropagation();
- // }}
>
diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx
index 71319a7..a889098 100644
--- a/src/routes/AppRoutes.jsx
+++ b/src/routes/AppRoutes.jsx
@@ -33,6 +33,10 @@ import ContestResult from '@/pages/Contest/ContestResult';
import MyRequestDetail from '@/pages/chat/MyRequestDetail';
import NotFound from '@components/Layout/NotFound';
import PrivateRoute from './PrivateRoute';
+import AddDogProfile from '@/pages/Survey/AddDogProfile';
+import AddSalonProfile from '@/pages/Survey/AddSalonProfile';
+import EditReview from '@/pages/mypage/EditReview';
+import SalonReviews from '@/pages/SalonReviews';
const AppRoutes = () => {
return (
@@ -44,7 +48,7 @@ const AppRoutes = () => {
const AppContent = () => {
const location = useLocation();
- const role = 'salon';
+ const role = 'salon'; //TODO: remove
return (
{
} />
+ } />
}>
} />
- } />
} />
} />
} />
@@ -73,9 +77,10 @@ const AppContent = () => {
} />
} />
} />
- } />
+ } />
} />
} />
+ } />
} />
} />
} />
@@ -88,11 +93,17 @@ const AppContent = () => {
path={paths.editSalonProfile}
element={}
/>
- } />
+ } />
+ } />
} />
+ } />
+ }
+ />
} />
} />
- } />
+ } />
} />
{
- const [isLogin, setIsLogin] = useState(null);
+ const { setRole, loggedIn, setLoggedIn, setNotificationEnabled } =
+ useUserStore();
+ const [loading, setLoading] = useState(true);
+
useEffect(() => {
- const checkLogin = async () => {
- const res = await loginCheck();
- setIsLogin(res);
- };
- checkLogin();
- }, []);
+ if (!loggedIn) {
+ const checkLogin = async () => {
+ try {
+ const res = await loginCheck();
+ setLoggedIn(res.login);
+ setRole(res.role);
+ setNotificationEnabled(res.notificationEnabled);
+ setLoading(false);
- if (isLogin === null) {
- return Loading
;
- }
+ if (res.notificationEnabled) {
+ notificationServiceInstance.registerServiceWorker();
+ handleEnableNotifications();
+ }
+ } catch (error) {
+ console.error('로그인 체크에 실패했습니다:', error);
+ setLoggedIn(false);
+ }
+ };
+ checkLogin();
+ }
+ }, [loggedIn, setLoggedIn]);
- return isLogin ? : ;
+ if (loading) return Loading;
+ return loggedIn ? : ;
};
export default PrivateRoute;
diff --git a/src/routes/paths.jsx b/src/routes/paths.jsx
index c1361e1..86fd90d 100644
--- a/src/routes/paths.jsx
+++ b/src/routes/paths.jsx
@@ -5,6 +5,8 @@ const paths = {
root: '/survey',
groomer: '/survey/groomer',
user: '/survey/user',
+ dogProfile: '/survey/newdog',
+ groomerProfile: '/survey/newgroomer',
},
contest: '/contest',
entry: '/contest/entry',
@@ -17,19 +19,21 @@ const paths = {
notification: '/notification',
newRequest: '/newrequest',
newReview: '/newreview',
+ editReview: '/editreview/:id',
estimate: '/estimate',
editEstimate: '/estimate/edit',
editSocialProfile: '/mypage/editsocialprofile',
editSalonProfile: '/mypage/editsalonprofile',
- dogProfile: '/mypage/dogprofile',
+ editDogProfile: '/mypage/editDogProfile/:id',
myCoupons: '/mypage/coupons',
paymentHistory: '/mypage/paymenthistory',
myReviews: '/mypage/myreviews',
requestHistory: '/mypage/requesthistory',
requestHistoryDetail: '/mypage/requesthistorydetail',
- salonProfile: '/salonprofile',
+ salonProfile: '/salonprofile/:id',
+ salonReviews: '/salonreviews',
contestResult: '/contestresult',
};
diff --git a/src/store/useSurveyGroomerStore.js b/src/store/useSurveyGroomerStore.js
index a717930..6b9449b 100644
--- a/src/store/useSurveyGroomerStore.js
+++ b/src/store/useSurveyGroomerStore.js
@@ -1,6 +1,8 @@
import { services, serviceTypes } from '@/constants/services';
import { create } from 'zustand';
+const defaultImgPath = '/images/default-groomer-profile.png';
+
const initialGroomerInfo = {
name: '',
servicesOfferedId: [],
@@ -11,7 +13,7 @@ const initialGroomerInfo = {
};
const initialBusniessInfo = {
- imageKey: null,
+ imageKey: defaultImgPath,
businessNumber: '',
address: '',
experience: '',
diff --git a/src/store/useSurveyUserStore.js b/src/store/useSurveyUserStore.js
index c04afac..92dae36 100644
--- a/src/store/useSurveyUserStore.js
+++ b/src/store/useSurveyUserStore.js
@@ -1,6 +1,7 @@
import { characteristics } from '@/constants/features';
import { create } from 'zustand';
+const defaultImgPath = '/images/default-dog-profile.png';
const initialPetInfo = {
name: '',
ageYear: 0,
@@ -11,7 +12,7 @@ const initialPetInfo = {
weight: 0,
featureIds: [],
additionalFeature: '',
- profileImage: null,
+ profileImage: defaultImgPath,
};
const useSurveyUserStore = create((set) => ({
diff --git a/src/store/useUserStore.js b/src/store/useUserStore.js
new file mode 100644
index 0000000..6b58933
--- /dev/null
+++ b/src/store/useUserStore.js
@@ -0,0 +1,14 @@
+import { create } from 'zustand';
+
+const useUserStore = create((set) => ({
+ role: 'ROLE_PENDING',
+ setRole: (newRole) => set({ role: newRole }),
+
+ loggedIn: false,
+ setLoggedIn: (bool) => set({ loggedIn: bool }),
+
+ notificationEnabled: false,
+ setNotificationEnabled: (bool) => set({ notificationEnabled: bool }),
+}));
+
+export default useUserStore;
diff --git a/src/utils/NotificationService.jsx b/src/utils/NotificationService.jsx
new file mode 100644
index 0000000..1d3c432
--- /dev/null
+++ b/src/utils/NotificationService.jsx
@@ -0,0 +1,113 @@
+import { initializeApp } from 'firebase/app';
+import {
+ getMessaging,
+ getToken,
+ onMessage,
+ deleteToken,
+} from 'firebase/messaging';
+import { postFcmToken } from '@/api/notification';
+
+const firebaseConfig = {
+ apiKey: 'AIzaSyDV1rn-AOUbRKnUrlZTWxs7DRmpLd7ZfY0',
+ authDomain: 'dangdangsalon-50432.firebaseapp.com',
+ projectId: 'dangdangsalon-50432',
+ storageBucket: 'dangdangsalon-50432.firebasestorage.app',
+ messagingSenderId: '441665534881',
+ appId: '1:441665534881:web:442db19619f35ba4f6a9e0',
+ measurementId: 'G-23L8ZGFYT0',
+};
+
+const vapidKey =
+ 'BK0rq1l6wWkjwd2tOQ_2LQVfdhEmCWE9ysr0wucnrLzCzufwYSTZlzbPMaIsm5Bv9Y92UYlYgEli_uHVasIpWT4';
+
+class NotificationService {
+ constructor() {
+ if (NotificationService.instance) return NotificationService.instance;
+
+ this.app = initializeApp(firebaseConfig);
+ this.messaging = getMessaging(this.app);
+ this.callbacks = []; // Store all callbacks for real-time updates
+
+ NotificationService.instance = this;
+ }
+
+ async requestPermission() {
+ const permission = await Notification.requestPermission();
+ if (permission === 'granted') {
+ const token = await getToken(this.messaging, { vapidKey });
+ return token;
+ }
+ throw new Error('Notification permission denied.');
+ }
+
+ listenForMessages(callback) {
+ if (callback && typeof callback === 'function') {
+ this.callbacks.push(callback); // Add the callback to the list
+ }
+
+ if (!this.isListening) {
+ onMessage(this.messaging, (payload) => {
+ if (payload.notification) {
+ const { title, body } = payload.notification;
+ const notificationData = { title, body };
+
+ // Trigger all registered callbacks with the new notification
+ this.callbacks.forEach((cb) => cb(notificationData));
+ }
+ });
+
+ this.isListening = true; // Avoid duplicate listeners
+ }
+ }
+
+ registerServiceWorker() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker
+ .register('/firebase-messaging-sw.js')
+ .then((registration) => {
+ console.log('Service Worker registered:', registration.scope);
+ })
+ .catch((error) => {
+ console.error('Service Worker registration failed:', error);
+ });
+ }
+ }
+
+ unregisterServiceWorker() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.getRegistrations().then((registrations) => {
+ registrations.forEach((registration) => {
+ registration.unregister();
+ console.log('Service worker unregistered:', registration.scope);
+ });
+ });
+ }
+ }
+
+ async unsubscribeFromNotifications() {
+ try {
+ const currentToken = await getToken(this.messaging, { vapidKey });
+
+ if (currentToken) {
+ await deleteToken(this.messaging);
+ console.log('Successfully unsubscribed from FCM.');
+ } else {
+ console.log('No token found; already unsubscribed or not subscribed.');
+ }
+ } catch (error) {
+ console.error('Error unsubscribing from notifications:', error);
+ }
+ }
+}
+
+export const notificationServiceInstance = new NotificationService();
+
+export const handleEnableNotifications = async () => {
+ try {
+ const token = await notificationServiceInstance.requestPermission();
+ await postFcmToken(token);
+ console.log('FCM Token registered:', token);
+ } catch (e) {
+ console.error(e);
+ }
+};
diff --git a/src/utils/email-template.html b/src/utils/email-template.html
new file mode 100644
index 0000000..bc969ac
--- /dev/null
+++ b/src/utils/email-template.html
@@ -0,0 +1,51 @@
+
+
+
+
+ 댕댕살롱 예약 알림
+
+
+
+
🐾 댕댕살롱 예약 알림 🐾
+
+
+
+
안녕하세요, {userName}님!
+
반려견 미용 예약이 내일 예정되어 있습니다.
+
+
+
+
🕒 예약 일시: {reservationDateTime}
+
+
+
선택 준비사항:
+
+ - 건강검진 기록
+ - 반려견 좋아하는 간식 혹은 장난감
+ - 평소 사용하는 리드줄
+
+
+
+
+
diff --git a/src/utils/generateEmailTemplate.js b/src/utils/generateEmailTemplate.js
new file mode 100644
index 0000000..3b97ee8
--- /dev/null
+++ b/src/utils/generateEmailTemplate.js
@@ -0,0 +1,71 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const EmailTemplate = `
+
+
+
+
+ 댕댕살롱 예약 알림
+
+
+
+
🐾 댕댕살롱 예약 알림 🐾
+
+
+
+
+ 안녕하세요, {userName}님!
+
+
반려견 미용 예약이 내일 예정되어 있습니다.
+
+
+
+
🕒 예약 일시: {reservationDateTime}
+
+
+
선택 준비사항:
+
+ - 건강검진 기록
+ - 반려견 좋아하는 간식 혹은 장난감
+ - 평소 사용하는 리드줄
+
+
+
+
+
+ `;
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const filePath = path.join(__dirname, 'email-template.html');
+fs.writeFileSync(filePath, EmailTemplate);