diff --git a/src/app/(auth-required)/components/QRCode.tsx b/src/app/(auth-required)/components/QRCode.tsx deleted file mode 100644 index 63bfe76..0000000 --- a/src/app/(auth-required)/components/QRCode.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { QRCodeSVG } from 'qrcode.react'; -import { useQuery } from '@tanstack/react-query'; -import { COLORS, env, PATHS, SCREENS } from '@/constants'; -import { useResponsive } from '@/hooks'; -import { createQRToken } from '@/apis'; -import { Modal as Layout } from '@/components'; - -export const QRCode = () => { - const width = useResponsive(); - - const { data, isSuccess } = useQuery({ queryKey: [PATHS.QRLOGIN], queryFn: createQRToken }); - - return ( - - - {isSuccess ? ( - - ) : ( - - )} - - - ); -}; diff --git a/src/app/(auth-required)/components/header/index.tsx b/src/app/(auth-required)/components/header/index.tsx index f33caa4..55c8c12 100644 --- a/src/app/(auth-required)/components/header/index.tsx +++ b/src/app/(auth-required)/components/header/index.tsx @@ -12,7 +12,7 @@ import { useResponsive, useModal } from '@/hooks'; import { logout, me } from '@/apis'; import { defaultStyle, Section, textStyle } from './Section'; import { Modal } from '../notice/Modal'; -import { QRCode } from '../QRCode'; +import { QRCode } from '../qrcode'; const PARAMS = { MAIN: '?asc=false&sort=', diff --git a/src/app/(auth-required)/components/index.ts b/src/app/(auth-required)/components/index.ts index cfcae8a..5407401 100644 --- a/src/app/(auth-required)/components/index.ts +++ b/src/app/(auth-required)/components/index.ts @@ -1,3 +1,3 @@ export * from './header'; export * from './notice'; -export * from './QRCode'; +export * from './qrcode'; diff --git a/src/app/(auth-required)/components/qrcode/CopyButton.tsx b/src/app/(auth-required)/components/qrcode/CopyButton.tsx new file mode 100644 index 0000000..33b3961 --- /dev/null +++ b/src/app/(auth-required)/components/qrcode/CopyButton.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef, useState } from 'react'; + +const ROLLBACK_AFTER_CLICK_MS = 1000; + +interface IProp { + url?: string; + disabled?: boolean; +} + +export const CopyButton = ({ url, disabled }: IProp) => { + const [clicked, setClicked] = useState(false); + const clickedRef = useRef(null); + + useEffect(() => { + return () => { + if (clickedRef.current) clearTimeout(clickedRef.current); + }; + }, []); + + const handleClick = async () => { + if (clicked || !url) return; + + try { + await navigator.clipboard.writeText(url); + setClicked(true); + + if (clickedRef.current) clearTimeout(clickedRef.current); + + clickedRef.current = setTimeout(() => setClicked(false), ROLLBACK_AFTER_CLICK_MS); + } catch (err) { + console.error('클립보드 복사 실패:', err); + } + }; + + return ( + + {url} + + ); +}; diff --git a/src/app/(auth-required)/components/qrcode/index.tsx b/src/app/(auth-required)/components/qrcode/index.tsx new file mode 100644 index 0000000..5cbfa4d --- /dev/null +++ b/src/app/(auth-required)/components/qrcode/index.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { QRCodeSVG } from 'qrcode.react'; +import { useQuery } from '@tanstack/react-query'; +import { useState, useRef, useEffect } from 'react'; +import { COLORS, env, PATHS, SCREENS } from '@/constants'; +import { useResponsive } from '@/hooks'; +import { createQRToken } from '@/apis'; +import { Modal as Layout } from '@/components'; +import { formatTimeToMMSS } from '@/utils/dateUtil'; +import { CopyButton } from './CopyButton'; + +const TIMER_DURATION = 5 * 60; // 5분 = 300초 + +export const QRCode = () => { + const width = useResponsive(); + const [timeLeft, setTimeLeft] = useState(TIMER_DURATION); + + const timerRef = useRef(null); + const isExpired = timeLeft === 0; + + const { data, isLoading, refetch } = useQuery({ + queryKey: [PATHS.QRLOGIN], + queryFn: createQRToken, + refetchOnMount: true, + staleTime: 0, + refetchOnWindowFocus: false, + }); + const url = `${env.BASE_URL}/api/qr-login?token=${data?.token}`; + + // 타이머 시작 + useEffect(() => { + if (!isLoading) { + timerRef.current = setInterval(() => setTimeLeft((prev) => (prev <= 1 ? 0 : prev - 1)), 1000); + } + + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [isLoading]); + + return ( + + + + + + + 만료까지 + + {formatTimeToMMSS(timeLeft)} + + {isExpired && !isLoading && ( + { + await refetch(); + setTimeLeft(TIMER_DURATION); + }} + > + 새로고침 + + )} + + + + + + ); +}; diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts index d48c16d..4acbbab 100644 --- a/src/utils/dateUtil.ts +++ b/src/utils/dateUtil.ts @@ -46,3 +46,12 @@ export const convertDateToKST = (date?: string): KSTDateFormat | undefined => { full: kstDate, }; }; + +export const formatTimeToMMSS = (time: number) => { + const minute = Math.floor(time / 60) + .toString() + .padStart(2, '0'); + const second = (time % 60).toString().padStart(2, '0'); + + return `${minute}분 ${second}초`; +};