From e17c68b4aed9d1d7f31f071f14f83248acd0aa0d Mon Sep 17 00:00:00 2001 From: toris Date: Wed, 12 Mar 2025 02:30:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Style]=20motion=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 38 ++++ src/app/auth/signin/page.tsx | 285 ++++++++++++++++++++---------- src/app/cart/page.tsx | 233 +++++++++++++++++------- src/app/not-found.tsx | 51 +++++- src/components/Box/CompanyBox.tsx | 125 ++++++++----- src/components/Box/ResumeBox.tsx | 7 +- src/components/Header/Button.tsx | 21 ++- src/components/Loader/index.tsx | 87 +++++++-- src/components/Resume/index.tsx | 246 +++++++++++++------------- 10 files changed, 753 insertions(+), 341 deletions(-) diff --git a/package.json b/package.json index f035833..0de30e0 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "axios": "^1.7.9", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", + "framer-motion": "^12.5.0", "jsonwebtoken": "^9.0.2", "next": "14.2.3", "next-auth": "^4.24.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4cd253..47c2140 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 + framer-motion: + specifier: ^12.5.0 + version: 12.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -1682,6 +1685,20 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + framer-motion@12.5.0: + resolution: {integrity: sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2321,6 +2338,12 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + motion-dom@12.5.0: + resolution: {integrity: sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==} + + motion-utils@12.5.0: + resolution: {integrity: sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5366,6 +5389,15 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + framer-motion@12.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.5.0 + motion-utils: 12.5.0 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -6321,6 +6353,12 @@ snapshots: minipass@7.1.2: {} + motion-dom@12.5.0: + dependencies: + motion-utils: 12.5.0 + + motion-utils@12.5.0: {} + ms@2.1.3: {} mz@2.7.0: diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index 4ddf0bf..9230858 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -6,6 +6,7 @@ import PwdFindModal from '@/app/auth/_components/Modal/PwdFindModal'; import Button from '@/components/Header/Button'; import Input from '@/components/Input'; import { signFn } from '@/utils/actions/jwt'; +import { AnimatePresence, motion } from 'framer-motion'; import { signIn, useSession } from 'next-auth/react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; @@ -24,11 +25,13 @@ const SigninPage = () => { const pwdRef = useRef(null); const { status } = useSession(); const router = useRouter(); + useEffect(() => { if (status === 'authenticated') { router.replace('/'); } }, [router, status]); + const validateEmail = (email: string) => { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); @@ -60,9 +63,7 @@ const SigninPage = () => { password: token, redirect: false }); - if (result?.error) { - toast.error('로그인에 실패하였습니다.'); - } else if (result === null) { + if (result?.error || result === null) { toast.error('로그인에 실패하였습니다.'); } else { toast.success('로그인에 성공하였습니다.'); @@ -73,7 +74,6 @@ const SigninPage = () => { } }; - // 자동입력방지 ReCAPTCHA const onChange = () => { setRecaptcha(true); }; @@ -88,6 +88,7 @@ const SigninPage = () => { toast.error('로그인 실패하였습니다.'); } }; + const googleLoginHandler = () => { try { signIn('google', { @@ -98,145 +99,247 @@ const SigninPage = () => { toast.error('로그인 실패하였습니다.'); } }; + + // 애니메이션 변형 정의 + const containerVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.5, staggerChildren: 0.1 } + } + }; + + const itemVariants = { + hidden: { opacity: 0, y: 10 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3 } } + }; + + const buttonVariants = { + hover: { scale: 1.05 }, + tap: { scale: 0.95 } + }; + return ( -
-
- setAdminInOpen(false)} - title="관리자 로그인" - /> - - setIdFindIsOpen(false)} - title="아이디 찾기" - /> - - setPwdFindIsOpen(false)} - title="패스워드 찾기" - /> - -
- logoImage -
-

+ + {adminIsOpen && ( + + setAdminInOpen(false)} + title="관리자 로그인" + /> + + )} + {idFindIsOpen && ( + + setIdFindIsOpen(false)} + title="아이디 찾기" + /> + + )} + {pwdFindIsOpen && ( + + setPwdFindIsOpen(false)} + title="패스워드 찾기" + /> + + )} + + + + + logoImage + + + 안녕하세요! -

