디자인업데이트

This commit is contained in:
2026-02-02 22:20:08 +09:00
parent 3c2f4a0371
commit 84746d41b8
6 changed files with 334 additions and 326 deletions

View File

@@ -222,7 +222,7 @@ const AppContent: React.FC = () => {
<main className="flex-1 overflow-y-auto p-6 custom-scrollbar relative"> <main className="flex-1 overflow-y-auto p-6 custom-scrollbar relative">
<Routes> <Routes>
<Route path="/" element={<Dashboard marketMode={marketMode} watchlistGroups={watchlistGroups} stocks={visibleStocks} orders={orders} reservedOrders={reservedOrders} onManualOrder={handleManualOrder} onAddReservedOrder={async (o) => { await dbService.saveReservedOrder({id: 'res_'+Date.now(), ...o, status: 'WAITING', createdAt: new Date(), expiryDate: new Date()}); syncFromDb(); }} onDeleteReservedOrder={async (id) => { await dbService.deleteReservedOrder(id); syncFromDb(); }} onRefreshHoldings={syncFromDb} autoTrades={autoTrades} />} /> <Route path="/" element={<Dashboard marketMode={marketMode} stocks={visibleStocks} orders={orders} reservedOrders={reservedOrders} onManualOrder={handleManualOrder} onAddReservedOrder={async (o) => { await dbService.saveReservedOrder({id: 'res_'+Date.now(), ...o, status: 'WAITING', createdAt: new Date(), expiryDate: new Date()}); syncFromDb(); }} onDeleteReservedOrder={async (id) => { await dbService.deleteReservedOrder(id); syncFromDb(); }} onRefreshHoldings={syncFromDb} autoTrades={autoTrades} />} />
<Route path="/discovery" element={<Discovery stocks={masterStocks} orders={orders} onUpdateStock={handleUpdateStockMetadata} settings={settings} />} /> <Route path="/discovery" element={<Discovery stocks={masterStocks} orders={orders} onUpdateStock={handleUpdateStockMetadata} settings={settings} />} />
<Route path="/stocks" element={<Stocks marketMode={marketMode} stocks={visibleStocks} onTrade={(s, t) => addLog(`${s.name} ${t} 주문 패널 진입`, 'info')} onAddToWatchlist={handleAddToWatchlist} watchlistCodes={watchlistCodes} onSync={handleSyncStocks} />} /> <Route path="/stocks" element={<Stocks marketMode={marketMode} stocks={visibleStocks} onTrade={(s, t) => addLog(`${s.name} ${t} 주문 패널 진입`, 'info')} onAddToWatchlist={handleAddToWatchlist} watchlistCodes={watchlistCodes} onSync={handleSyncStocks} />} />
<Route path="/auto" element={<AutoTrading marketMode={marketMode} stocks={visibleStocks} configs={autoTrades} groups={watchlistGroups} onAddConfig={async (c) => { await dbService.saveAutoConfig({...c, id: 'auto_'+Date.now(), active: true}); syncFromDb(); }} onToggleConfig={async (id) => { const c = (await dbService.getAutoConfigs()).find(x => x.id === id); if(c) { await dbService.updateAutoConfig({...c, active: !c.active}); syncFromDb(); } }} onDeleteConfig={async (id) => { await dbService.deleteAutoConfig(id); syncFromDb(); }} />} /> <Route path="/auto" element={<AutoTrading marketMode={marketMode} stocks={visibleStocks} configs={autoTrades} groups={watchlistGroups} onAddConfig={async (c) => { await dbService.saveAutoConfig({...c, id: 'auto_'+Date.now(), active: true}); syncFromDb(); }} onToggleConfig={async (id) => { const c = (await dbService.getAutoConfigs()).find(x => x.id === id); if(c) { await dbService.updateAutoConfig({...c, active: !c.active}); syncFromDb(); } }} onDeleteConfig={async (id) => { await dbService.deleteAutoConfig(id); syncFromDb(); }} />} />

View File

@@ -72,7 +72,7 @@ interface InputGroupProps {
} }
export const InputGroup: React.FC<InputGroupProps> = ({ label, value, onChange, placeholder, type = "text", icon }) => ( export const InputGroup: React.FC<InputGroupProps> = ({ label, value, onChange, placeholder, type = "text", icon }) => (
<div className="space-y-3"> <div className="space-y-2">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2"> <label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
{icon} {label} {icon} {label}
</label> </label>
@@ -80,12 +80,36 @@ export const InputGroup: React.FC<InputGroupProps> = ({ label, value, onChange,
type={type} type={type}
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
className="w-full p-4 bg-slate-50 border border-slate-200 rounded-[1.2rem] focus:border-blue-500 focus:bg-white outline-none transition-all font-bold text-slate-800 placeholder:text-slate-300 shadow-sm" className="w-full p-3 bg-slate-50 border border-slate-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition-all font-bold text-slate-800 placeholder:text-slate-300 text-[13px] shadow-sm"
placeholder={placeholder} placeholder={placeholder}
/> />
</div> </div>
); );
// --- SelectGroup: 일관된 디자인의 셀렉트 박스 ---
interface SelectGroupProps {
label: string;
value: string;
onChange: (v: string) => void;
options: { value: string, label: string }[];
icon?: React.ReactNode;
}
export const SelectGroup: React.FC<SelectGroupProps> = ({ label, value, onChange, options, icon }) => (
<div className="space-y-2">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
{icon} {label}
</label>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full p-3 bg-slate-50 border border-slate-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition-all font-bold text-slate-800 text-[13px] shadow-sm appearance-none"
>
{options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
);
// --- ToggleButton: 활성화/비활성 스위치 --- // --- ToggleButton: 활성화/비활성 스위치 ---
interface ToggleButtonProps { interface ToggleButtonProps {
active: boolean; active: boolean;

View File

@@ -24,35 +24,16 @@ interface DashboardProps {
} }
const Dashboard: React.FC<DashboardProps> = ({ const Dashboard: React.FC<DashboardProps> = ({
marketMode, watchlistGroups, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders marketMode, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders
}) => { }) => {
const [holdings, setHoldings] = useState<HoldingItem[]>([]); const [holdings, setHoldings] = useState<HoldingItem[]>([]);
const [summary, setSummary] = useState({ totalAssets: 0, buyingPower: 0 }); const [summary, setSummary] = useState({ totalAssets: 0, buyingPower: 0 });
const [activeGroupId, setActiveGroupId] = useState<string | null>(null); // Data loading
const [detailStock, setDetailStock] = useState<StockItem | null>(null);
const [tradeContext, setTradeContext] = useState<{ stock: StockItem, type: OrderType } | null>(null);
const dbService = useMemo(() => new DbService(), []);
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [orders, marketMode, reservedOrders]); }, [orders, marketMode, reservedOrders]);
const activeMarketGroups = useMemo(() => {
return watchlistGroups.filter(group => group.market === marketMode);
}, [watchlistGroups, marketMode]);
useEffect(() => {
if (activeMarketGroups.length > 0) {
if (!activeGroupId || !activeMarketGroups.find(g => g.id === activeGroupId)) {
setActiveGroupId(activeMarketGroups[0].id);
}
} else {
setActiveGroupId(null);
}
}, [marketMode, activeMarketGroups, activeGroupId]);
const loadData = async () => { const loadData = async () => {
const allHoldings = await dbService.getHoldings(); const allHoldings = await dbService.getHoldings();
const filteredHoldings = allHoldings.filter(h => h.market === marketMode); const filteredHoldings = allHoldings.filter(h => h.market === marketMode);
@@ -83,46 +64,15 @@ const Dashboard: React.FC<DashboardProps> = ({
? (totalLiquidationSummary.totalPL / totalLiquidationSummary.totalCost) * 100 ? (totalLiquidationSummary.totalPL / totalLiquidationSummary.totalCost) * 100
: 0; : 0;
const selectedGroup = activeMarketGroups.find(g => g.id === activeGroupId) || activeMarketGroups[0]; // Remove detailStock and tradeContext initialization if not used elsewhere, but they are used in JSX below.
const [detailStock, setDetailStock] = useState<StockItem | null>(null);
const [tradeContext, setTradeContext] = useState<{ stock: StockItem, type: OrderType } | null>(null);
const dbService = useMemo(() => new DbService(), []);
return ( return (
<div className="space-y-6 animate-in fade-in duration-500 pb-20"> <div className="space-y-6 animate-in fade-in duration-500 pb-20">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px] lg:col-span-1"> <div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px]">
<div className="flex justify-between items-center mb-5">
<h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 uppercase tracking-tighter">
<PieChart size={20} className="text-blue-600" />
</h3>
</div>
<div className="flex gap-2 mb-6 overflow-x-auto pb-2 scrollbar-hide">
{activeMarketGroups.map(group => (
<button key={group.id} onClick={() => setActiveGroupId(group.id)} className={`relative px-4 py-2 rounded-xl font-black text-[12px] transition-all border-2 whitespace-nowrap ${activeGroupId === group.id ? 'bg-white border-blue-500 text-blue-600 shadow-sm' : 'bg-transparent border-slate-50 text-slate-400 hover:border-slate-200'}`}>
{group.name}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto pr-1 scrollbar-hide">
<table className="w-full">
<tbody className="divide-y divide-slate-50">
{selectedGroup?.codes.map(code => stocks.find(s => s.code === code)).filter(s => s?.market === marketMode).map(stock => {
if (!stock) return null;
return (
<StockRow
key={stock.code}
stock={stock}
showActions={true}
onTrade={(type) => setTradeContext({ stock, type })}
onClick={() => setDetailStock(stock)}
/>
);
})}
</tbody>
</table>
</div>
</div>
<div className="lg:col-span-2 space-y-6">
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[350px]">
<div className="flex justify-between items-center mb-5"> <div className="flex justify-between items-center mb-5">
<h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 tracking-tighter"> <h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 tracking-tighter">
<Database size={20} className="text-emerald-600" /> <Database size={20} className="text-emerald-600" />
@@ -158,7 +108,7 @@ const Dashboard: React.FC<DashboardProps> = ({
</div> </div>
</div> </div>
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[274px] overflow-hidden"> <div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px] overflow-hidden">
<h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 mb-5"> <h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 mb-5">
<Timer size={20} className="text-blue-600" /> <Timer size={20} className="text-blue-600" />
</h3> </h3>
@@ -177,7 +127,6 @@ const Dashboard: React.FC<DashboardProps> = ({
</div> </div>
</div> </div>
</div> </div>
</div>
{detailStock && <StockDetailModal stock={detailStock} onClose={() => setDetailStock(null)} />} {detailStock && <StockDetailModal stock={detailStock} onClose={() => setDetailStock(null)} />}
{tradeContext && <TradeModal stock={tradeContext.stock} type={tradeContext.type} onClose={() => setTradeContext(null)} onExecute={onAddReservedOrder} />} {tradeContext && <TradeModal stock={tradeContext.stock} type={tradeContext.type} onClose={() => setTradeContext(null)} onExecute={onAddReservedOrder} />}

View File

@@ -9,63 +9,63 @@ interface HistoryPageProps {
const HistoryPage: React.FC<HistoryPageProps> = ({ orders }) => { const HistoryPage: React.FC<HistoryPageProps> = ({ orders }) => {
return ( return (
<div className="space-y-10 animate-in fade-in duration-500"> <div className="space-y-6 animate-in fade-in duration-500">
<div className="bg-white rounded-[3.5rem] shadow-sm border border-slate-100 overflow-hidden"> <div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
<div className="p-8 border-b border-slate-50 flex justify-between items-center bg-slate-50/30"> <div className="p-5 border-b border-slate-50 flex justify-between items-center bg-slate-50/10">
<h3 className="text-2xl font-black text-slate-800 flex items-center gap-3 uppercase tracking-widest"> <h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 uppercase tracking-tight">
<History size={24} className="text-blue-600" /> <History size={20} className="text-blue-600" />
</h3> </h3>
<button className="text-[11px] font-black text-slate-400 hover:text-slate-600 flex items-center gap-2 uppercase tracking-widest"> <button className="text-[10px] font-black text-slate-400 hover:text-slate-600 flex items-center gap-1.5 uppercase tracking-widest transition-colors">
<FileText size={18} /> (CSV) <FileText size={16} /> (CSV)
</button> </button>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-slate-50/50"> <thead className="bg-slate-50/30">
<tr className="text-left text-[11px] font-black text-slate-400 uppercase tracking-widest border-b"> <tr className="text-left text-[10px] font-black text-slate-400 uppercase tracking-widest border-b">
<th className="px-10 py-6"> 퀀 </th> <th className="px-6 py-3"> 퀀 </th>
<th className="px-10 py-6"></th> <th className="px-6 py-3"></th>
<th className="px-10 py-6"></th> <th className="px-6 py-3"></th>
<th className="px-10 py-6"> </th> <th className="px-6 py-3"> </th>
<th className="px-10 py-6"> </th> <th className="px-6 py-3"> </th>
<th className="px-10 py-6"> </th> <th className="px-6 py-3"> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-50"> <tbody className="divide-y divide-slate-50">
{orders.map(order => ( {orders.map(order => (
<tr key={order.id} className="hover:bg-slate-50 transition-colors"> <tr key={order.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-10 py-6 text-sm font-mono text-slate-500"> <td className="px-6 py-4 text-[12px] font-mono text-slate-500">
{order.timestamp.toLocaleString()} {order.timestamp.toLocaleString()}
</td> </td>
<td className="px-10 py-6"> <td className="px-6 py-4">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-black text-slate-800 text-base">{order.stockName}</span> <span className="font-black text-slate-800 text-[14px]">{order.stockName}</span>
<span className="text-[11px] font-mono text-slate-400 uppercase tracking-widest">{order.stockCode}</span> <span className="text-[10px] font-mono text-slate-400 uppercase tracking-widest">{order.stockCode}</span>
</div> </div>
</td> </td>
<td className="px-10 py-6"> <td className="px-6 py-4">
<span className={`px-3 py-1.5 rounded-lg text-[11px] font-black tracking-widest ${order.type === 'BUY' ? 'bg-red-50 text-red-600' : 'bg-blue-50 text-blue-600'}`}> <span className={`px-2 py-1 rounded-lg text-[10px] font-black tracking-widest ${order.type === 'BUY' ? 'bg-rose-50 text-rose-600' : 'bg-blue-50 text-blue-600'}`}>
{order.type === 'BUY' ? '매수' : '매도'} {order.type === 'BUY' ? '매수' : '매도'}
</span> </span>
</td> </td>
<td className="px-10 py-6 text-base font-black text-slate-700"> <td className="px-6 py-4 text-[14px] font-black text-slate-700">
{order.quantity} UNIT {order.quantity} UNIT
</td> </td>
<td className="px-10 py-6 text-base font-mono font-bold text-slate-600"> <td className="px-6 py-4 text-[14px] font-mono font-bold text-slate-600">
{order.price.toLocaleString()} {order.price.toLocaleString()}
</td> </td>
<td className="px-10 py-6"> <td className="px-6 py-4">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2">
<CheckCircle size={18} className="text-emerald-500" /> <CheckCircle size={14} className="text-emerald-500" />
<span className="text-sm font-black text-slate-800 uppercase tracking-tighter"> </span> <span className="text-[12px] font-black text-slate-800 uppercase tracking-tight"> </span>
</div> </div>
</td> </td>
</tr> </tr>
))} ))}
{orders.length === 0 && ( {orders.length === 0 && (
<tr> <tr>
<td colSpan={6} className="px-10 py-32 text-center text-slate-400 italic text-base"> <td colSpan={6} className="px-6 py-16 text-center text-slate-400 italic text-[14px]">
. .
</td> </td>
</tr> </tr>

View File

@@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Save, Key, Shield, MessageCircle, Globe, Check, Cpu, Zap, Plus, Trash2, Edit3, X, BarChart4, Newspaper, Scale, PlusCircle, MinusCircle } from 'lucide-react'; import { Save, Key, Shield, MessageCircle, Globe, Check, Cpu, Zap, Plus, Trash2, Edit3, X, BarChart4, Newspaper, Scale, PlusCircle, MinusCircle } from 'lucide-react';
import { ApiSettings, AiConfig } from '../types'; import { ApiSettings, AiConfig } from '../types';
import { InputGroup, ToggleButton } from '../components/CommonUI'; import { InputGroup, ToggleButton, SelectGroup } from '../components/CommonUI';
interface SettingsProps { interface SettingsProps {
settings: ApiSettings; settings: ApiSettings;
@@ -56,18 +56,23 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave }) => {
}; };
return ( return (
<div className="max-w-5xl space-y-10 animate-in fade-in duration-500 pb-20 mx-auto"> <div className="w-full space-y-6 animate-in fade-in duration-500 pb-20">
<div className="bg-white p-12 rounded-[3.5rem] shadow-sm border border-slate-100"> <form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-14"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* Left Column: API & AI */}
{/* KIS API Section */} <div className="space-y-6">
<section> {/* KIS API Section Card */}
<div className="flex items-center justify-between mb-10"> <section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<h4 className="text-[12px] font-black text-slate-400 uppercase tracking-[0.25em] flex items-center gap-3"> <div className="flex items-center gap-3 mb-6">
<Key size={20} /> KIS API <div className="p-2 bg-blue-600 text-white rounded-xl shadow-lg shadow-blue-50">
</h4> <Key size={20} />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div>
<h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight">KIS API </h4>
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<InputGroup label="앱 키" value={formData.appKey} onChange={(v) => setFormData({...formData, appKey: v})} type="password" placeholder="App Key" /> <InputGroup label="앱 키" value={formData.appKey} onChange={(v) => setFormData({...formData, appKey: v})} type="password" placeholder="App Key" />
<InputGroup label="비밀 키" value={formData.appSecret} onChange={(v) => setFormData({...formData, appSecret: v})} type="password" placeholder="Secret Key" /> <InputGroup label="비밀 키" value={formData.appSecret} onChange={(v) => setFormData({...formData, appSecret: v})} type="password" placeholder="Secret Key" />
<div className="md:col-span-2"> <div className="md:col-span-2">
@@ -76,182 +81,202 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave }) => {
</div> </div>
</section> </section>
{/* AI 분석 자동화 설정 섹션 */} {/* AI 분석 자동화 설정 Card */}
<section className="bg-blue-50/20 p-10 rounded-[2.5rem] border border-blue-100"> <section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div className="flex items-center gap-4 mb-10"> <div className="flex items-center gap-3 mb-6">
<div className="p-3 bg-blue-600 text-white rounded-2xl shadow-lg shadow-blue-100"> <div className="p-2 bg-blue-600 text-white rounded-xl shadow-lg shadow-blue-50">
<Zap size={24} /> <Zap size={20} />
</div> </div>
<div> <div>
<h4 className="text-[12px] font-black text-slate-800 uppercase tracking-raw-2 mt-0.5">AI </h4> <h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight">AI </h4>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p> <p className="text-[11px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3"> <SelectGroup
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2"> label="뉴스 분석 Engine"
<Newspaper size={14} className="text-blue-500" /> icon={<Newspaper size={14} className="text-blue-500" />}
</label>
<select
className="w-full p-4 bg-white border border-slate-200 rounded-[1.2rem] focus:border-blue-500 outline-none transition-all font-bold text-slate-800 shadow-sm"
value={formData.preferredNewsAiId || ''} value={formData.preferredNewsAiId || ''}
onChange={(e) => setFormData({...formData, preferredNewsAiId: e.target.value})} onChange={(v) => setFormData({...formData, preferredNewsAiId: v})}
> options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
<option value=""> </option> />
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3"> <SelectGroup
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2"> label="종목 분석 Engine"
<BarChart4 size={14} className="text-purple-500" /> icon={<BarChart4 size={14} className="text-purple-500" />}
</label>
<select
className="w-full p-4 bg-white border border-slate-200 rounded-[1.2rem] focus:border-blue-500 outline-none transition-all font-bold text-slate-800 shadow-sm"
value={formData.preferredStockAiId || ''} value={formData.preferredStockAiId || ''}
onChange={(e) => setFormData({...formData, preferredStockAiId: e.target.value})} onChange={(v) => setFormData({...formData, preferredStockAiId: v})}
> options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
<option value=""> </option> />
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3"> <SelectGroup
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2"> label="뉴스 판단 Engine"
<Scale size={14} className="text-amber-500" /> icon={<Scale size={14} className="text-amber-500" />}
</label>
<select
className="w-full p-4 bg-white border border-slate-200 rounded-[1.2rem] focus:border-blue-500 outline-none transition-all font-bold text-slate-800 shadow-sm"
value={formData.preferredNewsJudgementAiId || ''} value={formData.preferredNewsJudgementAiId || ''}
onChange={(e) => setFormData({...formData, preferredNewsJudgementAiId: e.target.value})} onChange={(v) => setFormData({...formData, preferredNewsJudgementAiId: v})}
> options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
<option value=""> </option> />
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3"> <SelectGroup
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2"> label="자동매수 Engine"
<PlusCircle size={14} className="text-rose-500" /> icon={<PlusCircle size={14} className="text-rose-500" />}
</label>
<select
className="w-full p-4 bg-white border border-slate-200 rounded-[1.2rem] focus:border-blue-500 outline-none transition-all font-bold text-slate-800 shadow-sm"
value={formData.preferredAutoBuyAiId || ''} value={formData.preferredAutoBuyAiId || ''}
onChange={(e) => setFormData({...formData, preferredAutoBuyAiId: e.target.value})} onChange={(v) => setFormData({...formData, preferredAutoBuyAiId: v})}
> options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
<option value=""> </option> />
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3 md:col-span-2"> <div className="md:col-span-2">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2"> <SelectGroup
<MinusCircle size={14} className="text-blue-600" /> label="자동매도 Engine"
</label> icon={<MinusCircle size={14} className="text-blue-600" />}
<select
className="w-full p-4 bg-white border border-slate-200 rounded-[1.2rem] focus:border-blue-500 outline-none transition-all font-bold text-slate-800 shadow-sm"
value={formData.preferredAutoSellAiId || ''} value={formData.preferredAutoSellAiId || ''}
onChange={(e) => setFormData({...formData, preferredAutoSellAiId: e.target.value})} onChange={(v) => setFormData({...formData, preferredAutoSellAiId: v})}
> options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
<option value=""> </option> />
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div> </div>
</div> </div>
</section> </section>
</div>
{/* Telegram Notification Section */} {/* Right Column: Extensions & Notifications & AI Management */}
<section className="bg-slate-50 p-10 rounded-[2.5rem] border border-slate-100"> <div className="space-y-6">
<div className="flex items-center justify-between mb-10"> {/* AI Engine Management Card moved here */}
<div className="flex items-center gap-4"> <section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div className={`p-3 rounded-2xl ${formData.useTelegram ? 'bg-blue-100 text-blue-600' : 'bg-slate-200 text-slate-400'}`}> <div className="flex items-center justify-between mb-8">
<MessageCircle size={24} /> <div className="flex items-center gap-3">
<div className="p-2 bg-slate-900 text-white rounded-xl shadow-lg shadow-slate-100">
<Cpu size={20} />
</div> </div>
<div> <div>
<h4 className="text-[12px] font-black text-slate-800 uppercase tracking-[0.2em]"> </h4> <h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight">AI </h4>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p> <p className="text-[11px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> AI </p>
</div>
</div>
<button
type="button"
onClick={() => { setEditingAi({ id: Date.now().toString(), name: '', providerType: 'gemini', modelName: '' }); setShowAiModal(true); }}
className="p-2 bg-slate-50 text-slate-600 rounded-xl hover:bg-blue-50 hover:text-blue-600 transition-colors border border-slate-100"
>
<Plus size={20} />
</button>
</div>
<div className="space-y-3">
{formData.aiConfigs.length === 0 ? (
<div className="text-center py-8 bg-slate-50/50 rounded-xl border border-dashed border-slate-200">
<p className="text-[11px] font-black text-slate-300 uppercase tracking-widest"> </p>
</div>
) : (
formData.aiConfigs.map(config => (
<div key={config.id} className="flex items-center gap-4 p-4 bg-slate-50/50 rounded-xl border border-slate-100 group">
<div className={`p-2 rounded-lg ${config.providerType === 'gemini' ? 'bg-blue-100 text-blue-600' : 'bg-emerald-100 text-emerald-600'}`}>
<Cpu size={16} />
</div>
<div className="flex-1 min-w-0">
<h5 className="text-[13px] font-black text-slate-800 truncate">{config.name}</h5>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5">
{config.providerType === 'gemini' ? 'Gemini Flash' : 'Ollama (OpenAI)'} {config.modelName}
</p>
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button type="button" onClick={() => { setEditingAi(config); setShowAiModal(true); }} className="p-2 text-slate-400 hover:text-blue-600 transition-colors"><Edit3 size={16} /></button>
<button type="button" onClick={() => handleDeleteAi(config.id)} className="p-2 text-slate-400 hover:text-rose-500 transition-colors"><Trash2 size={16} /></button>
</div>
</div>
))
)}
</div>
</section>
<section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-xl shadow-lg ${formData.useTelegram ? 'bg-blue-100 text-blue-600 shadow-blue-50' : 'bg-slate-200 text-slate-400 shadow-slate-50'}`}>
<MessageCircle size={20} />
</div>
<div>
<h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight"> </h4>
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p>
</div> </div>
</div> </div>
<ToggleButton active={formData.useTelegram} onClick={() => toggleService('useTelegram')} /> <ToggleButton active={formData.useTelegram} onClick={() => toggleService('useTelegram')} />
</div> </div>
{formData.useTelegram && ( <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in slide-in-from-top-4 duration-300">
<InputGroup label="봇 토큰" value={formData.telegramToken} onChange={(v) => setFormData({...formData, telegramToken: v})} placeholder="Bot API Token" /> <InputGroup label="봇 토큰" value={formData.telegramToken} onChange={(v) => setFormData({...formData, telegramToken: v})} placeholder="Bot API Token" />
<InputGroup label="채팅 ID" value={formData.telegramChatId} onChange={(v) => setFormData({...formData, telegramChatId: v})} placeholder="Chat ID" /> <InputGroup label="채팅 ID" value={formData.telegramChatId} onChange={(v) => setFormData({...formData, telegramChatId: v})} placeholder="Chat ID" />
</div> </div>
)}
</section> </section>
{/* Naver News API Section */} <section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<section className="bg-slate-50 p-10 rounded-[2.5rem] border border-slate-100"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center justify-between mb-10"> <div className="flex items-center gap-3">
<div className="flex items-center gap-4"> <div className={`p-2 rounded-xl shadow-lg ${formData.useNaverNews ? 'bg-emerald-100 text-emerald-600 shadow-emerald-50' : 'bg-slate-200 text-slate-400 shadow-slate-50'}`}>
<div className={`p-3 rounded-2xl ${formData.useNaverNews ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-200 text-slate-400'}`}> <Globe size={20} />
<Globe size={24} />
</div> </div>
<div> <div>
<h4 className="text-[12px] font-black text-slate-800 uppercase tracking-[0.2em]"> </h4> <h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight"> </h4>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p> <p className="text-[11px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p>
</div> </div>
</div> </div>
<ToggleButton active={formData.useNaverNews} onClick={() => toggleService('useNaverNews')} /> <ToggleButton active={formData.useNaverNews} onClick={() => toggleService('useNaverNews')} />
</div> </div>
{formData.useNaverNews && ( <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in slide-in-from-top-4 duration-300">
<InputGroup label="Client ID" value={formData.naverClientId} onChange={(v) => setFormData({...formData, naverClientId: v})} placeholder="Naver Client ID" /> <InputGroup label="Client ID" value={formData.naverClientId} onChange={(v) => setFormData({...formData, naverClientId: v})} placeholder="Naver Client ID" />
<InputGroup label="Client Secret" value={formData.naverClientSecret} onChange={(v) => setFormData({...formData, naverClientSecret: v})} type="password" placeholder="Naver Client Secret" /> <InputGroup label="Client Secret" value={formData.naverClientSecret} onChange={(v) => setFormData({...formData, naverClientSecret: v})} type="password" placeholder="Naver Client Secret" />
</div> </div>
)}
</section> </section>
<div className="pt-10 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-8"> <div className="p-5 bg-slate-50/50 rounded-xl border border-slate-100 border-dashed text-center">
<div className="flex items-center gap-4 text-slate-400 text-sm font-medium bg-slate-50 px-6 py-4 rounded-2xl border border-slate-100"> <p className="text-[11px] font-black text-slate-400 uppercase tracking-widest"> </p>
<Shield size={22} className="text-emerald-500" /> </div>
</div>
</div>
{/* Save Bar */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-3 text-slate-400 text-[12px] font-medium bg-slate-50 px-4 py-3 rounded-xl border border-slate-100">
<Shield size={18} className="text-emerald-500" />
</div> </div>
<button <button
type="submit" type="submit"
className={`w-full sm:w-auto px-16 py-6 rounded-3xl font-black uppercase text-sm tracking-widest shadow-2xl transition-all flex items-center justify-center gap-4 ${isSaved ? 'bg-emerald-500 text-white shadow-emerald-200 scale-95' : 'bg-slate-900 text-white hover:bg-slate-800 shadow-slate-300 active:scale-95'}`} className={`w-full sm:w-auto px-12 py-3.5 rounded-xl font-black uppercase text-[14px] tracking-widest shadow-xl transition-all flex items-center justify-center gap-2 ${isSaved ? 'bg-emerald-500 text-white shadow-emerald-100 scale-95' : 'bg-slate-900 text-white hover:bg-slate-800 shadow-slate-100 active:scale-95'}`}
> >
{isSaved ? <><Check size={24} /> </> : <><Save size={24} /> </>} {isSaved ? <><Check size={18} /> </> : <><Save size={18} /> </>}
</button> </button>
</div> </div>
</form> </form>
</div>
{/* AI Engine Modal */} {/* AI Engine Modal */}
{showAiModal && ( {showAiModal && (
<div className="fixed inset-0 z-[150] bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-6"> <div className="fixed inset-0 z-[150] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-6">
<div className="bg-white w-full max-w-lg rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95 duration-200 border border-slate-100"> <div className="bg-white w-full max-w-sm rounded-2xl p-6 shadow-2xl animate-in zoom-in-95 duration-200 border border-slate-100">
<div className="flex justify-between items-center mb-10"> <div className="flex justify-between items-center mb-8">
<h3 className="text-2xl font-black text-slate-900 flex items-center gap-3 uppercase tracking-tight"> <h3 className="text-[18px] font-black text-slate-900 flex items-center gap-2 uppercase tracking-tight">
<Cpu className="text-blue-600" /> AI <Cpu size={20} className="text-blue-600" /> AI
</h3> </h3>
<button onClick={() => setShowAiModal(false)} className="p-2 hover:bg-slate-100 rounded-full transition-colors"><X size={28} className="text-slate-400" /></button> <button onClick={() => setShowAiModal(false)} className="p-1 hover:bg-slate-100 rounded-full transition-colors"><X size={24} className="text-slate-400" /></button>
</div> </div>
<div className="space-y-6"> <div className="space-y-5">
<InputGroup label="엔진 식별 이름" value={editingAi?.name || ''} onChange={(v) => setEditingAi({...editingAi, name: v})} placeholder="예: 구글 고성능 모델, Ollama Llama3" /> <InputGroup label="엔진 식별 이름" value={editingAi?.name || ''} onChange={(v) => setEditingAi({...editingAi, name: v})} placeholder="예: 구글 고성능 모델" />
<div className="space-y-3"> <div className="space-y-2">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1"> </label> <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1"></label>
<div className="flex bg-slate-100 p-1.5 rounded-2xl"> <div className="flex bg-slate-100 p-1 rounded-xl">
<button type="button" onClick={() => setEditingAi({...editingAi, providerType: 'gemini'})} className={`flex-1 py-3 rounded-xl text-[11px] font-black transition-all ${editingAi?.providerType === 'gemini' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>Gemini</button> <button type="button" onClick={() => setEditingAi({...editingAi, providerType: 'gemini'})} className={`flex-1 py-2 rounded-lg text-[10px] font-black transition-all ${editingAi?.providerType === 'gemini' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>Gemini Flash</button>
<button type="button" onClick={() => setEditingAi({...editingAi, providerType: 'openai-compatible'})} className={`flex-1 py-3 rounded-xl text-[11px] font-black transition-all ${editingAi?.providerType === 'openai-compatible' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>Ollama / OpenAI</button> <button type="button" onClick={() => setEditingAi({...editingAi, providerType: 'openai-compatible'})} className={`flex-1 py-2 rounded-lg text-[10px] font-black transition-all ${editingAi?.providerType === 'openai-compatible' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>Ollama (OpenAI)</button>
</div> </div>
</div> </div>
<InputGroup label="모델명" value={editingAi?.modelName || ''} onChange={(v) => setEditingAi({...editingAi, modelName: v})} placeholder={editingAi?.providerType === 'gemini' ? 'gemini-3-flash-preview' : 'llama3'} /> <InputGroup label="모델명" value={editingAi?.modelName || ''} onChange={(v) => setEditingAi({...editingAi, modelName: v})} placeholder="gemini-pros" />
{editingAi?.providerType === 'openai-compatible' && (
<InputGroup label="베이스 URL (API End-point)" value={editingAi?.baseUrl || ''} onChange={(v) => setEditingAi({...editingAi, baseUrl: v})} placeholder="http://localhost:11434/v1" />
)}
<button <button
type="button" type="button"
onClick={handleSaveAi} onClick={handleSaveAi}
className="w-full py-5 bg-blue-600 text-white rounded-[1.5rem] font-black uppercase text-[12px] tracking-widest hover:bg-blue-700 transition-all shadow-xl shadow-blue-100 mt-6" className="w-full py-3 bg-blue-600 text-white rounded-xl font-black uppercase text-[12px] tracking-widest hover:bg-blue-700 transition-all shadow-lg shadow-blue-50 mt-4"
> >
</button> </button>
</div> </div>
</div> </div>

View File

@@ -120,53 +120,53 @@ const WatchlistManagement: React.FC<WatchlistManagementProps> = ({ marketMode, s
}; };
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-10 animate-in fade-in duration-500 pb-20"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 animate-in fade-in duration-500 pb-20">
<div className="lg:col-span-1 bg-white p-10 rounded-[3.5rem] shadow-sm border border-slate-100 flex flex-col h-[750px]"> <div className="lg:col-span-1 bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px]">
<div className="flex justify-between items-center mb-10"> <div className="flex justify-between items-center mb-5">
<h3 className="text-2xl font-black text-slate-800 flex items-center gap-3 uppercase tracking-tighter"> <h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 uppercase tracking-tighter">
<Star size={24} className="text-blue-600" /> {marketMode === MarketType.DOMESTIC ? '국내' : '해외'} <Star size={20} className="text-blue-600" /> {marketMode === MarketType.DOMESTIC ? '국내' : '해외'}
</h3> </h3>
</div> </div>
<button onClick={() => setShowAddGroupModal(true)} className="w-full py-5 bg-slate-900 text-white rounded-[2rem] font-black text-[12px] uppercase tracking-widest flex items-center justify-center gap-3 mb-8 shadow-xl shadow-slate-200 hover:bg-slate-800 transition-all active:scale-95"><FolderPlus size={18} /> </button> <button onClick={() => setShowAddGroupModal(true)} className="w-full py-3 bg-slate-900 text-white rounded-xl font-black text-[12px] uppercase tracking-widest flex items-center justify-center gap-2 mb-6 shadow-lg shadow-slate-200 hover:bg-slate-800 transition-all active:scale-95"><FolderPlus size={16} /> </button>
<div className="flex-1 overflow-y-auto pr-2 space-y-4 scrollbar-hide"> <div className="flex-1 overflow-y-auto pr-1 space-y-2 scrollbar-hide">
{filteredGroups.map(group => ( {filteredGroups.map(group => (
<div key={group.id} onClick={() => setSelectedGroupId(group.id)} className={`p-6 rounded-[2rem] border-2 transition-all cursor-pointer group flex justify-between items-center ${selectedGroupId === group.id ? 'border-blue-500 bg-blue-50/20' : 'border-transparent bg-slate-50/70 hover:bg-white hover:border-slate-200'}`}> <div key={group.id} onClick={() => setSelectedGroupId(group.id)} className={`p-4 rounded-xl border-2 transition-all cursor-pointer group flex justify-between items-center ${selectedGroupId === group.id ? 'border-blue-500 bg-blue-50/20' : 'border-transparent bg-slate-50/70 hover:bg-white hover:border-slate-200'}`}>
<div className="flex-1 min-w-0 pr-2"> <div className="flex-1 min-w-0 pr-2">
<p className={`font-black text-base truncate ${selectedGroupId === group.id ? 'text-blue-600' : 'text-slate-800'}`}>{group.name}</p> <p className={`font-black text-[14px] truncate ${selectedGroupId === group.id ? 'text-blue-600' : 'text-slate-800'}`}>{group.name}</p>
<p className="text-[11px] font-bold text-slate-400 uppercase tracking-widest mt-1">{group.codes.length} </p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">{group.codes.length} </p>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button onClick={(e) => { e.stopPropagation(); setEditingGroup(group); setRenameValue(group.name); setShowRenameModal(true); }} className="p-2 opacity-0 group-hover:opacity-100 hover:bg-blue-50 text-slate-300 hover:text-blue-500 rounded-xl transition-all"><Edit3 size={16} /></button> <button onClick={(e) => { e.stopPropagation(); setEditingGroup(group); setRenameValue(group.name); setShowRenameModal(true); }} className="p-1.5 opacity-0 group-hover:opacity-100 hover:bg-blue-50 text-slate-300 hover:text-blue-500 rounded-lg transition-all"><Edit3 size={14} /></button>
<button onClick={(e) => { e.stopPropagation(); handleDeleteGroup(group.id); }} className="p-2 opacity-0 group-hover:opacity-100 hover:bg-rose-50 text-slate-300 hover:text-rose-500 rounded-xl transition-all"><Trash2 size={16} /></button> <button onClick={(e) => { e.stopPropagation(); handleDeleteGroup(group.id); }} className="p-1.5 opacity-0 group-hover:opacity-100 hover:bg-rose-50 text-slate-300 hover:text-rose-500 rounded-lg transition-all"><Trash2 size={14} /></button>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="lg:col-span-3 space-y-10"> <div className="lg:col-span-3 space-y-6">
<div className="bg-white p-12 rounded-[4rem] shadow-sm border border-slate-100 flex flex-col h-[750px]"> <div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px]">
{selectedGroup ? ( {selectedGroup ? (
<> <>
<div className="flex justify-between items-center mb-12"> <div className="flex justify-between items-center mb-8">
<div> <div>
<h3 className="text-3xl font-black text-slate-900 italic tracking-tighter uppercase mb-3">{selectedGroup.name}</h3> <h3 className="text-[20px] font-black text-slate-900 italic tracking-tighter uppercase mb-2">{selectedGroup.name}</h3>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<span className="text-[11px] font-black text-slate-400 tracking-widest bg-slate-100 px-4 py-1.5 rounded-full"> </span> <span className="text-[10px] font-black text-slate-400 tracking-widest bg-slate-50 px-3 py-1 rounded-full"> </span>
</div> </div>
</div> </div>
<div className="relative w-80"> <div className="relative w-64">
<Search className="absolute left-5 top-1/2 -translate-y-1/2 text-slate-400" size={20} /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
<input type="text" placeholder="종목 추가..." className="w-full pl-14 pr-6 py-4 bg-slate-50 border-2 border-transparent rounded-[1.8rem] focus:border-blue-500 focus:bg-white outline-none text-sm font-bold shadow-inner" value={stockSearch} onChange={(e) => setStockSearch(e.target.value)} /> <input type="text" placeholder="종목 추가..." className="w-full pl-11 pr-4 py-2 bg-slate-50 border-2 border-transparent rounded-xl focus:border-blue-500 focus:bg-white outline-none text-[12px] font-bold shadow-sm" value={stockSearch} onChange={(e) => setStockSearch(e.target.value)} />
{filteredSearchStocks.length > 0 && ( {filteredSearchStocks.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-3 bg-white border border-slate-100 shadow-2xl rounded-[2.5rem] overflow-hidden z-[50]"> <div className="absolute top-full left-0 right-0 mt-2 bg-white border border-slate-100 shadow-xl rounded-2xl overflow-hidden z-[50]">
{filteredSearchStocks.map(s => ( {filteredSearchStocks.map(s => (
<div key={s.code} onClick={() => handleAddStockToGroup(s.code)} className="p-5 hover:bg-blue-50 cursor-pointer flex justify-between items-center border-b last:border-none border-slate-50"> <div key={s.code} onClick={() => handleAddStockToGroup(s.code)} className="p-3 hover:bg-blue-50 cursor-pointer flex justify-between items-center border-b last:border-none border-slate-50">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center font-black text-slate-400 text-[10px]">{s.code.substring(0,2)}</div> <div className="w-8 h-8 bg-slate-100 rounded-lg flex items-center justify-center font-black text-slate-400 text-[9px]">{s.code.substring(0,2)}</div>
<p className="font-black text-slate-800 text-sm">{s.name}</p> <p className="font-black text-slate-800 text-[12px]">{s.name}</p>
</div> </div>
<Plus size={18} className="text-blue-600" /> <Plus size={14} className="text-blue-600" />
</div> </div>
))} ))}
</div> </div>
@@ -201,11 +201,21 @@ const WatchlistManagement: React.FC<WatchlistManagementProps> = ({ marketMode, s
{/* 모달은 기존 코드 유지 (생략 가능하나 유저 요청에 따라 전체 포함) */} {/* 모달은 기존 코드 유지 (생략 가능하나 유저 요청에 따라 전체 포함) */}
{showAddGroupModal && ( {showAddGroupModal && (
<div className="fixed inset-0 z-[150] bg-slate-900/70 backdrop-blur-md flex items-center justify-center p-6"> <div className="fixed inset-0 z-[150] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-6">
<div className="bg-white w-full max-w-lg rounded-[3.5rem] p-12 shadow-2xl border border-slate-200"> <div className="bg-white w-full max-w-sm rounded-2xl p-6 shadow-2xl border border-slate-200">
<div className="flex justify-between items-center mb-10"><h3 className="text-2xl font-black text-slate-900 uppercase tracking-tight"> </h3><button onClick={() => setShowAddGroupModal(false)}><X size={28} className="text-slate-400" /></button></div> <div className="flex justify-between items-center mb-6"><h3 className="text-[18px] font-black text-slate-900 uppercase tracking-tight"> </h3><button onClick={() => setShowAddGroupModal(false)}><X size={24} className="text-slate-400" /></button></div>
<input type="text" className="w-full p-6 bg-slate-50 border-2 border-transparent focus:border-blue-500 rounded-3xl font-black text-lg" placeholder="그룹 명칭" value={newGroupName} onChange={(e) => setNewGroupName(e.target.value)} /> <input type="text" className="w-full p-4 bg-slate-50 border-2 border-transparent focus:border-blue-500 rounded-xl font-black text-sm outline-none" placeholder="그룹 명칭" value={newGroupName} onChange={(e) => setNewGroupName(e.target.value)} />
<button onClick={handleAddGroup} className="w-full py-5 bg-blue-600 text-white rounded-[2rem] font-black mt-8"> </button> <button onClick={handleAddGroup} className="w-full py-3 bg-blue-600 text-white rounded-xl font-black text-sm mt-6 shadow-lg shadow-blue-100 active:scale-[0.98] transition-all"> </button>
</div>
</div>
)}
{showRenameModal && (
<div className="fixed inset-0 z-[150] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-6">
<div className="bg-white w-full max-w-sm rounded-2xl p-6 shadow-2xl border border-slate-200">
<div className="flex justify-between items-center mb-6"><h3 className="text-[18px] font-black text-slate-900 uppercase tracking-tight"> </h3><button onClick={() => setShowRenameModal(false)}><X size={24} className="text-slate-400" /></button></div>
<input type="text" className="w-full p-4 bg-slate-50 border-2 border-transparent focus:border-blue-500 rounded-xl font-black text-sm outline-none" placeholder="새로운 명칭" value={renameValue} onChange={(e) => setRenameValue(e.target.value)} />
<button onClick={handleRenameGroup} className="w-full py-3 bg-blue-600 text-white rounded-xl font-black text-sm mt-6 shadow-lg shadow-blue-100 active:scale-[0.98] transition-all"> </button>
</div> </div>
</div> </div>
)} )}