Compare commits
2 Commits
e74061057d
...
53dea825b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53dea825b0 | ||
|
|
71feecb8d1 |
@@ -26,7 +26,7 @@ export default function LoginOption({
|
|||||||
className={`fixed bottom-2 right-2 bg-red-400 cursor-pointer rounded-full w-[40px] h-[40px] shadow-xl z-100`}
|
className={`fixed bottom-2 right-2 bg-red-400 cursor-pointer rounded-full w-[40px] h-[40px] shadow-xl z-100`}
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
{ isOpen && (
|
{isOpen && (
|
||||||
<div className="fixed inset-0 flex items-center justify-center z-50">
|
<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">
|
<div className="w-[500px] h-[600px] flex bg-white/80 p-10 border rounded-lg relative">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function LoginPage() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch("https://hrdi.coconutmeet.net/auth/login", {
|
const response = await fetch("https://hrdi.coconutmeet.net/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {"Content-Type": "application/json",},
|
headers: { "Content-Type": "application/json", },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: userId,
|
email: userId,
|
||||||
password: password
|
password: password
|
||||||
@@ -104,7 +104,7 @@ export default function LoginPage() {
|
|||||||
{/* 로고 영역 */}
|
{/* 로고 영역 */}
|
||||||
<div className="my-15 flex flex-col items-center">
|
<div className="my-15 flex flex-col items-center">
|
||||||
<div className="mb-[7px]">
|
<div className="mb-[7px]">
|
||||||
<MainLogo/>
|
<MainLogo />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[28.8px] font-extrabold leading-[145%] text-neutral-700" >
|
<div className="text-[28.8px] font-extrabold leading-[145%] text-neutral-700" >
|
||||||
XR LMS
|
XR LMS
|
||||||
@@ -256,4 +256,3 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|||||||
import LoginCheckboxActiveSvg from "@/app/svgs/logincheckboxactivesvg";
|
import LoginCheckboxActiveSvg from "@/app/svgs/logincheckboxactivesvg";
|
||||||
import LoginCheckboxInactiveSvg from "@/app/svgs/logincheckboxinactivesvg";
|
import LoginCheckboxInactiveSvg from "@/app/svgs/logincheckboxinactivesvg";
|
||||||
import LoginInputSvg from "@/app/svgs/inputformx";
|
import LoginInputSvg from "@/app/svgs/inputformx";
|
||||||
|
import CalendarSvg from "@/app/svgs/callendar";
|
||||||
|
|
||||||
type Gender = "MALE" | "FEMALE" | "";
|
type Gender = "MALE" | "FEMALE" | "";
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
const [gender, setGender] = useState<Gender>("");
|
const [gender, setGender] = useState<Gender>("");
|
||||||
const [birthdate, setBirthdate] = useState("");
|
const [birthdate, setBirthdate] = useState("");
|
||||||
|
const [birthdateInput, setBirthdateInput] = useState("");
|
||||||
|
|
||||||
// 이메일 인증 관련
|
// 이메일 인증 관련
|
||||||
const [emailCodeSent, setEmailCodeSent] = useState(false);
|
const [emailCodeSent, setEmailCodeSent] = useState(false);
|
||||||
@@ -41,9 +43,57 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
const [focused, setFocused] = useState<Record<string, boolean>>({});
|
const [focused, setFocused] = useState<Record<string, boolean>>({});
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 휴대폰 번호 포맷팅 함수
|
||||||
|
function formatPhoneNumber(phoneNumber: string): string {
|
||||||
|
const numbers = phoneNumber.replace(/[^0-9]/g, "");
|
||||||
|
if (numbers.length <= 3) return numbers;
|
||||||
|
if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
|
||||||
|
if (numbers.length <= 11) return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`;
|
||||||
|
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 생년월일 YYYYMMDD를 YYYY-MM-DD로 변환하는 함수
|
||||||
|
function parseBirthdate(input: string): string {
|
||||||
|
// 숫자만 추출
|
||||||
|
const numbers = input.replace(/[^0-9]/g, "");
|
||||||
|
|
||||||
|
// YYYYMMDD 형식인지 확인 (8자리)
|
||||||
|
if (numbers.length === 8) {
|
||||||
|
const year = numbers.slice(0, 4);
|
||||||
|
const month = numbers.slice(4, 6);
|
||||||
|
const day = numbers.slice(6, 8);
|
||||||
|
|
||||||
|
// 유효한 날짜인지 검증
|
||||||
|
const yearNum = parseInt(year, 10);
|
||||||
|
const monthNum = parseInt(month, 10);
|
||||||
|
const dayNum = parseInt(day, 10);
|
||||||
|
|
||||||
|
if (yearNum >= 1900 && yearNum <= 2100 && monthNum >= 1 && monthNum <= 12 && dayNum >= 1 && dayNum <= 31) {
|
||||||
|
// 날짜 유효성 검사
|
||||||
|
const date = new Date(yearNum, monthNum - 1, dayNum);
|
||||||
|
if (date.getFullYear() === yearNum && date.getMonth() === monthNum - 1 && date.getDate() === dayNum) {
|
||||||
|
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// YYYY-MM-DD 형식이면 그대로 반환
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
const isEmailValid = useMemo(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), [email]);
|
const isEmailValid = useMemo(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), [email]);
|
||||||
const isPhoneValid = useMemo(() => /^\d{9,11}$/.test(phone), [phone]);
|
const isPhoneValid = useMemo(() => /^\d{9,11}$/.test(phone), [phone]);
|
||||||
const isPasswordValid = useMemo(() => password.length >= 8 && password.length <= 16, [password]);
|
const isPasswordValid = useMemo(() => {
|
||||||
|
if (password.length < 8 || password.length > 16) return false;
|
||||||
|
const hasEnglish = /[a-zA-Z]/.test(password);
|
||||||
|
const hasNumber = /[0-9]/.test(password);
|
||||||
|
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
|
||||||
|
return hasEnglish && hasNumber && hasSpecialChar;
|
||||||
|
}, [password]);
|
||||||
const isPasswordConfirmValid = useMemo(() => passwordConfirm === password && passwordConfirm.length > 0, [password, passwordConfirm]);
|
const isPasswordConfirmValid = useMemo(() => passwordConfirm === password && passwordConfirm.length > 0, [password, passwordConfirm]);
|
||||||
|
|
||||||
const canSubmit = useMemo(() => {
|
const canSubmit = useMemo(() => {
|
||||||
@@ -64,7 +114,18 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
if (name.trim().length === 0) nextErrors.name = "이름을 입력해 주세요.";
|
if (name.trim().length === 0) nextErrors.name = "이름을 입력해 주세요.";
|
||||||
if (!isPhoneValid) nextErrors.phone = "휴대폰 번호를 숫자만 9~11자리로 입력해 주세요.";
|
if (!isPhoneValid) nextErrors.phone = "휴대폰 번호를 숫자만 9~11자리로 입력해 주세요.";
|
||||||
if (!isEmailValid) nextErrors.email = "올바른 이메일 형식을 입력해 주세요.";
|
if (!isEmailValid) nextErrors.email = "올바른 이메일 형식을 입력해 주세요.";
|
||||||
if (!isPasswordValid) nextErrors.password = "비밀번호는 8~16자여야 합니다.";
|
if (!isPasswordValid) {
|
||||||
|
if (password.length < 8 || password.length > 16) {
|
||||||
|
nextErrors.password = "비밀번호는 8~16자여야 합니다.";
|
||||||
|
} else {
|
||||||
|
const hasEnglish = /[a-zA-Z]/.test(password);
|
||||||
|
const hasNumber = /[0-9]/.test(password);
|
||||||
|
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
|
||||||
|
if (!hasEnglish || !hasNumber || !hasSpecialChar) {
|
||||||
|
nextErrors.password = "8~16자의 영문/숫자/특수문자를 조합해서 입력해 주세요.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!isPasswordConfirmValid) nextErrors.passwordConfirm = "비밀번호가 일치하지 않습니다.";
|
if (!isPasswordConfirmValid) nextErrors.passwordConfirm = "비밀번호가 일치하지 않습니다.";
|
||||||
if (gender === "") nextErrors.gender = "성별을 선택해 주세요.";
|
if (gender === "") nextErrors.gender = "성별을 선택해 주세요.";
|
||||||
if (birthdate.trim().length === 0) nextErrors.birthdate = "생년월일을 선택해 주세요.";
|
if (birthdate.trim().length === 0) nextErrors.birthdate = "생년월일을 선택해 주세요.";
|
||||||
@@ -97,12 +158,12 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function verifyEmailCode() {
|
async function verifyEmailCode() {
|
||||||
try{
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://hrdi.coconutmeet.net/auth/verify-email/confirm",
|
"https://hrdi.coconutmeet.net/auth/verify-email/confirm",
|
||||||
{
|
{
|
||||||
method: "POST", headers: {"Content-Type": "application/json",},
|
method: "POST", headers: { "Content-Type": "application/json", },
|
||||||
body: JSON.stringify({email: email,emailCode: emailCode})
|
body: JSON.stringify({ email: email, emailCode: emailCode })
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error("이메일 인증번호 검증 실패:", response.statusText);
|
console.error("이메일 인증번호 검증 실패:", response.statusText);
|
||||||
@@ -112,7 +173,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
// 인증 성공 시 상태 업데이트
|
// 인증 성공 시 상태 업데이트
|
||||||
setEmailCodeVerified(true);
|
setEmailCodeVerified(true);
|
||||||
}
|
}
|
||||||
catch(error){
|
catch (error) {
|
||||||
console.error("이메일 인증번호 검증 오류:", error);
|
console.error("이메일 인증번호 검증 오류:", error);
|
||||||
onOpenCodeError();
|
onOpenCodeError();
|
||||||
}
|
}
|
||||||
@@ -128,8 +189,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
"https://hrdi.coconutmeet.net/auth/verify-email/send",
|
"https://hrdi.coconutmeet.net/auth/verify-email/send",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {"Content-Type": "application/json",},
|
headers: { "Content-Type": "application/json", },
|
||||||
body: JSON.stringify({email: email})
|
body: JSON.stringify({ email: email })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -166,7 +227,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
try {
|
try {
|
||||||
const response = await fetch("https://hrdi.coconutmeet.net/auth/signup", {
|
const response = await fetch("https://hrdi.coconutmeet.net/auth/signup", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {"Content-Type": "application/json",},
|
headers: { "Content-Type": "application/json", },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email,
|
email: email,
|
||||||
emailCode: emailCode,
|
emailCode: emailCode,
|
||||||
@@ -254,11 +315,11 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
name="phone"
|
name="phone"
|
||||||
type="tel"
|
type="tel"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
value={phone}
|
value={formatPhoneNumber(phone)}
|
||||||
|
placeholder="-없이 입력해 주세요."
|
||||||
onChange={(e) => setPhone(e.target.value.replace(/[^0-9]/g, ""))}
|
onChange={(e) => setPhone(e.target.value.replace(/[^0-9]/g, ""))}
|
||||||
onFocus={() => setFocused((p) => ({ ...p, phone: true }))}
|
onFocus={() => setFocused((p) => ({ ...p, phone: true }))}
|
||||||
onBlur={() => setFocused((p) => ({ ...p, phone: false }))}
|
onBlur={() => setFocused((p) => ({ ...p, phone: false }))}
|
||||||
placeholder="-없이 입력해 주세요."
|
|
||||||
className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px]"
|
className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px]"
|
||||||
/>
|
/>
|
||||||
{phone.trim().length > 0 && focused.phone && (
|
{phone.trim().length > 0 && focused.phone && (
|
||||||
@@ -449,8 +510,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
onChange={() => setGender("MALE")}
|
onChange={() => setGender("MALE")}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<span className={`flex items-center justify-center rounded-full size-[18px] border ${gender === "MALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
|
<span className={`relative inline-flex items-center justify-center rounded-full w-[18px] h-[18px] shrink-0 border box-border ${gender === "MALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
|
||||||
{gender === "MALE" && <span className="block size-[9px] rounded-full bg-active-button" />}
|
{gender === "MALE" && <span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 block size-[9px] rounded-full bg-active-button" />}
|
||||||
</span>
|
</span>
|
||||||
남성
|
남성
|
||||||
</label>
|
</label>
|
||||||
@@ -463,8 +524,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
onChange={() => setGender("FEMALE")}
|
onChange={() => setGender("FEMALE")}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<span className={`flex items-center justify-center rounded-full size-[18px] border ${gender === "FEMALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
|
<span className={`relative inline-flex items-center justify-center rounded-full w-[18px] h-[18px] shrink-0 border box-border ${gender === "FEMALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
|
||||||
{gender === "FEMALE" && <span className="block size-[9px] rounded-full bg-active-button" />}
|
{gender === "FEMALE" && <span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 block size-[9px] rounded-full bg-active-button" />}
|
||||||
</span>
|
</span>
|
||||||
여성
|
여성
|
||||||
</label>
|
</label>
|
||||||
@@ -475,14 +536,56 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
|
|||||||
{/* 생년월일 */}
|
{/* 생년월일 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="birthdate" className="text-[15px] font-semibold text-[#6c7682]">생년월일</label>
|
<label htmlFor="birthdate" className="text-[15px] font-semibold text-[#6c7682]">생년월일</label>
|
||||||
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
id="birthdate"
|
id="birthdate"
|
||||||
name="birthdate"
|
name="birthdate"
|
||||||
|
type="text"
|
||||||
|
value={birthdateInput || (birthdate ? birthdate.replace(/-/g, ".") : "")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
setBirthdateInput(inputValue);
|
||||||
|
if (inputValue === "") {
|
||||||
|
setBirthdate("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// 포커스를 잃을 때 YYYYMMDD 형식이면 변환
|
||||||
|
const formatted = parseBirthdate(e.target.value);
|
||||||
|
if (formatted) {
|
||||||
|
setBirthdate(formatted);
|
||||||
|
setBirthdateInput("");
|
||||||
|
} else if (e.target.value === "") {
|
||||||
|
setBirthdate("");
|
||||||
|
setBirthdateInput("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="생년월일"
|
||||||
|
className="h-[40px] px-[12px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px] flex items-center"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const dateInput = document.getElementById("birthdate-date-picker") as HTMLInputElement;
|
||||||
|
dateInput?.showPicker?.();
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||||
|
aria-label="날짜 선택"
|
||||||
|
>
|
||||||
|
<CalendarSvg width="20" height="20" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
id="birthdate-date-picker"
|
||||||
type="date"
|
type="date"
|
||||||
value={birthdate}
|
value={birthdate}
|
||||||
onChange={(e) => setBirthdate(e.target.value)}
|
onChange={(e) => {
|
||||||
className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[16px] text-neutral-700"
|
setBirthdate(e.target.value);
|
||||||
|
setBirthdateInput("");
|
||||||
|
}}
|
||||||
|
className="absolute right-50 top-1 h-full w-[20px] opacity-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{errors.birthdate && <p className="text-error text-[13px] leading-tight">{errors.birthdate}</p>}
|
{errors.birthdate && <p className="text-error text-[13px] leading-tight">{errors.birthdate}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
46
src/app/svgs/callendar.tsx
Normal file
46
src/app/svgs/callendar.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const CalendarSvg: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 2V6"
|
||||||
|
stroke="#333C47"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 2V6"
|
||||||
|
stroke="#333C47"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 9H21"
|
||||||
|
stroke="#333C47"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M19 4H5C3.895 4 3 4.895 3 6V19C3 20.105 3.895 21 5 21H19C20.105 21 21 20.105 21 19V6C21 4.895 20.105 4 19 4Z"
|
||||||
|
stroke="#333C47"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<rect x="3" y="4" width="18" height="5" rx="2" fill="#333C47" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CalendarSvg;
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
/**
|
|
||||||
* 이메일 템플릿 유틸리티
|
|
||||||
* Figma 디자인에 맞춘 이메일 템플릿을 생성합니다.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 이메일 인증번호 템플릿 생성
|
|
||||||
* @param verificationCode - 인증번호 (6자리)
|
|
||||||
* @param baseUrl - 이미지 베이스 URL (선택사항, 기본값: '')
|
|
||||||
* @returns HTML 형식의 이메일 템플릿
|
|
||||||
*/
|
|
||||||
export function generateVerificationEmailTemplate(
|
|
||||||
verificationCode: string,
|
|
||||||
baseUrl: string = ''
|
|
||||||
): string {
|
|
||||||
// 이미지 URL 생성 (baseUrl이 제공되면 사용, 없으면 상대 경로)
|
|
||||||
const getImageUrl = (imagePath: string) => {
|
|
||||||
if (baseUrl) {
|
|
||||||
return `${baseUrl}${imagePath}`;
|
|
||||||
}
|
|
||||||
return imagePath;
|
|
||||||
};
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>[XR LMS] 이메일 인증 번호</title>
|
|
||||||
<style>
|
|
||||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css');
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #333c47;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-top: 4px solid #384fbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-content {
|
|
||||||
padding: 80px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-title-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-title-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
font-family: 'Pretendard', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #333c47;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-code {
|
|
||||||
font-family: 'Pretendard', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 36px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #384fbf;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-instructions-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-instructions-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
font-family: 'Pretendard', sans-serif;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #6c7682;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
font-family: 'Pretendard', 'Noto Sans', sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #8c95a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 이미지가 로드되지 않을 때를 대비한 fallback 텍스트 숨김 */
|
|
||||||
.email-title-fallback,
|
|
||||||
.verification-instructions-fallback,
|
|
||||||
.email-footer-fallback {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
|
||||||
.email-content {
|
|
||||||
padding: 40px 20px;
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-title-image {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-code {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verification-instructions-image {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="email-container">
|
|
||||||
<div class="email-content">
|
|
||||||
<div class="email-title-wrapper">
|
|
||||||
<img
|
|
||||||
src="${getImageUrl('/imgs/email-title.png')}"
|
|
||||||
alt="[XR LMS] 이메일 인증 번호"
|
|
||||||
class="email-title-image"
|
|
||||||
style="display: block; width: 100%; height: auto;"
|
|
||||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
|
|
||||||
/>
|
|
||||||
<div class="email-title-fallback" style="font-family: 'Pretendard', sans-serif; font-weight: 700; font-size: 24px; line-height: 1.5; color: #333c47;">
|
|
||||||
[XR LMS] 이메일 인증 번호
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="verification-section">
|
|
||||||
<div class="verification-code">
|
|
||||||
${verificationCode}
|
|
||||||
</div>
|
|
||||||
<div class="verification-instructions-wrapper">
|
|
||||||
<img
|
|
||||||
src="${getImageUrl('/imgs/email-instructions.png')}"
|
|
||||||
alt="위 번호를 인증번호 입력창에 입력해 주세요. 인증번호 입력 후 해당 화면 내 [인증번호 확인]버튼을 클릭해 주세요."
|
|
||||||
class="verification-instructions-image"
|
|
||||||
style="display: block; width: 100%; height: auto;"
|
|
||||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
|
|
||||||
/>
|
|
||||||
<div class="verification-instructions-fallback" style="font-family: 'Pretendard', sans-serif; font-weight: 500; font-size: 18px; line-height: 1.5; color: #6c7682;">
|
|
||||||
위 번호를 인증번호 입력창에 입력해 주세요.<br>
|
|
||||||
인증번호 입력 후 해당 화면 내 [인증번호 확인]버튼을 클릭해 주세요.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="email-footer-wrapper">
|
|
||||||
<img
|
|
||||||
src="${getImageUrl('/imgs/email-footer.png')}"
|
|
||||||
alt="Copyright ⓒ 2025 XL LMS. All rights reserved"
|
|
||||||
class="email-footer-image"
|
|
||||||
style="display: block; width: 100%; height: auto;"
|
|
||||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
|
|
||||||
/>
|
|
||||||
<div class="email-footer-fallback" style="font-family: 'Pretendard', 'Noto Sans', sans-serif; font-weight: 400; font-size: 14px; line-height: 1.5; color: #8c95a1;">
|
|
||||||
Copyright ⓒ 2025 XL LMS. All rights reserved
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 이메일 인증번호 템플릿 (텍스트 버전)
|
|
||||||
* @param verificationCode - 인증번호 (6자리)
|
|
||||||
* @returns 텍스트 형식의 이메일 템플릿
|
|
||||||
*/
|
|
||||||
export function generateVerificationEmailTextTemplate(verificationCode: string): string {
|
|
||||||
return `
|
|
||||||
[XR LMS] 이메일 인증 번호
|
|
||||||
|
|
||||||
인증번호: ${verificationCode}
|
|
||||||
|
|
||||||
위 번호를 인증번호 입력창에 입력해 주세요.
|
|
||||||
인증번호 입력 후 해당 화면 내 [인증번호 확인]버튼을 클릭해 주세요.
|
|
||||||
|
|
||||||
Copyright ⓒ 2025 XL LMS. All rights reserved
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
|
||||||
prisma: PrismaClient | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const prisma =
|
|
||||||
globalForPrisma.prisma ??
|
|
||||||
new PrismaClient({
|
|
||||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user