From 23b5d9a132e269dcd30a40a42825d58d009291a9 Mon Sep 17 00:00:00 2001 From: dasosann Date: Thu, 30 Apr 2026 16:01:06 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=B4=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/matching/_components/AgeRangeDrawer.tsx | 202 ++++++++++++++++++ .../_components/MatchingAgeOption.tsx | 138 ++++++++++++ app/matching/_components/ScreenMatching.tsx | 66 +++--- 3 files changed, 381 insertions(+), 25 deletions(-) create mode 100644 app/matching/_components/AgeRangeDrawer.tsx create mode 100644 app/matching/_components/MatchingAgeOption.tsx diff --git a/app/matching/_components/AgeRangeDrawer.tsx b/app/matching/_components/AgeRangeDrawer.tsx new file mode 100644 index 0000000..b233b9f --- /dev/null +++ b/app/matching/_components/AgeRangeDrawer.tsx @@ -0,0 +1,202 @@ +"use client"; + +import React, { useState } from "react"; +import { Minus, Plus } from "lucide-react"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { cn } from "@/lib/utils"; +import Button from "@/components/ui/Button"; + +interface AgeRangeDrawerProps { + trigger: React.ReactNode; + minAge: number; + maxAge: number; + onConfirm: (minAge: number, maxAge: number) => void; +} + +const MIN_AGE = 20; +const MAX_AGE = 29; +const ALL_AGES = Array.from( + { length: MAX_AGE - MIN_AGE + 1 }, + (_, i) => MIN_AGE + i, +); + +export default function AgeRangeDrawer({ + trigger, + minAge: initialMin, + maxAge: initialMax, + onConfirm, +}: AgeRangeDrawerProps) { + const [localMin, setLocalMin] = useState(initialMin); + const [localMax, setLocalMax] = useState(initialMax); + + // Drawer가 열릴 때 현재 값으로 리셋 + const handleOpenChange = (open: boolean) => { + if (open) { + setLocalMin(initialMin); + setLocalMax(initialMax); + } + }; + + const handleConfirm = () => { + onConfirm(localMin, localMax); + }; + + const increaseMin = () => { + if (localMin < localMax) setLocalMin((v) => v + 1); + }; + const decreaseMin = () => { + if (localMin > MIN_AGE) setLocalMin((v) => v - 1); + }; + const increaseMax = () => { + if (localMax < MAX_AGE) setLocalMax((v) => v + 1); + }; + const decreaseMax = () => { + if (localMax > localMin) setLocalMax((v) => v - 1); + }; + + return ( + + {trigger} + +
+ +
+
+ + 나이 구간 설정 + + + 닫기 + +
+

+ 원하는 나이 범위를 설정하세요. +

+ + + {/* 시각적 나이 구간 표시 */} +
+
+ {ALL_AGES.map((age, i) => { + const isInRange = age >= localMin && age <= localMax; + const isStart = age === localMin; + const isEnd = age === localMax; + const isFirst = i === 0; + const isLast = i === ALL_AGES.length - 1; + + return ( +
+ {/* 셀 */} +
+ {/* 나이 레이블 */} + + {age} + +
+ ); + })} +
+
+ + {/* +/- 조절 영역 */} +
+ {/* 최소 나이 조절 */} +
+ + 최소 나이 + +
+ + + {localMin} + + +
+
+ + {/* 구분선 */} + ~ + + {/* 최대 나이 조절 */} +
+ + 최대 나이 + +
+ + + {localMax} + + +
+
+
+ + {/* 확인 버튼 */} + + + +
+ + + ); +} diff --git a/app/matching/_components/MatchingAgeOption.tsx b/app/matching/_components/MatchingAgeOption.tsx new file mode 100644 index 0000000..b9d45eb --- /dev/null +++ b/app/matching/_components/MatchingAgeOption.tsx @@ -0,0 +1,138 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Image from "next/image"; +import { Check, Delete } from "lucide-react"; +import { cn } from "@/lib/utils"; +import AgeRangeDrawer from "./AgeRangeDrawer"; + +interface MatchingAgeOptionProps { + onAgeRangeSelect: (minAge: number, maxAge: number) => void; + onReset: () => void; + minAge?: number; + maxAge?: number; +} + +const DEFAULT_MIN = 20; +const DEFAULT_MAX = 29; + +export default function MatchingAgeOption({ + onAgeRangeSelect, + onReset, + minAge, + maxAge, +}: MatchingAgeOptionProps) { + const isSelected = minAge !== undefined && maxAge !== undefined; + const [showCheck, setShowCheck] = useState(false); + + const handleConfirm = (min: number, max: number) => { + onAgeRangeSelect(min, max); + setShowCheck(true); + }; + + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onReset(); + }; + + useEffect(() => { + if (showCheck) { + const timer = setTimeout(() => setShowCheck(false), 2000); + return () => clearTimeout(timer); + } + }, [showCheck]); + + const displayText = + isSelected && minAge === maxAge + ? `${minAge}살` + : isSelected + ? `${minAge}~${maxAge}살` + : ""; + + // 선택된 상태: Delete 버튼은 Drawer 밖에서 독립적으로 동작 + if (isSelected) { + return ( +
+ +

