diff --git a/app/matching/_components/AgeRangeDrawer.tsx b/app/matching/_components/AgeRangeDrawer.tsx new file mode 100644 index 0000000..ad3f37c --- /dev/null +++ b/app/matching/_components/AgeRangeDrawer.tsx @@ -0,0 +1,227 @@ +"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/MatchingAgeSection.tsx b/app/matching/_components/MatchingAgeSection.tsx index 9d3972b..23c7a15 100644 --- a/app/matching/_components/MatchingAgeSection.tsx +++ b/app/matching/_components/MatchingAgeSection.tsx @@ -2,31 +2,40 @@ import React from "react"; import ProfileButton from "../../profile-builder/_components/ProfileButton"; +import { cn } from "@/lib/utils"; + interface MatchingAgeSectionProps { onAgeGroupSelect: (ageGroup: string) => void; - defaultValue?: string; + selected: string; + disabled?: boolean; } const AGE_GROUPS = ["연하", "동갑", "연상"]; export default function MatchingAgeSection({ onAgeGroupSelect, - defaultValue = "", + selected, + disabled = false, }: MatchingAgeSectionProps) { - const [selected, setSelected] = React.useState(defaultValue); - const handleSelect = (group: string) => { - setSelected(group); + if (disabled) return; onAgeGroupSelect(group); }; return ( -
+

나이

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

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 1c1ecf1..7b1a083 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< @@ -54,42 +57,57 @@ 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 ); - 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) => { + if (selectedAgeGroup) { + alert( + "나이 구간 옵션을 사용하면 기존에 선택한 나이 옵션(연하/동갑/연상)이 해제됩니다.", + ); + setSelectedAgeGroup(""); + } + setMinAge(min); + setMaxAge(max); + }; + const calculateAgeOffsets = ( group: string, ): { @@ -118,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] @@ -128,8 +146,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); @@ -143,7 +161,7 @@ const ScreenMatching = () => {
{ { CONTACT: selectedFrequency || "", }} /> + { + setMinAge(undefined); + setMaxAge(undefined); + }} + minAge={minAge} + maxAge={maxAge} + />