diff --git a/src/App.tsx b/src/App.tsx index bee32d0..f41efdd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { import NotFound from "./pages/NotFound"; import Intro from "./pages/Intro"; import SearchPage from "./pages/SearchPage"; +import MyPage from "./pages/myPage/myPage"; import CustomerSupportPage from "./pages/CustomerSupportPage"; import PublicLayout from "./layouts/PublicLayout"; @@ -35,6 +36,11 @@ const routes: RouteObject[] = [ element: , errorElement: , }, + { + path: "/mypage/*", + element: , + errorElement: , + }, ]; const router = createBrowserRouter(routes); diff --git a/src/components/modals/paymentAddModal.tsx b/src/components/modals/paymentAddModal.tsx new file mode 100644 index 0000000..c5441c8 --- /dev/null +++ b/src/components/modals/paymentAddModal.tsx @@ -0,0 +1,91 @@ +import { X } from "lucide-react"; +import { useState } from "react"; +import { cn } from "../../lib/cn"; + +interface Props { + isOpen: boolean; + onClose: () => void; +} + +type PaymentType = "kakao" | "toss"; + +export default function PaymentAddModal({ isOpen, onClose }: Props) { + const [selectedType, setSelectedType] = useState("kakao"); + + const handleAddSubmit = () =>{ + alert("결제수단이 성공적으로 추가되었습니다."); + onClose(); + } + + if (!isOpen) return null; + + return ( +
+
+
+

결제수단 추가

+ +
+ +
+ {/* 결제 방법 선택 */} +
+

결제 방법 선택

+
+ setSelectedType("kakao")} + label="카카오페이" + icon={💛} + /> + setSelectedType("toss")} + label="토스페이" + icon={💙} + /> +
+
+ + {/* 하단 옵션 및 버튼 */} +
+ + +
+ + +
+
+
+
+
+ ); +} + +function PaymentTypeButton({ active, onClick, label, icon }: { active: boolean; onClick: () => void; label: string; icon: React.ReactNode }) { + return ( + + ); +} \ No newline at end of file diff --git a/src/components/profile/profileAvatar.tsx b/src/components/profile/profileAvatar.tsx new file mode 100644 index 0000000..095b5d8 --- /dev/null +++ b/src/components/profile/profileAvatar.tsx @@ -0,0 +1,11 @@ +interface Props { + name: string; +} + +export default function ProfileAvatar({ name }: Props) { + return ( +
+ {name[0]} +
+ ); +} diff --git a/src/layouts/myPageLayout.tsx b/src/layouts/myPageLayout.tsx new file mode 100644 index 0000000..f1aef85 --- /dev/null +++ b/src/layouts/myPageLayout.tsx @@ -0,0 +1,85 @@ +import { Outlet, NavLink } from "react-router-dom"; +import { + Calendar, + CreditCard, + Crown, + Settings, + Store, + User, +} from "lucide-react"; +import { cn } from "../lib/cn"; + +const sidebarItems = [ + { to: "/mypage/info", label: "내 정보", icon: User }, + { to: "/mypage/settings", label: "계정 설정", icon: Settings }, + { to: "/mypage/payment", label: "결제수단", icon: CreditCard }, + { to: "/mypage/subscription", label: "구독 관리", icon: Crown }, + { to: "/mypage/reservations", label: "예약 현황", icon: Calendar }, + { to: "/mypage/store", label: "내 가게 관리", icon: Store }, +]; + +export default function MyPageLayout() { + return ( +
+ {/* Header */} +
+
+
+ 잇츠파인 Eatsfine +
+ +
+
+ + {/* Page Title */} +
+

마이페이지

+

+ 계정 정보와 예약 내역을 관리하세요 +

+
+ + {/* Content */} +
+
+ {/* Sidebar */} + + + {/* Right content */} +
+ +
+
+
+
+ ); +} diff --git a/src/lib/cn.ts b/src/lib/cn.ts new file mode 100644 index 0000000..f9816e0 --- /dev/null +++ b/src/lib/cn.ts @@ -0,0 +1,3 @@ +export function cn(...classes: (string | undefined | false)[]) { + return classes.filter(Boolean).join(" "); +} diff --git a/src/pages/myPage/myInfoPage.tsx b/src/pages/myPage/myInfoPage.tsx new file mode 100644 index 0000000..d21fda3 --- /dev/null +++ b/src/pages/myPage/myInfoPage.tsx @@ -0,0 +1,184 @@ +import { Camera, Save } from "lucide-react"; +import { useRef, useState } from "react"; + +export default function MyInfoPage() { + const [isEditing, setIsEditing] = useState(false); + const fileInputRef = useRef(null); + const [profileImage, setProfileImage] = useState(null); + + const [form, setForm] = useState({ + email: "user@example.com", + nickname: "맛있는유저", + phone: "010-1234-5678", + }); + + const handleChange = (key: keyof typeof form, value: string) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleImageClick = () => { + fileInputRef.current?.click(); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const previewUrl = URL.createObjectURL(file); + setProfileImage(previewUrl); + + // TODO: 서버 업로드용 file은 따로 저장해도 됨 +}; + + const handleSave = () => { + // TODO: API 호출 + setIsEditing(false); + }; + + const handleCancel = () => { + // TODO: 원래 값으로 롤백하고 싶으면 여기서 처리 + setIsEditing(false); + }; + + return ( +
+ {/* title */} +
+

내 정보

+ + {!isEditing ? ( + + ) : ( +
+ + +
+ )} +
+ + {/* content */} +
+ {/* avatar */} +
+
+ {profileImage ? ( + 프로필 이미지 + ) : ( + + )} +
+ + {isEditing && ( + <> + + + + )} +
+ + + {/* form */} +
+ {/* 아이디 */} +
+ + +

+ 아이디는 변경할 수 없습니다 +

+
+ + {/* 이메일 */} +
+ + handleChange("email", e.target.value)} + className={`w-full rounded-lg border px-4 py-3 text-md ${ + isEditing + ? "border-gray-300 bg-white" + : "border-gray-200 bg-gray-50 text-gray-500" + }`} + /> +
+ + {/* 닉네임 */} +
+ + handleChange("nickname", e.target.value)} + className={`w-full rounded-lg border px-4 py-3 text-md ${ + isEditing + ? "border-gray-300 bg-white" + : "border-gray-200 bg-gray-50 text-gray-500" + }`} + /> +
+ + {/* 전화번호 */} +
+ + handleChange("phone", e.target.value)} + className={`w-full rounded-lg border px-4 py-3 text-md ${ + isEditing + ? "border-gray-300 bg-white" + : "border-gray-200 bg-gray-50 text-gray-500" + }`} + /> +
+
+
+
+ ); +} diff --git a/src/pages/myPage/myPage.tsx b/src/pages/myPage/myPage.tsx new file mode 100644 index 0000000..edf6d51 --- /dev/null +++ b/src/pages/myPage/myPage.tsx @@ -0,0 +1,24 @@ +import { Routes, Route, Navigate } from "react-router-dom"; +import MyPageLayout from "../../layouts/myPageLayout"; +import MyInfoPage from "./myInfoPage"; +import SettingPage from "./settingPage"; +import PaymentPage from "./paymentPage"; +import SubscriptionPage from "./subscriptionPage"; +import ReservationPage from "./reservationPage"; +import StorePage from "./storePage"; + +export default function MyPage() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/src/pages/myPage/paymentPage.tsx b/src/pages/myPage/paymentPage.tsx new file mode 100644 index 0000000..22ef05f --- /dev/null +++ b/src/pages/myPage/paymentPage.tsx @@ -0,0 +1,127 @@ +import { Plus, Trash2, CreditCard } from "lucide-react"; +import { useState } from "react"; +import { cn } from "../../lib/cn"; +import PaymentAddModal from "@/components/modals/paymentAddModal"; + +interface PaymentMethod { + id: string; + type: "easy-pay"; + name: string; + detail: string; + isDefault: boolean; + iconUrl?: string; // 간편결제 로고 URL +} + +export default function PaymentPage() { + const [methods, setMethods] = useState([ + { + id: "1", + type: "easy-pay", + name: "카카오페이", + detail: "", + isDefault: false, + }, + ]); + + const handleDelete = (id: string) => { + if (confirm("결제 수단을 삭제하시겠습니까?")) { + setMethods(methods.filter((m) => m.id !== id)); + } + }; + + const handleSetDefault = (id: string) => { + setMethods( + methods.map((m) => ({ + ...m, + isDefault: m.id === id, + })) + ); + }; + + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( +
+ {/* 헤더 영역 */} +
+
+

결제수단 관리

+

예약 시 사용할 결제수단을 관리하세요

+
+ +
+ + {/* 결제 수단 리스트 */} +
+ {methods.map((method) => ( +
+
+ {/* 아이콘 영역 */} +
+ 💛 + +
+ + {/* 정보 영역 */} +
+
+ {method.name} + {method.detail} + {method.isDefault && ( + + 기본 + + )} +
+

+ 간편결제 +

+
+
+ + {/* 액션 버튼 영역 */} +
+ {!method.isDefault && ( + + )} + +
+
+ ))} +
+ + setIsModalOpen(false)} + /> + + {methods.length === 0 && ( +
+

등록된 결제 수단이 없습니다.

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/myPage/reservationPage.tsx b/src/pages/myPage/reservationPage.tsx new file mode 100644 index 0000000..ed8285c --- /dev/null +++ b/src/pages/myPage/reservationPage.tsx @@ -0,0 +1,177 @@ +import { Calendar, Clock, User, CreditCard, Pencil, X } from "lucide-react"; +import { useState } from "react"; +import { cn } from "../../lib/cn"; + +type ReservationStatus = "전체" | "예정된 예약" | "방문 완료" | "취소된 예약"; + +export default function ReservationPage() { + const [activeTab, setActiveTab] = useState("전체"); + + const reservations = [ + { + id: 1, + shopName: "더 플레이스 강남점", + status: "예약 확정", + address: "서울 강남구 테헤란로 123", + date: "2025-12-01", + time: "18:30", + people: "4명 / 테이블 A-05", + payment: "₩50,000", + method: "신한카드 (****1234)", + step: "결제 완료", + }, + { + id: 2, + shopName: "이탈리안 키친", + status: "예약 확정", + address: "서울 서초구 서초대로 456", + date: "2025-11-28", + time: "19:00", + people: "2명 / 테이블 B-12", + payment: "₩80,000", + method: "카카오페이", + step: "결제 완료", + }, + { + id: 3, + shopName: "스시 마스터", + status: "방문 완료", + address: "서울 강남구 논현로 789", + date: "2025-11-20", + time: "20:00", + people: "3명 / 테이블 C-03", + payment: "₩120,000", + method: "토스페이", + step: "결제 완료", + }, + { + id: 4, + shopName: "한식당 정", + status: "취소됨", + address: "서울 종로구 인사동길 321", + date: "2025-11-15", + time: "12:30", + people: "5명 / 테이블 D-07", + payment: "₩90,000", + method: "신한카드 (****1234)", + step: "환불 완료", + }, + ]; + + // 필터링 로직: 이 변수를 사용하여 렌더링합니다. + const filteredReservations = reservations.filter((res) => { + if (activeTab === "전체") return true; + if (activeTab === "예정된 예약") return res.status === "예약 확정"; + if (activeTab === "방문 완료") return res.status === "방문 완료"; + if (activeTab === "취소된 예약") return res.status === "취소됨"; + return true; + }); + + return ( +
+
+

예약 현황

+

내 예약 내역을 확인하고 관리하세요

+
+ + {/* 탭 메뉴 */} +
+ {["전체", "예정된 예약", "방문 완료", "취소된 예약"].map((tab) => ( + + ))} +
+ + {/* 예약 리스트: reservations 대신 filteredReservations를 사용합니다. */} +
+ {filteredReservations.length > 0 ? ( + filteredReservations.map((res) => ( +
+
+
+
+

{res.shopName}

+ + {res.status} + +
+

{res.address}

+
+
+ + {/* 상세 정보 그리드 */} +
+ } label="예약 날짜" value={res.date} /> + } label="예약 시간" value={res.time} /> + } label="인원" value={res.people} /> + } label="결제 정보" value={`${res.payment}\n${res.method}`} isMultiLine /> +
+ + {/* 하단 버튼 영역 */} +
+ {res.step} +
+ {res.status === "예약 확정" && ( + <> + + + + )} + {res.status === "방문 완료" && ( + + )} +
+
+
+ )) + ) : ( +
+ 해당 내역이 없습니다. +
+ )} +
+
+ ); +} + +// 정보 아이템 컴포넌트 +function InfoItem({ icon, label, value, isMultiLine = false }: { icon: React.ReactNode, label: string, value: string, isMultiLine?: boolean }) { + return ( +
+
+ {icon} +
+
+

