알림목록 시간봉 필터 추가
This commit is contained in:
@@ -23,13 +23,18 @@ import TradeNotificationGridLayoutPicker from './tradeNotification/TradeNotifica
|
|||||||
import {
|
import {
|
||||||
loadTradeNotificationGridPreset,
|
loadTradeNotificationGridPreset,
|
||||||
loadTradeNotificationListLayout,
|
loadTradeNotificationListLayout,
|
||||||
|
loadTradeNotificationCandleFilter,
|
||||||
loadTradeNotificationListSort,
|
loadTradeNotificationListSort,
|
||||||
|
saveTradeNotificationCandleFilter,
|
||||||
saveTradeNotificationGridPreset,
|
saveTradeNotificationGridPreset,
|
||||||
saveTradeNotificationListLayout,
|
saveTradeNotificationListLayout,
|
||||||
saveTradeNotificationListSort,
|
saveTradeNotificationListSort,
|
||||||
|
TRADE_NOTIFICATION_CANDLE_FILTER_OPTIONS,
|
||||||
|
type TradeNotificationCandleFilter,
|
||||||
type TradeNotificationListLayout,
|
type TradeNotificationListLayout,
|
||||||
type TradeNotificationListSort,
|
type TradeNotificationListSort,
|
||||||
} from '../utils/tradeNotificationListLayout';
|
} from '../utils/tradeNotificationListLayout';
|
||||||
|
import { normalizeStartCandleType } from '../utils/strategyStartNodes';
|
||||||
import {
|
import {
|
||||||
getTradeNotificationGridPreset,
|
getTradeNotificationGridPreset,
|
||||||
type TradeNotificationGridPresetId,
|
type TradeNotificationGridPresetId,
|
||||||
@@ -120,6 +125,9 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
const [listSort, setListSort] = useState<TradeNotificationListSort>(
|
const [listSort, setListSort] = useState<TradeNotificationListSort>(
|
||||||
() => loadTradeNotificationListSort(),
|
() => loadTradeNotificationListSort(),
|
||||||
);
|
);
|
||||||
|
const [candleFilter, setCandleFilter] = useState<TradeNotificationCandleFilter>(
|
||||||
|
() => loadTradeNotificationCandleFilter(),
|
||||||
|
);
|
||||||
const [listLayout, setListLayout] = useState<TradeNotificationListLayout>(
|
const [listLayout, setListLayout] = useState<TradeNotificationListLayout>(
|
||||||
() => loadTradeNotificationListLayout(),
|
() => loadTradeNotificationListLayout(),
|
||||||
);
|
);
|
||||||
@@ -246,9 +254,17 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
else setFillSell(req);
|
else setFillSell(req);
|
||||||
}, [selectedMarket]);
|
}, [selectedMarket]);
|
||||||
|
|
||||||
const filtered = filter === 'unread'
|
const filtered = useMemo(() => {
|
||||||
? allNotifications.filter(n => !n.isRead)
|
let items = filter === 'unread'
|
||||||
: allNotifications;
|
? allNotifications.filter(n => !n.isRead)
|
||||||
|
: allNotifications;
|
||||||
|
if (candleFilter) {
|
||||||
|
items = items.filter(
|
||||||
|
n => normalizeStartCandleType(n.candleType) === candleFilter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, [allNotifications, filter, candleFilter]);
|
||||||
|
|
||||||
const sorted = useMemo(
|
const sorted = useMemo(
|
||||||
() => sortNotifications(filtered, listSort),
|
() => sortNotifications(filtered, listSort),
|
||||||
@@ -260,6 +276,11 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
saveTradeNotificationListSort(sort);
|
saveTradeNotificationListSort(sort);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleCandleFilterChange = useCallback((value: TradeNotificationCandleFilter) => {
|
||||||
|
setCandleFilter(value);
|
||||||
|
saveTradeNotificationCandleFilter(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleListLayoutChange = useCallback((layout: TradeNotificationListLayout) => {
|
const handleListLayoutChange = useCallback((layout: TradeNotificationListLayout) => {
|
||||||
setListLayout(layout);
|
setListLayout(layout);
|
||||||
saveTradeNotificationListLayout(layout);
|
saveTradeNotificationListLayout(layout);
|
||||||
@@ -360,23 +381,35 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
이전시간순
|
이전시간순
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<select
|
||||||
|
className="tnl-candle-filter-select"
|
||||||
|
value={candleFilter}
|
||||||
|
aria-label="시간봉 필터"
|
||||||
|
onChange={e => handleCandleFilterChange(e.target.value as TradeNotificationCandleFilter)}
|
||||||
|
>
|
||||||
|
<option value="">전체</option>
|
||||||
|
{TRADE_NOTIFICATION_CANDLE_FILTER_OPTIONS.map(o => (
|
||||||
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="tnl-layout-toggle vtd-view-toggle" role="group" aria-label="목록 표시 방식">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`vtd-view-toggle-btn${isListView ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||||
|
onClick={() => handleListLayoutChange('list')}
|
||||||
|
>
|
||||||
|
목록형
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`vtd-view-toggle-btn${!isListView ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||||
|
onClick={() => handleListLayoutChange('grid')}
|
||||||
|
>
|
||||||
|
그리드형
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span className="header-toolbar-divider" aria-hidden="true" />
|
||||||
<div className="tnl-toolbar-end">
|
<div className="tnl-toolbar-end">
|
||||||
<div className="tnl-layout-toggle vtd-view-toggle" role="group" aria-label="목록 표시 방식">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`vtd-view-toggle-btn${isListView ? ' vtd-view-toggle-btn--on' : ''}`}
|
|
||||||
onClick={() => handleListLayoutChange('list')}
|
|
||||||
>
|
|
||||||
목록형
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`vtd-view-toggle-btn${!isListView ? ' vtd-view-toggle-btn--on' : ''}`}
|
|
||||||
onClick={() => handleListLayoutChange('grid')}
|
|
||||||
>
|
|
||||||
그리드형
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={['tnl-icon-btn', refreshing ? 'tnl-icon-btn--refreshing' : ''].filter(Boolean).join(' ')}
|
className={['tnl-icon-btn', refreshing ? 'tnl-icon-btn--refreshing' : ''].filter(Boolean).join(' ')}
|
||||||
@@ -392,7 +425,6 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
onChange={handleGridPresetChange}
|
onChange={handleGridPresetChange}
|
||||||
disabled={isListView}
|
disabled={isListView}
|
||||||
/>
|
/>
|
||||||
<span className="tnl-toolbar-divider" aria-hidden="true" />
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="tnl-icon-btn"
|
className="tnl-icon-btn"
|
||||||
|
|||||||
@@ -260,7 +260,12 @@ export default function BuilderPageShell({
|
|||||||
{headerCenter ? (
|
{headerCenter ? (
|
||||||
<>
|
<>
|
||||||
<div className="bps-header-center">{headerCenter}</div>
|
<div className="bps-header-center">{headerCenter}</div>
|
||||||
{headerActions && <div className="bps-header-actions">{headerActions}</div>}
|
{headerActions && (
|
||||||
|
<>
|
||||||
|
<span className="header-toolbar-divider" aria-hidden="true" />
|
||||||
|
<div className="bps-header-actions">{headerActions}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
headerActions && <div className="bps-header-actions">{headerActions}</div>
|
headerActions && <div className="bps-header-actions">{headerActions}</div>
|
||||||
|
|||||||
@@ -38,6 +38,17 @@
|
|||||||
|
|
||||||
.bps-header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
.bps-header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
/** 옵션 컨트롤(토글·셀렉트)과 아이콘 버튼 영역 사이 구분선 */
|
||||||
|
.header-toolbar-divider,
|
||||||
|
.tnl-toolbar-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 22px;
|
||||||
|
margin: 0 2px;
|
||||||
|
background: color-mix(in srgb, var(--se-border, var(--border)) 90%, var(--se-text-muted, var(--text3)));
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* 가상투자 — 타이틀바 3열 (좌측패널폭 | 세션설정 | 뷰옵션) */
|
/* 가상투자 — 타이틀바 3열 (좌측패널폭 | 세션설정 | 뷰옵션) */
|
||||||
.bps-header--vtd {
|
.bps-header--vtd {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -953,6 +953,23 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tnl-candle-filter-select {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--se-border, var(--border));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg2, var(--se-panel-card-bg));
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tnl-candle-filter-select:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--accent, #3f7ef5) 55%, transparent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.tnl-toolbar-end {
|
.tnl-toolbar-end {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export interface UiPreferences {
|
|||||||
gridPreset?: string;
|
gridPreset?: string;
|
||||||
/** 목록 정렬 — market | newest | oldest */
|
/** 목록 정렬 — market | newest | oldest */
|
||||||
listSort?: 'market' | 'newest' | 'oldest';
|
listSort?: 'market' | 'newest' | 'oldest';
|
||||||
|
/** 시간봉 필터 — 빈 문자열=전체, 그 외 candleType (1m, 5m, …) */
|
||||||
|
candleFilter?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { getUiPreferences, patchUiPreferences } from './uiPreferencesDb';
|
import { getUiPreferences, patchUiPreferences } from './uiPreferencesDb';
|
||||||
|
import {
|
||||||
|
normalizeStartCandleType,
|
||||||
|
STRATEGY_CANDLE_TYPE_OPTIONS,
|
||||||
|
} from './strategyStartNodes';
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRADE_NOTIFICATION_GRID_PRESET,
|
DEFAULT_TRADE_NOTIFICATION_GRID_PRESET,
|
||||||
normalizeTradeNotificationGridPreset,
|
normalizeTradeNotificationGridPreset,
|
||||||
@@ -38,3 +42,21 @@ export function loadTradeNotificationListSort(): TradeNotificationListSort {
|
|||||||
export function saveTradeNotificationListSort(sort: TradeNotificationListSort): void {
|
export function saveTradeNotificationListSort(sort: TradeNotificationListSort): void {
|
||||||
patchUiPreferences({ tradeNotifications: { listSort: sort } });
|
patchUiPreferences({ tradeNotifications: { listSort: sort } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 알림 목록 시간봉 필터 — '' = 전체 */
|
||||||
|
export type TradeNotificationCandleFilter = '' | (typeof STRATEGY_CANDLE_TYPE_OPTIONS)[number]['value'];
|
||||||
|
|
||||||
|
export function loadTradeNotificationCandleFilter(): TradeNotificationCandleFilter {
|
||||||
|
const raw = getUiPreferences().tradeNotifications?.candleFilter;
|
||||||
|
if (raw == null || raw === '') return '';
|
||||||
|
const n = normalizeStartCandleType(raw);
|
||||||
|
return STRATEGY_CANDLE_TYPE_OPTIONS.some(o => o.value === n)
|
||||||
|
? (n as TradeNotificationCandleFilter)
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveTradeNotificationCandleFilter(filter: TradeNotificationCandleFilter): void {
|
||||||
|
patchUiPreferences({ tradeNotifications: { candleFilter: filter || '' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { STRATEGY_CANDLE_TYPE_OPTIONS as TRADE_NOTIFICATION_CANDLE_FILTER_OPTIONS };
|
||||||
|
|||||||
Reference in New Issue
Block a user