Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions component/src/stories/ChatBubble/HeaderSearchMenu.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react';

import HeaderSearchMenu from './HeaderSearchMenu';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
title: 'Example/HeaderSearchMenu',
component: HeaderSearchMenu,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
} satisfies Meta<typeof HeaderSearchMenu>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const headerSearchMenu: Story = {
args: {
onSearchResult: () => {},
onSetSearchResult: () => {},
onSelectedSearchIndex: 0, // 초기값 설정
onSetSearchValue: () => {},
onSetGoToanotherPage: () => {},
},
};
115 changes: 115 additions & 0 deletions component/src/stories/ChatBubble/HeaderSearchMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import LanguageIcon from '@mui/icons-material/Language';
import CallMadeIcon from '@mui/icons-material/CallMade';
import { isSearchClickedState } from '@monorepo/service/src/recoil/atom/isSearchClickedState';
import { useSetRecoilState } from 'recoil';
import React, { useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import SearchIcon from '@mui/icons-material/Search';
import CloseIcon from '@mui/icons-material/Close';
import {
getSearchListStorage,
saveSearchListStorage,
} from '@monorepo/service/src/repository/SearchListRepository';

interface ISearchProps {
onSearchResult: any;
onSetSearchResult: React.Dispatch<React.SetStateAction<string[]>>;

onSetSearchValue: React.Dispatch<React.SetStateAction<string>>;
onSelectedSearchIndex: number;
}

export default function HeaderSearchMenu({
onSearchResult,
onSetSearchResult,
onSelectedSearchIndex,
onSetSearchValue,
}: ISearchProps) {
const setIsSearchClicked = useSetRecoilState(isSearchClickedState);
const searchBoxRef = useRef<HTMLDivElement>(null);
const KEY = 'search';

const handleCloseSearchResult = (num: number) => {
const getRecentSearchesData = getSearchListStorage(KEY);
onSetSearchValue('');
const filterRecentSearchesData = getRecentSearchesData.filter(
(e: string, index: number) => index !== num,
);
onSetSearchResult(filterRecentSearchesData);
saveSearchListStorage(KEY, filterRecentSearchesData);
};

useEffect(() => {
// 검색 상자가 표시되면 외부 클릭을 감지하여 상자를 숨깁니다.
const handleClickOutside = (event: MouseEvent) => {
if (
searchBoxRef.current &&
!searchBoxRef.current.contains(event.target as HTMLElement)
) {
setIsSearchClicked(false);
}
};

// 페이지가 로드될 때 이벤트 리스너를 추가합니다.
document.addEventListener('mousedown', handleClickOutside);

// 컴포넌트가 언마운트될 때 이벤트 리스너를 제거합니다.
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

return (
<div className="absolute z-10 w-80 mt-2" ref={searchBoxRef}>
<div className="relative flex-1 p-2 mb-2 text-black bg-white shadow-xl rounded-lg">
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col w-full p-2">
<h1 className="p-2 text-sm text-[#6B6B6B]">RECENT SEARCHES</h1>
<hr />
{onSearchResult.length !== 0 &&
onSearchResult?.map((value: string, index: number) => (
<div
key={index}
className={`flex items-center w-full gap-4 bg-opacity-20 border-b-2
${index === onSelectedSearchIndex ? 'bg-gray-400' : ''}`}
>
<Link
to={`/search/${value}`}
className="flex items-center w-full gap-2 p-4 text-black hover:text-black"
onClick={() => onSetSearchValue(value)}
>
<p className="flex items-center w-full gap-4">
<SearchIcon />
{value}
</p>
</Link>
<div className="flex justify-end w-full mr-4">
<CloseIcon
sx={{ opacity: '50%' }}
key={index}
onClick={e => {
e.preventDefault();
handleCloseSearchResult(index);
}}
/>
</div>
</div>
))}

<Link
to="/Exploretopics"
className={`flex items-center gap-2 p-4 text-black hover:text-black `}
>
<p className="flex items-center w-full gap-4 text-sm">
<LanguageIcon />
Explore topics
</p>
<CallMadeIcon />
</Link>
</div>
</div>
<div className="absolute left-5 w-3 h-3 transform rotate-45 -translate-x-1/2 bg-white top-[-5px]"></div>
</div>
</div>
);
}
6 changes: 5 additions & 1 deletion component/src/stories/header/Header.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const header: Story = {};
export const header: Story = {
args: {
authToken: '1111',
},
};
148 changes: 120 additions & 28 deletions component/src/stories/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
/* eslint-disable react-hooks/rules-of-hooks */
import styled from '@emotion/styled';
import { TextField, IconButton, Avatar, Button } from '@mui/material';
import {Avatar, Button, IconButton, TextField} from '@mui/material';
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
import { Login } from '@monorepo/component/src/stories/login/Login';
import { useDarkMode } from '@monorepo/service/src/ThemeContext/ThemeProvider';
import { DarkModeOutlined, LightModeOutlined } from '@mui/icons-material';
import { removeAuthStorage } from '@monorepo/service/src/repository/AuthRepository';
import {
getProfileImgStorage,
removeProfileImgStorage,
} from '@monorepo/service/src/repository/ProfileimgRepository';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { profileModalOpen } from '@monorepo/service/src/recoil/atom/profileModalOpen';
import { isLoggedInState } from '@monorepo/service/src/recoil/atom/isLoggedInState';
import {Login} from '@monorepo/component/src/stories/login/Login';
import {useDarkMode} from '@monorepo/service/src/ThemeContext/ThemeProvider';
import {DarkModeOutlined, LightModeOutlined} from '@mui/icons-material';
import {getAuthStorage, removeAuthStorage} from '@monorepo/service/src/repository/AuthRepository';
import {getProfileImgStorage, removeProfileImgStorage,} from '@monorepo/service/src/repository/ProfileimgRepository';
import {useRecoilState, useRecoilValue, useSetRecoilState} from 'recoil';
import {profileModalOpen} from '@monorepo/service/src/recoil/atom/profileModalOpen';
import {isLoggedInState} from '@monorepo/service/src/recoil/atom/isLoggedInState';
import darkLogo from '@monorepo/service/src/assets/darkLogo.webp';
import lightLogo from '@monorepo/service/src/assets/lightLogo.webp';
import { useEffect } from 'react';
import { getAuthStorage } from '@monorepo/service/src/repository/AuthRepository';
import HeaderSearchMenu from '../ChatBubble/HeaderSearchMenu';
import {isSearchClickedState} from '@monorepo/service/src/recoil/atom/isSearchClickedState';
import {useEffect, useState} from 'react';
import {getHeaderKeywordSearch} from '@monorepo/service/src/service/HeaderSearchService';
import {HeaderSearchDataState} from '@monorepo/service/src/recoil/atom/HeaderSearchDataState';
import {PageState} from '@monorepo/service/src/recoil/atom/PageState';
import {Link, useNavigate} from 'react-router-dom';
import {getSearchListStorage} from '@monorepo/service/src/repository/SearchListRepository';
import useHandleKeyPress from '@monorepo/service/src/hooks/useHandleKeyPress';

const InputTextField = styled(TextField)({
'& label': {
Expand Down Expand Up @@ -49,6 +54,8 @@ interface IHandleProps {

function AuthHeader({ onLogout }: IHandleProps) {
const [profileOpen, setProfileOpen] = useRecoilState(profileModalOpen);
const setIsSearchClicked = useSetRecoilState(isSearchClickedState);

const KEY = 'imgUrl';
const img = getProfileImgStorage(KEY);
let imgUrl;
Expand All @@ -60,6 +67,7 @@ function AuthHeader({ onLogout }: IHandleProps) {
//위 코드가 없을 경우 header 영역 밖을 클릭할 경우의 코드를 넣으면 profilediv 영역이 나타나지 않는다.
e.stopPropagation();
setProfileOpen(!profileOpen);
setIsSearchClicked(false);
};

return (
Expand Down Expand Up @@ -87,12 +95,20 @@ function AuthHeader({ onLogout }: IHandleProps) {
}

export default function Header() {
const [isSearchClicked, setIsSearchClicked] = useRecoilState(isSearchClickedState);
const [searchValue, setSearchValue] = useState('');
const [getSearchLocalResult, setGetSearchLocalResult] = useState<any[]>([]);
const [selectedSearchIndex, setSelectedSearchIndex] = useState<number>(-1);
const setTechBlogSearchData = useSetRecoilState(HeaderSearchDataState);
const { darkMode, toggleDarkMode } = useDarkMode();
const page = useRecoilValue(PageState);
const setProfileOpen = useSetRecoilState(profileModalOpen);
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedInState);
const TOKEN_KEY = 'accessToken';
const token = getAuthStorage(TOKEN_KEY);

const KEY = 'search';
const size = 10;
const navigate = useNavigate();
const handleModalClose = () => {
setProfileOpen(false);
};
Expand All @@ -104,34 +120,110 @@ export default function Header() {
setLoggedIn(false);
};

const searchItem = getSearchListStorage(KEY);

async function getKeywordSerchRender() {
const keywordSearchData = await getHeaderKeywordSearch({
page,
size,
searchValue,
});
setTechBlogSearchData(prev => [...prev, ...keywordSearchData.content]);
}

const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
const value = (e.target as HTMLInputElement).value;
const key = e.key;
if (value !== '' && searchValue !== '') {
if (e.key === 'Enter') {
// 엔터 키를 눌렀을 때 실행할 동작
setTechBlogSearchData([]);
getKeywordSerchRender();
setGetSearchLocalResult(prev => {
const uniqueValuesSet = new Set([...prev, value]);
const uniqueValuesArray = Array.from(uniqueValuesSet);
return uniqueValuesArray;
});
setIsSearchClicked(false);
navigate(`/search/${searchValue}`);
}
}

useHandleKeyPress({
key,
e,
getSearchLocalResult,
setSearchValue,
selectedSearchIndex,
setSelectedSearchIndex,
});
};
const handleInputClicked = (e: React.MouseEvent<HTMLDivElement>) => {
setIsSearchClicked(!isSearchClicked);

setSearchValue((e.target as HTMLInputElement).value);
};

const handleSearchValue = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value;
setSearchValue(value);
};

useEffect(() => {
if (token) {
setLoggedIn(true);
}
}, [token]);

useEffect(() => {
getKeywordSerchRender();
}, [page]);

useEffect(() => {
setGetSearchLocalResult(searchItem);
}, []);

useEffect(() => {
if (selectedSearchIndex === -1) setSearchValue('');
}, [selectedSearchIndex]);
return (
<header className={`w-full flex justify-center`}>
<header className={`w-full flex justify-center `}>
<div
className="flex justify-between w-full max-w-screen-xl py-4"
onClick={handleModalClose}
>
<div className="flex items-center">
<div className="mr-4 none">
{darkMode === 'light' ? (
<img src={lightLogo} alt="로고" />
) : (
<img src={darkLogo} alt="로고" />
<Link to="/">
{darkMode === 'light' ? (
<img src={lightLogo} alt="로고" />
) : (
<img src={darkLogo} alt="로고" />
)}
</Link>
</div>
<div>
<InputTextField
type="text"
className="relative max-w-sm w-80"
variant="outlined"
label="검색"
aria-label="검색"
onClick={e => handleInputClicked(e)}
onKeyDown={e => handleKeyPress(e)}
onChange={e => handleSearchValue(e)}
autoComplete="off"
value={searchValue}
/>
{isSearchClicked && (
<HeaderSearchMenu
onSearchResult={getSearchLocalResult}
onSetSearchResult={setGetSearchLocalResult}
onSetSearchValue={setSearchValue}
onSelectedSearchIndex={selectedSearchIndex}
/>
)}
</div>
<InputTextField
type="text"
variant="outlined"
aria-label="검색"
sx={{ width: '18rem' }}
placeholder="검색"
autoComplete="off"
/>
</div>
<div className="flex items-center gap-1">
<IconButton onClick={toggleDarkMode} size="large" color="inherit">
Expand Down
2 changes: 1 addition & 1 deletion component/src/stories/listbox/Listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IListBoxProps } from './type';
export default function ListBox({ items, onCategoryId, onFilterItems }: IListBoxProps) {
return (
<>
{onCategoryId === 0 ? (
{onCategoryId === 0 || onCategoryId === undefined ? (
<div className="flex flex-col w-full gap-6 pr-10">
{items.map((item: any) => (
<ListboxItem key={item.id} item={item} />
Expand Down
2 changes: 2 additions & 0 deletions component/src/stories/listbox/ListboxItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ItemProps } from './type';

export default function ListboxItem({ item }: ItemProps) {
console.log(item);

return (
<>
<div
Expand Down
1 change: 1 addition & 0 deletions service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"cross-env": "^7.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-window": "^1.8.10",
"recoil": "^0.7.7"
},
"devDependencies": {
Expand Down
Loading