{label}

+

+ {value} +

+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/myPage/settingPage.tsx b/src/pages/myPage/settingPage.tsx new file mode 100644 index 0000000..547432d --- /dev/null +++ b/src/pages/myPage/settingPage.tsx @@ -0,0 +1,143 @@ +import { Lock, Bell, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { cn } from "../../lib/cn"; + +export default function SettingsPage() { + // 알림 설정을 위한 상태 관리 + const [notifications, setNotifications] = useState({ + reservation: true, + promotion: true, + review: false, + email: true, + sms: false, + }); + + const toggleNotification = (key: keyof typeof notifications) => { + setNotifications((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + return ( +
+
+

계정 설정

+ + {/* 비밀번호 변경 섹션 */} +
+
+ +
+
+
+

비밀번호 변경

+

정기적인 비밀번호 변경으로 계정을 안전하게 보호하세요

+
+ +
+
+ + {/* 알림 설정 섹션 */} +
+
+ +
+
+
+

알림 설정

+

받고 싶은 알림을 선택하세요

+
+ +
+ toggleNotification('reservation')} + /> + toggleNotification('promotion')} + /> + toggleNotification('review')} + /> +
+ + + +
+

알림 수신 방법

+
+ 이메일 + toggleNotification('email')} /> +
+
+ SMS + toggleNotification('sms')} /> +
+
+ + +
+
+ + {/* 계정 탈퇴 섹션 */} +
+
+ +
+
+
+

