Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
169 changes: 169 additions & 0 deletions app/matching-result/_components/MatchingResult.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
Comment on lines +8 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

allHobbies, allAdvantages 변수와 findWithEmoji 함수는 컴포넌트의 상태나 props에 의존하지 않는 순수 로직입니다. 프로젝트 규칙에 따라 이를 공통 유틸리티 파일로 추출하여 재사용성을 높이고 관심사를 분리하는 것을 권장합니다.

References
  1. Pure functions, such as validation logic, should be extracted into common utility files to improve reusability and separate concerns.


// 임시 데이터 (상수에 존재하는 값들로 구성)
const data = {
nickname: "겨울이오길",
major: "정보통신전자공학부",
age: "21",
mbti: "ENTP",
contactFrequency: "보통",
hobbies: ["축구", "영화감상", "캠핑", "코딩", "게임"],
strengths: ["다정다감", "유머러스", "계획적"],
song: "한로로 - 사랑하게 될 거야",
intro: "친하게 지내요@!🙃",
instagram: "@winterizcoming_",
};
Comment on lines +16 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 매칭 결과 데이터(data)가 컴포넌트 내부에 하드코딩되어 있습니다. 컴포넌트의 재사용성을 높이기 위해 이 데이터를 props로 전달받도록 리팩토링하는 것을 권장합니다.


return (
<div className="mt-6 flex w-full flex-col gap-6 rounded-[24px] border border-white/30 bg-white/50 p-6 shadow-[0px_0px_8px_rgba(0,0,0,0.08)] backdrop-blur-[50px]">
<div className="flex w-full flex-col gap-4">
{/* Header Section (Frame 2612385) */}
<div className="flex w-full flex-row items-center gap-4">
{/* Profile Image (Container + Image (Profile)) */}
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full border-2 border-white bg-white/0 p-[2px] shadow-[0px_1px_3px_rgba(0,0,0,0.1),0px_1px_2px_-1px_rgba(0,0,0,0.1)]">
<div className="relative h-11 w-11 overflow-hidden rounded-full">
<Image
src="/animal/cat_female 1.png"
alt="Profile"
fill
className="object-cover"
/>
</div>
</div>
{/* Nickname & Label (Frame 2612933) */}
<div className="flex flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
내가 뽑은 사람
</span>
<span className="typo-16-600 flex items-center text-black">
{data.nickname}
</span>
</div>
</div>

{/* Major Section */}
<div className="flex w-full flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
전공
</span>
<span className="typo-16-700 flex items-center text-black">
{data.major}
</span>
</div>

{/* Stats Section (Frame 22) */}
<div className="flex w-full flex-row items-start gap-2">
{/* Age (Std_Num) */}
<div className="flex flex-1 flex-col items-center gap-1">
<span className="typo-12-600 flex w-full items-center text-[#777777]">
나이
</span>
<span className="typo-16-700 flex w-full items-center text-black">
{data.age}
</span>
</div>
{/* MBTI (Major) */}
<div className="flex flex-1 flex-col items-start gap-1">
<span className="typo-12-600 flex w-full items-center text-[#777777]">
MBTI
</span>
<span className="typo-16-700 flex w-full items-center text-black">
{data.mbti}
</span>
</div>
{/* Contact (Std_Num) */}
<div className="flex flex-1 flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
연락빈도
</span>
<span className="typo-16-700 flex w-full items-center text-black">
{data.contactFrequency}
</span>
</div>
</div>
</div>

{/* Hobbies Section */}
<div className="flex w-full flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
취미
</span>
<div className="flex w-full flex-row flex-wrap items-start gap-1 py-1">
{data.hobbies.map((hobby, index) => (
<div
key={index}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

리스트 렌더링 시 index를 key로 사용하는 대신, 고유한 값인 hobby를 사용하는 것이 React의 재조정(Reconciliation) 프로세스에 더 효율적입니다.

Suggested change
key={index}
key={hobby}

className="flex h-8 items-center justify-center gap-[10px] rounded-full border border-[#DFDFDF] bg-[#B3B3B3]/10 px-3 py-2 backdrop-blur-[50px]"
>
<span className="typo-14-500 text-black">
{findWithEmoji(allHobbies, hobby)}
</span>
</div>
))}
</div>
</div>

{/* Strengths Section */}
<div className="flex w-full flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
장점
</span>
<div className="flex w-full flex-row flex-wrap items-start gap-1 py-1">
{data.strengths.map((strength, index) => (
<div
key={index}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

리스트 렌더링 시 index를 key로 사용하는 대신, 고유한 값인 strength를 사용하는 것이 좋습니다.

Suggested change
key={index}
key={strength}

className="flex h-8 items-center justify-center gap-[10px] rounded-full border border-[#DFDFDF] bg-[#B3B3B3]/10 px-3 py-2 backdrop-blur-[50px]"
>
<span className="typo-14-500 text-black">
{findWithEmoji(allAdvantages, strength)}
</span>
</div>
))}
</div>
</div>

{/* Song Section */}
<div className="flex w-full flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
좋아하는 노래
</span>
<span className="typo-16-700 flex items-center text-black">
{data.song}
</span>
</div>

{/* Intro Section */}
<div className="flex w-full flex-col items-start gap-1">
<span className="typo-12-600 flex items-center text-[#777777]">
나를 소개하는 한마디
</span>
<span className="typo-16-700 flex items-center text-black">
{data.intro}
</span>
</div>

{/* SNS Section (Contacts) */}
<div className="flex w-full flex-col items-start gap-1 py-2">
<span className="typo-12-600 flex w-full items-center justify-center text-center text-[#777777]">
Instagram
</span>
<span className="typo-16-700 flex w-full items-center justify-center text-center text-[#FF4D61]">
{data.instagram}
</span>
</div>
</div>
);
};

