디자인업데이트

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

@@ -2,7 +2,7 @@
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 { ApiSettings, AiConfig } from '../types';
import { InputGroup, ToggleButton } from '../components/CommonUI';
import { InputGroup, ToggleButton, SelectGroup } from '../components/CommonUI';
interface SettingsProps {
settings: ApiSettings;
@@ -56,202 +56,227 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave }) => {
};
return (
<div className="max-w-5xl space-y-10 animate-in fade-in duration-500 pb-20 mx-auto">
<div className="bg-white p-12 rounded-[3.5rem] shadow-sm border border-slate-100">
<form onSubmit={handleSubmit} className="space-y-14">
{/* KIS API Section */}
<section>
<div className="flex items-center justify-between mb-10">
<h4 className="text-[12px] font-black text-slate-400 uppercase tracking-[0.25em] flex items-center gap-3">
<Key size={20} /> KIS API
</h4>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<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" />
<div className="md:col-span-2">
<InputGroup label="계좌 번호" value={formData.accountNumber} onChange={(v) => setFormData({...formData, accountNumber: v})} placeholder="예: 50061234-01" />
</div>
</div>
</section>
{/* AI 분석 자동화 설정 섹션 */}
<section className="bg-blue-50/20 p-10 rounded-[2.5rem] border border-blue-100">
<div className="flex items-center gap-4 mb-10">
<div className="p-3 bg-blue-600 text-white rounded-2xl shadow-lg shadow-blue-100">
<Zap size={24} />
</div>
<div>
<h4 className="text-[12px] font-black text-slate-800 uppercase tracking-raw-2 mt-0.5">AI </h4>
<p className="text-[10px] 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-8">
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
<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 || ''}
onChange={(e) => setFormData({...formData, preferredNewsAiId: e.target.value})}
>
<option value=""> </option>
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
<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 || ''}
onChange={(e) => setFormData({...formData, preferredStockAiId: e.target.value})}
>
<option value=""> </option>
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
<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 || ''}
onChange={(e) => setFormData({...formData, preferredNewsJudgementAiId: e.target.value})}
>
<option value=""> </option>
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
<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 || ''}
onChange={(e) => setFormData({...formData, preferredAutoBuyAiId: e.target.value})}
>
<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">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
<MinusCircle size={14} className="text-blue-600" />
</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.preferredAutoSellAiId || ''}
onChange={(e) => setFormData({...formData, preferredAutoSellAiId: e.target.value})}
>
<option value=""> </option>
{formData.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
</div>
</section>
{/* Telegram Notification Section */}
<section className="bg-slate-50 p-10 rounded-[2.5rem] border border-slate-100">
<div className="flex items-center justify-between mb-10">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-2xl ${formData.useTelegram ? 'bg-blue-100 text-blue-600' : 'bg-slate-200 text-slate-400'}`}>
<MessageCircle size={24} />
<div className="w-full space-y-6 animate-in fade-in duration-500 pb-20">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* Left Column: API & AI */}
<div className="space-y-6">
{/* KIS API Section Card */}
<section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-600 text-white rounded-xl shadow-lg shadow-blue-50">
<Key size={20} />
</div>
<div>
<h4 className="text-[12px] font-black text-slate-800 uppercase tracking-[0.2em]"> </h4>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p>
<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>
<ToggleButton active={formData.useTelegram} onClick={() => toggleService('useTelegram')} />
</div>
{formData.useTelegram && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in slide-in-from-top-4 duration-300">
<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.appSecret} onChange={(v) => setFormData({...formData, appSecret: v})} type="password" placeholder="Secret Key" />
<div className="md:col-span-2">
<InputGroup label="계좌 번호" value={formData.accountNumber} onChange={(v) => setFormData({...formData, accountNumber: v})} placeholder="예: 50061234-01" />
</div>
</div>
</section>
{/* AI 분석 자동화 설정 Card */}
<section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-600 text-white rounded-xl shadow-lg shadow-blue-50">
<Zap size={20} />
</div>
<div>
<h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight">AI </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">
<SelectGroup
label="뉴스 분석 Engine"
icon={<Newspaper size={14} className="text-blue-500" />}
value={formData.preferredNewsAiId || ''}
onChange={(v) => setFormData({...formData, preferredNewsAiId: v})}
options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
/>
<SelectGroup
label="종목 분석 Engine"
icon={<BarChart4 size={14} className="text-purple-500" />}
value={formData.preferredStockAiId || ''}
onChange={(v) => setFormData({...formData, preferredStockAiId: v})}
options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
/>
<SelectGroup
label="뉴스 판단 Engine"
icon={<Scale size={14} className="text-amber-500" />}
value={formData.preferredNewsJudgementAiId || ''}
onChange={(v) => setFormData({...formData, preferredNewsJudgementAiId: v})}
options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
/>
<SelectGroup
label="자동매수 Engine"
icon={<PlusCircle size={14} className="text-rose-500" />}
value={formData.preferredAutoBuyAiId || ''}
onChange={(v) => setFormData({...formData, preferredAutoBuyAiId: v})}
options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
/>
<div className="md:col-span-2">
<SelectGroup
label="자동매도 Engine"
icon={<MinusCircle size={14} className="text-blue-600" />}
value={formData.preferredAutoSellAiId || ''}
onChange={(v) => setFormData({...formData, preferredAutoSellAiId: v})}
options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]}
/>
</div>
</div>
</section>
</div>
{/* Right Column: Extensions & Notifications & AI Management */}
<div className="space-y-6">
{/* AI Engine Management Card moved here */}
<section className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div className="flex items-center justify-between mb-8">
<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>
<h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight">AI </h4>
<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>
<ToggleButton active={formData.useTelegram} onClick={() => toggleService('useTelegram')} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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" />
</div>
)}
</section>
</section>
{/* Naver News API Section */}
<section className="bg-slate-50 p-10 rounded-[2.5rem] border border-slate-100">
<div className="flex items-center justify-between mb-10">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-2xl ${formData.useNaverNews ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-200 text-slate-400'}`}>
<Globe size={24} />
</div>
<div>
<h4 className="text-[12px] font-black text-slate-800 uppercase tracking-[0.2em]"> </h4>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5"> </p>
<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.useNaverNews ? 'bg-emerald-100 text-emerald-600 shadow-emerald-50' : 'bg-slate-200 text-slate-400 shadow-slate-50'}`}>
<Globe 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>
<ToggleButton active={formData.useNaverNews} onClick={() => toggleService('useNaverNews')} />
</div>
<ToggleButton active={formData.useNaverNews} onClick={() => toggleService('useNaverNews')} />
</div>
{formData.useNaverNews && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in slide-in-from-top-4 duration-300">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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" />
</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="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">
<Shield size={22} className="text-emerald-500" />
<div className="p-5 bg-slate-50/50 rounded-xl border border-slate-100 border-dashed text-center">
<p className="text-[11px] font-black text-slate-400 uppercase tracking-widest"> </p>
</div>
<button
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'}`}
>
{isSaved ? <><Check size={24} /> </> : <><Save size={24} /> </>}
</button>
</div>
</form>
</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>
<button
type="submit"
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={18} /> </> : <><Save size={18} /> </>}
</button>
</div>
</form>
{/* AI Engine Modal */}
{showAiModal && (
<div className="fixed inset-0 z-[150] bg-slate-900/60 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="flex justify-between items-center mb-10">
<h3 className="text-2xl font-black text-slate-900 flex items-center gap-3 uppercase tracking-tight">
<Cpu className="text-blue-600" /> AI
<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 animate-in zoom-in-95 duration-200 border border-slate-100">
<div className="flex justify-between items-center mb-8">
<h3 className="text-[18px] font-black text-slate-900 flex items-center gap-2 uppercase tracking-tight">
<Cpu size={20} className="text-blue-600" /> AI
</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 className="space-y-6">
<InputGroup label="엔진 식별 이름" value={editingAi?.name || ''} onChange={(v) => setEditingAi({...editingAi, name: v})} placeholder="예: 구글 고성능 모델, Ollama Llama3" />
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1"> </label>
<div className="flex bg-slate-100 p-1.5 rounded-2xl">
<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: '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>
<div className="space-y-5">
<InputGroup label="엔진 식별 이름" value={editingAi?.name || ''} onChange={(v) => setEditingAi({...editingAi, name: v})} placeholder="예: 구글 고성능 모델" />
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1"></label>
<div className="flex bg-slate-100 p-1 rounded-xl">
<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-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>
<InputGroup label="모델명" value={editingAi?.modelName || ''} onChange={(v) => setEditingAi({...editingAi, modelName: v})} placeholder={editingAi?.providerType === 'gemini' ? 'gemini-3-flash-preview' : 'llama3'} />
{editingAi?.providerType === 'openai-compatible' && (
<InputGroup label="베이스 URL (API End-point)" value={editingAi?.baseUrl || ''} onChange={(v) => setEditingAi({...editingAi, baseUrl: v})} placeholder="http://localhost:11434/v1" />
)}
<InputGroup label="모델명" value={editingAi?.modelName || ''} onChange={(v) => setEditingAi({...editingAi, modelName: v})} placeholder="gemini-pros" />
<button
type="button"
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>
</div>
</div>