계정 탈퇴

+

계정을 삭제하면 모든 데이터가 영구적으로 삭제됩니다

+
+ +
+
+
+
+ ); +} + +// 토글 아이템 컴포넌트 +function ToggleButton({ label, description, enabled, onClick }: { label: string; description: string; enabled: boolean; onClick: () => void }) { + return ( +
+
+

{label}

+

{description}

+
+ +
+ ); +} + +// 스위치 UI 컴포넌트 +function Switch({ enabled, onClick }: { enabled: boolean; onClick: () => void }) { + return ( + + ); +} \ No newline at end of file diff --git a/src/pages/myPage/storePage.tsx b/src/pages/myPage/storePage.tsx new file mode 100644 index 0000000..b0a455e --- /dev/null +++ b/src/pages/myPage/storePage.tsx @@ -0,0 +1,147 @@ +import { Store, Calendar, Star, Plus, ChevronRight, BarChart3 } from "lucide-react"; +import { cn } from "../../lib/cn"; + +export default function StorePage() { + const stats = [ + { label: "총 가게 수", value: "3개", icon: , bgColor: "bg-blue-50", iconColor: "text-blue-500" }, + { label: "총 예약 수", value: "2126건", icon: , bgColor: "bg-indigo-50", iconColor: "text-indigo-500" }, + { label: "평균 평점", value: "4.8", icon: , bgColor: "bg-green-50", iconColor: "text-yellow-500" }, + ]; + + const shops = [ + { + id: 1, + name: "더 플레이스 강남점", + status: "운영중", + address: "서울 강남구 테헤란로 123", + category: "이탈리안", + totalReservations: "1234건", + rating: "4.8", + reviews: "256개", + }, + { + id: 2, + name: "일식당 스시마루", + status: "운영중", + address: "서울 강남구 논현로 456", + category: "일식", + totalReservations: "892건", + rating: "4.9", + reviews: "189개", + }, + { + id: 3, + name: "한식당 정", + status: "승인 대기", + address: "서울 종로구 인사동길 321", + category: "한식", + totalReservations: "-", + rating: "-", + reviews: "-", + notice: "가게 승인 대기 중입니다. 영업일 기준 1-2일 내에 승인이 완료됩니다.", + }, + ]; + + return ( +
+
+
+

