마이페이지, 리포트, 메일인증
CI / test (push) Failing after 13m23s
CI / e2e (push) Has been cancelled
CI / pr_and_merge (push) Has been cancelled

This commit is contained in:
2025-12-15 21:59:43 +09:00
parent a8e1a4a960
commit 87cdc1868b
30 changed files with 5488 additions and 16 deletions
+4 -1
View File
@@ -11,12 +11,14 @@ export interface AccessTokenPayload extends JwtPayload {
sub: string;
email: string;
tokenVersion: number;
emailVerified?: boolean;
}
export interface JwtUserLike {
id: string;
email: string;
tokenVersion: number;
emailVerified?: boolean;
}
// 내부에서 사용할 JWT 시크릿을 가져온다.
@@ -72,9 +74,10 @@ export async function signAccessToken(user: JwtUserLike): Promise<string> {
sub: user.id,
email: user.email,
tokenVersion: user.tokenVersion,
emailVerified: user.emailVerified ?? false,
};
// 만료 시간은 기본 7일로 설정한다.
// 만료 시간은 기본 7일로 설정한다.
const token = jwt.sign(payload, secret, {
algorithm: "HS256",
expiresIn: "7d",
@@ -0,0 +1,48 @@
import nodemailer from "nodemailer";
export type SendVerificationEmailParams = {
to: string;
token: string;
};
export async function sendVerificationEmail(params: SendVerificationEmailParams): Promise<void> {
const host = process.env.EMAIL_SERVER_HOST;
const portRaw = process.env.EMAIL_SERVER_PORT;
const user = process.env.EMAIL_SERVER_USER;
const pass = process.env.EMAIL_SERVER_PASSWORD;
const from = process.env.EMAIL_FROM ?? user;
if (!host || !user || !pass) {
throw new Error("Email server configuration is missing.");
}
const port = portRaw ? Number(portRaw) : 587;
const baseUrl =
process.env.NEXTAUTH_URL ?? process.env.APP_BASE_URL ?? "http://localhost:3000";
const verifyUrl = `${baseUrl}/api/auth/verify-email?token=${encodeURIComponent(
params.token,
)}`;
const transporter = nodemailer.createTransport({
host,
port,
secure: port === 465,
auth: {
user,
pass,
},
});
const subject = "이메일 인증을 완료해 주세요";
const text = `아래 링크를 클릭해서 이메일 인증을 완료해 주세요.\n\n${verifyUrl}\n\n이 링크는 1시간 후 만료됩니다.`;
const html = `<p>아래 링크를 클릭해서 이메일 인증을 완료해 주세요.</p><p><a href="${verifyUrl}">${verifyUrl}</a></p><p>이 링크는 1시간 후 만료됩니다.</p>`;
await transporter.sendMail({
from: from ?? undefined,
to: params.to,
subject,
text,
html,
});
}
+9
View File
@@ -12,10 +12,13 @@ export type DashboardMessages = {
navDashboard: string;
navProjects: string;
navSubmissions: string;
navReports?: string;
// 헤더 오른쪽 액션 버튼/메뉴
themeToggleLabel: string;
menuLabel: string;
logoutLabel: string;
myPageLabel: string;
emailNotVerifiedBanner: string;
summaryTotalProjectsLabel: string;
summaryTotalSubmissionsLabel: string;
summaryTodaySubmissionsLabel: string;
@@ -43,9 +46,12 @@ const DASHBOARD_MESSAGES: Record<AppLocale, DashboardMessages> = {
navDashboard: "Dashboard",
navProjects: "Projects",
navSubmissions: "All submissions",
navReports: "Reports",
themeToggleLabel: "Toggle theme",
menuLabel: "Menu",
logoutLabel: "Log out",
myPageLabel: "My page",
emailNotVerifiedBanner: "Your email is not verified yet.",
summaryTotalProjectsLabel: "Projects",
summaryTotalSubmissionsLabel: "Total form submissions",
summaryTodaySubmissionsLabel: "Submissions today",
@@ -71,9 +77,12 @@ const DASHBOARD_MESSAGES: Record<AppLocale, DashboardMessages> = {
navDashboard: "대시보드",
navProjects: "프로젝트 목록",
navSubmissions: "전체 제출 내역",
navReports: "리포트",
themeToggleLabel: "테마 전환",
menuLabel: "메뉴",
logoutLabel: "로그아웃",
myPageLabel: "마이페이지",
emailNotVerifiedBanner: "이메일 인증이 아직 완료되지 않았습니다.",
summaryTotalProjectsLabel: "프로젝트 수",
summaryTotalSubmissionsLabel: "전체 폼 제출 수",
summaryTodaySubmissionsLabel: "오늘 제출 수",
+114
View File
@@ -0,0 +1,114 @@
import type { AppLocale } from "../locale";
import { DEFAULT_LOCALE } from "../locale";
// 마이페이지(/mypage) 전용 i18n 메시지
export type MyPageMessages = {
title: string;
description: string;
loadError: string;
loadingText: string;
profileSectionTitle: string;
profileEmailLabel: string;
profileUserIdLabel: string;
emailVerifiedBadge: string;
backToDashboardLabel: string;
emailVerificationNotice: string;
emailVerificationButtonLabel: string;
emailVerificationSuccess: string;
emailVerificationError: string;
changePasswordTitle: string;
changePasswordCurrentLabel: string;
changePasswordNewLabel: string;
changePasswordConfirmLabel: string;
changePasswordSubmitLabel: string;
changePasswordErrorRequired: string;
changePasswordErrorMismatch: string;
changePasswordErrorNetwork: string;
changePasswordSuccess: string;
changeEmailTitle: string;
changeEmailNewLabel: string;
changeEmailCurrentPasswordLabel: string;
changeEmailSubmitLabel: string;
changeEmailErrorRequired: string;
changeEmailErrorNetwork: string;
changeEmailSuccess: string;
};
const MYPAGE_MESSAGES: Record<AppLocale, MyPageMessages> = {
en: {
title: "My page",
description: "Manage your account information, password, and email.",
loadError: "An error occurred while loading your account information.",
loadingText: "Loading account information...",
profileSectionTitle: "Profile",
profileEmailLabel: "Email:",
profileUserIdLabel: "User ID:",
emailVerifiedBadge: "Your email has been verified.",
backToDashboardLabel: "Go to dashboard",
emailVerificationNotice:
"If your email is not verified yet, you can send a verification email from here.",
emailVerificationButtonLabel: "Send verification email",
emailVerificationSuccess: "Verification email has been sent.",
emailVerificationError:
"Failed to send verification email. Please try again.",
changePasswordTitle: "Change password",
changePasswordCurrentLabel: "Current password",
changePasswordNewLabel: "New password",
changePasswordConfirmLabel: "Confirm new password",
changePasswordSubmitLabel: "Update password",
changePasswordErrorRequired: "Please fill in all password fields.",
changePasswordErrorMismatch: "New password and confirmation do not match.",
changePasswordErrorNetwork:
"A network error occurred while changing your password. Please try again.",
changePasswordSuccess: "Your password has been changed.",
changeEmailTitle: "Change email",
changeEmailNewLabel: "New email",
changeEmailCurrentPasswordLabel: "Current password",
changeEmailSubmitLabel: "Update email",
changeEmailErrorRequired: "Please enter your new email and current password.",
changeEmailErrorNetwork:
"A network error occurred while changing your email. Please try again.",
changeEmailSuccess: "Your email has been changed.",
},
ko: {
title: "마이페이지",
description: "내 계정 정보, 비밀번호, 이메일을 관리할 수 있는 페이지입니다.",
loadError: "계정 정보를 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
loadingText: "계정 정보를 불러오는 중입니다...",
profileSectionTitle: "프로필",
profileEmailLabel: "이메일:",
profileUserIdLabel: "사용자 ID:",
emailVerifiedBadge: "이메일 인증이 완료되었습니다.",
backToDashboardLabel: "대시보드로 이동",
emailVerificationNotice:
"이메일 인증이 아직 완료되지 않았다면, 여기에서 인증 메일을 보낼 수 있습니다.",
emailVerificationButtonLabel: "인증 메일 보내기",
emailVerificationSuccess: "인증 메일을 보냈습니다.",
emailVerificationError:
"인증 메일 전송 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
changePasswordTitle: "비밀번호 변경",
changePasswordCurrentLabel: "현재 비밀번호",
changePasswordNewLabel: "새 비밀번호",
changePasswordConfirmLabel: "새 비밀번호 확인",
changePasswordSubmitLabel: "비밀번호 업데이트",
changePasswordErrorRequired: "모든 비밀번호 입력란을 채워주세요.",
changePasswordErrorMismatch: "새 비밀번호와 확인 비밀번호가 일치하지 않습니다.",
changePasswordErrorNetwork:
"비밀번호 변경 중 네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
changePasswordSuccess: "비밀번호가 변경되었습니다.",
changeEmailTitle: "이메일 변경",
changeEmailNewLabel: "새 이메일",
changeEmailCurrentPasswordLabel: "현재 비밀번호",
changeEmailSubmitLabel: "이메일 업데이트",
changeEmailErrorRequired: "새 이메일과 현재 비밀번호를 모두 입력해주세요.",
changeEmailErrorNetwork:
"이메일 변경 중 네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
changeEmailSuccess: "이메일이 변경되었습니다.",
},
};
export function getMyPageMessages(locale: AppLocale | null | undefined): MyPageMessages {
const key = (locale ?? DEFAULT_LOCALE) as AppLocale;
return MYPAGE_MESSAGES[key] ?? MYPAGE_MESSAGES[DEFAULT_LOCALE];
}