From 37146a2a45210ef0aee90ab09c617872850fdd8a Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 13 Mar 2026 17:25:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main/_components/ContactUserProfile.tsx | 220 ++++++++++---------- 1 file changed, 105 insertions(+), 115 deletions(-) diff --git a/app/main/_components/ContactUserProfile.tsx b/app/main/_components/ContactUserProfile.tsx index 212affa..b470e02 100644 --- a/app/main/_components/ContactUserProfile.tsx +++ b/app/main/_components/ContactUserProfile.tsx @@ -1,6 +1,6 @@ import { Hobby, ProfileData } from "@/lib/types/profile"; import Image from "next/image"; -import { ChevronDown, ChevronUp } from "lucide-react"; +import { ChevronDown, ChevronUp, Send } from "lucide-react"; import React, { useEffect, useRef, useState } from "react"; /* ── 유틸 함수 ── */ @@ -28,23 +28,19 @@ const getAge = (birthDate?: string) => { return new Date().getFullYear() - new Date(birthDate).getFullYear() + 1; }; -/* ── 밑줄 값 텍스트 (반복 패턴) ── */ -const UnderlinedValue = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - -const Label = ({ children }: { children: React.ReactNode }) => ( - {children} +/* ── 태그 컴포넌트 ── */ +const Tag = ({ text }: { text: string }) => ( +
+ {text} +
); /* ── 프로필 헤더 (이미지 + 닉네임 + 액션 아이콘) ── */ const ProfileHeader = ({ profile }: { profile: ProfileData }) => (
- {/* 프로필 이미지 */} + {/* 프로필 이미지 (48x48 container, 44x44 image) */}
-
+
Profile ( {/* 닉네임 */}
- 내가 뽑은 사람 - + 내가 뽑은 사람 + {profile.nickname || "익명"}
- {/* 액션 아이콘 */}
@@ -84,71 +81,29 @@ const ProfileHeader = ({ profile }: { profile: ProfileData }) => ( /* ── 나이 + 전공 ── */ const ProfileStats = ({ profile }: { profile: ProfileData }) => ( -
-
- 나이 - +
+
+ 나이 + {getAge(profile.birthDate)}
-
- 전공 - - {profile.major || "미지정"} - -
-
-); - -/* ── 소개 텍스트 (MBTI, 연락빈도, 취미) ── */ -const ProfileIntro = ({ profile }: { profile: ProfileData }) => ( - <> -
- - {profile.mbti} - - - {getContactFrequencyLabel(profile.contactFrequency)} ➡️ - - -
-
- - {getHobbyLabel(profile.hobbies)} - +
+ MBTI + {profile.mbti}
- -); - -/* ── 라벨 + 밑줄 값 행 (반복 패턴 제거) ── */ -const IntroRow = ({ - label, - value, - suffix, -}: { - label: string; - value: React.ReactNode; - suffix?: string; -}) => ( -
- -
-
- - {value} - -
- {suffix && ( - - {suffix} - - )} +
+ + 연락빈도 + + + {getContactFrequencyLabel(profile.contactFrequency)} +
); -/* ── 펼치기 영역 (장점, 노래, 한마디) ── */ -const ExpandableDetails = ({ +const ProfileDetails = ({ profile, isExpanded, }: { @@ -156,26 +111,66 @@ const ExpandableDetails = ({ isExpanded: boolean; }) => (
Click) 블러가 나중에 생기도록 delay 부여 + transition: isExpanded + ? "max-height 500ms ease-in-out, mask-image 300ms ease-out, -webkit-mask-image 300ms ease-out" + : "max-height 500ms ease-in-out, mask-image 400ms ease-in 100ms, -webkit-mask-image 400ms ease-in 100ms", + }} > -
- - - + {/* 관심사 */} +
+ 관심사 +
+ {profile.hobbies && profile.hobbies.length > 0 ? ( + profile.hobbies.map((hobby, idx) => ( + + )) + ) : ( + + )} +
+
+ + {/* 장점 */} +
+ 장점 +
+ {profile.advantages && profile.advantages.length > 0 ? ( + profile.advantages.map((adv, idx) => ) + ) : ( + + )} +
+
+ + {/* 좋아하는 노래 */} +
+ 좋아하는 노래 + + {profile.favoriteSong || "아직 없어요!"} + +
+ + {/* 나를 소개하는 한마디 */} +
+ 나를 소개하는 한마디 + + {profile.intro || "잘 부탁드립니다!! 😆"} +
); @@ -208,6 +203,7 @@ const ContactUserProfile = ({ profiles }: ContactUserProfileProps) => { const [activeIndex, setActiveIndex] = useState(0); const [isExpanded, setIsExpanded] = useState(false); const scrollRef = useRef(null); + const touchStartTime = useRef(0); useEffect(() => { const el = scrollRef.current; @@ -222,6 +218,14 @@ const ContactUserProfile = ({ profiles }: ContactUserProfileProps) => { return () => el.removeEventListener("scroll", handleScroll); }, []); + const handleCardClick = () => { + const touchDuration = Date.now() - touchStartTime.current; + // 200ms 미만의 짧은 터치(클릭)일 때만 확장 상태 토글 + if (touchDuration < 200) { + setIsExpanded(!isExpanded); + } + }; + if (!profiles || profiles.length === 0) return null; return ( @@ -235,14 +239,15 @@ const ContactUserProfile = ({ profiles }: ContactUserProfileProps) => { {profiles.map((profile) => (
(touchStartTime.current = Date.now())} + onMouseDown={() => (touchStartTime.current = Date.now())} + onClick={handleCardClick} + className="flex w-full shrink-0 cursor-pointer snap-center flex-col items-center justify-start gap-3 p-4" > -
- - +
))} @@ -257,21 +262,6 @@ const ContactUserProfile = ({ profiles }: ContactUserProfileProps) => { className="flex h-[42px] w-full items-center justify-between rounded-b-[24px] border border-t-0 border-white/30 px-4 backdrop-blur-[50px]" > - - From d39367027cad312f1db576eab5804364a51826ed Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 13 Mar 2026 17:31:49 +0900 Subject: [PATCH 2/3] fix/profile-card-design --- ...ContactUserProfile.tsx => ProfileCard.tsx} | 153 ++++-------------- app/main/_components/ProfileSlider.tsx | 100 ++++++++++++ app/main/_components/ScreenMainPage.tsx | 4 +- 3 files changed, 137 insertions(+), 120 deletions(-) rename app/main/_components/{ContactUserProfile.tsx => ProfileCard.tsx} (58%) create mode 100644 app/main/_components/ProfileSlider.tsx diff --git a/app/main/_components/ContactUserProfile.tsx b/app/main/_components/ProfileCard.tsx similarity index 58% rename from app/main/_components/ContactUserProfile.tsx rename to app/main/_components/ProfileCard.tsx index b470e02..1288ee0 100644 --- a/app/main/_components/ContactUserProfile.tsx +++ b/app/main/_components/ProfileCard.tsx @@ -1,7 +1,9 @@ +"use client"; + import { Hobby, ProfileData } from "@/lib/types/profile"; import Image from "next/image"; -import { ChevronDown, ChevronUp, Send } from "lucide-react"; -import React, { useEffect, useRef, useState } from "react"; +import { Send } from "lucide-react"; +import React, { useRef } from "react"; /* ── 유틸 함수 ── */ const getContactFrequencyLabel = (freq?: string) => { @@ -17,12 +19,6 @@ const getContactFrequencyLabel = (freq?: string) => { } }; -const getHobbyLabel = (hobbies?: (Hobby | string)[]) => { - if (!hobbies || hobbies.length === 0) return "없음"; - const hobby = hobbies[0]; - return typeof hobby === "string" ? hobby : hobby.name; -}; - const getAge = (birthDate?: string) => { if (!birthDate) return "?? "; return new Date().getFullYear() - new Date(birthDate).getFullYear() + 1; @@ -79,7 +75,7 @@ const ProfileHeader = ({ profile }: { profile: ProfileData }) => (
); -/* ── 나이 + 전공 ── */ +/* ── 나이 + MBTI + 연락빈도 ── */ const ProfileStats = ({ profile }: { profile: ProfileData }) => (
@@ -103,6 +99,7 @@ const ProfileStats = ({ profile }: { profile: ProfileData }) => (
); +/* ── 확장 가능한 상세 정보 ── */ const ProfileDetails = ({ profile, isExpanded, @@ -194,130 +191,50 @@ const SocialIdDisplay = ({ profile }: { profile: ProfileData }) => { ); }; -/* ── 메인 컴포넌트 ── */ -interface ContactUserProfileProps { - profiles: ProfileData[]; +/* ── 메인 프로필 카드 컴포넌트 ── */ +interface ProfileCardProps { + profile: ProfileData; } -const ContactUserProfile = ({ profiles }: ContactUserProfileProps) => { - const [activeIndex, setActiveIndex] = useState(0); - const [isExpanded, setIsExpanded] = useState(false); - const scrollRef = useRef(null); +const ProfileCard = ({ profile }: ProfileCardProps) => { + const [isExpanded, setIsExpanded] = React.useState(false); const touchStartTime = useRef(0); - useEffect(() => { - const el = scrollRef.current; - if (!el) return; - - const handleScroll = () => { - const index = Math.round(el.scrollLeft / el.clientWidth); - setActiveIndex(index); - }; - - el.addEventListener("scroll", handleScroll); - return () => el.removeEventListener("scroll", handleScroll); - }, []); - const handleCardClick = () => { const touchDuration = Date.now() - touchStartTime.current; - // 200ms 미만의 짧은 터치(클릭)일 때만 확장 상태 토글 if (touchDuration < 200) { setIsExpanded(!isExpanded); } }; - if (!profiles || profiles.length === 0) return null; - return ( -
-
- {/* 스와이프 카드 영역 */} -
- {profiles.map((profile) => ( -
(touchStartTime.current = Date.now())} - onMouseDown={() => (touchStartTime.current = Date.now())} - onClick={handleCardClick} - className="flex w-full shrink-0 cursor-pointer snap-center flex-col items-center justify-start gap-3 p-4" - > - - -
- -
-
- ))} +
+ {/* 카드 본체 */} +
(touchStartTime.current = Date.now())} + onMouseDown={() => (touchStartTime.current = Date.now())} + onClick={handleCardClick} + className="flex w-full cursor-pointer flex-col items-center justify-start gap-3 p-4" + > + + +
+
+
- {/* 그라디언트 푸터 */} -
- -
-
- - {/* 인디케이터 도트 (슬라이딩 트랙 방식) */} - {profiles.length > 1 && ( -
- {/* 도트 5개 너비 (6px * 5 + 6px 간격 * 4 = 54px) */} -
-
- {profiles.map((profile, i) => { - // 현재 보여지는 5개 도트의 윈도우 범위 계산 - const windowStart = Math.max( - 0, - Math.min(profiles.length - 5, activeIndex - 2), - ); - const windowEnd = windowStart + 4; - - // 윈도우 안에 있는지 확인 - const isVisible = i >= windowStart && i <= windowEnd; - // 윈도우의 양 끝 도트인지 확인 (더 있다는 표시로 작게 만듦) - const isEdge = - (i === windowStart && i > 0) || - (i === windowEnd && i < profiles.length - 1); - - return ( -
- ); - })} -
-
-
- )} + {/* 그라디언트 푸터 */} +
+ +
); }; -export default ContactUserProfile; +export default ProfileCard; diff --git a/app/main/_components/ProfileSlider.tsx b/app/main/_components/ProfileSlider.tsx new file mode 100644 index 0000000..19592d1 --- /dev/null +++ b/app/main/_components/ProfileSlider.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { ProfileData } from "@/lib/types/profile"; +import React, { useEffect, useRef, useState } from "react"; +import ProfileCard from "./ProfileCard"; + +/* ── 프로필 슬라이더 (스와이프 + 인디케이터) ── */ +interface ProfileSliderProps { + profiles: ProfileData[]; +} + +const ProfileSlider = ({ profiles }: ProfileSliderProps) => { + const [activeIndex, setActiveIndex] = useState(0); + const scrollRef = useRef(null); + + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + + const handleScroll = () => { + const index = Math.round(el.scrollLeft / el.clientWidth); + setActiveIndex(index); + }; + + el.addEventListener("scroll", handleScroll); + return () => el.removeEventListener("scroll", handleScroll); + }, []); + + if (!profiles || profiles.length === 0) return null; + + return ( +
+ {/* 스와이프 카드 영역 */} +
+ {profiles.map((profile) => ( +
+ +
+ ))} +
+ + {/* 인디케이터 도트 (슬라이딩 트랙 방식) */} + {profiles.length > 1 && ( +
+ {/* 도트 5개 너비 (6px * 5 + 6px 간격 * 4 = 54px) */} +
+
+ {profiles.map((profile, i) => { + // 현재 보여지는 5개 도트의 윈도우 범위 계산 + const windowStart = Math.max( + 0, + Math.min(profiles.length - 5, activeIndex - 2), + ); + const windowEnd = windowStart + 4; + + // 윈도우 안에 있는지 확인 + const isVisible = i >= windowStart && i <= windowEnd; + // 윈도우의 양 끝 도트인지 확인 (더 있다는 표시로 작게 만듦) + const isEdge = + (i === windowStart && i > 0) || + (i === windowEnd && i < profiles.length - 1); + + return ( +
+ ); + })} +
+
+
+ )} +
+ ); +}; + +export default ProfileSlider; diff --git a/app/main/_components/ScreenMainPage.tsx b/app/main/_components/ScreenMainPage.tsx index cea1ced..9355ffa 100644 --- a/app/main/_components/ScreenMainPage.tsx +++ b/app/main/_components/ScreenMainPage.tsx @@ -10,7 +10,7 @@ import { import BusinessInfo from "@/components/common/BusinessInfo"; import NoticeSection from "./NoticeSection"; -import ContactUserProfile from "./ContactUserProfile"; +import ProfileSlider from "./ProfileSlider"; import { ProfileData } from "@/lib/types/profile"; import ChargeRequestWaiting from "./ChargeRequestWaiting"; @@ -139,7 +139,7 @@ const ScreenMainPage = () => { /> )} {/* */} - +
From 439156e53a3b52a923b31377b157102a678c8a20 Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 13 Mar 2026 17:41:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main/_components/ProfileCard.tsx | 52 ++++++++++++++++++---------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/app/main/_components/ProfileCard.tsx b/app/main/_components/ProfileCard.tsx index 1288ee0..923c1da 100644 --- a/app/main/_components/ProfileCard.tsx +++ b/app/main/_components/ProfileCard.tsx @@ -27,7 +27,9 @@ const getAge = (birthDate?: string) => { /* ── 태그 컴포넌트 ── */ const Tag = ({ text }: { text: string }) => (
- {text} + + {text} +
); @@ -35,11 +37,11 @@ const Tag = ({ text }: { text: string }) => ( const ProfileHeader = ({ profile }: { profile: ProfileData }) => (
{/* 프로필 이미지 (48x48 container, 44x44 image) */} -
+
Profile @@ -49,7 +51,7 @@ const ProfileHeader = ({ profile }: { profile: ProfileData }) => ( {/* 닉네임 */}
내가 뽑은 사람 - + {profile.nickname || "익명"}
@@ -57,19 +59,21 @@ const ProfileHeader = ({ profile }: { profile: ProfileData }) => (
@@ -80,19 +84,19 @@ const ProfileStats = ({ profile }: { profile: ProfileData }) => (
나이 - + {getAge(profile.birthDate)}
MBTI - {profile.mbti} + {profile.mbti}
연락빈도 - + {getContactFrequencyLabel(profile.contactFrequency)}
@@ -130,9 +134,9 @@ const ProfileDetails = ({ 관심사
{profile.hobbies && profile.hobbies.length > 0 ? ( - profile.hobbies.map((hobby, idx) => ( + profile.hobbies.map((hobby) => ( )) @@ -147,7 +151,7 @@ const ProfileDetails = ({ 장점
{profile.advantages && profile.advantages.length > 0 ? ( - profile.advantages.map((adv, idx) => ) + profile.advantages.map((adv) => ) ) : ( )} @@ -157,7 +161,7 @@ const ProfileDetails = ({ {/* 좋아하는 노래 */}
좋아하는 노래 - + {profile.favoriteSong || "아직 없어요!"}
@@ -165,7 +169,7 @@ const ProfileDetails = ({ {/* 나를 소개하는 한마디 */}
나를 소개하는 한마디 - + {profile.intro || "잘 부탁드립니다!! 😆"}
@@ -180,14 +184,16 @@ const SocialIdDisplay = ({ profile }: { profile: ProfileData }) => { return (
kakao - + {profile.socialAccountId}
); } return ( - @{profile.socialAccountId} + + @{profile.socialAccountId} + ); }; @@ -211,9 +217,19 @@ const ProfileCard = ({ profile }: ProfileCardProps) => {
{/* 카드 본체 */}
(touchStartTime.current = Date.now())} onMouseDown={() => (touchStartTime.current = Date.now())} onClick={handleCardClick} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setIsExpanded(!isExpanded); + } + }} className="flex w-full cursor-pointer flex-col items-center justify-start gap-3 p-4" >