내 가게 관리

+

등록한 식당을 관리하고 대시보드로 이동하세요

+
+ +
+ + {/* 요약 통계 카드 */} +
+ {stats.map((stat, index) => ( +
+
+

{stat.label}

+

{stat.value}

+
+
{stat.icon}
+
+ ))} +
+ + {/* 가게 리스트 */} +
+ {shops.map((shop) => ( +
+
+
+
+ +
+
+
+

{shop.name}

+ + {shop.status} + +
+

{shop.address}

+ + {shop.category} + +
+
+ +
+ + {/* 가게별 상세 수치: 승인 대기일 때는 아예 렌더링하지 않음 */} + {shop.status !== "승인 대기" && ( +
+
+

총 예약

+

{shop.totalReservations}

+
+
+

평점

+

+ {shop.rating} +

+
+
+

리뷰

+

{shop.reviews}

+
+
+ )} + + {/* 승인 대기 알림창: 승인 대기일 때만 표시 */} + {shop.status === "승인 대기" && shop.notice && ( +
+

+ {shop.notice} +

+
+ )} +
+ ))} +
+ + {/* 하단 프리미엄 홍보 배너 */} +
+
+
+ +
+
+

더 많은 데이터가 필요하신가요?

+

프리미엄 플랜으로 업그레이드하고 AI 데이터 인사이트, 상세 분석 리포트를 받아보세요.

