ㄱㄱ
This commit is contained in:
149
src/app/menu/ChangePasswordModal.tsx
Normal file
149
src/app/menu/ChangePasswordModal.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit?: (payload: { email: string; code: string; newPassword: string }) => void;
|
||||
};
|
||||
|
||||
export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) {
|
||||
const [email, setEmail] = useState("xrlms2025@gmail.com");
|
||||
const [code, setCode] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null); // 인증번호 오류 등
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleSubmit = () => {
|
||||
setError(null);
|
||||
if (!code) {
|
||||
setError("인증번호를 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
if (!newPassword || !confirmPassword) {
|
||||
setError("새 비밀번호를 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError("새 비밀번호가 일치하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
onSubmit?.({ email, code, newPassword });
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-[2px]"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="w-[480px] rounded-[12px] border border-[#dee1e6] bg-white shadow-[0_10px_30px_rgba(0,0,0,0.06)]">
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<h2 className="text-[20px] font-bold leading-[1.5] text-[#333c47]">비밀번호 변경</h2>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={onClose}
|
||||
className="inline-flex size-6 items-center justify-center text-[#333c47] hover:opacity-80"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-[15.2px]">
|
||||
<path fillRule="evenodd" d="M6.225 4.811a1 1 0 0 1 1.414 0L12 9.172l4.361-4.361a1 1 0 1 1 1.414 1.414L13.414 10.586l4.361 4.361a1 1 0 0 1-1.414 1.414L12 12l-4.361 4.361a1 1 0 1 1-1.414-1.414l4.361-4.361-4.361-4.361a1 1 0 0 1 0-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* body */}
|
||||
<div className="flex flex-col gap-[10px] px-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="w-[100px] text-[15px] font-semibold leading-[1.5] text-[#6c7682]">아이디 (이메일)</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="h-10 flex-1 rounded-[8px] border border-[#dee1e6] bg-white px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none"
|
||||
placeholder="이메일"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 w-[136px] rounded-[8px] bg-[#f1f3f5] px-3 text-[16px] font-semibold leading-[1.5] text-[#333c47]"
|
||||
>
|
||||
인증번호 재전송
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-[100px] text-[15px] font-semibold leading-[1.5] text-[#6c7682]">인증번호</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
className="h-10 flex-1 rounded-[8px] border border-[#dee1e6] bg-white px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none"
|
||||
placeholder="인증번호 6자리"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 w-[136px] rounded-[8px] bg-[#f1f3f5] px-3 text-[16px] font-semibold leading-[1.5] text-[#4c5561]"
|
||||
>
|
||||
인증번호 확인
|
||||
</button>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="px-1 text-[13px] font-semibold leading-[1.4] text-[#f64c4c]">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682]">새 비밀번호</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="h-10 rounded-[8px] border border-[#dee1e6] bg-neutral-50 px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none"
|
||||
placeholder="새 비밀번호"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682]">
|
||||
새 비밀번호 확인
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-10 rounded-[8px] border border-[#dee1e6] bg-neutral-50 px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none"
|
||||
placeholder="새 비밀번호 확인"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
<div className="flex items-center justify-center gap-3 p-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="h-12 w-[136px] rounded-[10px] bg-[#f1f3f5] px-4 text-[16px] font-semibold leading-[1.5] text-[#4c5561]"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="h-12 w-[136px] rounded-[10px] bg-[#8598e8] px-4 text-[16px] font-semibold leading-[1.5] text-white"
|
||||
>
|
||||
비밀번호 변경
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
70
src/app/menu/MenuSidebar.tsx
Normal file
70
src/app/menu/MenuSidebar.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type NavItem = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
const learningItems: NavItem[] = [
|
||||
{ label: "내 강좌실", href: "/menu/courses" },
|
||||
{ label: "학습 결과", href: "/menu/results" },
|
||||
];
|
||||
|
||||
const accountItems: NavItem[] = [
|
||||
{ label: "내 정보 수정", href: "/menu/account" },
|
||||
{ label: "로그아웃", href: "/logout" },
|
||||
];
|
||||
|
||||
export default function MenuSidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const renderItem = (item: NavItem) => {
|
||||
const isActive =
|
||||
pathname === item.href || (item.href !== "/logout" && pathname.startsWith(item.href));
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={[
|
||||
"flex h-12 items-center gap-2 rounded-lg px-2",
|
||||
isActive ? "bg-[rgba(236,240,255,0.5)]" : "",
|
||||
].join(" ")}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
"text-[16px] leading-[1.5]",
|
||||
isActive ? "font-bold text-[#1f2b91]" : "font-medium text-[#333c47]",
|
||||
].join(" ")}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="flex w-full flex-col gap-6">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<div className="w-[288px]">
|
||||
<div className="px-2 pb-2">
|
||||
<span className="text-[13px] font-normal leading-[1.4] text-[#6c7682]">
|
||||
강좌 및 학습
|
||||
</span>
|
||||
</div>
|
||||
<div>{learningItems.map(renderItem)}</div>
|
||||
</div>
|
||||
<div className="w-[288px]">
|
||||
<div className="px-2 pb-2">
|
||||
<span className="text-[13px] font-normal leading-[1.4] text-[#6c7682]">계정</span>
|
||||
</div>
|
||||
<div>{accountItems.map(renderItem)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
60
src/app/menu/account/page.tsx
Normal file
60
src/app/menu/account/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import ChangePasswordModal from "../ChangePasswordModal";
|
||||
|
||||
export default function AccountPage() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<main className="flex w-full flex-col">
|
||||
<div className="flex h-[100px] items-center px-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">내 정보 수정</h1>
|
||||
</div>
|
||||
<div className="px-8 pb-20">
|
||||
<div className="rounded-lg border border-[#dee1e6] bg-white p-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="w-[100px] text-[15px] font-semibold leading-[1.5] text-[#6c7682]">
|
||||
아이디 (이메일)
|
||||
</label>
|
||||
<div className="h-10 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
||||
<span className="text-[16px] leading-[1.5] text-[#333c47]">skyblue@edu.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col gap-2">
|
||||
<label className="w-[100px] text-[15px] font-semibold leading-[1.5] text-[#6c7682]">
|
||||
비밀번호 변경
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 flex-1 rounded-lg border border-[#dee1e6] bg-neutral-50 px-3 py-2">
|
||||
<span className="text-[16px] leading-[1.5] text-[#333c47]">●●●●●●●●●●</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="h-10 rounded-lg bg-[#f1f3f5] px-4 text-[16px] font-semibold leading-[1.5] text-[#4c5561]"
|
||||
>
|
||||
비밀번호 변경
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button className="text-[15px] font-medium leading-[1.5] text-[#f64c4c] underline">
|
||||
회원 탈퇴하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChangePasswordModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onSubmit={() => {
|
||||
// TODO: integrate API
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
14
src/app/menu/courses/page.tsx
Normal file
14
src/app/menu/courses/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function CoursesPage() {
|
||||
return (
|
||||
<main className="flex w-full flex-col">
|
||||
<div className="flex h-[100px] items-center px-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">내 강좌실</h1>
|
||||
</div>
|
||||
<div className="px-8 pb-20">
|
||||
<p className="text-[16px] leading-[1.5] text-[#4c5561]">콘텐츠 준비 중입니다.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
15
src/app/menu/layout.tsx
Normal file
15
src/app/menu/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ReactNode } from "react";
|
||||
import MenuSidebar from "./MenuSidebar";
|
||||
|
||||
export default function MenuLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1440px]">
|
||||
<aside className="w-[320px] border-r border-[#dee1e6] px-4 py-6">
|
||||
<MenuSidebar />
|
||||
</aside>
|
||||
<section className="flex-1">{children}</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
12
src/app/menu/page.tsx
Normal file
12
src/app/menu/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function MenuPage() {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-[1440px] px-8 py-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">교육 과정 목록</h1>
|
||||
<p className="mt-6 text-[16px] leading-[1.5] text-[#4c5561]">
|
||||
메뉴 페이지 준비 중입니다.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
14
src/app/menu/results/page.tsx
Normal file
14
src/app/menu/results/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function ResultsPage() {
|
||||
return (
|
||||
<main className="flex w-full flex-col">
|
||||
<div className="flex h-[100px] items-center px-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">학습 결과</h1>
|
||||
</div>
|
||||
<div className="px-8 pb-20">
|
||||
<p className="text-[16px] leading-[1.5] text-[#4c5561]">콘텐츠 준비 중입니다.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user