/** * 설정 페이지 * macOS 시스템 환경설정 스타일 — 좌측 카테고리 목록 + 우측 설정 패널 */ import React, { useState, useEffect, useCallback } from 'react'; import type { Theme } from '../types'; import { saveLiveStrategySettings, type LiveStrategySettingsDto, } from '../utils/backendApi'; import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync'; import { BacktestSettingsPanel } from './BacktestSettingsPanel'; import IndicatorMainDefaultsPanel, { type IndicatorSaveUiState, } from './IndicatorMainDefaultsPanel'; import type { IndicatorConfig } from '../types'; import type { PlotDef, HLineDef } from '../utils/indicatorRegistry'; import type { IchimokuCloudColors } from '../utils/ichimokuConfig'; import ChartTimeFormatPicker from './ChartTimeFormatPicker'; import DesktopUpdatePanel from './DesktopUpdatePanel'; import { TRADE_ALERT_SOUND_OPTIONS, normalizeTradeAlertSoundId, previewTradeAlertSound, } from '../utils/tradeAlertSound'; import { TRADE_ALERT_GRID_COL_OPTIONS, TRADE_ALERT_POPUP_LAYOUTS, TRADE_ALERT_POPUP_POSITIONS, normalizeTradeAlertGridCols, normalizeTradeAlertPopupLayout, normalizeTradeAlertPopupPosition, } from '../utils/tradeAlertPopupLayout'; import TradeAlertPopupPreview from './TradeAlertPopupPreview'; import TrendSearchSettingsPanel from './trendSearch/TrendSearchSettingsPanel'; import type { TrendSearchAppSettings } from '../utils/trendSearchAppSettings'; import { isTradingSettingsCategory } from '../utils/tradingAccess'; import { CHART_LEGEND_SETTING_ITEMS, type ChartLegendVisibility, } from '../types/chartLegend'; import type { ChartPaneSeparatorOptions, PaneSeparatorStyle } from '../types/chartPaneSeparator'; import { cloneChartPaneSeparatorOptions } from '../types/chartPaneSeparator'; import ColorInput from './ColorInput'; import { LINE_STYLE_LABELS, LINE_STYLE_OPTIONS, LINE_WIDTH_OPTIONS, } from '../utils/plotColorUtils'; import TimezonePicker from './TimezonePicker'; import AdminPasswordGate from './AdminPasswordGate'; import AdminSettingsPanel from './AdminSettingsPanel'; import { type SettingsCategoryId, settingsCategoryToMenuId, canAccessMenu, } from '../utils/permissions'; interface SettingsPageProps { theme: Theme; magnetMode?: 'off' | 'weak' | 'strong'; onMagnetMode?: (m: 'off' | 'weak' | 'strong') => void; btAutoPopup?: boolean; onBtAutoPopup?: (v: boolean) => void; btShowPrice?: boolean; onBtShowPrice?: (v: boolean) => void; /** 실시간 전략 체크 — 현재 차트 종목 */ liveMarket?: string; /** 실시간 전략 체크 — 선택 가능한 전략 목록 */ liveStrategies?: { id: number; name: string }[]; /** 마커 초기화 콜백 */ onClearLiveMarkers?: () => void; /** 매매 알림 팝업 ON/OFF */ tradeAlertPopup?: boolean; onTradeAlertPopup?: (v: boolean) => void; /** 알림 설정 탭 */ tradeAlertSoundEnabled?: boolean; onTradeAlertSoundEnabled?: (v: boolean) => void; tradeAlertSound?: string; onTradeAlertSound?: (v: string) => void; tradeAlertPopupPosition?: string; onTradeAlertPopupPosition?: (v: string) => void; tradeAlertPopupLayout?: string; onTradeAlertPopupLayout?: (v: string) => void; tradeAlertPopupGridCols?: number; onTradeAlertPopupGridCols?: (v: number) => void; liveSettings?: LiveStrategySettingsDto; onLiveSettingsChange?: (settings: LiveStrategySettingsDto) => void; watchlistCount?: number; monitoredMarkets?: string[]; /** 가상투자 세션 실행 중 — 전략 설정은 가상투자 화면에서 */ virtualDriven?: boolean; /** Main 탭 보조지표 기본 파라미터 (DB) */ getIndicatorParams?: (type: string, defaults: Record) => Record; saveIndicatorParams?: (type: string, params: Record) => void; getIndicatorVisual?: (type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[]) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors; }; saveIndicatorVisual?: ( type: string, plots?: PlotDef[], hlines?: HLineDef[], cloudColors?: IchimokuCloudColors, ) => void; /** 설정 화면 — 보조지표 기본값 일괄 DB 저장 */ saveAllIndicatorDefaults?: (configs: Record) => Promise; /** 보조지표 DB 캐시 갱신 revision */ indicatorSettingsRevision?: number; /** 설정 화면에서 차트 on/off 연동 */ chartIndicators?: IndicatorConfig[]; onAddIndicatorToChart?: (type: string, config?: IndicatorConfig) => void; onRemoveIndicatorFromChart?: (type: string) => void; /** 보조지표 설정 목록 순서 변경 → 차트 pane 순서 */ onIndicatorListOrderChange?: (orderedTypes: string[]) => void; chartCandleAreaPriceLabels?: boolean; onChartCandleAreaPriceLabels?: (v: boolean) => void; chartSeriesPriceLabels?: boolean; onChartSeriesPriceLabels?: (v: boolean) => void; chartVolumeVisible?: boolean; onChartVolumeVisible?: (v: boolean) => void; chartLiveReceiveHighlight?: boolean; onChartLiveReceiveHighlight?: (v: boolean) => void; chartLegendOptions?: ChartLegendVisibility; onChartLegendOptionsChange?: (patch: Partial) => void; chartCrosshairInfoVisible?: boolean; onChartCrosshairInfoVisible?: (v: boolean) => void; chartHoverToolbarVisible?: boolean; onChartHoverToolbarVisible?: (v: boolean) => void; chartPaneSeparator?: ChartPaneSeparatorOptions; onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void; paperTradingEnabled?: boolean; onPaperTradingEnabled?: (v: boolean) => void; paperInitialCapital?: number; onPaperInitialCapital?: (v: number) => void; paperFeeRatePct?: number; onPaperFeeRatePct?: (v: number) => void; paperSlippagePct?: number; onPaperSlippagePct?: (v: number) => void; paperMinOrderKrw?: number; onPaperMinOrderKrw?: (v: number) => void; paperAutoTradeEnabled?: boolean; onPaperAutoTradeEnabled?: (v: boolean) => void; paperAutoTradeBudgetPct?: number; onPaperAutoTradeBudgetPct?: (v: number) => void; paperOrderFillMode?: 'LIMIT_ONLY' | 'NEXT_TICK' | 'AUTO_CANCEL'; onPaperOrderFillMode?: (v: 'LIMIT_ONLY' | 'NEXT_TICK' | 'AUTO_CANCEL') => void; paperOrderAutoCancelPct?: number; onPaperOrderAutoCancelPct?: (v: number) => void; virtualTargetMaxCount?: number; onVirtualTargetMaxCount?: (v: number) => void; onPaperAccountReset?: () => void; tradingMode?: string; onTradingMode?: (v: string) => void; liveAutoTradeEnabled?: boolean; onLiveAutoTradeEnabled?: (v: boolean) => void; hasUpbitKeys?: boolean; upbitAccessKeyMasked?: string | null; onUpbitKeys?: (access: string, secret: string) => void; chartRealtimeSource?: string; onChartRealtimeSource?: (v: string) => void; liveAutoTradeBudgetPct?: number; onLiveAutoTradeBudgetPct?: (v: number) => void; fcmPushEnabled?: boolean; onFcmPushEnabled?: (v: boolean) => void; onFcmTest?: () => void; displayTimezone?: string; onDisplayTimezoneChange?: (tz: string) => void; chartTimeFormat?: string; onChartTimeFormatChange?: (format: string) => void; tradeAlertTimeFormat?: string; onTradeAlertTimeFormatChange?: (format: string) => void; menuPermissions?: Record; verificationIssueNotify?: boolean; onVerificationIssueNotify?: (v: boolean) => void; trendSearchSettings?: TrendSearchAppSettings; onTrendSearchSettingsChange?: (s: TrendSearchAppSettings) => void; /** 설정 화면 진입 시 선택할 카테고리 (예: 모의투자 화면에서 링크) */ initialCategory?: SettingsCategoryId; /** 정식 로그인 여부 (게스트는 매매·전략 설정 탭 차단) */ isFormalLogin?: boolean; onRequireFormalLogin?: () => void; } // ── 카테고리 정의 ──────────────────────────────────────────────────────────── type CategoryId = SettingsCategoryId; interface Category { id: CategoryId; label: string; icon: React.ReactNode; desc: string; } const IcGeneral = () => ( ); const IcChart = () => ( ); const IcStrategy = () => ( ); const IcAlert = () => ( ); const IcNetwork = () => ( ); const IcIndicators = () => ( ); const IcPaper = () => ( ); const IcVirtual = () => ( ); const IcTrading = () => ( ); const IcLive = () => ( ); const IcTrendSearch = () => ( ); const IcBacktest = () => ( ); const IcAdmin = () => ( ); const ALL_CATEGORIES: Category[] = [ { id: 'general', label: '일반 설정', icon: , desc: '언어, 테마, 날짜 형식 등 기본 설정' }, { id: 'chart', label: '차트 설정', icon: , desc: '캔들 색상, 격자선, 크로스헤어, 가격축 등' }, { id: 'indicators', label: '보조지표 설정', icon: , desc: '지표 추가 Main 탭 보조지표 기본 파라미터·색상·기준선' }, { id: 'backtest', label: '백테스팅', icon: , desc: '백테스팅 실행 옵션, 자본·비용·리스크 관리 설정' }, { id: 'strategy', label: '전략 설정', icon: , desc: '투자 전략 기본 파라미터 및 조건 설정' }, { id: 'trading', label: '매매설정', icon: , desc: '모의투자·가상매매 통합 설정 — 자동매매, 미체결 주문 처리, 계좌·비용 등' }, { id: 'live', label: '실거래', icon: , desc: '매매 운영 모드, 업비트 API 키, 실거래 자동매매' }, { id: 'trend-search', label: '추세검색', icon: , desc: '상승추세 배점, 합격 점수, 결과 수, 자동갱신·투자대상 자동추가' }, { id: 'alert', label: '알림 설정', icon: , desc: '가격 알림, 신호 알림, 알림 방식 설정' }, { id: 'network', label: '네트워크', icon: , desc: 'API 서버 주소, WebSocket, 연결 상태' }, { id: 'admin', label: '관리자 설정', icon: , desc: '사용자·역할·메뉴 접근 권한 관리 (관리자 비밀번호 필요)' }, ]; function filterCategories(permissions?: Record): Category[] { return ALL_CATEGORIES.filter(c => canAccessMenu(permissions, settingsCategoryToMenuId(c.id)), ); } // ── 설정 행 공통 컴포넌트 ──────────────────────────────────────────────────── const SettingRow: React.FC<{ label: string; desc?: string; className?: string; children: React.ReactNode; }> = ({ label, desc, className, children }) => (
{label} {desc && {desc}}
{children}
); const SettingSection: React.FC<{ title: string; children: React.ReactNode; grid?: boolean }> = ({ title, children, grid, }) => (
{title}
{children}
); const StgCard: React.FC<{ label: string; desc?: string; full?: boolean; children: React.ReactNode; }> = ({ label, desc, full, children }) => (
{label} {desc && {desc}}
{children}
); // ── 각 카테고리 패널 ───────────────────────────────────────────────────────── interface PaperTradingSettingsPanelProps { paperTradingEnabled?: boolean; onPaperTradingEnabled?: (v: boolean) => void; paperInitialCapital?: number; onPaperInitialCapital?: (v: number) => void; paperFeeRatePct?: number; onPaperFeeRatePct?: (v: number) => void; paperSlippagePct?: number; onPaperSlippagePct?: (v: number) => void; paperMinOrderKrw?: number; onPaperMinOrderKrw?: (v: number) => void; paperAutoTradeEnabled?: boolean; onPaperAutoTradeEnabled?: (v: boolean) => void; paperAutoTradeBudgetPct?: number; onPaperAutoTradeBudgetPct?: (v: number) => void; onPaperAccountReset?: () => void; } const PaperTradingSettingsPanel: React.FC = ({ paperTradingEnabled = true, onPaperTradingEnabled, paperInitialCapital = 10_000_000, onPaperInitialCapital, paperFeeRatePct = 0.05, onPaperFeeRatePct, paperSlippagePct = 0, onPaperSlippagePct, paperMinOrderKrw = 5000, onPaperMinOrderKrw, paperAutoTradeEnabled = false, onPaperAutoTradeEnabled, paperAutoTradeBudgetPct = 95, onPaperAutoTradeBudgetPct, onPaperAccountReset, }) => ( <> onPaperAutoTradeBudgetPct?.(Number(e.target.value))} /> onPaperInitialCapital?.(Number(e.target.value))} /> onPaperFeeRatePct?.(Number(e.target.value))} /> onPaperSlippagePct?.(Number(e.target.value))} /> onPaperMinOrderKrw?.(Number(e.target.value))} /> ); // ── 통합 매매설정 패널 ──────────────────────────────────────────────────────── interface TradingSettingsPanelProps { paperTradingEnabled?: boolean; onPaperTradingEnabled?: (v: boolean) => void; paperInitialCapital?: number; onPaperInitialCapital?: (v: number) => void; paperFeeRatePct?: number; onPaperFeeRatePct?: (v: number) => void; paperSlippagePct?: number; onPaperSlippagePct?: (v: number) => void; paperMinOrderKrw?: number; onPaperMinOrderKrw?: (v: number) => void; paperAutoTradeEnabled?: boolean; onPaperAutoTradeEnabled?: (v: boolean) => void; paperAutoTradeBudgetPct?: number; onPaperAutoTradeBudgetPct?: (v: number) => void; paperOrderFillMode?: 'LIMIT_ONLY' | 'NEXT_TICK' | 'AUTO_CANCEL'; onPaperOrderFillMode?: (v: 'LIMIT_ONLY' | 'NEXT_TICK' | 'AUTO_CANCEL') => void; paperOrderAutoCancelPct?: number; onPaperOrderAutoCancelPct?: (v: number) => void; virtualTargetMaxCount?: number; onVirtualTargetMaxCount?: (v: number) => void; onPaperAccountReset?: () => void; } const TradingSettingsPanel: React.FC = ({ paperTradingEnabled = true, onPaperTradingEnabled, paperInitialCapital = 10_000_000, onPaperInitialCapital, paperFeeRatePct = 0.05, onPaperFeeRatePct, paperSlippagePct = 0, onPaperSlippagePct, paperMinOrderKrw = 5000, onPaperMinOrderKrw, paperAutoTradeEnabled = false, onPaperAutoTradeEnabled, paperAutoTradeBudgetPct = 95, onPaperAutoTradeBudgetPct, paperOrderFillMode = 'LIMIT_ONLY', onPaperOrderFillMode, paperOrderAutoCancelPct = 1.0, onPaperOrderAutoCancelPct, virtualTargetMaxCount = 20, onVirtualTargetMaxCount, onPaperAccountReset, }) => ( <> {/* ── 모의투자 ────────────────────────────────────────── */} {/* ── 자동매매 ─────────────────────────────────────────── */} onPaperAutoTradeBudgetPct?.(Number(e.target.value))} /> {/* ── 미체결 주문 처리 ─────────────────────────────────── */}
{paperOrderFillMode === 'AUTO_CANCEL' && ( onPaperOrderAutoCancelPct?.(Number(e.target.value))} /> % )}
{/* ── 계좌·비용 ─────────────────────────────────────────── */} onPaperInitialCapital?.(Number(e.target.value))} /> onPaperFeeRatePct?.(Number(e.target.value))} /> onPaperSlippagePct?.(Number(e.target.value))} /> onPaperMinOrderKrw?.(Number(e.target.value))} /> {/* ── 가상매매 ─────────────────────────────────────────── */} onVirtualTargetMaxCount?.(Number(e.target.value))} /> {/* ── 계좌 관리 ─────────────────────────────────────────── */} ); const LiveTradingSettingsPanel: React.FC<{ tradingMode?: string; onTradingMode?: (v: string) => void; liveAutoTradeEnabled?: boolean; onLiveAutoTradeEnabled?: (v: boolean) => void; hasUpbitKeys?: boolean; upbitAccessKeyMasked?: string | null; onUpbitKeys?: (access: string, secret: string) => void; liveAutoTradeBudgetPct?: number; onLiveAutoTradeBudgetPct?: (v: number) => void; }> = ({ tradingMode = 'PAPER', onTradingMode, liveAutoTradeEnabled = false, onLiveAutoTradeEnabled, hasUpbitKeys = false, upbitAccessKeyMasked = null, onUpbitKeys, liveAutoTradeBudgetPct = 95, onLiveAutoTradeBudgetPct, }) => { const [accessKey, setAccessKey] = useState(''); const [secretKey, setSecretKey] = useState(''); return ( <> {hasUpbitKeys ? '연결됨' : '미등록'} setAccessKey(e.target.value)} autoComplete="off" spellCheck={false} /> setSecretKey(e.target.value)} autoComplete="off" spellCheck={false} /> {(accessKey.trim() || secretKey.trim()) && ( )} onLiveAutoTradeBudgetPct?.(Number(e.target.value))} /> % ); }; // ── 일반 설정 패널 ──────────────────────────────────────────────────────────── const GeneralPanel: React.FC<{ displayTimezone: string; onDisplayTimezoneChange: (tz: string) => void; chartTimeFormat?: string; onChartTimeFormatChange?: (format: string) => void; tradeAlertTimeFormat?: string; onTradeAlertTimeFormatChange?: (format: string) => void; verificationIssueNotify?: boolean; onVerificationIssueNotify?: (v: boolean) => void; showVerificationNotify?: boolean; }> = ({ displayTimezone, onDisplayTimezoneChange, chartTimeFormat = 'MM-dd HH:mm', onChartTimeFormatChange, tradeAlertTimeFormat = 'MM-dd HH:mm', onTradeAlertTimeFormatChange, verificationIssueNotify = true, onVerificationIssueNotify, showVerificationNotify = true, }) => { const [lang, setLang] = useState('ko'); const [startPage, setStart] = useState('chart'); const [animations, setAnim] = useState(true); const [tooltips, setTool] = useState(true); return ( <> onChartTimeFormatChange?.(fmt)} /> onTradeAlertTimeFormatChange?.(fmt)} /> {showVerificationNotify && ( )} ); }; function PaneSeparatorStyleFields({ style, onChange, }: { style: PaneSeparatorStyle; onChange: (next: PaneSeparatorStyle) => void; }) { return ( <> {style.visible && ( <> onChange({ ...style, color })} showCompactNative={false} /> )} ); } interface ChartPanelProps { chartRealtimeSource?: string; onChartRealtimeSource?: (v: string) => void; magnetMode?: 'off' | 'weak' | 'strong'; onMagnetMode?: (m: 'off' | 'weak' | 'strong') => void; chartCandleAreaPriceLabels?: boolean; onChartCandleAreaPriceLabels?: (v: boolean) => void; chartSeriesPriceLabels?: boolean; onChartSeriesPriceLabels?: (v: boolean) => void; chartVolumeVisible?: boolean; onChartVolumeVisible?: (v: boolean) => void; chartLiveReceiveHighlight?: boolean; onChartLiveReceiveHighlight?: (v: boolean) => void; chartLegendOptions?: ChartLegendVisibility; onChartLegendOptionsChange?: (patch: Partial) => void; chartCrosshairInfoVisible?: boolean; onChartCrosshairInfoVisible?: (v: boolean) => void; chartHoverToolbarVisible?: boolean; onChartHoverToolbarVisible?: (v: boolean) => void; chartPaneSeparator?: ChartPaneSeparatorOptions; onChartPaneSeparatorChange?: (opts: ChartPaneSeparatorOptions) => void; chartTimeFormat?: string; onChartTimeFormatChange?: (format: string) => void; btShowPrice?: boolean; onBtShowPrice?: (v: boolean) => void; } const ChartPanel: React.FC = ({ chartRealtimeSource = 'BACKEND_STOMP', onChartRealtimeSource, magnetMode = 'off', onMagnetMode, chartCandleAreaPriceLabels = true, onChartCandleAreaPriceLabels, chartSeriesPriceLabels = true, onChartSeriesPriceLabels, chartVolumeVisible = true, onChartVolumeVisible, chartLiveReceiveHighlight = true, onChartLiveReceiveHighlight, chartLegendOptions, onChartLegendOptionsChange, chartCrosshairInfoVisible = true, onChartCrosshairInfoVisible, chartHoverToolbarVisible = true, onChartHoverToolbarVisible, chartPaneSeparator, onChartPaneSeparatorChange, chartTimeFormat = 'MM-dd HH:mm', onChartTimeFormatChange, btShowPrice = true, onBtShowPrice, }) => { const [upColor, setUp] = useState('#26a69a'); const [downColor, setDown] = useState('#ef5350'); const [bgColor, setBg] = useState('#131722'); const [gridLines, setGrid] = useState(true); const [crosshair, setCh] = useState('normal'); const [priceScale, setPs] = useState('right'); const [precision, setPrec] = useState('2'); return ( <> onChartTimeFormatChange?.(fmt)} />
setUp(e.target.value)} /> {upColor}
setDown(e.target.value)} /> {downColor}
setBg(e.target.value)} /> {bgColor}
캔들 차트 영역 캔들·거래량 pane 위에 겹쳐 표시되는 지표(이동평균·일목 등)
보조지표 영역 캔들 아래 별도 pane에 표시되는 RSI·MACD 등
{chartPaneSeparator && onChartPaneSeparatorChange && (
캔들·보조지표 캔들(·거래량) 영역과 첫 보조지표 pane 사이
onChartPaneSeparatorChange( cloneChartPaneSeparatorOptions({ ...chartPaneSeparator, mainToIndicator, }), )} />
보조지표 간 보조지표 pane 사이
onChartPaneSeparatorChange( cloneChartPaneSeparatorOptions({ ...chartPaneSeparator, indicatorToIndicator, }), )} />
)} {chartLegendOptions && onChartLegendOptionsChange && ( )} {chartLegendOptions && onChartLegendOptionsChange && ( {CHART_LEGEND_SETTING_ITEMS.map(({ key, label, desc }) => ( ))} )} ); }; interface StrategyPanelProps { liveMarket?: string; liveStrategies?: { id: number; name: string }[]; liveSettings?: LiveStrategySettingsDto; watchlistCount?: number; monitoredMarkets?: string[]; virtualDriven?: boolean; onClearLiveMarkers?: () => void; tradeAlertPopup?: boolean; onTradeAlertPopup?: (v: boolean) => void; onLiveSettingsChange?: (settings: LiveStrategySettingsDto) => void; } const StrategyPanel: React.FC = ({ liveMarket = 'KRW-BTC', liveStrategies = [], liveSettings: liveSettingsProp, watchlistCount = 0, monitoredMarkets = [], virtualDriven = false, onClearLiveMarkers, tradeAlertPopup = true, onTradeAlertPopup, onLiveSettingsChange, }) => { const [defaultRisk, setRisk] = useState('1'); const [maxPos, setMax] = useState('5'); const [trailingStop, setTrail] = useState(false); const [reentry, setRe] = useState(false); const [backtestFee, setFee] = useState('0.05'); const [slippage, setSlip] = useState('0.01'); const [evalMetric, setEval] = useState('sharpe'); // ── 실시간 전략 체크 설정 상태 ────────────────────────────────────────────── const [liveSettings, setLiveSettings] = useState( liveSettingsProp ?? { market: liveMarket, strategyId: null, isLiveCheck: false, executionType: 'CANDLE_CLOSE', }, ); const [liveSaving, setLiveSaving] = useState(false); useEffect(() => { if (liveSettingsProp) setLiveSettings(liveSettingsProp); }, [liveSettingsProp]); const persistLive = useCallback(async (patch: Partial) => { if (virtualDriven) return; const next: LiveStrategySettingsDto = { ...liveSettings, ...patch, market: liveMarket }; if (next.isLiveCheck && next.strategyId != null) { const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId); if (!synced) return; } setLiveSaving(true); try { const saved = await saveLiveStrategySettings(next); if (saved) { setLiveSettings(saved); onLiveSettingsChange?.(saved); } onClearLiveMarkers?.(); } finally { setLiveSaving(false); } }, [liveSettings, liveMarket, onClearLiveMarkers, onLiveSettingsChange, virtualDriven]); const isOn = liveSettings.isLiveCheck; const execType = liveSettings.executionType; const stratId = liveSettings.strategyId; const monCount = monitoredMarkets.length; return ( <> {/* ── 실시간 전략 체크 ─────────────────────────────────────────────── */} {virtualDriven && (

가상투자 세션이 실행 중입니다. 전략·실행방식·시그널 모드는 가상투자 화면에서 변경하세요.

)}
관심 {watchlistCount}개 {' · '}체크 대상 0 ? '#00BCD4' : 'var(--text3)' }}>{monCount} {monCount > 0 && ( {monitoredMarkets.map(m => m.replace('KRW-', '')).join(', ')} )}
{liveSaving && 저장 중…} {liveMarket}
{isOn && (
{stratId ? `${liveStrategies.find(s => s.id === stratId)?.name ?? '전략'} 모니터링 중` : '전략을 선택하면 모니터링이 시작됩니다'}
)}
{/* ── 매매 알림 팝업 ────────────────────────────────────────────────── */} {tradeAlertPopup && (
시그널 발생 시 알림 팝업이 자동으로 표시됩니다
)}
{/* ── 리스크 관리 ──────────────────────────────────────────────────── */} setRisk(e.target.value)} /> setMax(e.target.value)} /> {/* ── 백테스팅 기본값 ──────────────────────────────────────────────── */} setFee(e.target.value)} /> setSlip(e.target.value)} /> ); }; interface AlertPanelProps { tradeAlertPopup?: boolean; onTradeAlertPopup?: (v: boolean) => void; tradeAlertSoundEnabled?: boolean; onTradeAlertSoundEnabled?: (v: boolean) => void; tradeAlertSound?: string; onTradeAlertSound?: (v: string) => void; tradeAlertPopupPosition?: string; onTradeAlertPopupPosition?: (v: string) => void; tradeAlertPopupLayout?: string; onTradeAlertPopupLayout?: (v: string) => void; tradeAlertPopupGridCols?: number; onTradeAlertPopupGridCols?: (v: number) => void; fcmPushEnabled?: boolean; onFcmPushEnabled?: (v: boolean) => void; onFcmTest?: () => void; } const AlertPanel: React.FC = ({ tradeAlertPopup = true, onTradeAlertPopup, tradeAlertSoundEnabled = true, onTradeAlertSoundEnabled, tradeAlertSound = 'bell', onTradeAlertSound, tradeAlertPopupPosition = 'right', onTradeAlertPopupPosition, tradeAlertPopupLayout = 'stack', onTradeAlertPopupLayout, tradeAlertPopupGridCols = 2, onTradeAlertPopupGridCols, fcmPushEnabled = false, onFcmPushEnabled, onFcmTest, }) => { const [browserNotif, setBn] = useState(false); const [emailAlert, setEmail] = useState(false); const [emailAddr, setAddr] = useState(''); const [priceAlert, setPa] = useState(true); const [signalAlert, setSig] = useState(true); const [cooldown, setCool] = useState('60'); const soundId = normalizeTradeAlertSoundId(tradeAlertSound); const popupPosition = normalizeTradeAlertPopupPosition(tradeAlertPopupPosition); const popupLayout = normalizeTradeAlertPopupLayout(tradeAlertPopupLayout); const gridCols = normalizeTradeAlertGridCols(tradeAlertPopupGridCols); const handlePreviewSound = () => { if (!tradeAlertSoundEnabled) { window.alert('알림 사운드가 꺼져 있습니다. 먼저 사운드를 켜 주세요.'); return; } if (soundId === 'silent') { window.alert('무음이 선택되었습니다. 알림 발생 시 소리가 재생되지 않습니다.'); return; } previewTradeAlertSound(soundId); }; return ( <> {tradeAlertPopup && ( <>
{TRADE_ALERT_POPUP_POSITIONS.map(opt => ( ))}
{TRADE_ALERT_POPUP_LAYOUTS.map(opt => ( ))}
{popupLayout === 'grid' && ( )} )}
{emailAlert && ( setAddr(e.target.value)} /> )} setCool(e.target.value)} /> ); }; const NetworkPanel: React.FC = () => { const [apiHost, setApi] = useState('http://localhost:8080'); const [wsHost, setWs] = useState('ws://localhost:8080/ws'); const [timeout, setTo] = useState('10000'); const [retries, setRt] = useState('3'); const [proxyUrl, setProxy] = useState(''); const [connected, _] = useState(true); const handleTest = () => { fetch(apiHost + '/actuator/health') .then(() => alert('✅ 서버 연결 성공')) .catch(() => alert('❌ 서버 연결 실패')); }; return ( <> setApi(e.target.value)} placeholder="http://localhost:8080" /> {connected ? '● 연결됨' : '● 연결 끊김'} setWs(e.target.value)} placeholder="ws://localhost:8080/ws" /> setTo(e.target.value)} /> setRt(e.target.value)} /> setProxy(e.target.value)} placeholder="http://proxy:8888 (선택)" /> ); }; // ── 메인 컴포넌트 ───────────────────────────────────────────────────────────── const SettingsPage: React.FC = ({ theme, magnetMode = 'off', onMagnetMode, btAutoPopup = true, onBtAutoPopup, btShowPrice = true, onBtShowPrice, liveMarket = 'KRW-BTC', liveStrategies = [], onClearLiveMarkers, tradeAlertPopup = true, onTradeAlertPopup, liveSettings, onLiveSettingsChange, watchlistCount = 0, monitoredMarkets = [], virtualDriven = false, getIndicatorParams, saveIndicatorParams, getIndicatorVisual, saveIndicatorVisual, saveAllIndicatorDefaults, indicatorSettingsRevision = 0, chartIndicators = [], onAddIndicatorToChart, onRemoveIndicatorFromChart, onIndicatorListOrderChange, chartCandleAreaPriceLabels = true, onChartCandleAreaPriceLabels, chartSeriesPriceLabels = true, onChartSeriesPriceLabels, chartVolumeVisible = true, onChartVolumeVisible, chartLiveReceiveHighlight = true, onChartLiveReceiveHighlight, chartLegendOptions, onChartLegendOptionsChange, chartCrosshairInfoVisible = true, onChartCrosshairInfoVisible, chartHoverToolbarVisible = true, onChartHoverToolbarVisible, chartPaneSeparator, onChartPaneSeparatorChange, tradeAlertSoundEnabled = true, onTradeAlertSoundEnabled, tradeAlertSound = 'bell', onTradeAlertSound, tradeAlertPopupPosition = 'right', onTradeAlertPopupPosition, tradeAlertPopupLayout = 'stack', onTradeAlertPopupLayout, tradeAlertPopupGridCols = 2, onTradeAlertPopupGridCols, paperTradingEnabled = true, onPaperTradingEnabled, paperInitialCapital = 10_000_000, onPaperInitialCapital, paperFeeRatePct = 0.05, onPaperFeeRatePct, paperSlippagePct = 0, onPaperSlippagePct, paperMinOrderKrw = 5000, onPaperMinOrderKrw, paperAutoTradeEnabled = false, onPaperAutoTradeEnabled, paperAutoTradeBudgetPct = 95, onPaperAutoTradeBudgetPct, paperOrderFillMode = 'LIMIT_ONLY', onPaperOrderFillMode, paperOrderAutoCancelPct = 1.0, onPaperOrderAutoCancelPct, virtualTargetMaxCount = 20, onVirtualTargetMaxCount, onPaperAccountReset, tradingMode = 'PAPER', onTradingMode, liveAutoTradeEnabled = false, onLiveAutoTradeEnabled, hasUpbitKeys = false, upbitAccessKeyMasked = null, onUpbitKeys, chartRealtimeSource = 'BACKEND_STOMP', onChartRealtimeSource, liveAutoTradeBudgetPct = 95, onLiveAutoTradeBudgetPct, fcmPushEnabled = false, onFcmPushEnabled, onFcmTest, displayTimezone = 'Asia/Seoul', onDisplayTimezoneChange, chartTimeFormat = 'MM-dd HH:mm', onChartTimeFormatChange, tradeAlertTimeFormat = 'MM-dd HH:mm', onTradeAlertTimeFormatChange, menuPermissions, verificationIssueNotify = true, onVerificationIssueNotify, trendSearchSettings, onTrendSearchSettingsChange, initialCategory, isFormalLogin = true, onRequireFormalLogin, }) => { const categories = filterCategories(menuPermissions); const [active, setActive] = useState(initialCategory ?? 'general'); const [indicatorSaveUi, setIndicatorSaveUi] = useState(null); useEffect(() => { if (categories.length === 0) return; if (!categories.some(c => c.id === active)) { setActive(categories[0].id); } }, [categories, active]); useEffect(() => { if (!initialCategory) return; if (!isFormalLogin && isTradingSettingsCategory(initialCategory)) { onRequireFormalLogin?.(); return; } if (categories.some(c => c.id === initialCategory)) { setActive(initialCategory); } }, [initialCategory, categories, isFormalLogin, onRequireFormalLogin]); useEffect(() => { if (active !== 'indicators') setIndicatorSaveUi(null); }, [active]); const activeCat = categories.find(c => c.id === active) ?? categories[0]; if (!activeCat) { return (

