ㄱㄱㄱ
This commit is contained in:
70
src/app/menu/AccountDeleteModal.tsx
Normal file
70
src/app/menu/AccountDeleteModal.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import ModalCloseSvg from "../svgs/closexsvg";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
};
|
||||
|
||||
export default function AccountDeleteModal({ open, onClose, onConfirm }: Props) {
|
||||
if (!open) return null;
|
||||
|
||||
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-[528px] 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 cursor-pointer"
|
||||
>
|
||||
<ModalCloseSvg />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* body */}
|
||||
<div className="px-6">
|
||||
<div className="rounded-[16px] border border-[#dee1e6] bg-gray-50 p-6">
|
||||
<p className="mb-3 text-[15px] font-bold leading-[1.5] text-[#4c5561]">
|
||||
회원 탈퇴 시 유의사항을 확인해주세요.
|
||||
</p>
|
||||
<div className="text-[15px] leading-[1.5] text-[#4c5561]">
|
||||
<p className="mb-0">- 탈퇴 후에도 재가입은 가능합니다.</p>
|
||||
<p className="mb-0">- 수강 및 학습 이력이 모두 삭제되며, 복구가 불가능합니다.</p>
|
||||
<p>- 수강 서비스 이용 권한이 즉시 종료됩니다.</p>
|
||||
</div>
|
||||
</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] cursor-pointer"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
className="h-12 w-[136px] rounded-[10px] bg-red-50 px-4 text-[16px] font-semibold leading-[1.5] text-[#f64c4c] cursor-pointer"
|
||||
>
|
||||
회원 탈퇴
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ModalCloseSvg from "../svgs/closexsvg";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit?: (payload: { email: string; code: string; newPassword: string }) => void;
|
||||
onSubmit?: (payload: { email: string; code?: string; newPassword: string }) => void;
|
||||
showVerification?: boolean;
|
||||
devVerificationState?: 'initial' | 'sent' | 'verified' | 'failed';
|
||||
};
|
||||
|
||||
export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) {
|
||||
export default function ChangePasswordModal({ open, onClose, onSubmit, showVerification = false, devVerificationState }: 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); // 인증번호 오류 등
|
||||
const [requireCode, setRequireCode] = useState<boolean>(showVerification);
|
||||
const [isCodeSent, setIsCodeSent] = useState<boolean>(showVerification);
|
||||
const canConfirm = code.trim().length > 0;
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
const hasError = !!error;
|
||||
|
||||
// 외부에서 전달된 개발모드 상태(devVerificationState)에 따라 UI 동기화
|
||||
useEffect(() => {
|
||||
if (!devVerificationState) return;
|
||||
switch (devVerificationState) {
|
||||
case 'initial':
|
||||
setRequireCode(false);
|
||||
setIsCodeSent(false);
|
||||
setCode("");
|
||||
setError(null);
|
||||
setIsVerified(false);
|
||||
break;
|
||||
case 'sent':
|
||||
setRequireCode(true);
|
||||
setIsCodeSent(true);
|
||||
setCode("");
|
||||
setError(null);
|
||||
setIsVerified(false);
|
||||
break;
|
||||
case 'verified':
|
||||
setRequireCode(true);
|
||||
setIsCodeSent(true);
|
||||
setCode("123456");
|
||||
setError(null);
|
||||
setIsVerified(true);
|
||||
break;
|
||||
case 'failed':
|
||||
setRequireCode(true);
|
||||
setIsCodeSent(true);
|
||||
setCode("");
|
||||
setError("올바르지 않은 인증번호입니다. 인증번호를 확인해주세요.");
|
||||
setIsVerified(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [devVerificationState]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleSubmit = () => {
|
||||
setError(null);
|
||||
if (!code) {
|
||||
setError("인증번호를 입력해 주세요.");
|
||||
return;
|
||||
if (requireCode) {
|
||||
if (!code) {
|
||||
setError("인증번호를 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!newPassword || !confirmPassword) {
|
||||
setError("새 비밀번호를 입력해 주세요.");
|
||||
@@ -31,7 +78,7 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props)
|
||||
setError("새 비밀번호가 일치하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
onSubmit?.({ email, code, newPassword });
|
||||
onSubmit?.({ email, code: requireCode ? code : undefined, newPassword });
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -49,11 +96,9 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props)
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={onClose}
|
||||
className="inline-flex size-6 items-center justify-center text-[#333c47] hover:opacity-80"
|
||||
className="inline-flex size-6 items-center justify-center text-[#333c47] hover:opacity-80 cursor-pointer"
|
||||
>
|
||||
<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>
|
||||
<ModalCloseSvg />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -66,47 +111,75 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props)
|
||||
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"
|
||||
className={[
|
||||
"h-10 flex-1 rounded-[8px] border border-[#dee1e6] px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none",
|
||||
hasError ? "bg-white" : isCodeSent ? "bg-neutral-50" : "bg-white",
|
||||
].join(" ")}
|
||||
placeholder="이메일"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 w-[136px] rounded-[8px] bg-[#f1f3f5] px-3 text-[16px] font-semibold leading-[1.5] text-[#333c47]"
|
||||
onClick={() => {
|
||||
setRequireCode(true);
|
||||
setIsCodeSent(true);
|
||||
setError(null);
|
||||
}}
|
||||
className="h-10 w-[136px] rounded-[8px] bg-[#f1f3f5] px-3 text-[16px] font-semibold leading-[1.5] text-[#333c47] cursor-pointer"
|
||||
>
|
||||
인증번호 재전송
|
||||
{isCodeSent ? "인증번호 재전송" : "인증번호 전송"}
|
||||
</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>
|
||||
{requireCode ? (
|
||||
<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="인증번호를 입력해 주세요."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canConfirm}
|
||||
className={[
|
||||
"h-10 w-[136px] rounded-[8px] px-3 text-[16px] font-semibold leading-[1.5] cursor-pointer disabled:cursor-default",
|
||||
canConfirm ? "bg-[#f1f3f5] text-[#4c5561]" : "bg-gray-50 text-[#b1b8c0]",
|
||||
].join(" ")}
|
||||
>
|
||||
인증번호 확인
|
||||
</button>
|
||||
</div>
|
||||
{isCodeSent && !hasError && !isVerified ? (
|
||||
<div className="px-1">
|
||||
<p className="text-[13px] font-semibold leading-[1.4] text-[#384fbf]">
|
||||
인증 확인을 위해 작성한 이메일로 인증번호를 발송했습니다.
|
||||
</p>
|
||||
<p className="text-[13px] font-semibold leading-[1.4] text-[#384fbf]">
|
||||
이메일을 확인해 주세요.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
<p className="px-1 text-[13px] font-semibold leading-[1.4] text-[#f64c4c]">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="px-1 text-[13px] font-semibold leading-[1.4] text-[#f64c4c]">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<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"
|
||||
disabled={!isVerified}
|
||||
className={[
|
||||
"h-10 rounded-[8px] border border-[#dee1e6] px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none",
|
||||
isVerified ? "bg-white" : "bg-neutral-50",
|
||||
].join(" ")}
|
||||
placeholder="새 비밀번호"
|
||||
/>
|
||||
</div>
|
||||
@@ -118,7 +191,11 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props)
|
||||
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"
|
||||
disabled={!isVerified}
|
||||
className={[
|
||||
"h-10 rounded-[8px] border border-[#dee1e6] px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none",
|
||||
isVerified ? "bg-white" : "bg-neutral-50",
|
||||
].join(" ")}
|
||||
placeholder="새 비밀번호 확인"
|
||||
/>
|
||||
</div>
|
||||
@@ -129,14 +206,14 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props)
|
||||
<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]"
|
||||
className="h-12 w-[136px] rounded-[10px] bg-[#f1f3f5] px-4 text-[16px] font-semibold leading-[1.5] text-[#4c5561] cursor-pointer"
|
||||
>
|
||||
취소
|
||||
</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"
|
||||
className="h-12 w-[136px] rounded-[10px] bg-[#8598e8] px-4 text-[16px] font-semibold leading-[1.5] text-white cursor-pointer"
|
||||
>
|
||||
비밀번호 변경
|
||||
</button>
|
||||
|
||||
56
src/app/menu/PasswordChangeDoneModal.tsx
Normal file
56
src/app/menu/PasswordChangeDoneModal.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import Link from "next/link";
|
||||
import ModalCloseSvg from "../svgs/closexsvg";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function PasswordChangeDoneModal({ open, onClose }: Props) {
|
||||
if (!open) return null;
|
||||
|
||||
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] h-[437px] rounded-[12px] border border-[#dee1e6] bg-white shadow-[0_10px_30px_rgba(0,0,0,0.06)] flex flex-col justify-between">
|
||||
<div className="flex h-[80px] items-center justify-end p-6">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={onClose}
|
||||
className="inline-flex size-6 items-center justify-center text-[#333c47] hover:opacity-80 cursor-pointer"
|
||||
>
|
||||
<ModalCloseSvg />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-0 ">
|
||||
<div className="w-full mx-auto flex flex-col items-center gap-4">
|
||||
<h2 className="text-[24px] font-extrabold leading-[1.45] text-[#333c47]">
|
||||
비밀번호 변경이 완료됐습니다.
|
||||
</h2>
|
||||
<p className="text-[18px] leading-[1.5] text-[#6c7682] text-center">
|
||||
새로운 비밀번호로 다시 로그인 해주세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-6">
|
||||
<Link
|
||||
href="/login"
|
||||
className="h-12 w-[284px] rounded-[10px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white inline-flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
로그인
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
151
src/app/menu/account/MenuAccountOption.tsx
Normal file
151
src/app/menu/account/MenuAccountOption.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed';
|
||||
|
||||
type MenuAccountOptionProps = {
|
||||
verificationState: VerificationState;
|
||||
setVerificationState: (state: VerificationState) => void;
|
||||
deleteOpen?: boolean;
|
||||
setDeleteOpen?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export default function MenuAccountOption({
|
||||
verificationState,
|
||||
setVerificationState,
|
||||
deleteOpen,
|
||||
setDeleteOpen,
|
||||
}: MenuAccountOptionProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const itemClass = (active: boolean) =>
|
||||
[
|
||||
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
|
||||
active ? "bg-blue-600" : "bg-gray-300",
|
||||
].join(" ");
|
||||
|
||||
const knobClass = (active: boolean) =>
|
||||
[
|
||||
"inline-block h-5 w-5 transform rounded-full bg-white transition",
|
||||
active ? "translate-x-5" : "translate-x-1",
|
||||
].join(" ");
|
||||
|
||||
const is = {
|
||||
initial: verificationState === 'initial',
|
||||
sent: verificationState === 'sent',
|
||||
verified: verificationState === 'verified',
|
||||
failed: verificationState === 'failed',
|
||||
changed: verificationState === 'changed',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`fixed bottom-2 right-2 bg-red-400 cursor-pointer rounded-full w-[40px] h-[40px] shadow-xl z-100`}
|
||||
>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||
<div className="w-[500px] h-[600px] flex bg-white/80 p-10 border rounded-lg relative">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="absolute top-3 right-3 inline-flex items-center justify-center rounded-full w-8 h-8 bg-gray-200 hover:bg-gray-300 text-gray-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="w-full h-full overflow-auto">
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-700">현재 상태: <span className="font-semibold">{verificationState}</span></p>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="mr-4">초기 상태</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="초기 상태로 설정"
|
||||
aria-pressed={is.initial}
|
||||
onClick={() => setVerificationState('initial')}
|
||||
className={itemClass(is.initial)}
|
||||
>
|
||||
<span className={knobClass(is.initial)} />
|
||||
</button>
|
||||
</li>
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="mr-4">인증번호 전송 상태</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="인증번호 전송 상태로 설정"
|
||||
aria-pressed={is.sent}
|
||||
onClick={() => setVerificationState('sent')}
|
||||
className={itemClass(is.sent)}
|
||||
>
|
||||
<span className={knobClass(is.sent)} />
|
||||
</button>
|
||||
</li>
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="mr-4">인증 완료 상태</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="인증 완료 상태로 설정"
|
||||
aria-pressed={is.verified}
|
||||
onClick={() => setVerificationState('verified')}
|
||||
className={itemClass(is.verified)}
|
||||
>
|
||||
<span className={knobClass(is.verified)} />
|
||||
</button>
|
||||
</li>
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="mr-4">인증 실패 상태</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="인증 실패 상태로 설정"
|
||||
aria-pressed={is.failed}
|
||||
onClick={() => setVerificationState('failed')}
|
||||
className={itemClass(is.failed)}
|
||||
>
|
||||
<span className={knobClass(is.failed)} />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-6 border-t pt-6">
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="mr-4">비밀번호 변경 완료 상태</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="비밀번호 변경 완료 상태로 설정"
|
||||
aria-pressed={is.changed}
|
||||
onClick={() =>
|
||||
setVerificationState(is.changed ? 'initial' : 'changed')
|
||||
}
|
||||
className={itemClass(is.changed)}
|
||||
>
|
||||
<span className={knobClass(is.changed)} />
|
||||
</button>
|
||||
</li>
|
||||
<li className="mt-4 flex items-center justify-between">
|
||||
<p className="mr-4">회원 탈퇴 모달</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="회원 탈퇴 모달 토글"
|
||||
aria-pressed={!!deleteOpen}
|
||||
onClick={() => setDeleteOpen?.(!deleteOpen)}
|
||||
className={itemClass(!!deleteOpen)}
|
||||
>
|
||||
<span className={knobClass(!!deleteOpen)} />
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ChangePasswordModal from "../ChangePasswordModal";
|
||||
import PasswordChangeDoneModal from "../PasswordChangeDoneModal";
|
||||
import AccountDeleteModal from "../AccountDeleteModal";
|
||||
import MenuAccountOption from "@/app/menu/account/MenuAccountOption";
|
||||
|
||||
type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed';
|
||||
|
||||
export default function AccountPage() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [verificationState, setVerificationState] = useState<VerificationState>('initial');
|
||||
const [doneOpen, setDoneOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
// 개발 옵션에서 'changed'로 전환하면 완료 모달 표시
|
||||
useEffect(() => {
|
||||
setDoneOpen(verificationState === 'changed');
|
||||
}, [verificationState]);
|
||||
|
||||
return (
|
||||
<main className="flex w-full flex-col">
|
||||
@@ -40,7 +53,11 @@ export default function AccountPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button className="text-[15px] font-medium leading-[1.5] text-[#f64c4c] underline">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
className="text-[15px] font-medium leading-[1.5] text-[#f64c4c] underline cursor-pointer"
|
||||
>
|
||||
회원 탈퇴하기
|
||||
</button>
|
||||
</div>
|
||||
@@ -52,6 +69,28 @@ export default function AccountPage() {
|
||||
onSubmit={() => {
|
||||
// TODO: integrate API
|
||||
}}
|
||||
devVerificationState={verificationState}
|
||||
/>
|
||||
|
||||
<MenuAccountOption
|
||||
verificationState={verificationState}
|
||||
setVerificationState={setVerificationState}
|
||||
deleteOpen={deleteOpen}
|
||||
setDeleteOpen={setDeleteOpen}
|
||||
/>
|
||||
|
||||
<PasswordChangeDoneModal
|
||||
open={doneOpen}
|
||||
onClose={() => setDoneOpen(false)}
|
||||
/>
|
||||
|
||||
<AccountDeleteModal
|
||||
open={deleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
onConfirm={() => {
|
||||
// TODO: 탈퇴 API 연동
|
||||
setDeleteOpen(false);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
131
src/app/menu/courses/CourseCard.tsx
Normal file
131
src/app/menu/courses/CourseCard.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
type Lesson = {
|
||||
id: string;
|
||||
title: string;
|
||||
durationMin: number;
|
||||
progressPct: number; // 0~100
|
||||
isCompleted: boolean;
|
||||
};
|
||||
|
||||
type Course = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: string;
|
||||
status: "전체" | "수강 예정" | "수강중" | "수강 완료";
|
||||
progressPct: number;
|
||||
lessons: Lesson[];
|
||||
};
|
||||
|
||||
function ProgressBar({ value }: { value: number }) {
|
||||
const pct = Math.max(0, Math.min(100, value));
|
||||
return (
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-[#ecf0ff]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#384fbf] transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CourseCard({ course }: { course: Course }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<article className="rounded-xl border border-[#ecf0ff] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<header className="flex items-center gap-4 p-4">
|
||||
<div className="relative h-[76px] w-[120px] overflow-hidden rounded-md bg-[#f1f3f5]">
|
||||
<Image src={course.thumbnail} alt="" fill sizes="120px" className="object-cover" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded bg-[#e5f5ec] px-2 py-0.5 text-[12px] font-semibold leading-[1.4] text-[#0c9d61]">
|
||||
{course.status}
|
||||
</span>
|
||||
<h2 className="truncate text-[18px] font-bold leading-[1.5] text-[#1b2027]">
|
||||
{course.title}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-[14px] leading-[1.5] text-[#4c5561]">{course.description}</p>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<ProgressBar value={course.progressPct} />
|
||||
<span className="w-[72px] text-right text-[13px] font-medium leading-[1.4] text-[#6c7682]">
|
||||
진행률 {course.progressPct}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-start">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-[#dee1e6] px-3 py-2 text-[13px] font-semibold leading-[1.4] text-[#333c47] hover:bg-[#f9fafb]"
|
||||
>
|
||||
이어 학습하기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[#dee1e6] text-[#6c7682] hover:bg-[#f9fafb]"
|
||||
aria-label={open ? "접기" : "펼치기"}
|
||||
>
|
||||
<span
|
||||
className={["inline-block transition-transform", open ? "rotate-180" : "rotate-0"].join(" ")}
|
||||
>
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{open ? (
|
||||
<div className="px-4 pb-4">
|
||||
<ul className="flex flex-col gap-2">
|
||||
{course.lessons.map((lesson, idx) => (
|
||||
<li key={lesson.id} className="rounded-lg border border-[#ecf0ff]">
|
||||
<div className="flex items-center justify-between gap-4 px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-[20px] text-[13px] font-semibold leading-[1.4] text-[#8c95a1]">
|
||||
{idx + 1}.
|
||||
</span>
|
||||
<p className="truncate text-[14px] font-medium leading-[1.5] text-[#333c47]">
|
||||
{lesson.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<ProgressBar value={lesson.progressPct} />
|
||||
<span className="w-[80px] text-right text-[13px] leading-[1.4] text-[#8c95a1]">
|
||||
{lesson.progressPct}% · {lesson.durationMin}분
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{lesson.isCompleted ? (
|
||||
<span className="rounded bg-[#e5f5ec] px-2 py-1 text-[12px] font-semibold leading-[1.4] text-[#0c9d61]">
|
||||
수료
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-[#dee1e6] px-3 py-2 text-[13px] font-semibold leading-[1.4] text-[#333c47] hover:bg-[#f9fafb]"
|
||||
>
|
||||
학습하기
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
61
src/app/menu/courses/CourseGridItem.tsx
Normal file
61
src/app/menu/courses/CourseGridItem.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
export type CourseGridItemProps = {
|
||||
id: string;
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
isNew?: boolean;
|
||||
category?: string; // ex) 안전, 공정 등
|
||||
meta?: string; // ex) "VOD · 총 6강 · 4시간 20분"
|
||||
};
|
||||
|
||||
export default function CourseGridItem({
|
||||
id,
|
||||
thumbnail,
|
||||
title,
|
||||
isNew,
|
||||
category,
|
||||
meta,
|
||||
}: CourseGridItemProps) {
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="group flex w-full flex-col gap-2"
|
||||
>
|
||||
<div className="relative aspect-[16/10] w-full overflow-hidden rounded-lg border border-[#ecf0ff] bg-[#f9fafb]">
|
||||
<Image
|
||||
src={thumbnail}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 240px"
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-[1.02]"
|
||||
priority={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
{category ? (
|
||||
<span className="rounded bg-[#e5f5ec] px-1.5 py-0.5 text-[12px] font-semibold leading-[1.4] text-[#0c9d61]">
|
||||
{category}
|
||||
</span>
|
||||
) : null}
|
||||
{isNew ? (
|
||||
<span className="rounded bg-[#ecf0ff] px-1.5 py-0.5 text-[12px] font-semibold leading-[1.4] text-[#1f2b91]">
|
||||
NEW
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="mt-1 line-clamp-1 text-[14px] font-semibold leading-[1.5] text-[#333c47]">
|
||||
{title}
|
||||
</h3>
|
||||
{meta ? (
|
||||
<p className="mt-0.5 text-[12px] leading-[1.4] text-[#8c95a1]">{meta}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import CourseCard from "./CourseCard";
|
||||
|
||||
type CourseStatus = "전체" | "수강 예정" | "수강중" | "수강 완료";
|
||||
|
||||
type Lesson = {
|
||||
id: string;
|
||||
title: string;
|
||||
durationMin: number;
|
||||
progressPct: number; // 0~100
|
||||
isCompleted: boolean;
|
||||
};
|
||||
|
||||
type Course = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: string;
|
||||
status: CourseStatus;
|
||||
progressPct: number;
|
||||
lessons: Lesson[];
|
||||
};
|
||||
|
||||
const MOCK_COURSES: Course[] = [
|
||||
{
|
||||
id: "c1",
|
||||
title: "원자재 출입 전 계동",
|
||||
description:
|
||||
"원자재 이송/보관 기준을 기반으로 가이드하며, 일련의 단계 별 강의에서 세부적인 작업 지침을 다룹니다.",
|
||||
thumbnail: "/imgs/talk.png",
|
||||
status: "수강중",
|
||||
progressPct: 80,
|
||||
lessons: [
|
||||
{ id: "c1l1", title: "1. 운반과 기준 및 방출 절차", durationMin: 12, progressPct: 100, isCompleted: true },
|
||||
{ id: "c1l2", title: "2. 라벨링 원칙과 현장 케어", durationMin: 9, progressPct: 100, isCompleted: true },
|
||||
{ id: "c1l3", title: "3. 배치/현황 기록 및 문서 관리", durationMin: 15, progressPct: 60, isCompleted: false },
|
||||
{ id: "c1l4", title: "4. 보관 적재 기준 점검", durationMin: 8, progressPct: 0, isCompleted: false },
|
||||
{ id: "c1l5", title: "5. 입고 검사 방법 (AQL) 및 유의점", durationMin: 11, progressPct: 0, isCompleted: false },
|
||||
{ id: "c1l6", title: "6. 장비 사용, 손질 및 일지 필기", durationMin: 13, progressPct: 0, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "c2",
|
||||
title: "학점과 평가",
|
||||
description:
|
||||
"학점과 평가 항목 기준을 가이드하며, 일과 관련된 기본 평가 체계와 피드백 처리 방법에 대해 배웁니다.",
|
||||
thumbnail: "/imgs/talk.png",
|
||||
status: "수강 완료",
|
||||
progressPct: 100,
|
||||
lessons: [
|
||||
{ id: "c2l1", title: "평가 기준 이해", durationMin: 10, progressPct: 100, isCompleted: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
title: "부서간 연협",
|
||||
description:
|
||||
"부서간 협업 절차와 기록 기준을 가이드하며, 일련 단계 별 협업에서 생길 수 있는 리스크 관리법을 다룹니다.",
|
||||
thumbnail: "/imgs/talk.png",
|
||||
status: "수강중",
|
||||
progressPct: 60,
|
||||
lessons: [
|
||||
{ id: "c3l1", title: "의사소통 원칙", durationMin: 9, progressPct: 100, isCompleted: true },
|
||||
{ id: "c3l2", title: "문서 공유와 승인", durationMin: 14, progressPct: 30, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "c4",
|
||||
title: "작업별 방사선의 이해",
|
||||
description:
|
||||
"작업별 방사선 안전 기준을 가이드하며, 일과 관련된 위험과 보호 장비 선택법을 배웁니다.",
|
||||
thumbnail: "/imgs/talk.png",
|
||||
status: "수강 예정",
|
||||
progressPct: 0,
|
||||
lessons: [
|
||||
{ id: "c4l1", title: "기초 이론", durationMin: 12, progressPct: 0, isCompleted: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const TABS: CourseStatus[] = ["전체", "수강 예정", "수강중", "수강 완료"];
|
||||
|
||||
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>
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<CourseStatus>("전체");
|
||||
|
||||
const countsByStatus = useMemo(() => {
|
||||
return {
|
||||
전체: MOCK_COURSES.length,
|
||||
"수강 예정": MOCK_COURSES.filter((c) => c.status === "수강 예정").length,
|
||||
수강중: MOCK_COURSES.filter((c) => c.status === "수강중").length,
|
||||
"수강 완료": MOCK_COURSES.filter((c) => c.status === "수강 완료").length,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (activeTab === "전체") return MOCK_COURSES;
|
||||
return MOCK_COURSES.filter((c) => c.status === activeTab);
|
||||
}, [activeTab]);
|
||||
|
||||
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">
|
||||
<div className="border-b border-[#ecf0ff]">
|
||||
<ul className="flex gap-6">
|
||||
{TABS.map((tab) => {
|
||||
const isActive = activeTab === tab;
|
||||
return (
|
||||
<li key={tab}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={[
|
||||
"relative -mb-px h-12 px-1 text-[14px] leading-[1.5]",
|
||||
isActive ? "font-semibold text-[#1f2b91]" : "font-medium text-[#6c7682]",
|
||||
].join(" ")}
|
||||
>
|
||||
{tab}{" "}
|
||||
<span className="ml-1 text-[#8c95a1]">
|
||||
{countsByStatus[tab as keyof typeof countsByStatus] ?? 0}
|
||||
</span>
|
||||
{isActive ? (
|
||||
<span className="absolute inset-x-0 -bottom-[1px] h-[2px] bg-[#1f2b91]" />
|
||||
) : null}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-16 pt-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
{filtered.map((course) => (
|
||||
<CourseCard key={course.id} course={course} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* pagination */}
|
||||
<div className="mt-10 flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full text-[#8c95a1] hover:bg-[#f1f3f5]"
|
||||
aria-label="이전 페이지"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
{[1, 2, 3].map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={[
|
||||
"flex h-8 w-8 items-center justify-center rounded-full text-[13px] leading-[1.4]",
|
||||
p === 2 ? "bg-[#1f2b91] text-white" : "text-[#4c5561] hover:bg-[#f1f3f5]",
|
||||
].join(" ")}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full text-[#8c95a1] hover:bg-[#f1f3f5]"
|
||||
aria-label="다음 페이지"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import MenuSidebar from "./MenuSidebar";
|
||||
|
||||
export default function MenuLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1440px]">
|
||||
<div className="mx-auto flex w-full max-w-[1440px] min-h-full">
|
||||
<aside className="w-[320px] border-r border-[#dee1e6] px-4 py-6">
|
||||
<MenuSidebar />
|
||||
</aside>
|
||||
|
||||
@@ -1,12 +1,118 @@
|
||||
type ResultRow = {
|
||||
programTitle: string;
|
||||
courseTitle: string;
|
||||
completedAt: string;
|
||||
score: string;
|
||||
instructor: string;
|
||||
feedbackLink?: string;
|
||||
certificateLink?: string;
|
||||
};
|
||||
|
||||
const mockResults: ResultRow[] = [
|
||||
{
|
||||
programTitle: "XR 안전 기본 과정",
|
||||
courseTitle: "{강좌명}",
|
||||
completedAt: "2025-09-10",
|
||||
score: "-",
|
||||
instructor: "이공필",
|
||||
},
|
||||
{
|
||||
programTitle: "건설 현장 안전 실무",
|
||||
courseTitle: "{강좌명}",
|
||||
completedAt: "2025-09-10",
|
||||
score: "70 / 100",
|
||||
instructor: "이공필",
|
||||
feedbackLink: "#",
|
||||
certificateLink: "#",
|
||||
},
|
||||
{
|
||||
programTitle: "전기 설비 위험성 평가",
|
||||
courseTitle: "{강좌명}",
|
||||
completedAt: "2025-09-10",
|
||||
score: "70 / 100",
|
||||
instructor: "이공필",
|
||||
feedbackLink: "#",
|
||||
certificateLink: "#",
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
|
||||
<section className="px-8 pb-20">
|
||||
<div className="rounded-[8px]">
|
||||
<div className="w-full overflow-x-auto rounded-[8px] border border-[#dee1e6]">
|
||||
<table className="min-w-full border-collapse">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
<col style={{ width: 140 }} />
|
||||
<col style={{ width: 140 }} />
|
||||
<col style={{ width: 140 }} />
|
||||
<col style={{ width: 76 }} />
|
||||
<col style={{ width: 76 }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="h-12 bg-gray-50 text-left">
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">교육 과정명</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">강좌명</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">수강 완료일</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">평가 점수(점)</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">평가 강사</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-center text-[14px] font-semibold leading-[1.5] text-[#4c5561]">피드백</th>
|
||||
<th className="px-4 text-center text-[14px] font-semibold leading-[1.5] text-[#4c5561]">수료증</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mockResults.map((row, idx) => (
|
||||
<tr
|
||||
key={`${row.programTitle}-${idx}`}
|
||||
className={idx === 1 ? "h-12 bg-[rgba(236,240,255,0.5)]" : "h-12"}
|
||||
>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.programTitle}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.courseTitle}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.completedAt}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.score}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.instructor}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-center text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.feedbackLink ? (
|
||||
<a href={row.feedbackLink} className="text-[12px] text-blue-500 underline underline-offset-[3px]">
|
||||
확인하기
|
||||
</a>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td className="border-t border-[#dee1e6] px-4 text-center text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.certificateLink ? (
|
||||
<a href={row.certificateLink} className="text-[12px] text-blue-500 underline underline-offset-[3px]">
|
||||
확인하기
|
||||
</a>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user