export default MatchingResult;
131 changes: 131 additions & 0 deletions app/matching-result/_components/ResultFooter.tsx
Original file line number Diff line number Diff line change
@@ -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<NodeJS.Timeout | null>(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("한 번 더 뽑기 로직 실행!"); // 로직 실행 위치
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

성공적인 동작 트리거 시 alert을 사용하는 것은 사용자 경험 측면에서 부적절할 수 있습니다. 프로젝트 규칙에 따라 alert()은 에러 메시지 표시용으로 권장되므로, 성공 케이스에는 다른 방식의 UI 피드백을 고려해 주세요.

References
  1. Prefer using the native alert() function for displaying error messages instead of toast notifications.

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 (
<div className="mt-6 flex w-full flex-col gap-3">
{/* Top Row: Retry & Mail Buttons */}
<div className="flex w-full flex-row gap-[10px]">
{/* Retry Button */}
<button className="flex flex-1 flex-col items-center justify-center rounded-[15px] bg-white px-[22px] py-4 shadow-[0px_4px_16px_rgba(0,0,0,0.12)] backdrop-blur-[50px]">
<span className="typo-18-700 text-[#4E4E4E]">다시 뽑기</span>
</button>

{/* Mail Button */}
<button className="bg-milky-pink flex flex-1 flex-col items-center justify-center rounded-[15px] px-[22px] py-4 text-white shadow-[0px_4px_16px_rgba(0,0,0,0.12)] backdrop-blur-[50px]">
<span className="typo-18-700 text-white">쪽지 보내기</span>
</button>
</div>

{/* Bottom Row: One More Button with Long Press */}
<button
onMouseDown={handleHoldStart}
onMouseUp={handleHoldEnd}
onMouseLeave={handleHoldEnd}
onTouchStart={handleHoldStart}
onTouchEnd={handleHoldEnd}
className="flex w-full flex-row items-center justify-center gap-2 rounded-[15px] bg-linear-to-r from-[#FF4D61] to-[#FF775E] px-[22px] py-4 text-white shadow-[0px_4px_16px_rgba(0,0,0,0.12)] backdrop-blur-[50px] transition-all select-none active:scale-[0.98]"
Comment on lines +82 to +88
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

롱프레스 동작은 보조 기술(스크린 리더 등)을 사용하는 사용자에게 접근성 장벽이 될 수 있습니다. 버튼에 aria-label을 추가하여 동작 방식에 대한 명확한 설명을 제공해 주세요.

>
{!isHolding && !isTriggered && (
<div
className="flex h-6 flex-row items-center gap-[10px] rounded-[36px] px-2 py-1 shadow-[0px_4px_16px_rgba(0,0,0,0.12)] backdrop-blur-[50px]"
style={{
background:
"radial-gradient(100% 99.65% at 0% -4.11%, #FFFFFF 0%, rgba(255, 255, 255, 0.85) 100%)",
}}
>
<div className="flex flex-row items-center gap-1">
<Image
src="/main/coin.png"
alt="coin"
width={16}
height={16}
className="scale-x-[-1]"
/>
<span className="typo-12-700 text-black">1</span>
</div>
<div className="flex flex-row items-center gap-1">
<Image
src="/main/elec-bulb.png"
alt="bulb"
width={16}
height={16}
/>
<span className="typo-12-700 text-black">1</span>
</div>
</div>
)}
<span className="typo-18-700">
{isHolding
? `${timeLeft}초간 길게 누르세요 ...`
: isTriggered
? "처리 중..."
: "같은 조건으로 한번 더 뽑기"}
</span>
</button>
</div>
);
};

export default ResultFooter;
48 changes: 48 additions & 0 deletions app/matching-result/_components/ScreenMatchingResult.tsx
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +13 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

3초의 고정된 지연 시간(setTimeout)은 실제 데이터 로딩 상태와 일치하지 않을 수 있습니다. 실제 API 호출의 완료 여부에 따라 isWaiting 상태를 변경하도록 구현하는 것이 더 정확한 사용자 경험을 제공합니다.


return () => clearTimeout(timer);
}, []);

return (
<main className="relative flex min-h-screen flex-col items-center px-4 py-2 pb-10">
<BackButton
text={
<div className="flex items-center gap-1.5">
<Image
src="/logo/comatching-logo.svg"
alt="Comatching"
width={96}
height={16}
priority
/>
<span className="typo-18-700 text-color-text-black">매칭 결과</span>
</div>
}
/>
{isWaiting ? (
<WaitingFrame />
) : (
<>
<MatchingResult />
<ResultFooter />
</>
)}
</main>
);
};

export default ScreenMatchingResult;
26 changes: 26 additions & 0 deletions app/matching-result/_components/WaitingFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";

const WaitingFrame = () => {
return (
<div
className="mt-6 flex h-[200px] w-full flex-col items-center gap-[10px] rounded-[24px] border border-white/30 bg-white/50 p-6 shadow-[0px_0px_8px_rgba(0,0,0,0.08)] backdrop-blur-[50px]"
style={{ boxSizing: "border-box" }}
>
<div className="flex w-full flex-col self-stretch">
<p
className="animate-shimmer typo-16-500 bg-linear-to-r from-[#666666] via-[#B3B3B3] to-[#666666] bg-[length:200%_auto] bg-clip-text text-start leading-[19px] text-transparent"
style={{
backgroundImage:
"linear-gradient(91.24deg, #666666 9.51%, #B3B3B3 35.68%, #666666 76.49%)",
}}
>
코매칭 AI가 입력하신 결과를 바탕으로
<br />
비슷한 매칭 상대를 찾고 있어요..
</p>
</div>
</div>
);
};

export default WaitingFrame;
Loading
Loading