Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -35,6 +36,11 @@ const routes: RouteObject[] = [
element: <CustomerSupportPage />,
errorElement: <NotFound />,
},
{
path: "/mypage/*",
element: <MyPage />,
errorElement: <NotFound />,
},
];

const router = createBrowserRouter(routes);
Expand Down
91 changes: 91 additions & 0 deletions src/components/modals/paymentAddModal.tsx
Original file line number Diff line number Diff line change
@@ -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<PaymentType>("kakao");

const handleAddSubmit = () =>{
alert("결제수단이 성공적으로 추가되었습니다.");
onClose();
}

if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">결제수단 추가</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X size={24} />
</button>
</div>

<div className="space-y-6">
{/* 결제 방법 선택 */}
<div className="space-y-3">
<p className="text-md font-medium text-gray-700">결제 방법 선택</p>
<div className="grid grid-cols-3 gap-3">
<PaymentTypeButton
active={selectedType === "kakao"}
onClick={() => setSelectedType("kakao")}
label="카카오페이"
icon={<span className="text-2xl">💛</span>}
/>
<PaymentTypeButton
active={selectedType === "toss"}
onClick={() => setSelectedType("toss")}
label="토스페이"
icon={<span className="text-2xl">💙</span>}
/>
</div>
</div>

{/* 하단 옵션 및 버튼 */}
<div className="space-y-6">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<span className="text-sm font-medium text-gray-700">기본 결제수단으로 설정</span>
</label>

<div className="flex gap-3">
<button
onClick={onClose}
className="flex-1 rounded-xl border border-gray-200 py-4 text-md font-bold text-gray-600 hover:bg-gray-50 transition-colors"
>
취소
</button>
<button className="flex-1 rounded-xl bg-blue-500 py-4 text-md font-bold text-white hover:bg-blue-600 transition-colors"
onClick={handleAddSubmit}>
추가하기
</button>
</div>
</div>
</div>
</div>
</div>
);
}

function PaymentTypeButton({ active, onClick, label, icon }: { active: boolean; onClick: () => void; label: string; icon: React.ReactNode }) {
return (
<button
onClick={onClick}
className={cn(
"flex flex-col items-center justify-center gap-2 rounded-xl border-2 py-4 transition-all",
active ? "border-blue-500 bg-blue-50/50" : "border-gray-100 bg-white hover:border-gray-200"
)}
>
{icon}
<span className={cn("text-sm font-medium", active ? "text-blue-600" : "text-gray-600")}>{label}</span>
</button>
);
}
11 changes: 11 additions & 0 deletions src/components/profile/profileAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
interface Props {
name: string;
}

export default function ProfileAvatar({ name }: Props) {
return (
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-gray-200 text-3xl font-semibold text-gray-600">
{name[0]}
</div>
);
}
85 changes: 85 additions & 0 deletions src/layouts/myPageLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="w-full border-b bg-white">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
<div className="text-md text-blue-600 font-medium">
잇츠파인 Eatsfine
</div>
<button className="flex items-center gap-2 px-4 py-2 text-md text-gray-600 hover:text-gray-900 transition-colors">
로그아웃
</button>
</div>
</header>

{/* Page Title */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
<p className="text-xl text-gray-900">마이페이지</p>
<p className="mt-2 text-md text-gray-600">
계정 정보와 예약 내역을 관리하세요
</p>
</div>

{/* Content */}
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-12">
<div className="flex flex-col md:flex-row gap-6 items-start">
{/* Sidebar */}
<aside className="w-full md:w-60 rounded-xl bg-white shadow-sm overflow-hidden">
<nav className="flex flex-col">
{sidebarItems.map(({ to, label, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
cn(
"relative flex h-12 items-center gap-3 px-5 py-7 text-md font-medium transition",
isActive
? "bg-blue-50 text-blue-600"
: "text-gray-700 hover:bg-gray-100"
)
}
>
{({ isActive }) => (
<>
{isActive && (
<span className="absolute left-0 h-full w-1 bg-blue-500" />
)}
<Icon size={18} />
<span>{label}</span>
</>
)}
</NavLink>
))}
</nav>
</aside>

{/* Right content */}
<main className="w-full flex-1">
<Outlet />
</main>
</div>
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions src/lib/cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function cn(...classes: (string | undefined | false)[]) {
return classes.filter(Boolean).join(" ");
}
184 changes: 184 additions & 0 deletions src/pages/myPage/myInfoPage.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement | null>(null);
const [profileImage, setProfileImage] = useState<string | null>(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<HTMLInputElement>) => {
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 (
<section className="rounded-xl bg-white p-8 shadow-sm">
{/* title */}
<div className="mb-8 flex items-center justify-between">
<h2 className="text-md text-gray-800">내 정보</h2>

{!isEditing ? (
<button
onClick={() => setIsEditing(true)}
className="cursor-pointer rounded-lg px-3 py-2 text-md font-medium text-blue-600 hover:bg-blue-50"
>
수정하기
</button>
) : (
<div className="flex gap-2">
<button
onClick={handleCancel}
className="cursor-pointer rounded-lg px-3 py-2 text-md text-gray-600 hover:bg-gray-100"
>
취소
</button>
<button
onClick={handleSave}
className="cursor-pointer flex items-center rounded-lg bg-blue-500 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
<Save size={16} />
<p className="px-2">
저장
</p>
</button>
</div>
)}
</div>

{/* content */}
<div className="flex flex-col items-center gap-10">
{/* avatar */}
<div className="relative">
<div className="flex h-30 w-30 items-center justify-center overflow-hidden rounded-full bg-gray-200">
{profileImage ? (
<img
src={profileImage}
alt="프로필 이미지"
className="h-full w-full object-cover"
/>
) : (
<span className="text-3xl text-gray-500">맛</span>
)}
</div>

{isEditing && (
<>
<button
onClick={handleImageClick}
className="cursor-pointer absolute bottom-1 right-1 flex h-9 w-9 items-center justify-center rounded-full bg-blue-500 text-white shadow hover:bg-blue-700"
>
<Camera size={20} />
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageChange}
className="hidden"
/>
</>
)}
</div>


{/* form */}
<div className="w-full space-y-5">
{/* 아이디 */}
<div>
<label className="mb-1 block text-md text-gray-600">
아이디
</label>
<input
disabled
value="user1234"
className="w-full rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-md text-gray-500"
/>
<p className="mt-1 text-sm text-gray-400">
아이디는 변경할 수 없습니다
</p>
</div>

{/* 이메일 */}
<div>
<label className="mb-1 block text-md text-gray-600">
이메일
</label>
<input
disabled={!isEditing}
value={form.email}
onChange={(e) => 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"
}`}
/>
</div>

{/* 닉네임 */}
<div>
<label className="mb-1 block text-md text-gray-600">
닉네임
</label>
<input
disabled={!isEditing}
value={form.nickname}
onChange={(e) => 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"
}`}
/>
</div>

{/* 전화번호 */}
<div>
<label className="mb-1 block text-md text-gray-600">
전화번호
</label>
<input
disabled={!isEditing}
value={form.phone}
onChange={(e) => 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"
}`}
/>
</div>
</div>
</div>
</section>
);
}
Loading