횡보컨트롤 배경색 적용
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DraggableModalFrame from '../DraggableModalFrame';
|
||||
import type { SidewaysFilterPaletteEntry } from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import {
|
||||
SIDEWAYS_CATEGORY_OPTIONS,
|
||||
SIDEWAYS_TEMPLATE_OPTIONS,
|
||||
} from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import type { SidewaysFilterMode } from '../../utils/sidewaysFilterPalette';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
mode: 'add' | 'edit';
|
||||
initial?: SidewaysFilterPaletteEntry | null;
|
||||
onClose: () => void;
|
||||
onSave: (entry: Omit<SidewaysFilterPaletteEntry, 'id' | 'builtIn'> & { id?: string; builtIn?: boolean }) => void;
|
||||
}
|
||||
|
||||
export default function SidewaysFilterItemModal({
|
||||
open, mode, initial, onClose, onSave,
|
||||
}: Props) {
|
||||
const [templateId, setTemplateId] = useState(SIDEWAYS_TEMPLATE_OPTIONS[0]?.value ?? 'adx-lt-25');
|
||||
const [label, setLabel] = useState('');
|
||||
const [desc, setDesc] = useState('');
|
||||
const [category, setCategory] = useState(SIDEWAYS_CATEGORY_OPTIONS[0]?.value ?? 'adx');
|
||||
const [filterMode, setFilterMode] = useState<SidewaysFilterMode>('include');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (mode === 'edit' && initial) {
|
||||
setTemplateId(initial.templateId);
|
||||
setLabel(initial.label);
|
||||
setDesc(initial.desc);
|
||||
setCategory(initial.category);
|
||||
setFilterMode(initial.mode);
|
||||
return;
|
||||
}
|
||||
const tpl = SIDEWAYS_TEMPLATE_OPTIONS[0];
|
||||
if (tpl) {
|
||||
setTemplateId(tpl.value);
|
||||
setLabel(`${tpl.label} (복사)`);
|
||||
setDesc(tpl.desc);
|
||||
setCategory(tpl.category);
|
||||
setFilterMode(tpl.mode);
|
||||
}
|
||||
}, [open, mode, initial]);
|
||||
|
||||
const handleTemplateChange = (next: string) => {
|
||||
setTemplateId(next);
|
||||
if (mode !== 'add') return;
|
||||
const tpl = SIDEWAYS_TEMPLATE_OPTIONS.find(o => o.value === next);
|
||||
if (!tpl) return;
|
||||
setLabel(`${tpl.label} (복사)`);
|
||||
setDesc(tpl.desc);
|
||||
setCategory(tpl.category);
|
||||
setFilterMode(tpl.mode);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const title = mode === 'add' ? '횡보 필터 추가' : '횡보 필터 수정';
|
||||
|
||||
const handleSubmit = () => {
|
||||
const trimmedLabel = label.trim();
|
||||
if (!trimmedLabel) {
|
||||
window.alert('표시 이름을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
onSave({
|
||||
id: initial?.id,
|
||||
builtIn: initial?.builtIn,
|
||||
templateId,
|
||||
label: trimmedLabel,
|
||||
desc: desc.trim() || trimmedLabel,
|
||||
category,
|
||||
mode: filterMode,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<DraggableModalFrame onClose={onClose} title={title}>
|
||||
<div className="se-modal-body se-palette-item-modal">
|
||||
<label className="se-field-lbl">기반 필터</label>
|
||||
<select
|
||||
className="se-field-inp"
|
||||
value={templateId}
|
||||
disabled={mode === 'edit'}
|
||||
onChange={e => handleTemplateChange(e.target.value)}
|
||||
>
|
||||
{SIDEWAYS_TEMPLATE_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label className="se-field-lbl">표시 이름</label>
|
||||
<input
|
||||
className="se-field-inp"
|
||||
value={label}
|
||||
onChange={e => setLabel(e.target.value)}
|
||||
placeholder="예: ADX < 25"
|
||||
/>
|
||||
|
||||
<label className="se-field-lbl">설명</label>
|
||||
<input
|
||||
className="se-field-inp"
|
||||
value={desc}
|
||||
onChange={e => setDesc(e.target.value)}
|
||||
placeholder="짧은 설명"
|
||||
/>
|
||||
|
||||
<label className="se-field-lbl">카테고리</label>
|
||||
<select
|
||||
className="se-field-inp"
|
||||
value={category}
|
||||
onChange={e => setCategory(e.target.value as typeof category)}
|
||||
>
|
||||
{SIDEWAYS_CATEGORY_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label className="se-field-lbl">모드</label>
|
||||
<select
|
||||
className="se-field-inp"
|
||||
value={filterMode}
|
||||
onChange={e => setFilterMode(e.target.value as SidewaysFilterMode)}
|
||||
>
|
||||
<option value="include">횡보 (include)</option>
|
||||
<option value="exclude">횡보 제외 (exclude)</option>
|
||||
</select>
|
||||
|
||||
<div className="se-modal-actions">
|
||||
<button type="button" className="se-btn se-btn--ghost" onClick={onClose}>취소</button>
|
||||
<button type="button" className="se-btn se-btn--gold" onClick={handleSubmit}>
|
||||
{mode === 'add' ? '추가' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableModalFrame>
|
||||
);
|
||||
}
|
||||
@@ -2,11 +2,15 @@ import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { DefType } from '../../utils/strategyEditorShared';
|
||||
import {
|
||||
DEFAULT_SIDEWAYS_FILTER_ITEMS,
|
||||
filterSidewaysItems,
|
||||
type SidewaysFilterItem,
|
||||
type SidewaysFilterMode,
|
||||
} from '../../utils/sidewaysFilterPalette';
|
||||
import {
|
||||
createSidewaysFilterPaletteId,
|
||||
saveSidewaysPaletteItems,
|
||||
type SidewaysFilterPaletteEntry,
|
||||
} from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import SidewaysFilterItemModal from './SidewaysFilterItemModal';
|
||||
|
||||
export interface SidewaysFilterDragPayload {
|
||||
type: 'sidewaysFilter';
|
||||
@@ -16,16 +20,18 @@ export interface SidewaysFilterDragPayload {
|
||||
|
||||
interface Props {
|
||||
def: DefType;
|
||||
items: SidewaysFilterPaletteEntry[];
|
||||
onItemsChange: (items: SidewaysFilterPaletteEntry[]) => void;
|
||||
searchQuery: string;
|
||||
selectedItemId: string | null;
|
||||
onSelectItem: (id: string | null) => void;
|
||||
onAddToCanvas: (item: SidewaysFilterItem) => void;
|
||||
onAddToCanvas: (item: SidewaysFilterPaletteEntry) => void;
|
||||
}
|
||||
|
||||
function modeIcon(mode: SidewaysFilterMode): { glyph: string; cls: string; aria: string } {
|
||||
function modeIcon(mode: SidewaysFilterMode): { glyph: string; aria: string } {
|
||||
return mode === 'include'
|
||||
? { glyph: '▣', cls: 'se-range-chip-icon--include', aria: '횡보' }
|
||||
: { glyph: '⊘', cls: 'se-range-chip-icon--exclude', aria: '제외' };
|
||||
? { glyph: '▣', aria: '횡보' }
|
||||
: { glyph: '⊘', aria: '제외' };
|
||||
}
|
||||
|
||||
interface HoverHintState {
|
||||
@@ -42,7 +48,7 @@ function SidewaysFilterChip({
|
||||
onAdd,
|
||||
onHintChange,
|
||||
}: {
|
||||
item: SidewaysFilterItem;
|
||||
item: SidewaysFilterPaletteEntry;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
onAdd: () => void;
|
||||
@@ -89,7 +95,7 @@ function SidewaysFilterChip({
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
className={`se-palette-card se-palette-card--range se-range-chip${selected ? ' se-palette-card--selected' : ''}`}
|
||||
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`}
|
||||
onDragStart={onDragStart}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
@@ -102,17 +108,29 @@ function SidewaysFilterChip({
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={item.label}
|
||||
>
|
||||
<div className="se-range-chip-icons">
|
||||
<span className="se-range-chip-icon se-range-chip-icon--left" aria-hidden>↔</span>
|
||||
<div className="se-palette-card-head">
|
||||
<span className="se-palette-card-icon" aria-hidden>↔</span>
|
||||
<div className="se-palette-card-meta">
|
||||
<span className="se-palette-card-name">{item.label}</span>
|
||||
<span className="se-palette-card-desc">{item.desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="se-palette-card-period">
|
||||
<span>모드</span>
|
||||
<input
|
||||
readOnly
|
||||
value={item.mode === 'include' ? '횡보' : '제외'}
|
||||
className="se-palette-period-input se-palette-mode-input"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<span
|
||||
className={`se-range-chip-icon se-range-chip-icon--right ${rightIcon.cls}`}
|
||||
aria-label={rightIcon.aria}
|
||||
title=""
|
||||
className={`se-palette-mode-mark se-palette-mode-mark--${item.mode}`}
|
||||
aria-hidden
|
||||
title={rightIcon.aria}
|
||||
>
|
||||
{rightIcon.glyph}
|
||||
</span>
|
||||
</div>
|
||||
<span className="se-range-chip-name">{item.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -136,6 +154,8 @@ function SidewaysFilterHintTooltip({ hint }: { hint: HoverHintState }) {
|
||||
|
||||
export default function SidewaysFilterPaletteTab({
|
||||
def,
|
||||
items,
|
||||
onItemsChange,
|
||||
searchQuery,
|
||||
selectedItemId,
|
||||
onSelectItem,
|
||||
@@ -144,18 +164,118 @@ export default function SidewaysFilterPaletteTab({
|
||||
void def;
|
||||
const [modeFilter, setModeFilter] = useState<'all' | SidewaysFilterMode>('all');
|
||||
const [hoverHint, setHoverHint] = useState<HoverHintState | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'add' | 'edit'>('add');
|
||||
const [editTarget, setEditTarget] = useState<SidewaysFilterPaletteEntry | null>(null);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => filterSidewaysItems(DEFAULT_SIDEWAYS_FILTER_ITEMS, searchQuery, modeFilter),
|
||||
[searchQuery, modeFilter],
|
||||
() => filterSidewaysItems(items, searchQuery, modeFilter),
|
||||
[items, searchQuery, modeFilter],
|
||||
);
|
||||
|
||||
const selected = items.find(i => i.id === selectedItemId) ?? null;
|
||||
|
||||
const persist = useCallback((next: SidewaysFilterPaletteEntry[]) => {
|
||||
onItemsChange(next);
|
||||
saveSidewaysPaletteItems(next);
|
||||
}, [onItemsChange]);
|
||||
|
||||
const openAdd = () => {
|
||||
setEditTarget(null);
|
||||
setModalMode('add');
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = () => {
|
||||
if (!selected) {
|
||||
window.alert('수정할 횡보 필터를 먼저 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
setEditTarget(selected);
|
||||
setModalMode('edit');
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!selected) {
|
||||
window.alert('삭제할 횡보 필터를 먼저 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`「${selected.label}」을(를) 목록에서 삭제할까요?`)) return;
|
||||
const next = items.filter(i => i.id !== selected.id);
|
||||
persist(next);
|
||||
onSelectItem(null);
|
||||
};
|
||||
|
||||
const handleSave = (draft: Omit<SidewaysFilterPaletteEntry, 'id' | 'builtIn'> & { id?: string; builtIn?: boolean }) => {
|
||||
if (modalMode === 'add') {
|
||||
const entry: SidewaysFilterPaletteEntry = {
|
||||
id: createSidewaysFilterPaletteId(),
|
||||
templateId: draft.templateId,
|
||||
label: draft.label,
|
||||
desc: draft.desc,
|
||||
category: draft.category,
|
||||
mode: draft.mode,
|
||||
};
|
||||
persist([...items, entry]);
|
||||
onSelectItem(entry.id);
|
||||
return;
|
||||
}
|
||||
const id = draft.id ?? editTarget?.id;
|
||||
if (!id) return;
|
||||
persist(items.map(i => (i.id === id ? {
|
||||
...i,
|
||||
label: draft.label,
|
||||
desc: draft.desc,
|
||||
category: draft.category,
|
||||
mode: draft.mode,
|
||||
templateId: draft.templateId,
|
||||
} : i)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="se-palette-section se-palette-section--range se-palette-section--scroll">
|
||||
{hoverHint && <SidewaysFilterHintTooltip hint={hoverHint} />}
|
||||
<div className="se-palette-toolbar">
|
||||
<span className="se-palette-toolbar-title">횡보 필터</span>
|
||||
<span className="se-range-count">{filtered.length}개</span>
|
||||
<div className="se-palette-toolbar-actions" role="toolbar" aria-label="횡보 필터 관리">
|
||||
<button
|
||||
type="button"
|
||||
className="se-palette-tool-btn"
|
||||
title="필터 추가"
|
||||
aria-label="필터 추가"
|
||||
onClick={openAdd}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden>
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-palette-tool-btn"
|
||||
title="선택 필터 수정"
|
||||
aria-label="선택 필터 수정"
|
||||
disabled={!selected}
|
||||
onClick={openEdit}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden>
|
||||
<path d="M4 20h4l10-10-4-4L4 16v4z" stroke="currentColor" strokeWidth="1.75" fill="none" strokeLinejoin="round" />
|
||||
<path d="M14 6l4 4" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-palette-tool-btn se-palette-tool-btn--danger"
|
||||
title="선택 필터 삭제"
|
||||
aria-label="선택 필터 삭제"
|
||||
disabled={!selected}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden>
|
||||
<path d="M6 7h12M9 7V5h6v2M10 11v6M14 11v6M8 7l1 12h6l1-12" stroke="currentColor" strokeWidth="1.75" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="se-range-mode-tabs" role="tablist" aria-label="횡보 필터 모드">
|
||||
@@ -204,6 +324,14 @@ export default function SidewaysFilterPaletteTab({
|
||||
{filtered.length === 0 && (
|
||||
<p className="se-palette-empty">검색 결과가 없습니다.</p>
|
||||
)}
|
||||
|
||||
<SidewaysFilterItemModal
|
||||
open={modalOpen}
|
||||
mode={modalMode}
|
||||
initial={editTarget}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
updateNode,
|
||||
type DefType,
|
||||
} from '../../utils/strategyEditorShared';
|
||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPalette';
|
||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import { setStochPairSecondary } from '../../utils/stochOverboughtPair';
|
||||
import {
|
||||
START_NODE_ID,
|
||||
|
||||
Reference in New Issue
Block a user