알림목록 시간봉 필터 추가

This commit is contained in:
Macbook
2026-05-30 16:37:18 +09:00
parent 42a9b6a1fb
commit d5699a827e
6 changed files with 110 additions and 21 deletions
@@ -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(() => {
let items = filter === 'unread'
? allNotifications.filter(n => !n.isRead) ? allNotifications.filter(n => !n.isRead)
: allNotifications; : 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,7 +381,17 @@ export const TradeNotificationListPage: React.FC<Props> = ({
</button> </button>
</div> </div>
<div className="tnl-toolbar-end"> <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="목록 표시 방식"> <div className="tnl-layout-toggle vtd-view-toggle" role="group" aria-label="목록 표시 방식">
<button <button
type="button" type="button"
@@ -377,6 +408,8 @@ export const TradeNotificationListPage: React.FC<Props> = ({
</button> </button>
</div> </div>
<span className="header-toolbar-divider" aria-hidden="true" />
<div className="tnl-toolbar-end">
<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>
+11
View File
@@ -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;
+2
View File
@@ -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 };