추세검색 설정 추가
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
BULLISH_WEIGHT_ITEMS,
|
||||
WEIGHT_STEP,
|
||||
adjustBullishWeight,
|
||||
snapToWeightStep,
|
||||
sumWeights,
|
||||
weightFromSliderPointer,
|
||||
type BullishWeightKey,
|
||||
} from '../../utils/trendSearchBullishWeights';
|
||||
|
||||
export type BullishWeightsRecord = Record<BullishWeightKey, number>;
|
||||
|
||||
interface Props {
|
||||
weights: BullishWeightsRecord;
|
||||
onChange: (next: BullishWeightsRecord) => void;
|
||||
/** 설정 화면용 컴팩트 레이아웃 */
|
||||
variant?: 'panel' | 'settings';
|
||||
}
|
||||
|
||||
const TrendSearchBullishWeightsSection: React.FC<Props> = ({
|
||||
weights,
|
||||
onChange,
|
||||
variant = 'panel',
|
||||
}) => {
|
||||
const weightTotal = useMemo(() => sumWeights(Object.values(weights)), [weights]);
|
||||
|
||||
const handleWeightChange = (key: BullishWeightKey, value: number) => {
|
||||
onChange(adjustBullishWeight(weights, key, snapToWeightStep(value)));
|
||||
};
|
||||
|
||||
const handleSliderPointer = (
|
||||
e: React.PointerEvent<HTMLInputElement>,
|
||||
key: BullishWeightKey,
|
||||
) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
handleWeightChange(key, weightFromSliderPointer(e.clientX, rect));
|
||||
};
|
||||
|
||||
const cardCls = variant === 'settings' ? 'tsd-bullish-card tsd-bullish-card--settings' : 'tsd-bullish-card';
|
||||
|
||||
return (
|
||||
<div className={cardCls}>
|
||||
<div className="tsd-bullish-card-head">
|
||||
<div>
|
||||
<span className="tsd-bullish-card-en">Bullish Trend Score</span>
|
||||
<span className="tsd-bullish-card-ko">상승추세 검색그룹 배점</span>
|
||||
</div>
|
||||
<span className="tsd-bullish-card-total">{weightTotal}점</span>
|
||||
</div>
|
||||
<div className="tsd-bullish-items">
|
||||
{BULLISH_WEIGHT_ITEMS.map(item => {
|
||||
const value = weights[item.key];
|
||||
return (
|
||||
<div key={item.key} className="tsd-bullish-item">
|
||||
<div className="tsd-bullish-item-head">
|
||||
<span className="tsd-bullish-item-label">{item.label}</span>
|
||||
<span className="tsd-bullish-item-desc" title={item.desc}>{item.desc}</span>
|
||||
</div>
|
||||
<div className="tsd-bullish-item-controls">
|
||||
<input
|
||||
type="range"
|
||||
className="tsd-slider tsd-bullish-slider"
|
||||
min={0}
|
||||
max={100}
|
||||
step={WEIGHT_STEP}
|
||||
value={value}
|
||||
onPointerDown={e => handleSliderPointer(e, item.key)}
|
||||
onChange={e => handleWeightChange(item.key, Number(e.target.value))}
|
||||
aria-label={`${item.label} 배점`}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="tsd-num tsd-bullish-score"
|
||||
min={0}
|
||||
max={100}
|
||||
step={WEIGHT_STEP}
|
||||
value={value}
|
||||
onChange={e => handleWeightChange(item.key, Number(e.target.value))}
|
||||
onBlur={e => handleWeightChange(item.key, Number(e.target.value))}
|
||||
aria-label={`${item.label} 배점 입력`}
|
||||
/>
|
||||
<span className="tsd-bullish-score-unit">점</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendSearchBullishWeightsSection;
|
||||
@@ -55,7 +55,7 @@ const TrendSearchCardSignalPanel: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="vtd-sig-detail-table" aria-label="지표별 상태 목록">
|
||||
<VirtualIndicatorCompareTable metrics={metrics} />
|
||||
<VirtualIndicatorCompareTable metrics={metrics} showStrategyCondition={false} />
|
||||
</div>
|
||||
<div className="vtd-card-foot">
|
||||
<span className="vtd-card-updated">갱신 {updatedLabel}</span>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TrendSearchRequest } from '../../utils/trendSearchApi';
|
||||
import { TREND_TIMEFRAMES } from '../../utils/trendSearchApi';
|
||||
import {
|
||||
BULLISH_WEIGHT_ITEMS,
|
||||
adjustBullishWeight,
|
||||
sumWeights,
|
||||
type BullishWeightKey,
|
||||
} from '../../utils/trendSearchBullishWeights';
|
||||
import { sumWeights } from '../../utils/trendSearchBullishWeights';
|
||||
import TrendSearchBullishWeightsSection from './TrendSearchBullishWeightsSection';
|
||||
|
||||
interface Props {
|
||||
filters: TrendSearchRequest;
|
||||
@@ -43,11 +39,6 @@ const TrendSearchFilterPanel: React.FC<Props> = ({ filters, onChange, onSearch,
|
||||
|
||||
const weightTotal = sumWeights(Object.values(weights));
|
||||
|
||||
const handleWeightChange = (key: BullishWeightKey, value: number) => {
|
||||
const next = adjustBullishWeight(weights, key, value);
|
||||
patch(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tsd-filter-panel">
|
||||
<div className="tsd-panel-head">
|
||||
@@ -60,51 +51,10 @@ const TrendSearchFilterPanel: React.FC<Props> = ({ filters, onChange, onSearch,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tsd-bullish-card">
|
||||
<div className="tsd-bullish-card-head">
|
||||
<div>
|
||||
<span className="tsd-bullish-card-en">Bullish Trend Score</span>
|
||||
<span className="tsd-bullish-card-ko">상승추세 검색그룹</span>
|
||||
</div>
|
||||
<span className="tsd-bullish-card-total">{weightTotal}점</span>
|
||||
</div>
|
||||
|
||||
<div className="tsd-bullish-items">
|
||||
{BULLISH_WEIGHT_ITEMS.map(item => {
|
||||
const value = weights[item.key];
|
||||
return (
|
||||
<div key={item.key} className="tsd-bullish-item">
|
||||
<div className="tsd-bullish-item-head">
|
||||
<span className="tsd-bullish-item-label">{item.label}</span>
|
||||
<span className="tsd-bullish-item-desc" title={item.desc}>{item.desc}</span>
|
||||
</div>
|
||||
<div className="tsd-bullish-item-controls">
|
||||
<input
|
||||
type="range"
|
||||
className="tsd-slider tsd-bullish-slider"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={value}
|
||||
onChange={e => handleWeightChange(item.key, Number(e.target.value))}
|
||||
aria-label={`${item.label} 배점`}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="tsd-num tsd-bullish-score"
|
||||
min={0}
|
||||
max={100}
|
||||
value={value}
|
||||
onChange={e => handleWeightChange(item.key, Number(e.target.value))}
|
||||
aria-label={`${item.label} 배점 입력`}
|
||||
/>
|
||||
<span className="tsd-bullish-score-unit">점</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<TrendSearchBullishWeightsSection
|
||||
weights={weights}
|
||||
onChange={w => patch(w)}
|
||||
/>
|
||||
|
||||
<div className="tsd-filter-section">
|
||||
<h4 className="tsd-filter-section-title">캔들 주기</h4>
|
||||
|
||||
@@ -9,6 +9,7 @@ import VirtualTargetQuote from '../virtual/VirtualTargetQuote';
|
||||
import VirtualLiveBadge from '../virtual/VirtualLiveBadge';
|
||||
import TrendSearchCardSignalPanel from './TrendSearchCardSignalPanel';
|
||||
import TrendSearchCardChart from './TrendSearchCardChart';
|
||||
import TrendSearchTargetActions from './TrendSearchTargetActions';
|
||||
|
||||
export type TrendSearchDisplayMode = 'summary' | 'chart';
|
||||
|
||||
@@ -24,6 +25,11 @@ interface Props {
|
||||
ticker?: TickerData;
|
||||
updatedAt?: number;
|
||||
flash?: boolean;
|
||||
inTargets?: boolean;
|
||||
targetPinned?: boolean;
|
||||
targetBusy?: boolean;
|
||||
onAddTarget?: () => void;
|
||||
onRemoveTarget?: () => void;
|
||||
}
|
||||
|
||||
function resultToTicker(result: TrendSearchResultDto): TickerData {
|
||||
@@ -55,6 +61,11 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
||||
ticker,
|
||||
updatedAt,
|
||||
flash = false,
|
||||
inTargets = false,
|
||||
targetPinned = false,
|
||||
targetBusy = false,
|
||||
onAddTarget,
|
||||
onRemoveTarget,
|
||||
}) => {
|
||||
const en = result.market.replace(/^KRW-/, '');
|
||||
const quoteTicker = ticker ?? resultToTicker(result);
|
||||
@@ -97,6 +108,18 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
||||
<span className="tsd-card-score">{result.matchRate}점</span>
|
||||
{result.highMatch && <span className="tsd-card-high-badge">HIGH MATCH</span>}
|
||||
</div>
|
||||
{(onAddTarget || onRemoveTarget) && (
|
||||
<div className="vtd-card-head-actions">
|
||||
<TrendSearchTargetActions
|
||||
market={result.market}
|
||||
inTargets={inTargets}
|
||||
targetPinned={targetPinned}
|
||||
busy={targetBusy}
|
||||
onAdd={() => onAddTarget?.()}
|
||||
onRemove={() => onRemoveTarget?.()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="vtd-card-meta">
|
||||
|
||||
@@ -19,6 +19,11 @@ interface Props {
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
tickers?: Map<string, TickerData>;
|
||||
lastUpdatedAt?: number;
|
||||
isInTargets?: (market: string) => boolean;
|
||||
isPinned?: (market: string) => boolean;
|
||||
onAddTarget?: (row: TrendSearchResultDto) => void;
|
||||
onRemoveTarget?: (market: string) => void;
|
||||
targetBusyMarket?: string | null;
|
||||
}
|
||||
|
||||
const TrendSearchResultsCardGrid: React.FC<Props> = ({
|
||||
@@ -34,6 +39,11 @@ const TrendSearchResultsCardGrid: React.FC<Props> = ({
|
||||
chartSeriesPriceLabels = true,
|
||||
tickers,
|
||||
lastUpdatedAt,
|
||||
isInTargets,
|
||||
isPinned,
|
||||
onAddTarget,
|
||||
onRemoveTarget,
|
||||
targetBusyMarket = null,
|
||||
}) => {
|
||||
const sortedResults = useMemo(
|
||||
() => sortTrendSearchByStrength(results),
|
||||
@@ -77,6 +87,11 @@ const TrendSearchResultsCardGrid: React.FC<Props> = ({
|
||||
ticker={tickers?.get(row.market)}
|
||||
updatedAt={lastUpdatedAt}
|
||||
flash={flashMarkets?.has(row.market)}
|
||||
inTargets={isInTargets?.(row.market) ?? false}
|
||||
targetPinned={isPinned?.(row.market) ?? false}
|
||||
targetBusy={targetBusyMarket === row.market}
|
||||
onAddTarget={onAddTarget ? () => onAddTarget(row) : undefined}
|
||||
onRemoveTarget={onRemoveTarget ? () => onRemoveTarget(row.market) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TrendSearchAppSettings } from '../../utils/trendSearchAppSettings';
|
||||
import {
|
||||
TREND_SEARCH_REFRESH_OPTIONS,
|
||||
formatTrendRefreshLabel,
|
||||
} from '../../utils/trendSearchAppSettings';
|
||||
import TrendSearchBullishWeightsSection from './TrendSearchBullishWeightsSection';
|
||||
|
||||
interface Props {
|
||||
settings: TrendSearchAppSettings;
|
||||
onChange: (next: TrendSearchAppSettings) => void;
|
||||
}
|
||||
|
||||
const SettingRow: React.FC<{
|
||||
label: string;
|
||||
desc?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ label, desc, children }) => (
|
||||
<div className="stg-row">
|
||||
<div className="stg-row-label">
|
||||
<span className="stg-row-title">{label}</span>
|
||||
{desc && <span className="stg-row-desc">{desc}</span>}
|
||||
</div>
|
||||
<div className="stg-row-ctrl">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SettingSection: React.FC<{ title: string; children: React.ReactNode }> = ({
|
||||
title, children,
|
||||
}) => (
|
||||
<div className="stg-section">
|
||||
<div className="stg-section-title">{title}</div>
|
||||
<div className="stg-section-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TrendSearchSettingsPanel: React.FC<Props> = ({ settings, onChange }) => {
|
||||
const patch = (p: Partial<TrendSearchAppSettings>) => onChange({ ...settings, ...p });
|
||||
|
||||
const weights = useMemo(() => ({
|
||||
weightMaAlignment: settings.weightMaAlignment,
|
||||
weightMaSlope: settings.weightMaSlope,
|
||||
weightAdxTrend: settings.weightAdxTrend,
|
||||
weightMacdMomentum: settings.weightMacdMomentum,
|
||||
weightPricePosition: settings.weightPricePosition,
|
||||
}), [settings]);
|
||||
|
||||
return (
|
||||
<div className="tsd-settings-panel">
|
||||
<SettingSection title="상승추세 검색그룹 배점">
|
||||
<TrendSearchBullishWeightsSection
|
||||
variant="settings"
|
||||
weights={weights}
|
||||
onChange={w => patch(w)}
|
||||
/>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="검색 조건">
|
||||
<SettingRow label="최소 합격 점수" desc="이 점수 이상인 종목만 결과에 포함됩니다.">
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
value={settings.minTrendScore}
|
||||
onChange={e => patch({ minTrendScore: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="결과 목록 최대 개수" desc="스캔 후 화면에 표시할 최대 종목 수입니다.">
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
min={5}
|
||||
max={50}
|
||||
value={settings.limit}
|
||||
onChange={e => patch({ limit: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="스캔 대상 종목 수" desc="거래대금 상위 N종목을 평가합니다.">
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
min={20}
|
||||
max={120}
|
||||
value={settings.scanLimit}
|
||||
onChange={e => patch({ scanLimit: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="자동 갱신">
|
||||
<SettingRow label="기본 갱신 간격" desc="추세검색 화면 자동 갱신 드롭다운의 기본값입니다.">
|
||||
<select
|
||||
className="stg-select"
|
||||
value={settings.autoRefreshSeconds}
|
||||
onChange={e => patch({ autoRefreshSeconds: Number(e.target.value) })}
|
||||
>
|
||||
{TREND_SEARCH_REFRESH_OPTIONS.map(sec => (
|
||||
<option key={sec} value={sec}>{formatTrendRefreshLabel(sec)}</option>
|
||||
))}
|
||||
</select>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="투자대상 자동추가">
|
||||
<SettingRow
|
||||
label="자동추가 사용"
|
||||
desc="추세검색 갱신 후 상위 순위 종목을 가상매매 투자대상에 자동 등록합니다."
|
||||
>
|
||||
<label className="stg-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.autoAddTargetsEnabled}
|
||||
onChange={e => patch({ autoAddTargetsEnabled: e.target.checked })}
|
||||
/>
|
||||
<span className="stg-toggle-slider" />
|
||||
</label>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="자동추가 순위 (상위 N위)"
|
||||
desc="추세점수 순위 1위부터 N위까지 자동추가 후보입니다. 투자대상 최대 개수를 초과하면 추가하지 않습니다."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
min={1}
|
||||
max={50}
|
||||
disabled={!settings.autoAddTargetsEnabled}
|
||||
value={settings.autoAddTopRank}
|
||||
onChange={e => patch({ autoAddTopRank: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="자동추가 최소 점수"
|
||||
desc="상위 순위 안에서도 추세점수가 이 값 이상인 종목만 투자대상에 자동추가합니다."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
disabled={!settings.autoAddTargetsEnabled}
|
||||
value={settings.autoAddMinScore}
|
||||
onChange={e => patch({ autoAddMinScore: Number(e.target.value) })}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendSearchSettingsPanel;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
inTargets: boolean;
|
||||
targetPinned?: boolean;
|
||||
busy?: boolean;
|
||||
onAdd: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const TrendSearchTargetActions: React.FC<Props> = ({
|
||||
market,
|
||||
inTargets,
|
||||
targetPinned = false,
|
||||
busy = false,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}) => (
|
||||
<div
|
||||
className="tsd-card-target-actions"
|
||||
role="group"
|
||||
aria-label={`${market} 투자대상`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="tsd-card-target-btn tsd-card-target-btn--add"
|
||||
title={inTargets ? '이미 투자대상에 등록됨' : '가상매매 투자대상에 추가'}
|
||||
aria-label="투자대상 추가"
|
||||
disabled={inTargets || busy}
|
||||
onClick={onAdd}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
|
||||
<line x1="7" y1="2" x2="7" y2="12" />
|
||||
<line x1="2" y1="7" x2="12" y2="7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tsd-card-target-btn tsd-card-target-btn--remove"
|
||||
title={
|
||||
!inTargets
|
||||
? '투자대상에 없음'
|
||||
: targetPinned
|
||||
? '고정된 종목은 삭제할 수 없습니다'
|
||||
: '투자대상에서 제거'
|
||||
}
|
||||
aria-label="투자대상 제거"
|
||||
disabled={!inTargets || targetPinned || busy}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
|
||||
<line x1="3" y1="3" x2="11" y2="11" />
|
||||
<line x1="11" y1="3" x2="3" y2="11" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TrendSearchTargetActions;
|
||||
@@ -1,11 +1,19 @@
|
||||
import React from 'react';
|
||||
import type { TrendSearchDisplayMode } from './TrendSearchResultCard';
|
||||
import {
|
||||
TREND_SEARCH_REFRESH_OPTIONS,
|
||||
formatTrendRefreshLabel,
|
||||
} from '../../utils/trendSearchAppSettings';
|
||||
|
||||
interface Props {
|
||||
displayMode: TrendSearchDisplayMode;
|
||||
onDisplayModeChange: (mode: TrendSearchDisplayMode) => void;
|
||||
autoRefresh: boolean;
|
||||
onAutoRefreshChange: (v: boolean) => void;
|
||||
refreshSeconds: number;
|
||||
onRefreshSecondsChange: (sec: number) => void;
|
||||
autoAddTargets: boolean;
|
||||
onAutoAddTargetsChange: (v: boolean) => void;
|
||||
searching?: boolean;
|
||||
onRefresh: () => void;
|
||||
resultCount?: number;
|
||||
@@ -16,6 +24,10 @@ const TrendSearchViewHeaderControls: React.FC<Props> = ({
|
||||
onDisplayModeChange,
|
||||
autoRefresh,
|
||||
onAutoRefreshChange,
|
||||
refreshSeconds,
|
||||
onRefreshSecondsChange,
|
||||
autoAddTargets,
|
||||
onAutoAddTargetsChange,
|
||||
searching,
|
||||
onRefresh,
|
||||
resultCount = 0,
|
||||
@@ -42,10 +54,34 @@ const TrendSearchViewHeaderControls: React.FC<Props> = ({
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<span className="tsd-result-count">{resultCount}종목</span>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<label className="tsd-auto-refresh">
|
||||
<input type="checkbox" checked={autoRefresh} onChange={e => onAutoRefreshChange(e.target.checked)} />
|
||||
<span>3초 자동 갱신</span>
|
||||
<label className="tsd-auto-add-toggle" title="검색 결과 상위 순위를 가상매매 투자대상에 자동 등록">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoAddTargets}
|
||||
onChange={e => onAutoAddTargetsChange(e.target.checked)}
|
||||
/>
|
||||
<span>투자대상 자동추가</span>
|
||||
</label>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<label className="tsd-auto-refresh">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRefresh}
|
||||
onChange={e => onAutoRefreshChange(e.target.checked)}
|
||||
/>
|
||||
<span>자동 갱신</span>
|
||||
</label>
|
||||
<select
|
||||
className="tsd-refresh-interval-select"
|
||||
value={refreshSeconds}
|
||||
disabled={!autoRefresh}
|
||||
aria-label="자동 갱신 간격"
|
||||
onChange={e => onRefreshSecondsChange(Number(e.target.value))}
|
||||
>
|
||||
{TREND_SEARCH_REFRESH_OPTIONS.map(sec => (
|
||||
<option key={sec} value={sec}>{formatTrendRefreshLabel(sec)}</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" className="tsd-refresh-btn" disabled={searching} onClick={onRefresh}>
|
||||
새로고침
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user