횡보컨트롤 배경색 적용

This commit is contained in:
Macbook
2026-06-12 11:08:58 +09:00
parent 981f09cf29
commit 9208418c33
9 changed files with 538 additions and 118 deletions
@@ -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,