-
충전 갯수
+
충전 갯수
-
{amount ? amount.toLocaleString() : 0}개 /
-
+
{amount ? amount.toLocaleString() : 0}개 /
+
{amount ? (amount * 100).toLocaleString() : 0}원
@@ -168,32 +186,30 @@ export default function ChargeModal({
type="submit"
aria-busy={isWaitingPayment}
disabled={isWaitingPayment}
- className="w-full rounded-lg bg-color-blue-300 py-4 text-lg font-semibold text-white hover:bg-color-blue-200"
+ className="semibold w-full rounded-lg bg-color-blue-300 py-4 text-lg text-white hover:bg-color-blue-200"
>
충전하기
- {paymentStatus.status === "FAILED" && (
-
+ {(paymentStatus.status === "FAILED" || paymentStatus.status === "PAID") && (
+
+
+
+ {paymentStatus.status === "FAILED" ? "결제 실패" : "결제 성공"}
+
+
+ {paymentStatus.status === "FAILED" ? paymentStatus.message : "결제에 성공했습니다."}
+
+
+
+
)}
-
>
);
diff --git a/src/components/Gnb/NavBar.tsx b/src/components/Gnb/NavBar.tsx
index 98573598..686a801f 100644
--- a/src/components/Gnb/NavBar.tsx
+++ b/src/components/Gnb/NavBar.tsx
@@ -19,6 +19,7 @@ import avatarImages from "@/utils/formatImage";
import { useRef } from "react";
import useRealTimeNotification from "@/stores/useRealTimeNotification";
import ChargeModal from "./ChargeModal";
+import { getAccessToken } from "@/utils/tokenUtils";
interface LinkItem {
href: string;
@@ -26,16 +27,27 @@ interface LinkItem {
group?: string;
}
+enum NavigationPath {
+ FINDING_MAKER = "/finding-maker",
+ PLAN_REQUEST = "/plan-request",
+ MY_TRIP_MANAGE = "/mytrip-manage/ongoing-plan",
+ RECEIVE = "/receive",
+ MANAGE_QUO = "/managequo",
+ LOGIN = "/login",
+ CHATTING = "/chatting",
+ LANDING = "/",
+}
+
const linkItems: Record<"guest" | "DREAMER" | "MAKER", LinkItem[]> = {
- guest: [{ href: "/finding-maker", label: "Maker 찾기" }],
+ guest: [{ href: NavigationPath.FINDING_MAKER, label: "Maker 찾기" }],
DREAMER: [
- { href: "/plan-request", label: "여행 요청" },
- { href: "/finding-maker", label: "Maker 찾기" },
- { href: "/mytrip-manage/ongoing-plan", label: "내 여행 관리", group: "mytrip-manage" },
+ { href: NavigationPath.PLAN_REQUEST, label: "여행 요청" },
+ { href: NavigationPath.FINDING_MAKER, label: "Maker 찾기" },
+ { href: NavigationPath.MY_TRIP_MANAGE, label: "내 여행 관리", group: "mytrip-manage" },
],
MAKER: [
- { href: "/receive", label: "받은 요청", group: "receive" },
- { href: "/managequo", label: "내 견적 관리", group: "managequo" },
+ { href: NavigationPath.RECEIVE, label: "받은 요청", group: "receive" },
+ { href: NavigationPath.MANAGE_QUO, label: "내 견적 관리", group: "managequo" },
],
};
@@ -45,7 +57,8 @@ const getNotification = () => {
};
const NavBar = () => {
- const { isLoggedIn, nickName, role, coconut, setLogin } = useAuthStore();
+ const { isLoggedIn, nickName, role, coconut, setCoconut, email, phoneNumber, setLogin } =
+ useAuthStore();
const [isOpenSidebar, setIsOpenSidebar] = useState
(false);
const [isOpenNotification, setIsOpenNotification] = useState(false);
const [isOpenUserMenu, setIsOpenUserMenu] = useState(false);
@@ -82,9 +95,9 @@ const NavBar = () => {
const isLinkActive = (link: LinkItem): boolean => {
switch (link.group) {
case "receive":
- return ["/receive", "/all-receive-plan"].includes(router.pathname);
+ return [NavigationPath.RECEIVE, "/all-receive-plan"].includes(router.pathname);
case "managequo":
- return ["/managequo", "/rejectlist"].includes(router.pathname);
+ return [NavigationPath.MANAGE_QUO, "/reject-list"].includes(router.pathname);
case "mytrip-manage":
return router.pathname.startsWith("/mytrip-manage/");
default:
@@ -136,8 +149,7 @@ const NavBar = () => {
}, [isLoggedIn, notificationData]);
useEffect(() => {
- const accessToken = localStorage.getItem("accessToken");
-
+ const accessToken = getAccessToken();
if (accessToken) {
const fetchUserInfo = async () => {
try {
@@ -147,7 +159,10 @@ const NavBar = () => {
setUserInfo(userData);
const avatarImage = avatarImages.find((avatar) => avatar.key === profileData.image);
setUserImage(avatarImage ? avatarImage.src : user_img.src);
- setLogin(userData.nickName, userData.role, userData.coconut,userData.email,userData.phoneNumber);
+ setLogin(nickName, role, coconut, email, phoneNumber);
+ if (userData.coconut !== coconut) {
+ handleCoconutChange(userData.coconut);
+ }
} catch (error) {
console.error(error);
}
@@ -155,7 +170,7 @@ const NavBar = () => {
fetchUserInfo();
}
- }, [setLogin]);
+ }, [setLogin, coconut]);
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
@@ -190,13 +205,18 @@ const NavBar = () => {
};
}, [isOpenUserMenu, isOpenNotification, isOpenSidebar]);
+ const handleCoconutChange = (newCoconut: number) => {
+ setCoconut(newCoconut);
+ };
+
const hasUnreadNotifications = notificationData.some((notification) => !notification.isRead);
+ const hasNotifications = realTimeNotifications.length > 0;
return (
-
+
-
-
+
+
@@ -208,10 +228,10 @@ const NavBar = () => {
<>
-
{coconut}p
+
{coconut}개
-
+
{
>
) : (
<>
-
+
@@ -294,28 +314,35 @@ const NavBar = () => {
{/* 사이드바 */}
- {isOpenSidebar && (
-
-
-
- setIsOpenSidebar(false)}
- />
-
-
-
+
+
+
+ setIsOpenSidebar(false)}
+ />
+
+
- )}
+
{/* 실시간 알림 */}
- {realTimeNotifications.length > 0 && (
+ {hasNotifications && (
{realTimeNotifications.map((notification) => (
{
return tripTypeMap[tripType] || "알 수 없는 여행 타입";
};
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getNotificationMessage = (event: string, payload: any) => {
switch (event) {
case "ARRIVE_REQUEST":
@@ -115,8 +114,10 @@ export default function Notification({ closeModal }: { closeModal: () => void })
patchNotiMutation.mutate(notificationId);
};
+ const hasNotification = !isLoading && initialNotificationData.length > 0;
+
useEffect(() => {
- if (!isLoading && initialNotificationData.length > 0) {
+ if (hasNotification) {
setNotificationData(initialNotificationData);
}
}, [isLoading, initialNotificationData]);
@@ -145,25 +146,28 @@ export default function Notification({ closeModal }: { closeModal: () => void })
className={`max-h-[300px] overflow-y-auto ${notificationData.length >= 4 ? "h-[300px]" : ""}`}
>
- {notificationData.map((notification, index) => (
-
-
- handleRead(notification.id)}
- className={`cursor-pointer pt-4 ${notification.isRead ? "bg-[#f1f1f1]" : "bg-color-gray-50"}`}
- >
-
- {getNotificationMessage(notification.event, notification.payload)}
-
-
- {formatRelativeTime(notification.createdAt)}
-
+ {notificationData
+ .slice()
+ .reverse()
+ .map((notification, index) => (
+
+
- handleRead(notification.id)}
+ className={`cursor-pointer pt-4 ${notification.isRead ? "bg-[#f1f1f1]" : "bg-color-gray-50"}`}
+ >
+
+ {getNotificationMessage(notification.event, notification.payload)}
+
+
+ {formatRelativeTime(notification.createdAt)}
+
- {index < notificationData.length - 1 && (
-
- )}
-
-
- ))}
+ {index < notificationData.length - 1 && (
+
+ )}
+
+
+ ))}
)}
diff --git a/src/components/Gnb/UserMenu.tsx b/src/components/Gnb/UserMenu.tsx
index b46ed8db..e8c82fc3 100644
--- a/src/components/Gnb/UserMenu.tsx
+++ b/src/components/Gnb/UserMenu.tsx
@@ -1,7 +1,7 @@
import useAuthStore from "@/stores/useAuthStore";
import Link from "next/link";
import { useRouter } from "next/router";
-
+import { removeAccessToken } from "@/utils/tokenUtils";
export interface UserMenuProps {
userId: string;
@@ -15,6 +15,13 @@ interface MenuItem {
onClick?: () => void;
}
+enum NavigationPath {
+ DREAMER_PROFILE = "/profile/dreamer/edit/",
+ FOLLOW_MAKER = "/follow-maker",
+ COMPLETED_TRIP_REVIEWS = "/myreview-manage/completed-trip",
+ MAKER_PROFILE = "/profile/maker/mypage/",
+}
+
export default function UserMenu({ userId, closeMenu, onChargeClick }: UserMenuProps) {
const { nickName, role, setLogout } = useAuthStore();
const router = useRouter();
@@ -23,16 +30,18 @@ export default function UserMenu({ userId, closeMenu, onChargeClick }: UserMenuP
const menuItems: Record
= {
DREAMER: [
- { href: userId ? `/profile/dreamer/edit/${userId}` : "", label: "프로필 수정" },
- { href: "/follow-maker", label: "찜한 Maker" },
- { href: "/myreview-manage/completed-trip", label: "여행 리뷰" },
+ { href: userId ? `${NavigationPath.DREAMER_PROFILE}${userId}` : "", label: "프로필 수정" },
+ { href: NavigationPath.FOLLOW_MAKER, label: "찜한 Maker" },
+ { href: NavigationPath.COMPLETED_TRIP_REVIEWS, label: "여행 리뷰" },
{
- href: "#",
+ href: "",
label: "코코넛 충전",
onClick: onChargeClick,
},
],
- MAKER: [{ href: userId ? `/profile/maker/mypage/${userId}` : "", label: "마이페이지" }],
+ MAKER: [
+ { href: userId ? `${NavigationPath.MAKER_PROFILE}${userId}` : "", label: "마이페이지" },
+ ],
};
const renderMenus = () => {
@@ -56,9 +65,9 @@ export default function UserMenu({ userId, closeMenu, onChargeClick }: UserMenuP
};
const handleLogout = () => {
- localStorage.removeItem("accessToken");
- router.reload();
+ removeAccessToken();
setLogout();
+ router.push("/login");
};
return (
diff --git a/src/components/Landing/FeatureCard.tsx b/src/components/Landing/FeatureCard.tsx
new file mode 100644
index 00000000..7fcca4a5
--- /dev/null
+++ b/src/components/Landing/FeatureCard.tsx
@@ -0,0 +1,25 @@
+import Image, { StaticImageData } from "next/image";
+
+interface FeatureCardProps {
+ imageUrl: string | StaticImageData;
+ title: string;
+ description: string;
+}
+
+export default function FeatureCard({ imageUrl, title, description }: FeatureCardProps) {
+ return (
+
+
+
+
+
{title}
+
{description}
+
+ );
+}
diff --git a/src/components/Landing/Features.tsx b/src/components/Landing/Features.tsx
new file mode 100644
index 00000000..c5e997a0
--- /dev/null
+++ b/src/components/Landing/Features.tsx
@@ -0,0 +1,36 @@
+import FeatureCard from "./FeatureCard";
+import img4 from "@public/assets/Landing-img/img_featuer_04.jpg";
+import img_sp2 from "@public/assets/Landing-img/img_sp_02.jpg";
+
+
+const featureData = [
+ {
+ imageUrl: "https://images.unsplash.com/photo-1542259009477-d625272157b7",
+ title: "완벽한 대리 여행",
+ description: "원하시는 모든 여행의 아름다운 순간을 담아드립니다",
+ },
+ {
+ imageUrl: img4,
+ title: "실시간 공유",
+ description: "특별한 순간을 실시간으로 전달받아 현장의 감동을 느껴보세요",
+ },
+ {
+ imageUrl: img_sp2,
+ title: "특별한 경험",
+ description: "현지에서만 경험할 수 있는 특별한 순간을 선사해드립니다",
+ },
+];
+
+export default function Features() {
+ return (
+
+
+ {featureData.map((feature, index) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/Landing/Hero.tsx b/src/components/Landing/Hero.tsx
new file mode 100644
index 00000000..af98cb68
--- /dev/null
+++ b/src/components/Landing/Hero.tsx
@@ -0,0 +1,26 @@
+import Image from "next/image";
+import logo from "@public/assets/icon_logo_img_remove.png";
+
+export default function Hero() {
+ return (
+
+
+
+
+ 시간이 없어도 괜찮아요
+
+
+
+ 당신을 대신해 여행하고,
+
+ 특별한 순간들을 공유해드립니다
+
+
+ );
+}
diff --git a/src/components/Landing/MapMarker.tsx b/src/components/Landing/MapMarker.tsx
new file mode 100644
index 00000000..1f3ea9c2
--- /dev/null
+++ b/src/components/Landing/MapMarker.tsx
@@ -0,0 +1,213 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps";
+import { PieChart, Pie, Tooltip, Cell } from "recharts";
+import planService from "@/services/planService";
+import planData from "@/types/planData";
+import Image from "next/image";
+import refresh_icon from "@public/assets/icon_refresh.svg";
+
+const regionNames = planData.locations.reduce(
+ (acc, { name, mapping }) => {
+ acc[mapping] = name;
+ return acc;
+ },
+ {} as Record,
+);
+
+const serviceNames = planData.services.reduce(
+ (acc, { name, mapping }) => {
+ acc[mapping] = name;
+ return acc;
+ },
+ {} as Record,
+);
+
+const markers = [
+ { name: "서울", code: "SEOUL", coordinates: [126.978, 37.5665] },
+ { name: "부산", code: "BUSAN", coordinates: [129.0756, 35.1796] },
+ { name: "인천", code: "INCHEON", coordinates: [126.7052, 37.4563] },
+ { name: "대구", code: "DAEGU", coordinates: [128.6014, 35.8684] },
+ { name: "대전", code: "DAEJEON", coordinates: [127.3845, 36.3504] },
+ { name: "광주", code: "GWANGJU", coordinates: [126.8515, 35.1595] },
+ { name: "울산", code: "ULSAN", coordinates: [129.3114, 35.5381] },
+ { name: "세종", code: "SEJONG", coordinates: [127.289, 36.4804] },
+ { name: "경기", code: "GYEONGGI", coordinates: [127.01, 37.275] },
+ { name: "강원", code: "GANGWON", coordinates: [128.208, 37.766] },
+ { name: "충북", code: "CHUNGBUK", coordinates: [127.635, 36.6291] },
+ { name: "충남", code: "CHUNGNAM", coordinates: [126.775, 36.635] },
+ { name: "전북", code: "JEONBUK", coordinates: [127.108, 35.719] },
+ { name: "전남", code: "JEONNAM", coordinates: [126.732, 34.814] },
+ { name: "경북", code: "GYEONGBUK", coordinates: [128.669, 36.575] },
+ { name: "경남", code: "GYEONGNAM", coordinates: [128.673, 35.461] },
+ { name: "제주", code: "JEJU", coordinates: [126.501, 33.35] },
+];
+
+const COLORS = ["#845ec2", "#d65db1", "#ff6f91", "#ff9671", "#FCC737", "#A7D477", "#00c9a7"];
+
+export default function MapMarker() {
+ const [selectedRegion, setSelectedRegion] = useState<{
+ name: string;
+ totalCount: number;
+ details: { name: string; value: number; fill: string }[];
+ } | null>(null);
+
+ const [geoData, setGeoData] = useState(null);
+
+ const getStatistics = async (serviceArea: string) => {
+ try {
+ const data = await planService.getStatistics(serviceArea);
+ if (data) {
+ const details = data.groupByCount.map((item: any, index: number) => {
+ const regionName = regionNames[item.serviceArea];
+
+ const serviceName = serviceNames[item.tripType];
+
+ return {
+ name: regionName || serviceName,
+ value: item.count,
+ fill: COLORS[index % COLORS.length],
+ };
+ });
+ setSelectedRegion({
+ name: serviceArea || "전체",
+ totalCount: data.totalCount,
+ details: details,
+ });
+ }
+ } catch (error) {
+ console.error("통계 자료 조회 실패", error);
+ }
+ };
+
+ useEffect(() => {
+ const loadGeoData = async () => {
+ try {
+ const response = await fetch("/korea-topo.json");
+ const data = await response.json();
+ setGeoData(data);
+ } catch (error) {
+ console.error("TopoJSON 로드 오류:", error);
+ }
+ };
+
+ loadGeoData();
+ }, []);
+
+ useEffect(() => {
+ getStatistics("");
+ }, []);
+
+ const handleMarkerClick = (region: string) => {
+ const englishRegion = Object.keys(regionNames).find((key) => regionNames[key] === region);
+
+ if (englishRegion) {
+ getStatistics(englishRegion);
+ }
+ };
+
+ return (
+ <>
+
+
+ 🛬 사용자 이용현황 🛬
+
+
+
+ {/* 지도 */}
+
+
+ {geoData && (
+
+ {({ geographies }) =>
+ geographies.map((geo) => (
+
+ ))
+ }
+
+ )}
+ {markers.map(({ name, coordinates }) => (
+ handleMarkerClick(name)}
+ >
+
+ {name}
+
+
+
+ ))}
+
+
+
+ {/* 통계 */}
+
+ {selectedRegion ? (
+ <>
+
+
+ {regionNames[selectedRegion.name]} 통계
+
+
+
+
+
+
+
총 서비스 수: {selectedRegion.totalCount}
+ {selectedRegion.totalCount === 0 ? (
+
+
+ 통계 자료가 없습니다!
+
+ 여러분의 꿈을 추가해 보세요! 🚀
+
+
+ ) : (
+
+
+ {selectedRegion.details.map((entry, index) => (
+ |
+ ))}
+
+
+
+ )}
+
마커를 클릭하면 해당 지역 통계를 볼 수 있습니다.
+ >
+ ) : (
+
마커를 클릭하면 해당 지역 통계를 볼 수 있습니다.
+ )}
+
+
+ >
+ );
+}
diff --git a/src/components/MyPlans/Cards/PlanCard.tsx b/src/components/MyPlans/Cards/PlanCard.tsx
index e39e2426..53845e7f 100644
--- a/src/components/MyPlans/Cards/PlanCard.tsx
+++ b/src/components/MyPlans/Cards/PlanCard.tsx
@@ -1,4 +1,7 @@
import { Plan } from "@/services/planService";
+import { formatToDetailedDate } from "@/utils/formatDate";
+import { convertRegionToKorean } from "@/utils/formatRegion";
+import Label from "@/components/Common/UI/Label";
interface PlanData {
planDetail: Plan;
@@ -6,36 +9,57 @@ interface PlanData {
export default function PlanCard({ planDetail }: PlanData) {
return (
-
-
+
+
-
-
+
여행 유형
- {planDetail ? planDetail.tripType : "-"}
+
+
+
-
+
여행 날짜
- {planDetail ? planDetail.tripDate : "-"}
+ {planDetail ? formatToDetailedDate(planDetail.tripDate) : "-"}
-
+
여행지
- {planDetail ? planDetail.serviceArea : "-"}
+
+ {planDetail ? convertRegionToKorean(planDetail.serviceArea) : "-"}
+
-
+
세부 요청 사항
{planDetail ? planDetail.details : "-"}
diff --git a/src/components/MyPlans/Cards/QuotationCard.tsx b/src/components/MyPlans/Cards/QuotationCard.tsx
index 74688518..821ceaef 100644
--- a/src/components/MyPlans/Cards/QuotationCard.tsx
+++ b/src/components/MyPlans/Cards/QuotationCard.tsx
@@ -1,10 +1,16 @@
import Image from "next/image";
-import Label from "@/components/Common/Label";
+import Label from "@/components/Common/UI/Label";
import icon_like_red from "@public/assets/icon_like_red.png";
-import img_avatar1 from "@public/assets/img_avatar1.svg";
import icon_active_star from "@public/assets/icon_active_star.svg";
import link from "@public/assets/icon_link.svg";
import Link from "next/link";
+import { Plan } from "@/services/planService";
+import { formatToDetailedDate } from "@/utils/formatDate";
+import { QuotationServiceDreamer } from "@/services/quotationServiceDreamer";
+import { convertRegionToKorean } from "@/utils/formatRegion";
+import { useRouter } from "next/router";
+import { useState, useEffect } from "react";
+import ModalLayout from "@/components/Common/Layout/ModalLayout";
interface MakerInfo {
nickName: string;
@@ -29,108 +35,183 @@ interface QuotationDetail {
}
interface QuotationCardProps {
- quotationDetail: QuotationDetail; // prop 타입 정의
+ planDetail: Plan;
+ quotationDetail: QuotationDetail;
}
-export default function QuotationCard({ quotationDetail }: QuotationCardProps) {
+export default function QuotationCard({ quotationDetail, planDetail }: QuotationCardProps) {
+ const router = useRouter();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [coconut, setCoconut] = useState(0);
+
+ useEffect(() => {
+ const authData = localStorage.getItem("auth");
+ if (authData) {
+ try {
+ const parsedData = JSON.parse(authData);
+ setCoconut(parsedData.state?.coconut ?? 0);
+ } catch (error) {
+ console.error("로컬스토리지 데이터 파싱 오류:", error);
+ }
+ }
+ }, []);
+
+ async function handleConfirmButton() {
+ try {
+ await QuotationServiceDreamer.confirmQuotation({ isConfirmed: true }, quotationDetail.id);
+ alert("견적이 확정되었습니다.");
+ window.location.reload();
+ router.push(`/mytrip-manage/ongoing-plan/detail/${planDetail.id}`);
+ } catch (error: any) {
+ if (error.status === 400) {
+ setIsModalOpen(true);
+ } else {
+ alert(`견적 확정에 실패했습니다. 다시 시도해주세요. ${error.message}`);
+ }
+ }
+ }
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
- {quotationDetail.maker.nickName}
-
-
-
-
-
{quotationDetail.maker.averageRating}
-
({quotationDetail.maker.totalReviews})
-
-
ㅣ
-
-
-
-
SNS
-
+
+ {isModalOpen && (
+
setIsModalOpen(false)}>
+
+
+
+
보유 코코넛 :
{coconut}
{" "}
+
개
-
ㅣ
-
-
{quotationDetail.maker.totalConfirms}건
-
확정
+
+
필요 코코넛 :
+
+ {quotationDetail.price.toLocaleString()}
+
+
개
-
+
+ )}
+
+
+
+ {quotationDetail.isConfirmed !== false && (
+
+ )}
+
+ {quotationDetail.isAssigned !== false && (
+
+ )}
+
+
+
- 136
+
+
+
+
+ {quotationDetail.maker.nickName}
+
+
+
+
+
{quotationDetail.maker.averageRating}
+
({quotationDetail.maker.totalReviews})
+
+
ㅣ
+
+
ㅣ
+
+
{quotationDetail.maker.totalConfirms}건
+
확정
+
+
+
+
+
+
+
{quotationDetail.maker.totalFollows}
+
+
-
-
-
-
-
- 여행일
-
-
- 2024.07.01(월)
-
+
+
+
+
+ 여행일
+
+
+ {formatToDetailedDate(planDetail.tripDate)}
+
+
+
ㅣ
+
+
+ 여행지
+
+
+ {convertRegionToKorean(planDetail.serviceArea)}
+
+
-
ㅣ
-
-
- 여행지
-
-
- 서울 강남구
+
+
견적 코코넛
+
+ {quotationDetail.price.toLocaleString()} 개
-
-
견적 금액
-
- {" "}
- {quotationDetail.price.toLocaleString()}원
-
+
+ {planDetail.status !== "CONFIRMED" && (
+
+ )}
+
+
+
-
-
-
-
);
}
diff --git a/src/components/MyPlans/Cards/QuotationCardCompleted.tsx b/src/components/MyPlans/Cards/QuotationCardCompleted.tsx
index 5f432b8b..d15f9f59 100644
--- a/src/components/MyPlans/Cards/QuotationCardCompleted.tsx
+++ b/src/components/MyPlans/Cards/QuotationCardCompleted.tsx
@@ -1,69 +1,112 @@
import Image from "next/image";
-import Label from "@/components/Common/Label";
+import Label from "@/components/Common/UI/Label";
import icon_like_red from "@public/assets/icon_like_red.png";
-import img_avatar1 from "@public/assets/img_avatar1.svg";
import icon_active_star from "@public/assets/icon_active_star.svg";
import link from "@public/assets/icon_link.svg";
import Link from "next/link";
+import { Plan } from "@/services/planService";
+import { formatToDetailedDate } from "@/utils/formatDate";
+import { convertRegionToKorean } from "@/utils/formatRegion";
-export default function QuotationCardCompleted() {
+interface MakerInfo {
+ nickName: string;
+ image: string;
+ gallery: string;
+ serviceTypes: string[];
+ isFollowed: boolean;
+ averageRating: number;
+ totalReviews: number;
+ totalFollows: number;
+ totalConfirms: number;
+}
+interface QuotationDetail {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ price: number;
+ content: string;
+ maker: MakerInfo;
+ isConfirmed: false;
+ isAssigned: false;
+}
+
+interface QuotationCardProps {
+ planDetail: Plan;
+ quotationDetail: QuotationDetail;
+}
+
+export default function QuotationCardCompleted({
+ quotationDetail,
+ planDetail,
+}: QuotationCardProps) {
return (
-
-
+ {quotationDetail.isConfirmed !== false && (
+
+ )}
+
+ {quotationDetail.isAssigned !== false && (
+
+ )}
-
-
+
+
-
-
김코드 Maker
-
+
+
+ {quotationDetail.maker.nickName}
+
+
-
55
-
(178)
+
{quotationDetail.maker.averageRating}
+
({quotationDetail.maker.totalReviews})
ㅣ
-
+
ㅣ
-
334건
+
{quotationDetail.maker.totalConfirms}건
확정
-
-
- 136
+
+
+
+
{quotationDetail.maker.totalFollows}
+
@@ -74,7 +117,7 @@ export default function QuotationCardCompleted() {
여행일
- 2024.07.01(월)
+ {formatToDetailedDate(planDetail.tripDate)}
ㅣ
@@ -83,15 +126,28 @@ export default function QuotationCardCompleted() {
여행지
- 서울 강남구
+ {convertRegionToKorean(planDetail.serviceArea)}
-
견적 금액
-
180,000원
+
견적 코코넛
+
+ {quotationDetail.price.toLocaleString()}개
+
+
+
+
);
}
diff --git a/src/components/MyPlans/MyPlanDetail.tsx b/src/components/MyPlans/MyPlanDetail.tsx
index f28a8615..bfc69d00 100644
--- a/src/components/MyPlans/MyPlanDetail.tsx
+++ b/src/components/MyPlans/MyPlanDetail.tsx
@@ -1,29 +1,65 @@
-import MyPlanNav from "./MyPlanNav";
-import Layout from "../Common/Layout";
-import PlanCard from "./Cards/PlanCard";
-import QuotationCardList from "./QuotationCardList";
-import { Plan } from "@/services/planService";
+import { useRouter } from "next/router";
+import MyPlanNav from "@/components/MyPlans/MyPlanNav";
+import Layout from "@/components/Common/Layout/Layout";
+import PlanCard from "@/components/MyPlans/Cards/PlanCard";
+import QuotationCardList from "@/components/MyPlans/QuotationCardList";
+import planService, { Plan } from "@/services/planService";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
interface PlanData {
planDetail: Plan;
}
export default function MyPlanDetail({ planDetail }: PlanData) {
+ const router = useRouter();
+
+ async function handleDeletePlan() {
+ try {
+ await planService.deletePlan(planDetail.id);
+ alert("플랜을 취소하였습니다.");
+ } catch (error) {
+ alert(`플랜 취소를 실패했습니다. 다시 시도해주세요. ${error}`);
+ } finally {
+ router.push("/mytrip-manage/ongoing-plan");
+ }
+ }
+
if (!planDetail) {
- return
Loading...
;
+ return (
+
+
+
+ );
}
return (
<>
-
-
-
{planDetail.title}
+
+
진행중인 플랜 정보
+
+
+
+ {planDetail.title}
+
+ {planDetail.status === "PENDING" && (
+
+ )}
+
+
-
>
);
diff --git a/src/components/MyPlans/MyPlanDetailCompleted.tsx b/src/components/MyPlans/MyPlanDetailCompleted.tsx
index 654c73cb..33e6055f 100644
--- a/src/components/MyPlans/MyPlanDetailCompleted.tsx
+++ b/src/components/MyPlans/MyPlanDetailCompleted.tsx
@@ -1,27 +1,30 @@
-import MyPlanNav from "./MyPlanNav";
-import Layout from "../Common/Layout";
-import PlanCard from "./Cards/PlanCard";
-import QuotationCardListCompleted from "./QuotationCardListCompleted";
-import { useRouter } from "next/router";
-import planService from "@/services/planService";
-import { useQuery } from "@tanstack/react-query";
+import MyPlanNav from "@/components/MyPlans/MyPlanNav";
+import Layout from "@/components/Common/Layout/Layout";
+import PlanCard from "@/components/MyPlans/Cards/PlanCard";
+import QuotationCardListCompleted from "@/components/MyPlans/QuotationCardListCompleted";
+import { Plan } from "@/services/planService";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
-export default function MyPlanDetailCompleted() {
- const router = useRouter();
- const { id } = router.query;
-
- const { data: planDetail, isLoading } = useQuery({
- queryKey: ["planDetail", id],
- queryFn: () => planService.getPlanDetail(id as string),
- enabled: !!id,
- });
+interface PlanData {
+ planDetail: Plan;
+}
- if (isLoading) {
- return
Loading...
;
+export default function MyPlanDetailCompleted({ planDetail }: PlanData) {
+ if (!planDetail) {
+ return (
+
+
+
+ );
}
- if (!planDetail) {
- return
Loading...
;
+ let title = "";
+
+ if (planDetail.status === "COMPLETED") {
+ title = "종료된";
+ } else if (planDetail.status === "OVERDUE") {
+ title = "만료된";
}
return (
@@ -29,14 +32,14 @@ export default function MyPlanDetailCompleted() {
diff --git a/src/components/MyPlans/MyPlanList.tsx b/src/components/MyPlans/MyPlanList.tsx
index ddd354f8..0de95ee3 100644
--- a/src/components/MyPlans/MyPlanList.tsx
+++ b/src/components/MyPlans/MyPlanList.tsx
@@ -2,6 +2,11 @@ import { forwardRef } from "react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { Plan } from "@/services/planService";
+import { formatToSimpleDate } from "@/utils/formatDate";
+import { convertRegionToKorean } from "@/utils/formatRegion";
+import Label from "@/components/Common/UI/Label";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
interface MyPlanListProps {
visiblePlans: Plan[];
@@ -10,10 +15,11 @@ interface MyPlanListProps {
fetchNextPage: () => void;
hasNextPage: boolean;
isFetchingNextPage: boolean;
+ isLoading: boolean;
}
const MyPlanList = forwardRef
(
- ({ visiblePlans, title, fetchNextPage, hasNextPage, isFetchingNextPage }, ref) => {
+ ({ visiblePlans, title, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading }, ref) => {
const router = useRouter();
const handleDetailClick = (planId: string) => {
@@ -49,29 +55,55 @@ const MyPlanList = forwardRef(
return (
-
{`${title} 플랜 목록`}
-
- {visiblePlans.map((plan) => (
-
-
-
{plan.title}
-
{plan.serviceArea}
-
-
-
- ))}
- {hasNextPage && !isFetchingNextPage && (
-
- 아래로 내려서 더 보기
+
{`${title} 플랜 목록`}
+
+ {isLoading ? (
+
+
+ ) : (
+ <>
+ {visiblePlans.map((plan) => (
+
+
+
{plan.title}
+
+
+
{convertRegionToKorean(plan.serviceArea)}
+
ㅣ
+
{formatToSimpleDate(plan.tripDate)}
+
+
ㅣ
+
+
+
+
+
+
+
+ ))}
+ {visiblePlans.length === 0 ? (
+
+ 아직 플랜이 없어요!
+
+ ) : !hasNextPage && visiblePlans.length > 0 ? (
+
+ 모든 플랜을 확인했어요!
+
+ ) : null}
+ >
)}
diff --git a/src/components/MyPlans/QuotationCardList.tsx b/src/components/MyPlans/QuotationCardList.tsx
index a4764a0f..30076888 100644
--- a/src/components/MyPlans/QuotationCardList.tsx
+++ b/src/components/MyPlans/QuotationCardList.tsx
@@ -1,30 +1,73 @@
-import QuotationCard from "./Cards/QuotationCard";
+import QuotationCard from "@/components/MyPlans/Cards/QuotationCard";
import { QuotationServiceDreamer } from "@/services/quotationServiceDreamer";
import { useRouter } from "next/router";
import { useQuery } from "@tanstack/react-query";
+import { Plan } from "@/services/planService";
+import Link from "next/link";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
+import { useEffect, useState } from "react";
-export default function QuotationCardList() {
+interface PlanData {
+ planDetail: Plan;
+}
+
+export default function QuotationCardList({ planDetail }: PlanData) {
const router = useRouter();
const { id } = router.query;
- const { data: quotations } = useQuery({
+ const { data: quotations, isLoading } = useQuery({
queryKey: ["Quotations", id],
queryFn: () => QuotationServiceDreamer.getQuotations({ planId: id as string }),
enabled: !!id,
});
+ //1440px이하부터 타블렛 디자인으로 변경
+ const [isTablet, setIsTablet] = useState(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsTablet(window.innerWidth <= 1440);
+ };
+
+ handleResize();
+ window.addEventListener("resize", handleResize);
+
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!quotations?.list || quotations.list.length === 0) {
+ return (
+
+
+
아직 받은 견적이 없어요!
+
+
지정견적요청을 해보세요!
+
+
+
+
+ );
+ }
+
return (
<>
-
- {quotations?.list && quotations.list.length > 0 ? (
- quotations.list.map((quotation) => (
-
- ))
- ) : (
-
- 아직 견적이 없어요! 지정견적요청을 해보세요!
-
- )}
+
+ {quotations.list.map((quotation) => (
+
+ ))}
>
);
diff --git a/src/components/MyPlans/QuotationCardListCompleted.tsx b/src/components/MyPlans/QuotationCardListCompleted.tsx
index 5b71dbc0..ae534024 100644
--- a/src/components/MyPlans/QuotationCardListCompleted.tsx
+++ b/src/components/MyPlans/QuotationCardListCompleted.tsx
@@ -1,17 +1,67 @@
-import QuotationCompleted from "./Cards/QuotationCardCompleted";
+import { QuotationServiceDreamer } from "@/services/quotationServiceDreamer";
+import { useRouter } from "next/router";
+import { useQuery } from "@tanstack/react-query";
+import { Plan } from "@/services/planService";
+import QuotationCardCompleted from "@/components/MyPlans/Cards/QuotationCardCompleted";
+import Image from "next/image";
+import loading from "@public/assets/icon_loading.gif";
+import { useEffect, useState } from "react";
-export default function QuotationCardListCompleted() {
- return (
- <>
-
-
-
-
-
-
-
-
+interface PlanData {
+ planDetail: Plan;
+}
+
+export default function QuotationCardListCompleted({ planDetail }: PlanData) {
+ const router = useRouter();
+ const { id } = router.query;
+
+ const { data: quotations, isLoading } = useQuery({
+ queryKey: ["Quotations", id],
+ queryFn: () => QuotationServiceDreamer.getQuotations({ planId: id as string }),
+ enabled: !!id,
+ });
+
+ //1440px이하부터 타블렛 디자인으로 변경
+ const [isTablet, setIsTablet] = useState(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsTablet(window.innerWidth <= 1440);
+ };
+
+ handleResize();
+ window.addEventListener("resize", handleResize);
+
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!quotations?.list || quotations.list.length === 0) {
+ return (
+
+
아직 받은 견적이 없어요!
+
+
지정견적요청을 해보세요!
- >
+ );
+ }
+
+ return (
+
+ {quotations.list.map((quotation) => (
+
+ ))}
+
);
}
diff --git a/src/components/MyReviews/Cards/ReviewCard.tsx b/src/components/MyReviews/Cards/ReviewCard.tsx
index c7e23d23..a7b5ceb9 100644
--- a/src/components/MyReviews/Cards/ReviewCard.tsx
+++ b/src/components/MyReviews/Cards/ReviewCard.tsx
@@ -1,23 +1,31 @@
import Image from "next/image";
-import img_avatar1 from "@public/assets/img_avatar1.svg";
-import Label from "@/components/Common/Label";
+import Label from "@/components/Common/UI/Label";
import StarRating from "@/components/Receive/StarRating";
+import { Review } from "@/services/reviewService";
+import { formatToSimpleDate } from "@/utils/formatDate";
-export default function ReviewCard() {
+interface ReviewCardProps {
+ reviewDetail: Review;
+}
+
+export default function ReviewCard({ reviewDetail }: ReviewCardProps) {
return (
-
+
+ {reviewDetail.plan.quotes[0].isAssigned !== false && (
+
+ )}
- 작성일 2024.07.02
+ 작성일 {formatToSimpleDate(reviewDetail.createdAt)}
-
김코드 Maker
+
{reviewDetail.owner.nickName}
여행일
-
2024.07.01(월)
+
+ {formatToSimpleDate(reviewDetail.plan.tripDate)}
+
ㅣ
플랜가
-
210,000원
+
{reviewDetail.plan.quotes[0].price} 개
@@ -46,13 +56,12 @@ export default function ReviewCard() {
- 처음 플랜 받아봤는데, 엄청 친절하시고 꼼꼼하세요! 귀찮게 이것저것 물어봤는데 잘
- 알려주셨습니다. 국내 여행은 믿고 맡기세요!
+ {reviewDetail.content}
- 작성일 2024.07.02
+ 작성일 {formatToSimpleDate(reviewDetail.createdAt)}
diff --git a/src/components/MyReviews/Cards/TripCard.tsx b/src/components/MyReviews/Cards/TripCard.tsx
index 09c49fb0..a7f4f5f5 100644
--- a/src/components/MyReviews/Cards/TripCard.tsx
+++ b/src/components/MyReviews/Cards/TripCard.tsx
@@ -1,13 +1,19 @@
import Image from "next/image";
-import img_avatar1 from "@public/assets/img_avatar1.svg";
-import Label from "@/components/Common/Label";
+import loading from "@public/assets/icon_loading.gif";
+import Label from "@/components/Common/UI/Label";
import ReceiveModalLayout from "@/components/Receive/ReceiveModalLayout";
-import ReviewForm from "@/components/Common/ReviewForm";
+import ReviewForm from "@/components/Common/Form/ReviewForm";
+import { formatToDetailedDate } from "@/utils/formatDate";
import { useRouter } from "next/router";
import { useState } from "react";
-import CompleteTrip from "@/components/Common/CompleteTrip";
+import CompleteTrip from "@/components/Common/Feature/CompleteTrip";
+import { Plan } from "@/services/planService";
-export default function TripCard() {
+interface TripCardProps {
+ planDetail: Plan;
+}
+
+export default function TripCard({ planDetail }: TripCardProps) {
const router = useRouter();
const [isReviewModalOpen, setIsReviewModalOpen] = useState
(false);
const [isCompleteModalOpen, setIsCompleteModalOpen] = useState(false);
@@ -21,6 +27,14 @@ export default function TripCard() {
const openCompleteModal = () => setIsCompleteModalOpen(true);
const closeCompleteModal = () => setIsCompleteModalOpen(false);
+ if (!planDetail) {
+ return (
+
+
+
+ );
+ }
+
return (
@@ -29,7 +43,7 @@ export default function TripCard() {
-
김코드 Maker
+
+ {planDetail ? planDetail.quotes?.[0]?.maker.nickName : "-"}
+
여행일
-
2024.07.01(월)
+
+ {planDetail ? formatToDetailedDate(planDetail.tripDate) : "-"}
+
ㅣ
플랜가
-
210,000원
+
+ {planDetail ? planDetail.quotes?.[0]?.price : "-"}원
+
@@ -73,12 +93,12 @@ export default function TripCard() {
{isCompleteModalOpen && (
-
+
)}
{isReviewModalOpen && (
-
+
)}
diff --git a/src/components/MyReviews/MyCompletedTripList.tsx b/src/components/MyReviews/MyCompletedTripList.tsx
index 4a5a4cce..d5d5bb79 100644
--- a/src/components/MyReviews/MyCompletedTripList.tsx
+++ b/src/components/MyReviews/MyCompletedTripList.tsx
@@ -1,15 +1,32 @@
-import TripCard from "./Cards/TripCard";
+import TripCard from "@/components/MyReviews/Cards/TripCard";
+import { Plan } from "@/services/planService";
+import { useEffect, useState } from "react";
+
+interface CompletedPlanListProps {
+ plans: Plan[];
+}
+
+export default function MyCompletedTripList({ plans }: CompletedPlanListProps) {
+ //1440px이하부터 타블렛 디자인으로 변경
+ const [isTablet, setIsTablet] = useState(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsTablet(window.innerWidth <= 1440);
+ };
+
+ handleResize();
+ window.addEventListener("resize", handleResize);
+
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
-export default function MyCompletedTripList() {
return (
<>
-
-
-
-
-
-
-
+
+ {plans.map((plan) => (
+
+ ))}
>
);
diff --git a/src/components/MyReviews/MyReviewList.tsx b/src/components/MyReviews/MyReviewList.tsx
index 48b5d695..7d9a825a 100644
--- a/src/components/MyReviews/MyReviewList.tsx
+++ b/src/components/MyReviews/MyReviewList.tsx
@@ -1,15 +1,31 @@
-import ReviewCard from "./Cards/ReviewCard";
+import ReviewCard from "@/components/MyReviews/Cards/ReviewCard";
+import { Review } from "@/services/reviewService";
+import { useEffect, useState } from "react";
+interface MyReviewListProps {
+ reviews: Review[];
+}
+
+export default function MyReviewList({ reviews }: MyReviewListProps) {
+ //1440px이하부터 타블렛 디자인으로 변경
+ const [isTablet, setIsTablet] = useState(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsTablet(window.innerWidth <= 1440);
+ };
+
+ handleResize();
+ window.addEventListener("resize", handleResize);
+
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
-export default function MyReviewList() {
return (
<>
-
-
-
-
-
-
-
+
+ {reviews.map((review) => (
+
+ ))}
>
);
diff --git a/src/components/Receive/FastDropdown.tsx b/src/components/Receive/FastDropdown.tsx
index 82a9ce4c..b95c0667 100644
--- a/src/components/Receive/FastDropdown.tsx
+++ b/src/components/Receive/FastDropdown.tsx
@@ -23,22 +23,22 @@ export default function FastDropdown({ onSort, currentSort }: FastDropdownProps)
{isOpen && (
-
+
- handleSort("SCHEDULE_FIRST")}
>
일정 빠른순
- handleSort("RECENT")}
>
최근 요청순
diff --git a/src/components/Receive/Quotation.tsx b/src/components/Receive/Quotation.tsx
index 9349f3f2..eb1989b1 100644
--- a/src/components/Receive/Quotation.tsx
+++ b/src/components/Receive/Quotation.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import Label from "../Common/Label";
+import Label from "../Common/UI/Label";
import { PlanItem } from "@/services/requestService";
import { UserInfo } from "@/services/userService";
import userService from "@/services/userService";
@@ -54,19 +54,19 @@ export default function Quotation({ data, closeModal }: QuotationProps) {
return (
<>
-
+
{specifyMaker}
-
+
{data.title}
{data.dreamer?.nickName} 고객님
-
+
여행일
@@ -76,7 +76,7 @@ export default function Quotation({ data, closeModal }: QuotationProps) {
-
+
여행지
@@ -88,7 +88,7 @@ export default function Quotation({ data, closeModal }: QuotationProps) {
-
견적 코코넛을 입력해 주세요
+
견적 코코넛을 입력해 주세요
-
- 코멘트를 입력해 주세요
-
+
코멘트를 입력해 주세요
- {showError &&
10글자 이상 작성해 주세요
}
+ {showError &&
10글자 이상 작성해 주세요
}
: "";
const waitingQuotation = () => {
if (data.plan.status === "PENDING") {
@@ -40,7 +39,7 @@ export default function QuotationDetailsContainer({
<>
-
+
{waitingQuotation()}
@@ -50,7 +49,7 @@ export default function QuotationDetailsContainer({
{writeTime}
-
{data.plan.title}
+
{data.plan.title}
{data.dreamer.nickName} 님
@@ -81,12 +80,12 @@ export default function QuotationDetailsContainer({
twoButton ? "hidden" : ""
}`}
>
-
견적 보내기
+
견적 보내기
diff --git a/src/components/Receive/ReceiveModalLayout.tsx b/src/components/Receive/ReceiveModalLayout.tsx
index 816c9876..0a456d68 100644
--- a/src/components/Receive/ReceiveModalLayout.tsx
+++ b/src/components/Receive/ReceiveModalLayout.tsx
@@ -18,7 +18,7 @@ export default function ReceiveModalLayout({ label, children, closeModal }: Moda
return (
-
+
{label}
-
+
{data.title}
@@ -76,7 +76,7 @@ export default function Reject({ data, closeModal }: RejectProps) {