접근 가능한 설정 메뉴가 없습니다.

); } const renderPanel = () => { switch (active) { case 'chart': return ( ); case 'indicators': if ( !getIndicatorParams || !saveIndicatorParams || !getIndicatorVisual || !saveIndicatorVisual || !saveAllIndicatorDefaults || !onAddIndicatorToChart || !onRemoveIndicatorFromChart ) { return (

보조지표 설정을 불러올 수 없습니다. 차트 화면에서 다시 시도해 주세요.

); } return ( ); case 'backtest': return ( ); case 'general': return ( {})} chartTimeFormat={chartTimeFormat} onChartTimeFormatChange={onChartTimeFormatChange} tradeAlertTimeFormat={tradeAlertTimeFormat} onTradeAlertTimeFormatChange={onTradeAlertTimeFormatChange} verificationIssueNotify={verificationIssueNotify} onVerificationIssueNotify={onVerificationIssueNotify} showVerificationNotify={menuPermissions == null || menuPermissions['verification-board'] === true} /> ); case 'admin': return ( ); case 'trading': case 'paper': case 'virtual': return ( ); case 'live': return ( ); case 'trend-search': return ( trendSearchSettings && onTrendSearchSettingsChange ? ( ) : (

추세검색 설정을 불러올 수 없습니다.

) ); case 'strategy': return ( ); case 'alert': return ( ); case 'network': return ; } }; return (
{/* ── 좌측 사이드바 ── */} {/* ── 우측 콘텐츠 ── */}
{/* 헤더 */}
{activeCat.icon}

{activeCat.label}

{activeCat.desc}

{active === 'indicators' && indicatorSaveUi && (
{indicatorSaveUi.saveMessage && ( {indicatorSaveUi.saveMessage} )}
)}
{/* 설정 패널 */}
{renderPanel()}
{/* 저장 버튼 (보조지표 설정은 변경 시 DB 자동 저장) */} {active !== 'indicators' && (
)}
); }; export default SettingsPage;