diff --git a/app/globals.css b/app/globals.css index 2506853..da94dde 100644 --- a/app/globals.css +++ b/app/globals.css @@ -45,6 +45,17 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); + + --animate-shimmer: shimmer 1s linear infinite; + + @keyframes shimmer { + from { + background-position: 0% 0%; + } + to { + background-position: 200% 0%; + } + } } :root { diff --git a/app/matching-result/_components/MatchingResult.tsx b/app/matching-result/_components/MatchingResult.tsx new file mode 100644 index 0000000..64925bf --- /dev/null +++ b/app/matching-result/_components/MatchingResult.tsx @@ -0,0 +1,169 @@ +import { ADVANTAGES } from "@/lib/constants/advantages"; +import { HOBBIES } from "@/lib/constants/hobbies"; +import Image from "next/image"; +import React from "react"; + +const MatchingResult = () => { + // 백엔드에서 내려오는 텍스트와 상수를 매칭하여 이모지를 포함한 전체 문자열을 반환하는 헬퍼 함수 + const allHobbies = Object.values(HOBBIES).flat(); + const allAdvantages = Object.values(ADVANTAGES).flat(); + + const findWithEmoji = (list: readonly string[] | string[], text: string) => { + return list.find((item) => item.includes(text)) || text; + }; + + // 임시 데이터 (상수에 존재하는 값들로 구성) + const data = { + nickname: "겨울이오길", + major: "정보통신전자공학부", + age: "21", + mbti: "ENTP", + contactFrequency: "보통", + hobbies: ["축구", "영화감상", "캠핑", "코딩", "게임"], + strengths: ["다정다감", "유머러스", "계획적"], + song: "한로로 - 사랑하게 될 거야", + intro: "친하게 지내요@!🙃", + instagram: "@winterizcoming_", + }; + + return ( +
+
+ {/* Header Section (Frame 2612385) */} +
+ {/* Profile Image (Container + Image (Profile)) */} +
+
+ Profile +
+
+ {/* Nickname & Label (Frame 2612933) */} +
+ + 내가 뽑은 사람 + + + {data.nickname} + +
+
+ + {/* Major Section */} +
+ + 전공 + + + {data.major} + +
+ + {/* Stats Section (Frame 22) */} +
+ {/* Age (Std_Num) */} +
+ + 나이 + + + {data.age} + +
+ {/* MBTI (Major) */} +
+ + MBTI + + + {data.mbti} + +
+ {/* Contact (Std_Num) */} +
+ + 연락빈도 + + + {data.contactFrequency} + +
+
+
+ + {/* Hobbies Section */} +
+ + 취미 + +
+ {data.hobbies.map((hobby, index) => ( +
+ + {findWithEmoji(allHobbies, hobby)} + +
+ ))} +
+
+ + {/* Strengths Section */} +
+ + 장점 + +
+ {data.strengths.map((strength, index) => ( +
+ + {findWithEmoji(allAdvantages, strength)} + +
+ ))} +
+
+ + {/* Song Section */} +
+ + 좋아하는 노래 + + + {data.song} + +
+ + {/* Intro Section */} +
+ + 나를 소개하는 한마디 + + + {data.intro} + +
+ + {/* SNS Section (Contacts) */} +
+ + Instagram + + + {data.instagram} + +
+
+ ); +}; + +export default MatchingResult; diff --git a/app/matching-result/_components/ResultFooter.tsx b/app/matching-result/_components/ResultFooter.tsx new file mode 100644 index 0000000..1534c57 --- /dev/null +++ b/app/matching-result/_components/ResultFooter.tsx @@ -0,0 +1,131 @@ +import Image from "next/image"; +import React, { useState, useEffect, useRef } from "react"; + +const ResultFooter = () => { + const [timeLeft, setTimeLeft] = useState(3); + const [isHolding, setIsHolding] = useState(false); + const [isTriggered, setIsTriggered] = useState(false); + const timerRef = useRef(null); + + const handleHoldStart = (e: React.MouseEvent | React.TouchEvent) => { + // 롱프레스 시 브라우저 기본 컨텍스트 메뉴 등이 뜨지 않도록 방지 (모바일 대응) + if ("button" in e && e.button !== 0) return; // 마우스 왼쪽 클릭만 허용 + + // 모바일에서 touchstart 후에 mousedown이 또 발생하는 것 방지 + if (e.type === "touchstart") { + // e.preventDefault(); // 필요 시 추가 (단, 스크롤 방해 가능성 있음) + } + + if (isHolding) return; // 이미 누르는 중이면 무시 + + setIsHolding(true); + setTimeLeft(3); + setIsTriggered(false); + }; + + const handleHoldEnd = () => { + setIsHolding(false); + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + // 성공적으로 트리거된 경우가 아니라면 시간 초기화 + if (!isTriggered) { + setTimeLeft(3); + } + }; + + useEffect(() => { + if (isHolding) { + timerRef.current = setInterval(() => { + setTimeLeft((prev) => { + if (prev <= 1) { + setIsTriggered((prevTriggered) => { + if (prevTriggered) return prevTriggered; + alert("한 번 더 뽑기 로직 실행!"); // 로직 실행 위치 + return true; + }); + setIsHolding(false); + return 0; + } + return prev - 1; + }); + }, 1000); + } else { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + } + + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [isHolding]); + + return ( +
+ {/* Top Row: Retry & Mail Buttons */} +
+ {/* Retry Button */} + + + {/* Mail Button */} + +
+ + {/* Bottom Row: One More Button with Long Press */} + +
+ ); +}; + +export default ResultFooter; diff --git a/app/matching-result/_components/ScreenMatchingResult.tsx b/app/matching-result/_components/ScreenMatchingResult.tsx new file mode 100644 index 0000000..7424437 --- /dev/null +++ b/app/matching-result/_components/ScreenMatchingResult.tsx @@ -0,0 +1,48 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { BackButton } from "@/components/ui/BackButton"; +import Image from "next/image"; +import WaitingFrame from "./WaitingFrame"; +import MatchingResult from "./MatchingResult"; +import ResultFooter from "./ResultFooter"; + +const ScreenMatchingResult = () => { + const [isWaiting, setIsWaiting] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setIsWaiting(false); + }, 3000); + + return () => clearTimeout(timer); + }, []); + + return ( +
+ + Comatching + 매칭 결과 + + } + /> + {isWaiting ? ( + + ) : ( + <> + + + + )} +
+ ); +}; + +export default ScreenMatchingResult; diff --git a/app/matching-result/_components/WaitingFrame.tsx b/app/matching-result/_components/WaitingFrame.tsx new file mode 100644 index 0000000..2d707fb --- /dev/null +++ b/app/matching-result/_components/WaitingFrame.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +const WaitingFrame = () => { + return ( +
+
+

+ 코매칭 AI가 입력하신 결과를 바탕으로 +
+ 비슷한 매칭 상대를 찾고 있어요.. +

+
+
+ ); +}; + +export default WaitingFrame; diff --git a/app/matching-result/page.tsx b/app/matching-result/page.tsx new file mode 100644 index 0000000..4099a8b --- /dev/null +++ b/app/matching-result/page.tsx @@ -0,0 +1,5 @@ +import ScreenMatchingResult from "./_components/ScreenMatchingResult"; + +export default function MatchingResultPage() { + return ; +} diff --git a/components/ui/BackButton.tsx b/components/ui/BackButton.tsx index 3ff5360..eabee02 100644 --- a/components/ui/BackButton.tsx +++ b/components/ui/BackButton.tsx @@ -7,7 +7,7 @@ import { cn } from "@/lib/utils"; type BackButtonProps = { className?: string; onClick?: () => void; - text?: string; + text?: React.ReactNode; }; export const BackButton = ({ @@ -33,7 +33,7 @@ export const BackButton = ({ > - {text} +
{text}
); }