Skip to content

Commit fb2f7c9

Browse files
authored
Merge pull request #97 from KW-AUTA/fix/94-serachbarFix
2 parents 2eee9b6 + 2f2f102 commit fb2f7c9

4 files changed

Lines changed: 76 additions & 46 deletions

File tree

src/components/layout/page-layout/Header.tsx

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef } from 'react';
1+
import { useState, useRef } from 'react';
22
import { useDispatch } from 'react-redux';
33
import { useNavigate } from 'react-router-dom';
44
import { useUserProfile } from '@/store/queries/user/useUserQueries';
@@ -11,6 +11,7 @@ import { useGetProjectList } from '@/store/queries/project/useProjectQueries';
1111
import { useDebounce } from '@/hooks/useDebounce';
1212
import BellBadge from '@/components/ui/BellBadge';
1313
import { ProjectListData } from '@/types/project.type';
14+
import StatusBadge, { StatusType } from '@/pages/project/_components/StatusBadge';
1415
// import { RootState } from '@/store/redux/store';
1516

1617
export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
@@ -22,6 +23,8 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
2223
const [showNotification, setShowNotification] = useState(false);
2324
const notificationRef = useRef<HTMLDivElement>(null);
2425
const debouncedInputValue = useDebounce(inputValue, 300);
26+
const inputRef = useRef<HTMLInputElement>(null);
27+
// const [dropdownPos, setDropdownPos] = useState({ left: 0, top: 0, width: 0 });
2528
// const projectName = useSelector((state: RootState) => state.searchReducer.projectName);
2629
const { data: suggestedProjects = [] } = useGetProjectList({
2730
projectName: debouncedInputValue,
@@ -38,16 +41,21 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
3841
// { enabled: !!debouncedInputValue }
3942
// );
4043

41-
useEffect(() => {
42-
if (!showNotification) return;
43-
const handleClickOutside = (e: MouseEvent) => {
44-
if (notificationRef.current && !notificationRef.current.contains(e.target as Node)) {
45-
setShowNotification(false);
46-
}
47-
};
48-
document.addEventListener('mousedown', handleClickOutside);
49-
return () => document.removeEventListener('mousedown', handleClickOutside);
50-
}, [showNotification]);
44+
// function updateDropdownPos() {
45+
// if (inputRef.current) {
46+
// const rect = inputRef.current.getBoundingClientRect();
47+
// setDropdownPos({ left: rect.left, top: rect.bottom, width: rect.width });
48+
// }
49+
// }
50+
51+
// useEffect(() => {
52+
// if (!showRecent) return;
53+
// updateDropdownPos();
54+
// window.addEventListener('resize', updateDropdownPos);
55+
// return () => {
56+
// window.removeEventListener('resize', updateDropdownPos);
57+
// };
58+
// }, [showRecent]);
5159

5260
const handleSearch = (keyword?: string) => {
5361
const searchWord = keyword ?? inputValue;
@@ -100,6 +108,7 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
100108
<img src={searchIcon} alt="search button" className="absolute left-4 top-2 w-6 h-6" />
101109
</button>
102110
<input
111+
ref={inputRef}
103112
type="text"
104113
placeholder="프로젝트 검색"
105114
className="w-full rounded-full pl-10 text-base"
@@ -114,7 +123,7 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
114123
/>
115124
{showRecent && (
116125
<div
117-
className="absolute left-0 top-full mt-2 w-full min-w-[180px] max-w-[360px] md:max-w-none bg-white border border-gray-200 rounded-lg shadow-lg p-4 z-50 max-h-60 overflow-auto sm:w-full sm:left-0 sm:right-0 sm:mx-auto"
126+
className="absolute left-0 top-full mt-2 w-full min-w-[180px] max-w-[360px] md:max-w-none bg-white border border-gray-200 rounded-lg shadow-lg p-4 max-h-60 overflow-auto sm:w-full sm:left-0 sm:right-0 sm:mx-auto z-[99999]"
118127
style={{ minHeight: 48 }}>
119128
{inputValue.trim() ? (
120129
suggestedProjects.length > 0 ? (
@@ -130,14 +139,7 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
130139
setTimeout(() => handleSearch(proj.projectName), 0);
131140
}}>
132141
<span>{proj.projectName}</span>
133-
<span
134-
className={
135-
proj.projectStatus === 'COMPLETED'
136-
? 'ml-2 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-700'
137-
: 'ml-2 px-2 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-700'
138-
}>
139-
{proj.projectStatus === 'COMPLETED' ? '완료' : '진행중'}
140-
</span>
142+
<StatusBadge status={proj.projectStatus as StatusType} className="ml-2" />
141143
</li>
142144
))}
143145
</ul>
@@ -161,7 +163,6 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
161163
<div className="text-xs text-gray-400 mb-1">최근 검색어</div>
162164
<ul className="mb-2">
163165
{recentSearches.map((item) => {
164-
// 프로젝트명과 완전 일치하는 프로젝트 찾기
165166
const matched = suggestedProjects.find(
166167
(p: ProjectListData) => p.projectName.toLowerCase() === item.toLowerCase()
167168
);
@@ -177,16 +178,7 @@ export default function Header({ onMenuClick }: { onMenuClick?: () => void }) {
177178
}}>
178179
{item}
179180
</span>
180-
{matched && (
181-
<span
182-
className={
183-
matched.projectStatus === 'COMPLETED'
184-
? 'ml-2 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-700'
185-
: 'ml-2 px-2 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-700'
186-
}>
187-
{matched.projectStatus === 'COMPLETED' ? '완료' : '진행중'}
188-
</span>
189-
)}
181+
{matched && <StatusBadge status={matched.projectStatus as StatusType} className="ml-2" />}
190182
<button
191183
className="ml-2 text-gray-400 hover:text-red-400 opacity-70 group-hover:opacity-100 transition"
192184
onMouseDown={(e) => {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createPortal } from 'react-dom';
2+
import { ReactNode } from 'react';
3+
4+
export default function Portal({ children }: { children: ReactNode }) {
5+
if (typeof window === 'undefined') return null;
6+
const el = document.getElementById('portal-root') || document.body;
7+
return createPortal(children, el);
8+
}

src/components/ui/input/Input.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeEvent, InputHTMLAttributes, useId } from 'react';
1+
import { forwardRef, ChangeEvent, InputHTMLAttributes, useId } from 'react';
22

33
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
44
label?: string;
@@ -10,17 +10,10 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
1010
onEnterPress?: () => void;
1111
}
1212

13-
export default function Input({
14-
label,
15-
name,
16-
value,
17-
className = '',
18-
labelClassName = '',
19-
required = false,
20-
onChange,
21-
onEnterPress,
22-
...props
23-
}: InputProps) {
13+
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
14+
{ label, name, value, className = '', labelClassName = '', required = false, onChange, onEnterPress, ...props },
15+
ref
16+
) {
2417
const inputId = useId();
2518

2619
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -38,6 +31,7 @@ export default function Input({
3831
</label>
3932
)}
4033
<input
34+
ref={ref}
4135
id={inputId}
4236
type="text"
4337
className={`w-full bg-background border-[0.5px] border-typography-gray rounded-15 px-4 py-3 placeholder:text-typography-gray focus:border-none focus:shadow-custom focus:outline-none ${className}`}
@@ -50,4 +44,6 @@ export default function Input({
5044
/>
5145
</div>
5246
);
53-
}
47+
});
48+
49+
export default Input;

src/pages/test/_components/searchHeader/SearchHeader.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { KeyboardEvent } from 'react';
1+
import { KeyboardEvent, useRef, useState, useEffect } from 'react';
22
import Input from '@/components/ui/input/Input';
33
import Select from '@/components/ui/select/Select';
44
import Button from '@/components/ui/button/Button';
@@ -29,6 +29,31 @@ export default function SearchHeader({
2929
onNameSortChange,
3030
onDateSortChange
3131
}: SearchHeaderProps) {
32+
const inputRef = useRef<HTMLInputElement>(null);
33+
const [showRecent, setShowRecent] = useState(false);
34+
35+
const handleInputFocus = () => {
36+
setShowRecent(true);
37+
// 최근 검색어 불러오기 등 추가 로직
38+
};
39+
40+
useEffect(() => {
41+
if (!showRecent) return;
42+
function handleClickOutside(e: MouseEvent | TouchEvent) {
43+
const input = inputRef.current;
44+
const dropdown = document.getElementById('search-dropdown');
45+
if (input && !input.contains(e.target as Node) && dropdown && !dropdown.contains(e.target as Node)) {
46+
setShowRecent(false);
47+
}
48+
}
49+
window.addEventListener('mousedown', handleClickOutside);
50+
window.addEventListener('touchstart', handleClickOutside);
51+
return () => {
52+
window.removeEventListener('mousedown', handleClickOutside);
53+
window.removeEventListener('touchstart', handleClickOutside);
54+
};
55+
}, [showRecent]);
56+
3257
return (
3358
<section className="flex items-center justify-between w-full gap-4 pt-5 pb-9">
3459
<div className="relative flex-1 max-w-[510px] min-w-0">
@@ -39,6 +64,7 @@ export default function SearchHeader({
3964
onClick={onSearch}
4065
/>
4166
<Input
67+
ref={inputRef}
4268
type="text"
4369
placeholder="프로젝트 검색"
4470
className="w-full max-h-[35px] rounded-20 pl-10 border-[0.5px] border-typography-gray max-md:placeholder:text-[12px]"
@@ -49,9 +75,17 @@ export default function SearchHeader({
4975
onSearch();
5076
}
5177
}}
78+
onFocus={handleInputFocus}
5279
/>
80+
{showRecent && (
81+
<div
82+
id="search-dropdown"
83+
className="absolute left-0 top-full mt-2 w-full min-w-[180px] max-w-[510px] md:max-w-none bg-white border border-gray-200 rounded-lg shadow-lg p-4 max-h-60 overflow-auto z-[9999]"
84+
style={{ minHeight: 48 }}>
85+
{/* 드롭다운 내용 */}
86+
</div>
87+
)}
5388
</div>
54-
5589
<div className="flex gap-2">
5690
<Button
5791
leftIcon={<ResetIcon className="transition-transform duration-500 ease-out group-hover:rotate-90" />}

0 commit comments

Comments
 (0)