..
This commit is contained in:
140
components/StockSearchModal.tsx
Normal file
140
components/StockSearchModal.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { X, Search, Star, Globe, Building2, TrendingUp, TrendingDown, ChevronRight } from 'lucide-react';
|
||||
import { StockItem, WatchlistGroup, MarketType } from '../types';
|
||||
import { DbService } from '../services/dbService';
|
||||
import { MOCK_STOCKS } from '../constants';
|
||||
|
||||
interface StockSearchModalProps {
|
||||
onClose: () => void;
|
||||
onSelect: (stock: StockItem) => void;
|
||||
currentMarket: MarketType;
|
||||
}
|
||||
|
||||
const StockSearchModal: React.FC<StockSearchModalProps> = ({ onClose, onSelect, currentMarket }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [groups, setGroups] = useState<WatchlistGroup[]>([]);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<string | 'ALL'>('ALL');
|
||||
const dbService = useMemo(() => new DbService(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGroups = async () => {
|
||||
const data = await dbService.getWatchlistGroups();
|
||||
setGroups(data.filter(g => g.market === currentMarket));
|
||||
};
|
||||
fetchGroups();
|
||||
}, [dbService, currentMarket]);
|
||||
|
||||
const filteredStocks = useMemo(() => {
|
||||
let base = MOCK_STOCKS.filter(s => s.market === currentMarket);
|
||||
|
||||
if (selectedGroupId !== 'ALL') {
|
||||
const group = groups.find(g => g.id === selectedGroupId);
|
||||
if (group) {
|
||||
base = base.filter(s => group.codes.includes(s.code));
|
||||
}
|
||||
}
|
||||
|
||||
if (searchQuery) {
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
base = base.filter(s =>
|
||||
s.name.toLowerCase().includes(lowerQuery) ||
|
||||
s.code.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
|
||||
return base;
|
||||
}, [searchQuery, selectedGroupId, groups, currentMarket]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[400] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-white w-full max-w-3xl h-[600px] rounded-3xl shadow-2xl flex overflow-hidden border border-slate-200 animate-in zoom-in-95 duration-200">
|
||||
|
||||
{/* 사이드바: 그룹 목록 */}
|
||||
<div className="w-56 bg-slate-50 border-r border-slate-100 flex flex-col">
|
||||
<div className="p-5 border-b border-slate-100">
|
||||
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<Star size={14} className="text-amber-500" /> 관심 그룹
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
<button
|
||||
onClick={() => setSelectedGroupId('ALL')}
|
||||
className={`w-full text-left px-4 py-2.5 rounded-xl text-[13px] font-bold transition-all flex items-center justify-between group ${selectedGroupId === 'ALL' ? 'bg-white text-slate-900 shadow-sm border border-slate-100' : 'text-slate-500 hover:bg-slate-100'}`}
|
||||
>
|
||||
전체 종목
|
||||
<ChevronRight size={14} className={`transition-transform ${selectedGroupId === 'ALL' ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100'}`} />
|
||||
</button>
|
||||
{groups.map(group => (
|
||||
<button
|
||||
key={group.id}
|
||||
onClick={() => setSelectedGroupId(group.id)}
|
||||
className={`w-full text-left px-4 py-2.5 rounded-xl text-[13px] font-bold transition-all flex items-center justify-between group ${selectedGroupId === group.id ? 'bg-white text-slate-900 shadow-sm border border-slate-100' : 'text-slate-500 hover:bg-slate-100'}`}
|
||||
>
|
||||
<span className="truncate">{group.name}</span>
|
||||
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 rounded-md">{group.codes.length}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인: 검색 및 목록 */}
|
||||
<div className="flex-1 flex flex-col bg-white">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between gap-4">
|
||||
<div className="flex-1 relative group">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-blue-500 transition-colors" size={18} />
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="종목명 또는 코드 입력..."
|
||||
className="w-full bg-slate-50 border-none rounded-2xl py-2.5 pl-10 pr-4 text-[14px] font-bold outline-none ring-2 ring-transparent focus:ring-blue-500/10 focus:bg-white transition-all"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full transition-colors text-slate-400">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2 custom-scrollbar">
|
||||
{filteredStocks.length > 0 ? (
|
||||
filteredStocks.map(stock => (
|
||||
<div
|
||||
key={stock.code}
|
||||
onClick={() => onSelect(stock)}
|
||||
className="p-3 rounded-2xl border border-slate-50 hover:border-blue-100 hover:bg-blue-50/30 cursor-pointer transition-all flex items-center justify-between group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center text-slate-400 font-bold group-hover:bg-blue-100 group-hover:text-blue-600 transition-colors">
|
||||
{stock.name[0]}
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-[14px] font-black text-slate-800">{stock.name}</h5>
|
||||
<span className="text-[11px] font-bold text-slate-400 tracking-wider">{stock.code}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[14px] font-black font-mono text-slate-900">
|
||||
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`}
|
||||
</p>
|
||||
<div className={`text-[11px] font-bold flex items-center justify-end gap-1 ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
{stock.changePercent >= 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
||||
{Math.abs(stock.changePercent)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-slate-300 space-y-3">
|
||||
<Search size={48} strokeWidth={1} />
|
||||
<p className="text-[13px] font-bold">검색 결과가 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockSearchModal;
|
||||
Reference in New Issue
Block a user