+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/myPage/subscriptionPage.tsx b/src/pages/myPage/subscriptionPage.tsx new file mode 100644 index 0000000..40ce119 --- /dev/null +++ b/src/pages/myPage/subscriptionPage.tsx @@ -0,0 +1,213 @@ +import { Check, Crown } from "lucide-react"; +import { useState } from "react"; +import { cn } from "../../lib/cn"; + +export default function SubscriptionPage() { + const [billingCycle, setBillingCycle] = useState<"monthly" | "yearly">("monthly"); + // 현재 사용 중인 플랜 이름을 관리하는 상태 (기본값: 무료) + const [selectedPlan, setSelectedPlan] = useState("무료"); + + const getNextBillingDate = () => { + const date = new Date(); + date.setMonth(date.getMonth() + 1); + return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`; + }; + + const plans = [ + { + name: "무료", + price: 0, + features: ["기본 예약 기능", "월 3회 예약 가능", "일반 리뷰 작성", "기본 알림"], + }, + { + name: "베이직", + price: billingCycle === "monthly" ? 9900 : 99000, + features: ["무제한 예약", "우선 예약 혜택", "리뷰 작성 포인트 적립", "실시간 알림", "예약 변경 수수료 면제"], + }, + { + name: "프리미엄", + price: billingCycle === "monthly" ? 19900 : 199000, + features: ["베이직 플랜 모든 혜택", "AI맞춤 식당 추천", "VIP 예약 우선권", "특별 할인 쿠폰", "프리미엄 고객 지원", "동반 1인 무료 혜택"], + isRecommended: true, // 추천 플랜 표시를 위한 플래그 + }, + { + name: "비즈니스 (사장님)", + price: billingCycle === "monthly" ? 49900 : 499000, + features: ["식당 등록 및 관리", "통합 대시보드", "예약 관리 시스템", "결제 및 정산 기능", "AI 데이터 인사이트", "프리미엄 매장 노출", "24/7 전담 지원"], + } + ]; + + // 플랜 변경 핸들러 + const handlePlanChange = (planName: string) => { + if (confirm(`플랜을 ${planName} 플랜으로 바꾸시겠습니까?`)) { + setSelectedPlan(planName); + alert(`${planName} 플랜으로 변경되었습니다.`); + } + }; + + const handlePause = () => { + if (confirm("구독을 일시정지하시겠습니까? 혜택이 즉시 중단됩니다.")) { + alert("구독이 일시정지되었습니다."); + } + }; + + const handleCancel = () => { + if (confirm("정말로 구독을 취소하시겠습니까? 다음 결제일부터는 혜택을 이용하실 수 없습니다.")) { + setSelectedPlan("무료"); // 취소 시 무료 플랜으로 변경 예시 + alert("구독 취소가 완료되었습니다."); + } + }; + + return ( +
+
+

구독 관리

+

나에게 맞는 플랜을 선택하세요

+
+ + {/* 현재 플랜 요약 박스 */} +
+
+
+ + 현재 플랜 +
+

{selectedPlan}

+
+ + {/* 결제일 정보: 무료 플랜이 아닐 때만 표시 */} + {selectedPlan !== "무료" && ( +
+

다음 결제일 :

+

{getNextBillingDate()}

+
+ )} + {selectedPlan === "무료" && ( +

무료로 잇츠파인을 이용 중입니다

+ )} +
+ + {/* 결제 주기 전환 토글 */} +
+
+ + +
+
+
+ + {/* 플랜 카드 리스트 */} +
+ {plans.map((plan) => { + const isCurrent = selectedPlan === plan.name; + const isRecommended = plan.name === "프리미엄"; + + return ( +
+ {isCurrent && ( +
+ 현재 플랜 +
+ )} + + {!isCurrent && isRecommended && ( +
+ 추천 +
+ )} + +
+

{plan.name}

+
+ + ₩{plan.price.toLocaleString()} + + / {billingCycle === "monthly" ? "월" : "년"} +
+
+ +
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + +
+ ); + })} +
+ + {/* 여기에 추가: 하단 구독 관리 섹션 */} + {selectedPlan !== "무료" && ( +
+
+

구독 관리

+

구독을 일시정지하거나 취소할 수 있습니다

+
+
+ + +
+
+ )} +
+ ); +} \ No newline at end of file