+ 나이 구간 선택하기 +

+

+ 선택됨: {displayText} +

+ + } + /> + {/* Delete 버튼: Drawer 트리거 밖에 위치 */} + +
+ ); + } + + // 미선택 상태: 전체가 Drawer 트리거 + return ( + +
+

+ 나이 구간 선택하기 +

+

+ 원하는 나이 범위를 설정해 보세요! +

+
+
+ bulb + + 1 + +
+ + } + /> + ); +} diff --git a/app/matching/_components/ScreenMatching.tsx b/app/matching/_components/ScreenMatching.tsx index 1c1ecf1..2dcfa60 100644 --- a/app/matching/_components/ScreenMatching.tsx +++ b/app/matching/_components/ScreenMatching.tsx @@ -4,6 +4,7 @@ import MyCoinSection from "@/components/common/MyCoinSection"; import { BackButton } from "@/components/ui/BackButton"; import Image from "next/image"; import React, { useState } from "react"; +import MatchingAgeOption from "./MatchingAgeOption"; import MatchingAgeSection from "./MatchingAgeSection"; import MatchingHobbySection from "./MatchingHobbySection"; import MatchingMBTISection from "./MatchingMBTISection"; @@ -41,10 +42,12 @@ import { useItems } from "@/hooks/useItems"; import { useMatching } from "@/hooks/useMatching"; const ScreenMatching = () => { - const { data: itemData } = useItems(); + const { data: itemData, isLoading: isItemsLoading } = useItems(); const { mutate: match, isPending } = useMatching(); const [selectedMBTI, setSelectedMBTI] = useState(""); const [selectedAgeGroup, setSelectedAgeGroup] = useState(""); + const [minAge, setMinAge] = useState(undefined); + const [maxAge, setMaxAge] = useState(undefined); const [selectedFrequency, setSelectedFrequency] = useState(""); const [isSameMajorExclude, setIsSameMajorExclude] = useState(false); const [selectedHobbyCategory, setSelectedHobbyCategory] = useState< @@ -64,32 +67,36 @@ const ScreenMatching = () => { matchingTicketCount > 0 ); - const bubbleText = - matchingTicketCount === 0 ? ( - "사용할 수 있는 매칭권이 없어요." - ) : ( -
- coin - 매칭권 1 - {hasExtraOption && ( - <> - bulb - 아이템 1 - - )} - 소모 -
- ); + const bubbleText = isItemsLoading ? null : matchingTicketCount === 0 ? ( + "사용할 수 있는 매칭권이 없어요." + ) : ( +
+ coin + 매칭권 1 + {hasExtraOption && ( + <> + bulb + 아이템 1 + + )} + 소모 +
+ ); const bubbleTextColor = matchingTicketCount === 0 ? "text-color-gray-600" : "text-black"; + const handleAgeRangeSelect = (min: number, max: number) => { + setMinAge(min); + setMaxAge(max); + }; + const calculateAgeOffsets = ( group: string, ): { @@ -128,8 +135,8 @@ const ScreenMatching = () => { : undefined, sameMajorOption: isSameMajorExclude, importantOption: importantOption || undefined, - minAgeOffset: ageInfo.min, - maxAgeOffset: ageInfo.max, + minAgeOffset: minAge ?? ageInfo.min, + maxAgeOffset: maxAge ?? ageInfo.max, }; match(payload); @@ -171,6 +178,15 @@ const ScreenMatching = () => { CONTACT: selectedFrequency || "", }} /> + { + setMinAge(undefined); + setMaxAge(undefined); + }} + minAge={minAge} + maxAge={maxAge} + /> Date: Thu, 30 Apr 2026 16:05:32 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=B4=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/MatchingAgeSection.tsx | 23 +++++++++++++++++-- app/matching/_components/ScreenMatching.tsx | 16 +++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/matching/_components/MatchingAgeSection.tsx b/app/matching/_components/MatchingAgeSection.tsx index 9d3972b..ceb246e 100644 --- a/app/matching/_components/MatchingAgeSection.tsx +++ b/app/matching/_components/MatchingAgeSection.tsx @@ -2,9 +2,12 @@ import React from "react"; import ProfileButton from "../../profile-builder/_components/ProfileButton"; +import { cn } from "@/lib/utils"; + interface MatchingAgeSectionProps { onAgeGroupSelect: (ageGroup: string) => void; defaultValue?: string; + disabled?: boolean; } const AGE_GROUPS = ["연하", "동갑", "연상"]; @@ -12,21 +15,37 @@ const AGE_GROUPS = ["연하", "동갑", "연상"]; export default function MatchingAgeSection({ onAgeGroupSelect, defaultValue = "", + disabled = false, }: MatchingAgeSectionProps) { const [selected, setSelected] = React.useState(defaultValue); + // disabled 상태가 되면 선택 초기화 + React.useEffect(() => { + if (disabled && selected) { + setSelected(""); + } + }, [disabled]); + const handleSelect = (group: string) => { + if (disabled) return; setSelected(group); onAgeGroupSelect(group); }; return ( -
+

나이

- 상대의 나이를 골라주세요. + {disabled + ? "나이 구간 옵션이 선택되어 비활성화되었습니다." + : "상대의 나이를 골라주세요."}

diff --git a/app/matching/_components/ScreenMatching.tsx b/app/matching/_components/ScreenMatching.tsx index 2dcfa60..0bbbb7e 100644 --- a/app/matching/_components/ScreenMatching.tsx +++ b/app/matching/_components/ScreenMatching.tsx @@ -57,11 +57,16 @@ const ScreenMatching = () => { useState(null); const matchingTicketCount = itemData?.data.matchingTicketCount ?? 0; - const hasExtraOption = !!(importantOption || isSameMajorExclude); + const isAgeRangeActive = minAge !== undefined && maxAge !== undefined; + const hasExtraOption = !!( + importantOption || + isSameMajorExclude || + isAgeRangeActive + ); const canSubmit = !!( selectedMBTI.length === 2 && - selectedAgeGroup && + (selectedAgeGroup || isAgeRangeActive) && selectedFrequency && selectedHobbyCategory && matchingTicketCount > 0 @@ -93,6 +98,12 @@ const ScreenMatching = () => { matchingTicketCount === 0 ? "text-color-gray-600" : "text-black"; const handleAgeRangeSelect = (min: number, max: number) => { + if (selectedAgeGroup) { + alert( + "나이 구간 옵션을 사용하면 기존에 선택한 나이 옵션(연하/동갑/연상)이 해제됩니다.", + ); + setSelectedAgeGroup(""); + } setMinAge(min); setMaxAge(max); }; @@ -161,6 +172,7 @@ const ScreenMatching = () => { Date: Thu, 30 Apr 2026 16:12:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=B4=20=EC=98=B5?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/matching/_components/ScreenMatching.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/matching/_components/ScreenMatching.tsx b/app/matching/_components/ScreenMatching.tsx index 0bbbb7e..5bbf568 100644 --- a/app/matching/_components/ScreenMatching.tsx +++ b/app/matching/_components/ScreenMatching.tsx @@ -136,7 +136,7 @@ const ScreenMatching = () => { const ageInfo = calculateAgeOffsets(selectedAgeGroup); const payload: MatchingRequest = { - ageOption: ageInfo.option, + ageOption: isAgeRangeActive ? undefined : ageInfo.option, mbtiOption: selectedMBTI || undefined, hobbyOption: selectedHobbyCategory ? hobbyMapping[selectedHobbyCategory] From 1673ac7befa325dba53a7ffc90c847623592ce90 Mon Sep 17 00:00:00 2001 From: dasosann Date: Thu, 30 Apr 2026 16:54:33 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/matching/_components/MatchingAgeSection.tsx | 14 ++------------ .../_components/MatchingFrequencySection.tsx | 7 ++----- app/matching/_components/MatchingMBTISection.tsx | 14 +++++--------- app/matching/_components/ScreenMatching.tsx | 6 +++--- 4 files changed, 12 insertions(+), 29 deletions(-) diff --git a/app/matching/_components/MatchingAgeSection.tsx b/app/matching/_components/MatchingAgeSection.tsx index ceb246e..23c7a15 100644 --- a/app/matching/_components/MatchingAgeSection.tsx +++ b/app/matching/_components/MatchingAgeSection.tsx @@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"; interface MatchingAgeSectionProps { onAgeGroupSelect: (ageGroup: string) => void; - defaultValue?: string; + selected: string; disabled?: boolean; } @@ -14,21 +14,11 @@ const AGE_GROUPS = ["연하", "동갑", "연상"]; export default function MatchingAgeSection({ onAgeGroupSelect, - defaultValue = "", + selected, disabled = false, }: MatchingAgeSectionProps) { - const [selected, setSelected] = React.useState(defaultValue); - - // disabled 상태가 되면 선택 초기화 - React.useEffect(() => { - if (disabled && selected) { - setSelected(""); - } - }, [disabled]); - const handleSelect = (group: string) => { if (disabled) return; - setSelected(group); onAgeGroupSelect(group); }; diff --git a/app/matching/_components/MatchingFrequencySection.tsx b/app/matching/_components/MatchingFrequencySection.tsx index 5534ed9..78fca2a 100644 --- a/app/matching/_components/MatchingFrequencySection.tsx +++ b/app/matching/_components/MatchingFrequencySection.tsx @@ -5,19 +5,16 @@ import ProfileButton from "../../profile-builder/_components/ProfileButton"; interface MatchingFrequencySectionProps { onFrequencySelect: (frequency: string) => void; - defaultValue?: string; + selected: string; } const OPTIONS = ["자주", "보통", "적음"]; export default function MatchingFrequencySection({ onFrequencySelect, - defaultValue = "", + selected, }: MatchingFrequencySectionProps) { - const [selected, setSelected] = React.useState(defaultValue); - const handleSelect = (frequency: string) => { - setSelected(frequency); onFrequencySelect(frequency); }; diff --git a/app/matching/_components/MatchingMBTISection.tsx b/app/matching/_components/MatchingMBTISection.tsx index 0acb70f..78f60b3 100644 --- a/app/matching/_components/MatchingMBTISection.tsx +++ b/app/matching/_components/MatchingMBTISection.tsx @@ -5,7 +5,7 @@ import ProfileButton from "../../profile-builder/_components/ProfileButton"; interface MatchingMBTISectionProps { onMBTISelect: (mbti: string) => void; - defaultValue?: string; + selected: string; } const OPPOSITES: Record = { @@ -26,15 +26,12 @@ const ROWS = [ export default function MatchingMBTISection({ onMBTISelect, - defaultValue = "", + selected, }: MatchingMBTISectionProps) { - // 온보딩 MBTI와 느낌을 맞추기 위해 내부 상태(useState) 사용 - const [selected, setSelected] = React.useState( - defaultValue.split("").filter(Boolean), - ); + const selectedChars = selected.split("").filter(Boolean); const handleSelect = (char: string) => { - const newSelection = [...selected]; + const newSelection = [...selectedChars]; const index = newSelection.indexOf(char); if (index > -1) { @@ -66,7 +63,6 @@ export default function MatchingMBTISection({ } } - setSelected(newSelection); onMBTISelect(newSelection.join("")); }; @@ -87,7 +83,7 @@ export default function MatchingMBTISection({ {row.map((char) => ( handleSelect(char)} > {char} diff --git a/app/matching/_components/ScreenMatching.tsx b/app/matching/_components/ScreenMatching.tsx index 5bbf568..7b1a083 100644 --- a/app/matching/_components/ScreenMatching.tsx +++ b/app/matching/_components/ScreenMatching.tsx @@ -161,7 +161,7 @@ const ScreenMatching = () => {
{ Date: Thu, 30 Apr 2026 16:57:30 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feaT:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/matching/_components/AgeRangeDrawer.tsx | 45 ++++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/app/matching/_components/AgeRangeDrawer.tsx b/app/matching/_components/AgeRangeDrawer.tsx index b233b9f..ad3f37c 100644 --- a/app/matching/_components/AgeRangeDrawer.tsx +++ b/app/matching/_components/AgeRangeDrawer.tsx @@ -72,8 +72,13 @@ export default function AgeRangeDrawer({ 나이 구간 설정 - - 닫기 + +

@@ -108,11 +113,19 @@ export default function AgeRangeDrawer({ isStart && !isFirst && "rounded-l-lg border-l", // 선택 끝점 오른쪽 둥글게 isEnd && !isLast && "rounded-r-lg", - // 색상 - isInRange - ? "border-[#ff4d61]/30 bg-gradient-to-b from-[#ff4d61]/20 to-[#ff775e]/10" - : "border-[#e8e8e8] bg-[#f8f8f8]", )} + style={ + isInRange + ? { + borderColor: "rgba(255, 77, 97, 0.3)", + background: + "linear-gradient(to bottom, rgba(255, 77, 97, 0.2), rgba(255, 119, 94, 0.1))", + } + : { + borderColor: "#e8e8e8", + backgroundColor: "#f8f8f8", + } + } /> {/* 나이 레이블 */}

@@ -150,9 +166,12 @@ export default function AgeRangeDrawer({ {localMin} @@ -169,9 +188,12 @@ export default function AgeRangeDrawer({
@@ -179,9 +201,12 @@ export default function AgeRangeDrawer({ {localMax}