-

+ + DevCV 입니다. -

-
-
- -
- - - + + - 로그인 - -
- setAdminInOpen(true)} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} > 관리자로그인 - - + router.push('/auth/signup')} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} > 회원가입 - +
- setIdFindIsOpen(true)} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} > ID 찾기 - - + setPwdFindIsOpen(false)} + onClick={() => setPwdFindIsOpen(true)} // 오타 수정: false -> true + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} > 비밀번호 찾기 - +
-
- +
+ -
+ -
+
-
+ 소셜 로그인
-
-
-
+ -
+
-
-
-
+ + + ); }; diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx index 67a9b90..1b3ed19 100644 --- a/src/app/cart/page.tsx +++ b/src/app/cart/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCartStore } from '@/store/useCartStore'; +import { AnimatePresence, motion } from 'framer-motion'; import Image from 'next/image'; import Link from 'next/link'; import { useState } from 'react'; @@ -12,105 +13,219 @@ export default function CartPage() { const totalPrice = getTotalPrice(); + // Variants for animations + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.5, + when: 'beforeChildren', + staggerChildren: 0.1 + } + } + }; + + const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + type: 'spring', + damping: 15, + stiffness: 100 + } + }, + exit: { + opacity: 0, + x: -100, + transition: { duration: 0.3 } + } + }; + + const summaryVariants = { + hidden: { opacity: 0, x: 20 }, + visible: { + opacity: 1, + x: 0, + transition: { + type: 'spring', + damping: 15, + stiffness: 100, + delay: 0.3 + } + } + }; + if (isLoading) { return (
-
+
); } return ( -
+
-
+

장바구니

총 {resumes.length}개의 이력서 -
+ {resumes.length === 0 ? ( -
+

장바구니가 비어있습니다

- - 이력서 구경하기 - -
+ + + 이력서 구경하기 + + + ) : (
{/* Cart Items */}
-
- {resumes.map((resume) => ( -
-
- {resume.title} -
-
-
-
-

- {resume.title} -

-

- 판매자: {resume.sellerNickname} + + + {resumes.map((resume) => ( + + + {resume.title} + +

+
+
+

+ {resume.title} +

+

+ 판매자: {resume.sellerNickname} +

+
+

+ {resume.price.toLocaleString()} Point

-

- {resume.price.toLocaleString()} Point -

+ removeResume(resume.resumeId)} + className="self-end flex items-center gap-1 text-red-500 hover:text-red-600" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + > + + 삭제 +
- -
-
- ))} -
+ + ))} + +
{/* Summary */} -
-
+ +

주문 요약

이력서 수 - {resumes.length}개 + + {resumes.length}개 +
총 결제 포인트 - + {totalPrice.toLocaleString()} Point - +
-
- - 결제하기 - -
+ + + 결제하기 + + + +
)}
-
+ ); } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 5913844..451d220 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,6 @@ 'use client'; +import { motion } from 'framer-motion'; import { useRouter, useSearchParams } from 'next/navigation'; export default function NotFound() { @@ -7,23 +8,57 @@ export default function NotFound() { const searchParams = useSearchParams(); const error = searchParams.keys(); + // 컨테이너 애니메이션 설정 + const containerVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } } + }; + + // 버튼 애니메이션 설정 + const buttonVariants = { + hover: { scale: 1.05 }, + tap: { scale: 0.95 } + }; + return ( -
-

404 Not Found

-

+ + + 404 Not Found + + {error.next().value ? '로그인 후 이용 가능한 서비스입니다.' : '해당 경로에 맞는 페이지를 찾을 수 없습니다.'} -

-
+ + -
-
+ + ); } diff --git a/src/components/Box/CompanyBox.tsx b/src/components/Box/CompanyBox.tsx index 7301164..b1c8659 100644 --- a/src/components/Box/CompanyBox.tsx +++ b/src/components/Box/CompanyBox.tsx @@ -2,6 +2,7 @@ import { COMPANIES } from '@/constants/companies'; import { CompanyType, JobType } from '@/utils/type'; +import { AnimatePresence, motion } from 'framer-motion'; import { FC, useState } from 'react'; import 'swiper/css'; import { Swiper, SwiperSlide } from 'swiper/react'; @@ -35,68 +36,108 @@ const CompanyBox: FC = ({ setSelectedType(type); }; + // 버튼 애니메이션 변형 + const buttonVariants = { + hover: { scale: 1.05 }, + tap: { scale: 0.95 } + }; + + // 항목 애니메이션 변형 + const itemVariants = { + hidden: { opacity: 0, y: 10 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3 } }, + exit: { opacity: 0, y: -10, transition: { duration: 0.2 } } + }; + return (
+ {/* 버튼 영역 */}
- - +
- - {items.map((item) => { - const isSelected = - selectedType === 'enterprise' - ? company === item.type - : job === item.type; - const isHovered = hoveredItem === item.type; - const Icon = item.icon; + {/* Swiper 슬라이드 영역 */} + + + + {items.map((item) => { + const isSelected = + selectedType === 'enterprise' + ? company === item.type + : job === item.type; + const isHovered = hoveredItem === item.type; + const Icon = item.icon; - return ( - -
{ - onClick(item.type as CompanyType | JobType); - resetPage(item.type as CompanyType | JobType); - }} - onMouseEnter={() => - setHoveredItem(item.type as CompanyType | JobType) - } - onMouseLeave={() => setHoveredItem(null)} - > - - - {item.name} - -
-
- ); - })} -
+ return ( + + { + onClick(item.type as CompanyType | JobType); + resetPage(item.type as CompanyType | JobType); + }} + onMouseEnter={() => + setHoveredItem(item.type as CompanyType | JobType) + } + onMouseLeave={() => setHoveredItem(null)} + variants={itemVariants} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + + {item.name} + + + + ); + })} +
+ +
); }; diff --git a/src/components/Box/ResumeBox.tsx b/src/components/Box/ResumeBox.tsx index 5a80feb..58fbb7b 100644 --- a/src/components/Box/ResumeBox.tsx +++ b/src/components/Box/ResumeBox.tsx @@ -22,12 +22,11 @@ const ResumeBox: FC = ({ return ( -
+
thumbnail; +type ButtonProps = Omit, 'ref'> & { + variants: Variants; +}; -const Button: FC = ({ className, children, ...rest }) => { +const Button: FC = ({ + className, + children, + variants, + ...rest +}) => { return ( - + ); }; diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx index da0ef04..36750ff 100644 --- a/src/components/Loader/index.tsx +++ b/src/components/Loader/index.tsx @@ -1,11 +1,36 @@ +'use client'; + import { cn } from '@/utils/style'; +import { motion } from 'framer-motion'; export function Loader({ className }: { className?: string }) { return (
-
-
-
+ + +
); } @@ -13,23 +38,41 @@ export function Loader({ className }: { className?: string }) { export function LoaderGrid({ className }: { className?: string }) { return (
- {[...Array(10)].map((e, i) => ( -
( + ))}
); } - export function FullPageLoader() { return (
-
+ + + 로딩 중... +
); } @@ -38,9 +81,31 @@ export function PrimaryLoader({ className }: { className?: string }) { return (
-
-
-
+ + +
); diff --git a/src/components/Resume/index.tsx b/src/components/Resume/index.tsx index 02d2c45..d8c562c 100644 --- a/src/components/Resume/index.tsx +++ b/src/components/Resume/index.tsx @@ -4,6 +4,7 @@ import { Company, Job } from '@/utils/constant'; import { getResumes } from '@/utils/fetch'; import { CompanyType, JobType, type ResumeResponse } from '@/utils/type'; import { useInfiniteQuery } from '@tanstack/react-query'; +import { AnimatePresence, motion, useScroll } from 'framer-motion'; import { useRouter, useSearchParams } from 'next/navigation'; import React, { FC, useEffect, useRef, useState } from 'react'; import { GrPowerReset } from 'react-icons/gr'; @@ -12,7 +13,6 @@ import CompanyBox from '../Box/CompanyBox'; import ResumeBox from '../Box/ResumeBox'; import { LoaderGrid } from '../Loader'; -// TODO: sticky 이 컴포넌트에서 문제 export const CategoryResume: FC = ({ content: initialResumes, currentPage, @@ -25,21 +25,18 @@ export const CategoryResume: FC = ({ }) => { const router = useRouter(); const params = useSearchParams(); - + const { scrollYProgress } = useScroll(); const [company, setCompany] = useState( params.get('companyType') as CompanyType ); const [job, setJob] = useState( params.get('jobType') as JobType ); - // const [page, setPage] = useState(1); - // const [totalPage, setTotalPage] = useState(0); const [isCompanyVisible, setIsCompanyVisible] = useState(true); const [isHeaderVisible, setIsHeaderVisible] = useState(true); const companyRef = useRef(null); const prevScrollPos = useRef(0); - // Intersection Observer hook for infinite scroll const { ref: loadMoreRef, inView } = useInView(); const { @@ -53,11 +50,7 @@ export const CategoryResume: FC = ({ } = useInfiniteQuery({ queryKey: ['resumes', company, job], queryFn: async ({ pageParam = 1 }) => { - const response = await getResumes({ - page: pageParam, - company, - job - }); + const response = await getResumes({ page: pageParam, company, job }); return response; }, initialPageParam: 1, @@ -86,45 +79,12 @@ export const CategoryResume: FC = ({ : undefined }); - // Fetch next page when bottom is visible useEffect(() => { if (inView && hasNextPage) { fetchNextPage(); } }, [inView, fetchNextPage, hasNextPage]); - // const handlePageClick = async (event: any) => { - // setPage(event.selected + 1); - // if (job && company) { - // router.push( - // `/?jobType=${job}&companyType=${company}&page=${event.selected + 1}`, - - // { scroll: false } - // ); - // } else if (job) { - // router.push(`/?jobType=${job}&page=${event.selected + 1}`, { - // scroll: false - // }); - // } else if (company) { - // router.push(`/?companyType=${company}&page=${event.selected + 1}`, { - // scroll: false - // }); - // } else { - // router.push(`/?page=${event.selected + 1}`, { - // scroll: false - // }); - // } - // }; - - // useEffect(() => { - // const pageNum = Number(params.get('page')); - // if (pageNum) { - // setPage(pageNum); - // } else { - // setPage(1); - // } - // }, [params]); - useEffect(() => { const handleScroll = () => { const currentScrollPos = window.scrollY; @@ -143,10 +103,7 @@ export const CategoryResume: FC = ({ ([entry]) => { setIsCompanyVisible(entry.isIntersecting); }, - { - threshold: 0, - rootMargin: '-80px 0px 0px 0px' - } + { threshold: 0, rootMargin: '-80px 0px 0px 0px' } ); if (companyRef.current) { @@ -167,17 +124,14 @@ export const CategoryResume: FC = ({ 'ventureE' ] as const; - const isCompanyTye = Object.values(COMPANY_TYPES).includes( - selectedType as CompanyType - ); + const isCompanyType = COMPANY_TYPES.includes(selectedType as CompanyType); - if (isCompanyTye) { + if (isCompanyType) { if (company === selectedType) { setCompany(undefined); router.push('/', { scroll: false }); } else { setCompany(selectedType as CompanyType); - // setPage(1); if (job) { router.push(`/?jobType=${job}&companyType=${selectedType}&page=1`, { scroll: false @@ -194,44 +148,91 @@ export const CategoryResume: FC = ({ router.push('/', { scroll: false }); } else { setJob(selectedType as JobType); - // setPage(1); if (company) { router.push( `/?jobType=${selectedType}&companyType=${company}&page=1`, - { - scroll: false - } + { scroll: false } ); } else { - router.push(`/?jobType=${selectedType}&page=1`, { - scroll: false - }); + router.push(`/?jobType=${selectedType}&page=1`, { scroll: false }); } } } }; + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1 } + } + }; + + const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }, + exit: { opacity: 0, y: -20, transition: { duration: 0.3 } } + }; + + const headerVariants = { + visible: { opacity: 1, y: 0 }, + hidden: { opacity: 0, y: -20 } + }; + + // 데이터 디버깅 + useEffect(() => { + if (data?.pages) { + console.log('Fetched Resumes:', data.pages); + data.pages.forEach((page, index) => { + console.log(`Page ${index} Content:`, page.content); + page.content?.forEach((resume) => { + console.log( + `Resume ${resume.resumeId} Thumbnail:`, + resume.imageList[0]?.resumeImgPath + ); + }); + }); + } + }, [data]); + return ( -
- {/* Company section - 모바일에서는 일반 스크롤, 태블릿 이상에서만 sticky */} -

+ + + + 기업 및 기술 선택 -

-
+
-
+ { - // setPage(1); if (job) { router.push( `/?jobType=${job}&companyType=${companyType}&page=1`, @@ -244,27 +245,34 @@ export const CategoryResume: FC = ({ } }} /> -
+
-
+ - {/* Resume section */} -
-
+

{Company[company!]} {Job[job!]} 이력서

선택된 기업의 이력서입니다. -
+ -
+ { setCompany(undefined); @@ -275,7 +283,7 @@ export const CategoryResume: FC = ({ size={20} /> 선택 초기화 -
+ {isPending ? ( @@ -283,63 +291,59 @@ export const CategoryResume: FC = ({
Error: {error.message}
) : ( <> -
- {data.pages.map((group, i) => ( - - {group.content?.map((resume) => ( - - ))} - - ))} -
+ + + {data.pages.map((group, i) => ( + + {group.content?.map((resume) => ( + + + + ))} + + ))} + + - {/* Loading more indicator */}
{isFetchingNextPage ? ( ) : hasNextPage ? ( -
// Spacer for intersection observer +
) : ( -

+ 더 이상 이력서가 없습니다. -

+ )}
)} - - {/* Pagination */} - {/* - 다음 - -
- } - previousLabel={ -
- - 이전 -
- } - onPageChange={handlePageClick} - pageRangeDisplayed={5} - pageCount={totalPage} - forcePage={page - 1} // 현재 페이지를 강제로 설정 - // renderOnZeroPageCount={() =>
이력서 없음
} - containerClassName="flex items-center justify-center gap-2 sm:gap-4 text-sm sm:text-base" - pageClassName="flex justify-center items-center size-6 sm:size-7 rounded-xl transition-colors" - activeClassName="bg-main text-white" - /> */} -
-
+ + ); }; From 0f7d5c3087ea6f4ce8c4d78fefe1774e6e5279aa Mon Sep 17 00:00:00 2001 From: toris Date: Wed, 26 Mar 2025 21:53:16 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[chore]=20ui=20=EA=B0=9C=EC=84=A0=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/signin/page.tsx | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index 9230858..6f6a492 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -25,6 +25,9 @@ const SigninPage = () => { const pwdRef = useRef(null); const { status } = useSession(); const router = useRouter(); + const [captchaSize, setCaptchaSize] = useState<'normal' | 'compact'>( + 'normal' + ); useEffect(() => { if (status === 'authenticated') { @@ -32,6 +35,29 @@ const SigninPage = () => { } }, [router, status]); + // 화면 크기에 따라 captcha 크기 조정 + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 640) { + // sm 브레이크포인트 (640px) + setCaptchaSize('compact'); + } else { + setCaptchaSize('normal'); + } + }; + + // 초기 로드 시 크기 설정 + handleResize(); + + // 창 크기 변경 시 감지 + window.addEventListener('resize', handleResize); + + // 컴포넌트 언마운트 시 이벤트 리스너 정리 + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + const validateEmail = (email: string) => { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); @@ -255,7 +281,7 @@ const SigninPage = () => {
@@ -298,16 +324,18 @@ const SigninPage = () => { - +
+ +