diff --git a/package-lock.json b/package-lock.json index c9540ef..557b8ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tanstack/react-query": "^5.59.8", "@tanstack/react-query-devtools": "^5.59.8", "axios": "^1.7.7", + "es-hangul": "^2.2.1", "jotai": "^2.10.0", "overlay-kit": "^1.4.1", "react": "^18.3.1", @@ -4441,6 +4442,17 @@ "node": ">= 0.4" } }, + "node_modules/es-hangul": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/es-hangul/-/es-hangul-2.2.1.tgz", + "integrity": "sha512-Ra5msEOzRdUBoKsdnkzGthstjehalXJAoG4EBZd2JsYErxiX3I7qQZNigbhowLhwDEt3qHcBCJp6PiOdp99yZw==", + "license": "MIT", + "workspaces": [ + ".", + "docs", + "benchmarks" + ] + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", diff --git a/package.json b/package.json index be2e676..881270e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@tanstack/react-query": "^5.59.8", "@tanstack/react-query-devtools": "^5.59.8", "axios": "^1.7.7", + "es-hangul": "^2.2.1", "jotai": "^2.10.0", "overlay-kit": "^1.4.1", "react": "^18.3.1", diff --git a/src/App.tsx b/src/App.tsx index 20ab36c..fde423f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,8 +9,8 @@ function App() { return ( - + diff --git a/src/features/search/data/professors.json b/src/features/search/data/professors.json new file mode 100644 index 0000000..ddd09c1 --- /dev/null +++ b/src/features/search/data/professors.json @@ -0,0 +1,65 @@ +[ + { + "name": "권순일", + "labLocation": "대양AI 624호", + "phoneNumber": "02-3408-3847", + "email": "sikwon@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "name": "백성욱", + "labLocation": "대양AI 622호", + "phoneNumber": "02-3408-3797", + "email": "sbaik@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "name": "이종원", + "labLocation": "대양AI 619호", + "phoneNumber": "02-3408-3798", + "email": "jwlee@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "name": "송오영", + "labLocation": "대양AI 625호", + "phoneNumber": "02-3408-3830", + "email": "oysong@sejong.edu", + "department": "소프트웨어학과" + }, + { + "name": "최준연", + "labLocation": "대양AI 620호", + "phoneNumber": "02-3408-3887", + "email": "zoon@sejong.edu", + "department": "소프트웨어학과" + }, + { + "name": "박상일", + "labLocation": "대양AI 626호", + "phoneNumber": "02-3408-3832", + "email": "sipark@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "name": "변재욱", + "labLocation": "대양AI 604호", + "phoneNumber": "02-3408-1847", + "email": "jwbyun@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "name": "이은상", + "labLocation": "대양AI 621호", + "phoneNumber": "02-3408-2975", + "email": "eslee3209@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "name": "정승화", + "labLocation": "대양AI 623호", + "phoneNumber": "02-3408-3795", + "email": "seunghwajeong@sejong.ac.kr", + "department": "소프트웨어학과" + } +] diff --git a/src/features/search/data/total.json b/src/features/search/data/total.json new file mode 100644 index 0000000..be5f1a1 --- /dev/null +++ b/src/features/search/data/total.json @@ -0,0 +1,95 @@ +{ + "professor": [ + { + "id": 0, + "name": "권순일", + "labLocation": "대양AI 624호", + "phoneNumber": "02-3408-3847", + "email": "sikwon@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "id": 1, + "name": "백성욱", + "labLocation": "대양AI 622호", + "phoneNumber": "02-3408-3797", + "email": "sbaik@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "id": 2, + "name": "이종원", + "labLocation": "대양AI 619호", + "phoneNumber": "02-3408-3798", + "email": "jwlee@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "id": 3, + "name": "송오영", + "labLocation": "대양AI 625호", + "phoneNumber": "02-3408-3830", + "email": "oysong@sejong.edu", + "department": "소프트웨어학과" + }, + { + "id": 4, + "name": "최준연", + "labLocation": "대양AI 620호", + "phoneNumber": "02-3408-3887", + "email": "zoon@sejong.edu", + "department": "소프트웨어학과" + }, + { + "id": 5, + "name": "박상일", + "labLocation": "대양AI 626호", + "phoneNumber": "02-3408-3832", + "email": "sipark@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "id": 6, + "name": "변재욱", + "labLocation": "대양AI 604호", + "phoneNumber": "02-3408-1847", + "email": "jwbyun@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "id": 7, + "name": "이은상", + "labLocation": "대양AI 621호", + "phoneNumber": "02-3408-2975", + "email": "eslee3209@sejong.ac.kr", + "department": "소프트웨어학과" + }, + { + "id": 8, + "name": "정승화", + "labLocation": "대양AI 623호", + "phoneNumber": "02-3408-3795", + "email": "seunghwajeong@sejong.ac.kr", + "department": "소프트웨어학과" + } + ], + "sites": [ + { + "id": 0, + "type": 1, + "name": "학사정보시스템", + "description": "", + "url": "https://sjpt.sejong.ac.kr", + "keywords" : ["수강신청", "등록금"] + }, + + { + "id": 1, + "type": 0, + "name": "Tutorial Sejong", + "description": "수강신청 연습 사이트", + "url": "https://tutorial-sejong.com", + "keywords" : ["수강신청", "연습"] + } + ] +} \ No newline at end of file diff --git a/src/features/search/logo/logo_0.svg b/src/features/search/logo/logo_0.svg new file mode 100755 index 0000000..3f41e2d --- /dev/null +++ b/src/features/search/logo/logo_0.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/search/logo/logo_1.svg b/src/features/search/logo/logo_1.svg new file mode 100755 index 0000000..5265876 --- /dev/null +++ b/src/features/search/logo/logo_1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/features/search/models/Professor.ts b/src/features/search/models/Professor.ts new file mode 100644 index 0000000..22b2141 --- /dev/null +++ b/src/features/search/models/Professor.ts @@ -0,0 +1,7 @@ +export interface Professor { + name: string; + labLocation: string; + phoneNumber: string; + email: string; + department: string; +} diff --git a/src/features/search/models/models.ts b/src/features/search/models/models.ts new file mode 100644 index 0000000..9c78280 --- /dev/null +++ b/src/features/search/models/models.ts @@ -0,0 +1,20 @@ +export interface Professor { + id: number; + name: string; + labLocation: string; + phoneNumber: string; + email: string; + department: string; +} + +export interface Site { + id: number; + type: number; + name: string; + description: string; + url: string; + keywords: string[]; +} + +// 검색 결과를 위한 유니온 타입 +export type SearchResultTypes = Professor | Site; diff --git a/src/features/search/ui/components/SearchInput.tsx b/src/features/search/ui/components/SearchInput.tsx index 6371b62..65ee5c1 100644 --- a/src/features/search/ui/components/SearchInput.tsx +++ b/src/features/search/ui/components/SearchInput.tsx @@ -1,29 +1,166 @@ -import styled from 'styled-components'; +import React from 'react'; +import styled, { keyframes } from 'styled-components'; +import { SearchResultTypes } from '@semo-client/features/search/models/models'; +import { SearchResults } from '@semo-client/features/search/ui/components/SearchResults'; +import { useSearchOverlayController } from '@semo-client/features/search/ui/hooks/useSearchOverlayController'; +import SearchIcon from '@semo-client/ui/assets/icons/Icon'; -export const SearchInput = () => { - return ; +interface SearchInputProps { + query: string; + setQuery: (query: string) => void; + onChange: (e: React.ChangeEvent) => void; + onSubmit: () => void; + results: SearchResultTypes[]; +} + +export const SearchInput: React.FC = ({ + query, + setQuery, + onChange, + onSubmit, + results, +}) => { + const { + isExpanded, + isOverlayVisible, + handleExpand, + handleCollapse, + handleKeyPress, + handleOverlayAnimationEnd, + } = useSearchOverlayController({ onSubmit, setQuery }); + + return ( + <> + + + + + + { + e.stopPropagation(); + handleExpand(); + }} + isExpanded={isExpanded} + autoFocus={isExpanded} + aria-label='검색 입력창' + /> + + {isExpanded && results.length > 0 && ( + + )} + + + {isOverlayVisible && ( + + )} + + ); }; -const StyledInput = styled.input` +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const fadeOut = keyframes` + from { + opacity: 1; + } + to { + opacity: 0; + } +`; + +const SearchInputWrapper = styled.div<{ isExpanded: boolean }>` + position: absolute; + top: ${({ isExpanded }) => (isExpanded ? '40%' : '65%')}; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + width: ${({ isExpanded }) => (isExpanded ? '50%' : '500px')}; + transition: 0.3s ease-in-out; +`; + +const InputContainer = styled.div<{ isExpanded: boolean }>` + display: flex; + align-items: center; width: 100%; height: 54px; - border-radius: 8px; + border-radius: ${({ isExpanded }) => (isExpanded ? '8px 8px 0 0' : '8px')}; + background-color: ${({ isExpanded, theme }) => + isExpanded + ? theme.searchPalette.blackGray[90] + : theme.searchPalette.blackGray[70]}; + border: 1px solid ${({ theme }) => theme.searchPalette.whiteGray[30]}; + padding: 0 14px; + gap: 10px; + font-size: 20px; + color: #fff; + + &:focus-within { + outline: none; + border-color: ${({ theme }) => theme.searchPalette.whiteGray[50]}; + background: ${({ isExpanded, theme }) => + isExpanded ? theme.searchPalette.whiteGray[70] : '#000000'}; + } + + transition: all 0.3s ease-in-out; +`; - background-color: rgba(0, 0, 0, 0.7); - border: 1px solid rgba(255, 255, 255, 0.3); - padding: 10px 14px; +const SearchIconWrapper = styled.div` display: flex; - gap: 10px; align-items: center; + justify-content: center; + transition: all 0.3s ease-in-out; +`; +const StyledInput = styled.input<{ isExpanded: boolean }>` + flex: 1; + height: 100%; + border: none; + background: transparent; + color: ${({ isExpanded, theme }) => + isExpanded ? theme.searchPalette.black : theme.searchPalette.white}; font-size: 20px; - color: #fff; + position: relative; + z-index: 2; &::placeholder { - color: rgba(255, 255, 255, 0.5); + color: ${({ isExpanded, theme }) => + isExpanded + ? theme.searchPalette.black + : theme.searchPalette.whiteGray[50]}; } &:focus { outline: none; } `; + +const Overlay = styled.div<{ isExpanded: boolean }>` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.searchPalette.blackGray[90]}; + z-index: 999; + animation: ${({ isExpanded }) => (isExpanded ? fadeIn : fadeOut)} 0.3s + forwards; + pointer-events: ${({ isExpanded }) => (isExpanded ? 'auto' : 'none')}; +`; diff --git a/src/features/search/ui/components/SearchResults.tsx b/src/features/search/ui/components/SearchResults.tsx new file mode 100644 index 0000000..e1f95a3 --- /dev/null +++ b/src/features/search/ui/components/SearchResults.tsx @@ -0,0 +1,276 @@ +// src/components/SearchResults.tsx +import React from 'react'; +import styled from 'styled-components'; +// SVG 로고 이미지 임포트 +import logo_0 from '@semo-client/features/search/logo/logo_0.svg'; +import logo_1 from '@semo-client/features/search/logo/logo_1.svg'; +import { + SearchResultTypes, + Professor, + Site, +} from '@semo-client/features/search/models/models'; + +interface SearchResultsProps { + value: string; + results: SearchResultTypes[]; +} + +const logoMap: { [key: number] } = { + 0: logo_0, + 1: logo_1, +}; + +export const SearchResults: React.FC = ({ + value, + results, +}) => { + return ( + + {results.map(result => ( + + {isProfessor(result) ? ( + + + {/* TODO: 이미지 추가 필요 */} + {/* */} + + + {result.name} 교수님 + {result.department} + + +
+
+ 연구실 +

{result.labLocation}

+
+
+ 전화번호 +

{result.phoneNumber}

+
+
+ 이메일 +

{result.email}

+
+
+ ) : ( + + + {/* 로고 이미지 동적 렌더링 */} + {result.id !== undefined && logoMap[result.id] ? ( + + ) : ( + + )} + + + {result.name} + {result.description && ( + + {' — '} + {result.description} + + )} + + {result.url} + + + window.open(result.url)}> + ↵으로 이동 + + + )} +
+ ))} +
+ ); +}; + +// 타입 가드 함수 +const isProfessor = (result: SearchResultTypes): result is Professor => { + return (result as Professor).department !== undefined; +}; + +// 스타일 정의 +const ResultsContainer = styled.div` + position: absolute; + z-index: 999; + width: 100%; + background: ${({ theme }) => theme.searchPalette.whiteGray[70]}; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + padding: 14px; + max-height: 400px; + overflow-y: auto; +`; + +const HoverBadge = styled.span` + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #fff; + border-radius: 4px; + opacity: 0; + visibility: hidden; + display: flex; + padding: 2px 6px; + justify-content: center; + align-items: center; + font-size: 14px; + background: #818181; +`; + +const ResultItem = styled.div` + position: relative; /* HoverBadge의 절대 위치를 위해 추가 */ + background-color: ${({ theme }) => theme.searchPalette.blackGray[10]}; + backdrop-filter: blur(1px); + border: 2px solid ${({ theme }) => theme.searchPalette.blackGray[10]}; + padding: 12px; + border-radius: 8px; + margin-bottom: 10px; + color: #fff; + cursor: pointer; + + &:last-child { + margin-bottom: 0; + } + + &:hover { + background-color: ${({ theme }) => theme.searchPalette.blackGray[20]}; + + /* HoverBadge 보이기 */ + + ${HoverBadge} { + opacity: 1; + visibility: visible; + } + } +`; + +const ProfessorResult = styled.div` + /* 교수 결과에 대한 추가 스타일 */ +`; + +const SiteResult = styled.div` + /* 사이트 결과에 대한 추가 스타일 */ + + a { + color: #007bff; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +`; + +const Profile = styled.div` + display: flex; + align-items: center; +`; + +const ProfileInfo = styled.div` + display: flex; + flex-direction: column; + margin-left: 10px; +`; + +const ProfileImage = styled.span` + background: #fff; + border-radius: 50%; + width: 36px; + height: 36px; +`; + +const SiteIcon = styled.img` + background: #ffffff; + border-radius: 8px; + width: 36px; + height: 36px; + padding: 3px; +`; + +const DefaultSiteIcon = styled.span` + background: #007bff; + border-radius: 50%; + width: 36px; + height: 36px; +`; + +const Name = styled.p` + font-size: 16px; + font-weight: 600; + color: ${({ theme }) => theme.searchPalette.black}; + margin: 0; +`; + +const Department = styled.p` + font-size: 14px; + font-weight: 400; + color: ${({ theme }) => theme.searchPalette.black}; + margin: 0; +`; + +const NameDescriptionBox = styled.div` + display: flex; + align-items: center; + justify-content: left; +`; + +const Description = styled.p` + margin-top: 2px; + margin-left: 10px; + color: #000000; + font-size: 12px; + font-weight: 400; +`; + +const URL = styled.p` + font-size: 12px; + font-weight: 300; + color: ${({ theme }) => theme.searchPalette.black}; + margin: 0; +`; + +const Hr = styled.hr` + width: 100%; + margin: 10px 0; + border: 1px solid ${({ theme }) => theme.searchPalette.blackGray[30]}; +`; + +const Details = styled.div` + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; + + p { + color: ${({ theme }) => theme.searchPalette.black}; + font-size: 16px; + font-weight: 400; + margin: 0; + } + + &:last-child { + margin-bottom: 0; + } +`; + +const Badge = styled.span` + display: flex; + padding: 2px 6px; + justify-content: center; + align-items: center; + border-radius: 4px; + font-size: 14px; + background: #818181; + color: #fff; +`; diff --git a/src/features/search/ui/hooks/useSearchOverlayController.tsx b/src/features/search/ui/hooks/useSearchOverlayController.tsx new file mode 100644 index 0000000..0bb1c0c --- /dev/null +++ b/src/features/search/ui/hooks/useSearchOverlayController.tsx @@ -0,0 +1,58 @@ +import React, { useState, useCallback } from 'react'; + +interface UseSearchOverlayControllerProps { + onSubmit: () => void; + setQuery: (query: string) => void; +} + +interface UseSearchOverlayControllerReturn { + isExpanded: boolean; + isOverlayVisible: boolean; + handleExpand: () => void; + handleCollapse: () => void; + handleKeyPress: (e: React.KeyboardEvent) => void; + handleOverlayAnimationEnd: () => void; +} + +export const useSearchOverlayController = ({ + onSubmit, + setQuery, +}: UseSearchOverlayControllerProps) => { + const [isExpanded, setIsExpanded] = useState(false); + const [isOverlayVisible, setIsOverlayVisible] = useState(false); + + const handleExpand = useCallback(() => { + setIsOverlayVisible(true); + setIsExpanded(true); + }, []); + + const handleCollapse = useCallback(() => { + setIsExpanded(false); + }, []); + + const handleKeyPress = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onSubmit(); + handleCollapse(); + } + }, + [onSubmit, handleCollapse], + ); + + const handleOverlayAnimationEnd = useCallback(() => { + if (!isExpanded) { + setIsOverlayVisible(false); + setQuery(''); + } + }, [isExpanded, setQuery]); + + return { + isExpanded, + isOverlayVisible, + handleExpand, + handleCollapse, + handleKeyPress, + handleOverlayAnimationEnd, + }; +}; diff --git a/src/main.tsx b/src/main.tsx index 8d46db3..fe707b1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import ReactDOM from 'react-dom/client'; import React from 'react'; -import App from './App.tsx'; +import App from './App'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index d8777fc..0a47949 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,18 +1,94 @@ +import debounce from 'lodash/debounce'; +import { useCallback, useState } from 'react'; import styled from 'styled-components'; import { BackgroundImageView } from '@semo-client/features/background-image/ui/components/BackgroundImageView'; import { Clock } from '@semo-client/features/clock/ui/components/Clock'; import { HeaderView } from '@semo-client/features/header/ui/components/HeaderView'; import { NoticeView } from '@semo-client/features/notice/ui/components/NoticeView'; +import totalData from '@semo-client/features/search/data/total.json'; +import { + Professor, + Site, + SearchResultTypes, +} from '@semo-client/features/search/models/models'; import { SearchInput } from '@semo-client/features/search/ui/components/SearchInput'; import { TrendingKeywords } from '@semo-client/features/search/ui/components/TrendingKeywords'; import { LoginButtonView } from '@semo-client/features/users/ui/components/LoginButtonView'; +import { getInitials } from '@semo-utils/search/hangulUtils'; /** * TODO 각 요소 컴포넌트 에는 추가 스타일(여백,마진) 들어있지 않은 순수 요소 컴포넌트 * 각 요소의 마진 요소들은 (레이아웃 잡기) 여기서 container 컴포넌트에서 잡아준다. */ export const Home = () => { - // load user login status + const [query, setQuery] = useState(''); + // 검색 결과 반영 (교수 및 사이트 포함) + const [results, setResults] = useState([]); + + /** + * 검색어에 따라 교수 및 사이트 데이터를 필터링합니다. + * 전체 이름, 초성, 일부 이름 또는 사이트 이름 및 키워드를 포함하는지 확인합니다. + * @param searchQuery 사용자 입력 검색어 + */ + const fetchData = (searchQuery: string) => { + if (searchQuery.trim() === '') { + setResults([]); + return; + } + + const normalizedQuery = searchQuery.trim().toLowerCase(); + const queryInitials = getInitials(normalizedQuery); + + // 교수 검색 + const filteredProfessors: Professor[] = totalData.professor.filter( + professor => { + const name = professor.name.toLowerCase(); + const initials = getInitials(name); + + return ( + name.includes(normalizedQuery) || // 전체 이름 또는 일부 이름 포함 + initials.includes(normalizedQuery) // 초성 포함 + ); + }, + ); + + // 사이트 검색 + const filteredSites: Site[] = totalData.sites.filter(site => { + const name = site.name.toLowerCase(); + const keywords = site.keywords.map(keyword => keyword.toLowerCase()); + + return ( + name.includes(normalizedQuery) || // 사이트 이름 포함 + keywords.some(keyword => keyword.includes(normalizedQuery)) // 키워드 포함 + ); + }); + + // 결과 합치기 + const combinedResults: SearchResult[] = [ + ...filteredProfessors, + ...filteredSites, + ]; + + setResults(combinedResults); + }; + + const debouncedSearch = useCallback( + debounce((searchTerm: string) => { + fetchData(searchTerm); + }, 300), + [], + ); + + const handleChange = (e: React.ChangeEvent) => { + const input = e.target.value; + setQuery(input); + debouncedSearch(input); + }; + + const handleSubmit = () => { + debouncedSearch.cancel(); + fetchData(query); + }; return ( @@ -29,7 +105,14 @@ export const Home = () => { {/* Search */} - + + @@ -100,3 +183,8 @@ const AppContainer = styled.main` display: none; } `; + +const DummyInputBox = styled.div` + width: 100%; + height: 90px; +`; diff --git a/src/ui/assets/icons/Google_Chrome_icon_(February_2022).svg b/src/ui/assets/icons/Google_Chrome_icon_(February_2022).svg new file mode 100644 index 0000000..3ffa2aa --- /dev/null +++ b/src/ui/assets/icons/Google_Chrome_icon_(February_2022).svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/ui/assets/icons/Icon.tsx b/src/ui/assets/icons/Icon.tsx new file mode 100644 index 0000000..1c63cc5 --- /dev/null +++ b/src/ui/assets/icons/Icon.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +interface IconProps extends React.SVGProps { + width?: number; + height?: number; + fill?: string; +} + +const SearchIcon: React.FC = ({ + width = 26, + height = 26, + fill = 'none', + ...props +}) => ( + + + + + + + + +); + +export default SearchIcon; diff --git a/src/ui/assets/icons/Prototype.zip b/src/ui/assets/icons/Prototype.zip new file mode 100644 index 0000000..0327674 Binary files /dev/null and b/src/ui/assets/icons/Prototype.zip differ diff --git a/src/ui/assets/icons/copy.svg b/src/ui/assets/icons/copy.svg new file mode 100755 index 0000000..de07f65 --- /dev/null +++ b/src/ui/assets/icons/copy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/ui/assets/icons/naver.svg b/src/ui/assets/icons/naver.svg new file mode 100644 index 0000000..6a36296 --- /dev/null +++ b/src/ui/assets/icons/naver.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/ui/assets/icons/north_east_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg b/src/ui/assets/icons/north_east_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..85163c5 --- /dev/null +++ b/src/ui/assets/icons/north_east_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ui/styles/colors/index.ts b/src/ui/styles/colors/index.ts index 1161b2c..d71cc47 100644 --- a/src/ui/styles/colors/index.ts +++ b/src/ui/styles/colors/index.ts @@ -3,5 +3,6 @@ export { borders } from './borders'; export { buttons } from './buttons'; export { dims } from './dims'; export { pallete } from '../pallete/pallete'; +export { searchPalette } from '../pallete//searchPalette'; export { shadows } from './shadows'; export { texts } from './texts'; diff --git a/src/ui/styles/pallete/searchPalette.ts b/src/ui/styles/pallete/searchPalette.ts new file mode 100644 index 0000000..103a948 --- /dev/null +++ b/src/ui/styles/pallete/searchPalette.ts @@ -0,0 +1,17 @@ +export const searchPalette = { + black: '#000000', + blackGray: { + 10: 'rgba(0, 0, 0, 0.1)', + 30: 'rgba(0, 0, 0, 0.3)', + 50: 'rgba(0, 0, 0, 0.5)', + 70: 'rgba(0, 0, 0, 0.7)', + 90: 'rgba(0, 0, 0, 0.9)', + }, + white: '#FFFFFF', + whiteGray: { + 30: 'rgba(255, 255, 255, 0.3)', + 50: 'rgba(255, 255, 255, 0.5)', + 70: 'rgba(255, 255, 255, 0.7)', + }, +} as const; +export type searchPalette = typeof searchPalette; diff --git a/src/ui/styles/theme.ts b/src/ui/styles/theme.ts index 1eadfe1..52e379b 100644 --- a/src/ui/styles/theme.ts +++ b/src/ui/styles/theme.ts @@ -4,12 +4,14 @@ import { buttons, dims, pallete, + searchPalette, shadows, texts, } from './colors'; export const theme = { pallete, + searchPalette, backgrounds, texts, shadows, diff --git a/src/utils/search/hangulUtils.ts b/src/utils/search/hangulUtils.ts new file mode 100644 index 0000000..2fd3b79 --- /dev/null +++ b/src/utils/search/hangulUtils.ts @@ -0,0 +1,16 @@ +import { disassembleCompleteCharacter } from 'es-hangul'; + +/** + * 한글 문자열을 초성으로 변환합니다. + * @param text 변환할 한글 문자열 + * @returns 초성 문자열 + */ +export const getInitials = (text: string): string => { + return text + .split('') + .map(char => { + const decomposed = disassembleCompleteCharacter(char); + return decomposed ? decomposed.choseong : char; + }) + .join(''); +};