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 (
+
+
+
+ 나이 구간 선택하기
+
+
+ 원하는 나이 범위를 설정해 보세요!
+
+
+
+
+
+ 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 ? (
- "사용할 수 있는 매칭권이 없어요."
- ) : (
-
-
- 매칭권 1
- {hasExtraOption && (
- <>
-
- 아이템 1
- >
- )}
- 소모
-
- );
+ const bubbleText = isItemsLoading ? null : matchingTicketCount === 0 ? (
+ "사용할 수 있는 매칭권이 없어요."
+ ) : (
+
+
+ 매칭권 1
+ {hasExtraOption && (
+ <>
+
+ 아이템 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}
+ />