diff --git a/src/app/(pages)/albaList/page.tsx b/src/app/(pages)/albaList/page.tsx index 1f7ea4aa..8f1e70b1 100644 --- a/src/app/(pages)/albaList/page.tsx +++ b/src/app/(pages)/albaList/page.tsx @@ -108,7 +108,7 @@ export default function AlbaList() {
- +
diff --git a/src/app/(pages)/albaTalk/page.tsx b/src/app/(pages)/albaTalk/page.tsx index 15f93ca0..ce7ab31a 100644 --- a/src/app/(pages)/albaTalk/page.tsx +++ b/src/app/(pages)/albaTalk/page.tsx @@ -74,7 +74,7 @@ export default function AlbaTalk() {
- +
diff --git a/src/app/(pages)/myAlbaform/(role)/applicant/page.tsx b/src/app/(pages)/myAlbaform/(role)/applicant/page.tsx index 06ddb049..b2708b2f 100644 --- a/src/app/(pages)/myAlbaform/(role)/applicant/page.tsx +++ b/src/app/(pages)/myAlbaform/(role)/applicant/page.tsx @@ -87,7 +87,7 @@ export default function ApplicantPage() { {/* 검색 섹션 */}
- +
diff --git a/src/app/(pages)/myAlbaform/(role)/owner/page.tsx b/src/app/(pages)/myAlbaform/(role)/owner/page.tsx index 0f5f15f8..3b378949 100644 --- a/src/app/(pages)/myAlbaform/(role)/owner/page.tsx +++ b/src/app/(pages)/myAlbaform/(role)/owner/page.tsx @@ -158,7 +158,7 @@ export default function AlbaList() {
- +
diff --git a/src/app/components/button/dropdown/DropdownList.tsx b/src/app/components/button/dropdown/DropdownList.tsx new file mode 100644 index 00000000..707a79b8 --- /dev/null +++ b/src/app/components/button/dropdown/DropdownList.tsx @@ -0,0 +1,90 @@ +"use client"; +import { cn } from "@/lib/tailwindUtil"; +import { useEffect, useRef } from "react"; + +// 드롭다운 메뉴의 각 항목 컴포넌트 +const DropdownItem = ({ + item, + onSelect, + itemStyle, +}: { + item: string; + onSelect: (item: string | null) => void; + itemStyle?: string; +}) => { + // 항목 클릭 핸들러 + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); // 이벤트 버블링 방지 + onSelect(item); + }; + + return ( +
  • + {item} +
  • + ); +}; + +// 드롭다운 메뉴 리스트 컴포넌트 +const DropdownList = ({ + list, + onSelect, + wrapperStyle, + itemStyle, +}: { + list: string[]; + onSelect: (item: string | null) => void; + wrapperStyle?: string; + itemStyle?: string; +}) => { + const dropdownRef = useRef(null); + + // 외부 클릭 감지 및 드롭다운 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + event.preventDefault(); + onSelect(null); + } + }; + + document.addEventListener("mousedown", handleClickOutside, { capture: true }); + return () => { + document.removeEventListener("mousedown", handleClickOutside, { capture: true }); + }; + }, [onSelect]); + + // 컨테이너 클릭 이벤트 처리 + const handleContainerClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( +
    + +
    + ); +}; + +export default DropdownList; diff --git a/src/app/components/button/dropdown/FilterDropdown.tsx b/src/app/components/button/dropdown/FilterDropdown.tsx index 28e723ce..38cc9780 100644 --- a/src/app/components/button/dropdown/FilterDropdown.tsx +++ b/src/app/components/button/dropdown/FilterDropdown.tsx @@ -12,11 +12,14 @@ interface FilterDropdownProps { readOnly?: boolean; } +// 필터링 옵션을 선택할 수 있는 드롭다운 컴포넌트 const FilterDropdown = ({ options, className = "", onChange, initialValue, readOnly = false }: FilterDropdownProps) => { + // 드롭다운 상태 관리 const [isOpen, setIsOpen] = useState(false); const [selectedLabel, setSelectedLabel] = useState(initialValue || options[0]); const dropdownRef = useRef(null); + // 외부 클릭 감지하여 드롭다운 닫기 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -30,21 +33,28 @@ const FilterDropdown = ({ options, className = "", onChange, initialValue, readO }; }, []); + // initialValue가 변경되면 선택된 라벨 업데이트 useEffect(() => { if (initialValue) { setSelectedLabel(initialValue); } }, [initialValue]); + // 드롭다운 토글 핸들러 const toggleDropdown = () => { if (readOnly) return; setIsOpen((prev) => !prev); }; - const handleSelect = (option: string) => { - setSelectedLabel(option); - setIsOpen(false); - onChange?.(option); + // 옵션 선택 핸들러 + const handleSelect = (option: string | null) => { + if (option !== null) { + setSelectedLabel(option); + setIsOpen(false); + onChange?.(option); + } else { + setIsOpen(false); + } }; return ( @@ -66,7 +76,10 @@ const FilterDropdown = ({ options, className = "", onChange, initialValue, readO ? "border border-grayscale-100 bg-white" : "border-primary-orange-300 bg-primary-orange-50" )} - onClick={toggleDropdown} + onClick={(e) => { + e.preventDefault(); + toggleDropdown(); + }} disabled={readOnly} > diff --git a/src/app/components/button/dropdown/InputDropdown.tsx b/src/app/components/button/dropdown/InputDropdown.tsx index 5afb3f41..8bbce79b 100644 --- a/src/app/components/button/dropdown/InputDropdown.tsx +++ b/src/app/components/button/dropdown/InputDropdown.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { forwardRef, useEffect, useState } from "react"; +import React, { forwardRef, useEffect, useState, useRef, useCallback } from "react"; import { IoMdArrowDropdown } from "react-icons/io"; import { cn } from "@/lib/tailwindUtil"; import DropdownList from "./dropdownComponent/DropdownList"; @@ -13,32 +13,88 @@ interface InputDropdownProps { value?: string; } +// 직접 입력이 가능한 드롭다운 컴포넌트 const InputDropdown = forwardRef( ({ options, className = "", errormessage, name }, ref) => { + // 드롭다운 상태 관리 const [isOpen, setIsOpen] = useState(false); const [selectedValue, setSelectedValue] = useState(""); const [isCustomInput, setIsCustomInput] = useState(false); const { setValue, watch } = useFormContext(); + const dropdownRef = useRef(null); + + // 외부 클릭 감지하여 드롭다운 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + // 드롭다운 토글 핸들러 + const toggleDropdown = (e: React.MouseEvent) => { + e.preventDefault(); + setIsOpen((prev) => !prev); + }; + + // 옵션 선택 핸들러 + const handleSelect = useCallback( + (option: string | null) => { + if (!option) { + setIsOpen(false); + return; + } + + if (option === "직접 입력") { + setIsCustomInput(true); + setSelectedValue(""); + setValue(name, "", { shouldDirty: true }); + setIsOpen(false); + return; + } - const handleOptionClick = (option: string) => { - if (option === "직접 입력") { - setIsCustomInput(true); - setSelectedValue(""); - // 동적으로 받아온 name에 값 할당 -> 훅폼에 저장 - setValue(name, selectedValue, { shouldDirty: true }); - } else { setSelectedValue(option); setIsCustomInput(false); setValue(name, option, { shouldDirty: true }); setIsOpen(false); - } - }; + }, + [name, setValue] + ); - // 작성중인 탭으로 다시 이동했을때 이전에 저장된 훅폼 데이터 연동 + // 입력값 변경 핸들러 + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + if (isCustomInput) { + const value = e.target.value; + setSelectedValue(value); + setValue(name, value, { shouldDirty: true }); + } + }, + [isCustomInput, name, setValue] + ); + + // 입력 필드 클릭 핸들러 + const handleInputClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (!isCustomInput) { + setIsOpen(true); + } + }, + [isCustomInput] + ); + + // 폼 값 변경 감지하여 입력값 동기화 useEffect(() => { - const value = watch(name); // 동적으로 필드 값 가져오기 + const value = watch(name); if (value !== undefined) { - setSelectedValue(value); // 초기값 동기화 + setSelectedValue(value); } }, [name, watch]); @@ -49,12 +105,15 @@ const InputDropdown = forwardRef( "absolute -bottom-[26px] text-[13px] text-sm font-medium leading-[22px] text-state-error lg:text-base lg:leading-[26px]"; return ( -
    +
    setIsOpen(!isOpen)} + onClick={isCustomInput ? undefined : toggleDropdown} className={cn( - "cursor-pointer rounded-md border border-transparent bg-background-200 p-2", - "hover:border-grayscale-200 hover:bg-background-300", + "rounded-md border border-transparent bg-background-200 p-2", + !isCustomInput && "cursor-pointer hover:border-grayscale-200 hover:bg-background-300", isOpen && "ring-1 ring-grayscale-300" )} > @@ -62,23 +121,32 @@ const InputDropdown = forwardRef( type="text" ref={ref} value={selectedValue} - onChange={(e) => { - if (isCustomInput) { - setSelectedValue(e.target.value); - setValue(name, e.target.value); - } - }} + onChange={handleInputChange} + onClick={handleInputClick} + readOnly={!isCustomInput} className={cn( - "text-grayscale-700 flex w-full items-center justify-between px-4 py-2 font-medium focus:outline-none", - "cursor-pointer bg-transparent" + "flex w-full items-center justify-between px-4 py-2 font-medium focus:outline-none", + isCustomInput ? "cursor-text bg-transparent" : "cursor-pointer bg-transparent", + "text-grayscale-700" )} placeholder={isCustomInput ? "직접 입력하세요" : "선택"} /> -
    - {isOpen && } + {isOpen && } {errormessage &&

    {errormessage}

    }
    ); diff --git a/src/app/components/button/dropdown/dropdownComponent/DropdownList.tsx b/src/app/components/button/dropdown/dropdownComponent/DropdownList.tsx index 2ff46193..94b36876 100644 --- a/src/app/components/button/dropdown/dropdownComponent/DropdownList.tsx +++ b/src/app/components/button/dropdown/dropdownComponent/DropdownList.tsx @@ -8,12 +8,18 @@ const DropdownItem = ({ itemStyle, }: { item: string; - onSelect: (item: string) => void; + onSelect: (item: string | null) => void; itemStyle?: string; }) => { + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onSelect(item); + }; + return (
  • onSelect(item)} + onClick={handleClick} className={cn( "flex w-full cursor-pointer bg-grayscale-50 px-[10px] py-2 text-sm font-normal leading-[18px] text-black-100 hover:bg-primary-orange-50 lg:text-lg lg:leading-[26px]", itemStyle @@ -23,6 +29,7 @@ const DropdownItem = ({
  • ); }; + const DropdownList = ({ list, onSelect, @@ -30,7 +37,7 @@ const DropdownList = ({ itemStyle, }: { list: string[]; - onSelect: (item: string) => void; + onSelect: (item: string | null) => void; wrapperStyle?: string; itemStyle?: string; }) => { @@ -39,15 +46,20 @@ const DropdownList = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - onSelect(""); + event.preventDefault(); + onSelect(null); } }; - document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("mousedown", handleClickOutside, { capture: true }); return () => { - document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside, { capture: true }); }; - }, []); + }, [onSelect]); + + const handleContainerClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; return (
    ((props, ref) => { const { value, onChange, errormessage } = props; + const dropdownRef = useRef(null); + const { isOpen, handleOpenDropdown, setIsOpen } = useDropdownOpen(); - const handleTimeSelect = (time: string) => { - if (onChange) { - onChange({ target: { value: time } } as React.ChangeEvent); - } - handleOpenDropdown(); - }; - const { isOpen, handleOpenDropdown } = useDropdownOpen(); - const beforeIconStyle = "text-grayscale-400 size-5 lg:size-8"; - const afterIconStyle = - "text-black-400 size-6 lg:size-9 transition-all transition-transform duration-200 ease-in-out"; - const width = "w-[150px] lg:w-[210px]"; + // 외부 클릭 감지하여 드롭다운 닫기 + const handleClickOutside = useCallback( + (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }, + [setIsOpen] + ); + + // 외부 클릭 이벤트 리스너 등록 + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [handleClickOutside]); + + // 시간 선택 핸들러 + const handleSelect = useCallback( + (time: string | null) => { + if (!time) { + setIsOpen(false); + return; + } + + if (onChange) { + const event = { + target: { value: time, name: props.name }, + } as React.ChangeEvent; + onChange(event); + } + + setIsOpen(false); + }, + [onChange, props.name, setIsOpen] + ); + // 24시간 형식의 시간 옵션 생성 (00:00 ~ 23:00) const timeOption = Array.from({ length: 24 }, (_, index) => { const hour = index.toString().padStart(2, "0"); return `${hour}:00`; }); + // 클릭 이벤트 핸들러 + const handleClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + handleOpenDropdown(); + }, + [handleOpenDropdown] + ); + return ( -
    -
    {isOpen}
    - } - afterIcon={} - errormessage={errormessage} - /> +
    +
    + } + afterIcon={ + + } + errormessage={errormessage} + /> +
    {isOpen && ( )}
    ); }); + TimePickerInput.displayName = "TimePickerInput"; export default TimePickerInput; diff --git a/src/app/components/layout/forms/SearchSection.tsx b/src/app/components/layout/forms/SearchSection.tsx index 77ed9476..332bda1b 100644 --- a/src/app/components/layout/forms/SearchSection.tsx +++ b/src/app/components/layout/forms/SearchSection.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import SearchInput from "@/app/components/input/text/SearchInput"; import Button from "../../button/default/Button"; -export default function SearchSection() { +export default function SearchSection({ pathname }: { pathname: string }) { const router = useRouter(); const searchParams = useSearchParams(); const [keyword, setKeyword] = useState(searchParams.get("keyword") || ""); @@ -20,7 +20,7 @@ export default function SearchSection() { params.delete("keyword"); } - router.push(`/albalist?${params.toString()}`); + router.push(`${pathname}?${params.toString()}`); }; return ( diff --git a/src/hooks/useDropdownOpen.ts b/src/hooks/useDropdownOpen.ts index c6a08a0a..f0f9e291 100644 --- a/src/hooks/useDropdownOpen.ts +++ b/src/hooks/useDropdownOpen.ts @@ -1,13 +1,15 @@ "use client"; -import { useState } from "react"; +import { useState, useCallback } from "react"; +// 드롭다운 메뉴의 열림/닫힘 상태를 관리하는 커스텀 훅 export const useDropdownOpen = () => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); - const handleOpenDropdown = (): void => { - setIsOpen(!isOpen); - }; + // 드롭다운 메뉴 토글 핸들러 + const handleOpenDropdown = useCallback(() => { + setIsOpen((prev) => !prev); + }, []); - return { isOpen, handleOpenDropdown }; + return { isOpen, setIsOpen, handleOpenDropdown }; };