From 3c269db6b76f0a7b4b2e7501fbe6dd951de03f9f Mon Sep 17 00:00:00 2001 From: Macbook Date: Wed, 17 Jun 2026 21:49:15 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=EC=B2=B4=ED=81=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desktop/src-tauri/tauri.conf.json | 3 +- docs/exdev-subdomain-setup.md | 40 +++++++ .../src/utils/strategyEvaluationParams.ts | 12 +- frontend/src/utils/strategyIndicatorSync.ts | 96 +++++++++++++++ .../src/utils/strategyOscillatorSeries.ts | 20 ++-- .../src/utils/strategyToChartIndicators.ts | 58 +++++---- scripts/setup-server-local-hosts.sh | 111 ++++++++++++++++++ 7 files changed, 300 insertions(+), 40 deletions(-) create mode 100644 frontend/src/utils/strategyIndicatorSync.ts create mode 100755 scripts/setup-server-local-hosts.sh diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 7512096..17b9494 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -58,7 +58,8 @@ "plugins": { "updater": { "endpoints": [ - "https://stock.exdev.co.kr/desktop/updates/latest.json" + "https://stock.exdev.co.kr/desktop/updates/latest.json", + "https://exdev.co.kr/desktop/updates/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDg3MUJCMjIyRkIyNzdFMUUKUldRZWZpZjdJckliaDlMS3pvRHpmQ2lnSFNjaDQrT1pEZTdDZEJ5S2RhOXV5aU1QY3JObEEwaFUK", "windows": { diff --git a/docs/exdev-subdomain-setup.md b/docs/exdev-subdomain-setup.md index c29a7be..51bb794 100644 --- a/docs/exdev-subdomain-setup.md +++ b/docs/exdev-subdomain-setup.md @@ -71,6 +71,46 @@ curl -sS https://chatbot.exdev.co.kr/chatbot/app/ | head 서버 SSH·배포 스크립트의 `REMOTE_HOST=exdev.co.kr` 는 **그대로** 둡니다 (서버 내부·스크립트용). +## 6. 서버(Mac Studio) 안에서 브라우저·데스크톱 앱 접속 + +원격에서는 `https://stock.exdev.co.kr` 이 정상이어도, **서버 본체 브라우저**는 공인 IP로 다시 들어오는 **NAT hairpin** 이 막혀 `stock` 서브도메인만 타임아웃될 수 있습니다. + +| URL | 서버 내부 (hosts 미설정) | 조치 후 | +|-----|--------------------------|---------| +| `http://exdev.co.kr` | ✅ (`/etc/hosts` → 127.0.0.1) | — | +| `https://exdev.co.kr` | ✅ (Caddy가 직접 프록시) | — | +| `https://stock.exdev.co.kr` | ❌ 타임아웃 | hosts에 `stock` 추가 | + +**1회 설정 (서버 콘솔 또는 SSH에서 sudo):** + +```bash +sudo bash /path/to/goldenChart/scripts/setup-server-local-hosts.sh +``` + +또는 서버 바탕화면 `Setup-GoldenChart-Hosts.command` 더블클릭 → 관리자 암호 입력. + +추가되는 항목: + +```text +127.0.0.1 exdev.co.kr +127.0.0.1 stock.exdev.co.kr +127.0.0.1 chatbot.exdev.co.kr +``` + +**검증:** + +```bash +curl -sS -o /dev/null -w "stock %{http_code}\n" https://stock.exdev.co.kr/ +curl -sS https://stock.exdev.co.kr/desktop/updates/latest.json | head +``` + +**데스크톱 앱 자동 업데이트:** 기본 엔드포인트는 `stock.exdev.co.kr` 이므로 위 hosts 설정이 필요합니다. +신규 빌드부터는 `https://exdev.co.kr/desktop/updates/latest.json` 을 **두 번째 fallback** 으로 포함합니다 (`desktop/src-tauri/tauri.conf.json`). + +**주의:** `networksetup -setdnsservers Ethernet 127.0.0.1` 처럼 DNS를 127.0.0.1 로 바꾸지 마세요 (로컬 DNS 서버가 없으면 전체 DNS가 깨집니다). 복구: `networksetup -setdnsservers Ethernet Empty` + +## 7. SSH / git remote (로컬 Mac) + **로컬 Mac에서 `git push` 시 `Could not resolve hostname exdev.co.kr`** 일부 네트워크(회사·VPN·ISP)에서는 apex `exdev.co.kr` DNS만 실패하고 `stock.exdev.co.kr` 은 정상입니다. diff --git a/frontend/src/utils/strategyEvaluationParams.ts b/frontend/src/utils/strategyEvaluationParams.ts index b6fb117..ee73dd3 100644 --- a/frontend/src/utils/strategyEvaluationParams.ts +++ b/frontend/src/utils/strategyEvaluationParams.ts @@ -2,7 +2,8 @@ * 전략 평가 — 전략에 포함된 지표 파라미터 맵 */ import type { StrategyDto } from './backendApi'; -import { collectStrategyRegistryTypes } from './strategyToChartIndicators'; +import { collectIndicatorRefs } from './strategyToChartIndicators'; +import { applyStrategyRefToParams } from './strategyIndicatorSync'; export type EvalIndicatorParams = Record>; @@ -10,10 +11,13 @@ export function buildEvalParamsFromStrategy( strategy: StrategyDto | null | undefined, getParams: (type: string) => Record, ): EvalIndicatorParams { - const types = collectStrategyRegistryTypes(strategy); + if (!strategy) return {}; + const out: EvalIndicatorParams = {}; - for (const type of types) { - out[type] = { ...getParams(type) }; + for (const ref of collectIndicatorRefs(strategy)) { + const base = { ...getParams(ref.registryType) }; + const merged = applyStrategyRefToParams(ref, base); + out[ref.registryType] = { ...(out[ref.registryType] ?? {}), ...merged }; } return out; } diff --git a/frontend/src/utils/strategyIndicatorSync.ts b/frontend/src/utils/strategyIndicatorSync.ts new file mode 100644 index 0000000..701bf6c --- /dev/null +++ b/frontend/src/utils/strategyIndicatorSync.ts @@ -0,0 +1,96 @@ +/** + * 전략 DSL 기간·임계값 → 차트 지표 params / hline 동기화 + * (Ta4j StrategyDslToTa4jAdapter.effectivePeriod 키와 동일) + */ +import type { ConditionDSL } from './strategyTypes'; +import { isThresholdSymbol } from './thresholdSymbols'; + +export interface StrategyIndicatorRef { + dslType: string; + registryType: string; + period?: number; + leftPeriod?: number; + rightPeriod?: number; + targetValue?: number | null; +} + +/** 조건 rightField·targetValue → 차트에 추가할 숫자 임계 hline (HL_* 는 DB visual 사용) */ +export function resolveConditionThresholdForChart(c: ConditionDSL): number | null { + if (c.thresholdOverride === false) return null; + if (c.targetValue != null && Number.isFinite(c.targetValue)) return c.targetValue; + if (c.compareValue != null && Number.isFinite(c.compareValue)) return c.compareValue; + const rf = c.rightField ?? ''; + if (rf.startsWith('K_')) { + const n = parseFloat(rf.slice(2)); + return Number.isFinite(n) ? n : null; + } + if (isThresholdSymbol(rf)) return null; + return null; +} + +const REGISTRY_PRIMARY_PERIOD_KEY: Record = { + RSI: 'length', + CCI: 'length', + WilliamsPercentRange: 'length', + Stochastic: 'kLength', + MACD: 'fastLength', + ADX: 'adxSmoothing', + DMI: 'diLength', + TRIX: 'length', + BollingerBands: 'length', + DonchianChannels: 'length', + Envelope: 'length', + Disparity: 'length', + Psychological: 'length', + NewPsychological: 'length', + InvestPsychological: 'length', + VR: 'length', + VolumeOscillator: 'shortLength', + MFI: 'length', + ATR: 'length', + ChandeMO: 'length', + DPO: 'length', + RVI: 'length', + BBPercentB: 'length', + BBBandWidth: 'length', + StochRSI: 'lengthRSI', +}; + +function setIfPositive(params: Record, key: string, value?: number) { + if (value != null && value > 0) params[key] = value; +} + +/** 전략 ref 기간을 지표 params에 덮어씀 (DB 기본값보다 우선) */ +export function applyStrategyRefToParams( + ref: StrategyIndicatorRef, + params: Record, +): Record { + const next = { ...params }; + const primaryKey = REGISTRY_PRIMARY_PERIOD_KEY[ref.registryType]; + if (primaryKey && ref.period != null && ref.period > 0) { + next[primaryKey] = ref.period; + } + + switch (ref.registryType) { + case 'SMA': + case 'EMA': + if (ref.period != null && ref.period > 0) next.length = ref.period; + setIfPositive(next, 'fastLength', ref.leftPeriod); + setIfPositive(next, 'slowLength', ref.rightPeriod); + break; + case 'MACross': + setIfPositive(next, 'fastLength', ref.leftPeriod ?? ref.period); + setIfPositive(next, 'slowLength', ref.rightPeriod); + break; + case 'Stochastic': + setIfPositive(next, 'dSmoothing', ref.rightPeriod); + break; + case 'StochRSI': + setIfPositive(next, 'lengthStoch', ref.rightPeriod); + break; + default: + break; + } + + return next; +} diff --git a/frontend/src/utils/strategyOscillatorSeries.ts b/frontend/src/utils/strategyOscillatorSeries.ts index 1a21afa..7c3c2b6 100644 --- a/frontend/src/utils/strategyOscillatorSeries.ts +++ b/frontend/src/utils/strategyOscillatorSeries.ts @@ -1,7 +1,7 @@ import type { OHLCVBar } from '../types'; import type { StrategyDto } from './backendApi'; -import { extractVirtualConditions } from './virtualStrategyConditions'; import { formatIndicatorDisplayLabel } from './indicatorRegistry'; +import { collectIndicatorRefs } from './strategyToChartIndicators'; const DSL_TO_REGISTRY: Record = { RSI: 'RSI', @@ -132,23 +132,23 @@ function refLinesFor(registryType: string): number[] | undefined { export function collectStrategyOscillatorPanes(strategy: StrategyDto | undefined): OscillatorPaneSpec[] { if (!strategy) return []; - const rows = extractVirtualConditions(strategy); const seen = new Set(); const out: OscillatorPaneSpec[] = []; - for (const row of rows) { - if (OVERLAY_DSL.has(row.indicatorType)) continue; - const registryType = DSL_TO_REGISTRY[row.indicatorType] ?? row.indicatorType; + for (const ref of collectIndicatorRefs(strategy)) { + if (OVERLAY_DSL.has(ref.dslType)) continue; + const registryType = DSL_TO_REGISTRY[ref.dslType] ?? ref.registryType; if (OVERLAY_DSL.has(registryType) || registryType === 'SMA' || registryType === 'EMA' || registryType === 'BollingerBands' || registryType === 'IchimokuCloud') { continue; } - if (seen.has(registryType)) continue; - seen.add(registryType); + const key = `${registryType}:${ref.period ?? ''}`; + if (seen.has(key)) continue; + seen.add(key); out.push({ - key: registryType, + key, registryType, - label: formatIndicatorDisplayLabel(row.indicatorType), - period: undefined, + label: formatIndicatorDisplayLabel(ref.dslType), + period: ref.period, }); } return out; diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index 5f8aeef..9671110 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -18,6 +18,11 @@ import { normalizeSmaConfig } from './smaConfig'; import { normalizeIchimokuConfig } from './ichimokuConfig'; import type { EditorConditionState } from './strategyConditionSerde'; import { collectUiEvaluationTimeframes } from './strategyTimeframeSync'; +import { + applyStrategyRefToParams, + resolveConditionThresholdForChart, + type StrategyIndicatorRef, +} from './strategyIndicatorSync'; const DSL_TO_REGISTRY: Record = { RSI: 'RSI', @@ -47,14 +52,7 @@ const DSL_TO_REGISTRY: Record = { MFI: 'MFI', }; -interface IndicatorRef { - dslType: string; - registryType: string; - period?: number; - leftPeriod?: number; - rightPeriod?: number; - targetValue?: number | null; -} +interface IndicatorRef extends StrategyIndicatorRef {} type ParamRecord = Record; type GetParams = (type: string, defaults?: ParamRecord) => ParamRecord; @@ -141,7 +139,7 @@ function walkIndicatorRefs( const c = node.condition; const dslType = c.indicatorType; const registryType = DSL_TO_REGISTRY[dslType] ?? dslType; - const key = `${registryType}:${c.period ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${c.targetValue ?? ''}`; + const key = `${registryType}:${c.period ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${resolveConditionThresholdForChart(c) ?? ''}`; if (seen.has(key)) return; seen.add(key); out.push({ @@ -150,7 +148,7 @@ function walkIndicatorRefs( period: c.period, leftPeriod: c.leftPeriod, rightPeriod: c.rightPeriod, - targetValue: c.targetValue ?? c.compareValue ?? null, + targetValue: resolveConditionThresholdForChart(c), }); return; } @@ -158,7 +156,7 @@ function walkIndicatorRefs( node.children?.forEach(ch => walkIndicatorRefs(ch, out, seen)); } -function collectIndicatorRefs(strategy: StrategyDto, side?: 'BUY' | 'SELL'): IndicatorRef[] { +export function collectIndicatorRefs(strategy: StrategyDto, side?: 'BUY' | 'SELL'): IndicatorRef[] { const out: IndicatorRef[] = []; const seen = new Set(); if (!side || side === 'BUY') { @@ -215,6 +213,11 @@ function buildOneIndicator( ); if (!cfg) return null; + cfg = { + ...cfg, + params: applyStrategyRefToParams(ref, cfg.params), + }; + if (ref.targetValue != null) { cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) }; } @@ -237,25 +240,17 @@ export function buildStrategyChartIndicators( if (!strategy) return []; const refs = collectIndicatorRefs(strategy, side); - const byType = new Map(); + const byKey = new Map(); for (const ref of refs) { - const existing = byType.get(ref.registryType); - if (!existing) { - byType.set(ref.registryType, ref); - continue; + const key = stableStrategyIndicatorId(ref); + if (!byKey.has(key)) { + byKey.set(key, ref); } - byType.set(ref.registryType, { - ...existing, - period: existing.period ?? ref.period, - leftPeriod: existing.leftPeriod ?? ref.leftPeriod, - rightPeriod: existing.rightPeriod ?? ref.rightPeriod, - targetValue: existing.targetValue ?? ref.targetValue, - }); } const result: IndicatorConfig[] = []; - for (const ref of byType.values()) { + for (const ref of byKey.values()) { const cfg = buildOneIndicator(ref, getParams, getVisual); if (cfg) result.push(cfg); } @@ -384,14 +379,27 @@ export function buildStrategyEvaluationChartIndicators( if (!strategy) return []; const strategyTypes = new Set(collectStrategyRegistryTypes(strategy)); + const refById = new Map( + collectIndicatorRefs(strategy).map(ref => [stableStrategyIndicatorId(ref), ref]), + ); let inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisual); inds = ensurePaperChartOverlays(inds, getParams, getVisual); return inds.map(ind => { let cfg = applyDbVisualSettingsForStrategyChart(ind, getVisual); + const ref = refById.get(ind.id); + if (ref) { + cfg = { + ...cfg, + params: applyStrategyRefToParams(ref, cfg.params), + }; + if (ref.targetValue != null) { + cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) }; + } + } if (strategyTypes.has(cfg.type)) { cfg = { ...cfg, hidden: false }; } - return cfg; + return enrichIndicatorConfig(cfg); }); } diff --git a/scripts/setup-server-local-hosts.sh b/scripts/setup-server-local-hosts.sh new file mode 100755 index 0000000..aa7c25b --- /dev/null +++ b/scripts/setup-server-local-hosts.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# exdev 서버(Mac Studio) — NAT hairpin 우회용 /etc/hosts 설정 +# stock.exdev.co.kr · chatbot.exdev.co.kr → 127.0.0.1 (Caddy 443 / nginx 80) +# +# 사용 (서버 SSH): +# bash scripts/setup-server-local-hosts.sh +# sudo bash scripts/setup-server-local-hosts.sh # 비대화형 +# +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${CYAN}[setup-local-hosts]${NC} $*"; } +ok() { echo -e "${GREEN}✓${NC} $*"; } +fail() { echo -e "${RED}✗${NC} $*"; exit 1; } + +HOSTS_FILE="/etc/hosts" +MARKER="# GoldenChart local (NAT hairpin bypass)" +ENTRIES=( + "127.0.0.1 exdev.co.kr" + "127.0.0.1 stock.exdev.co.kr" + "127.0.0.1 chatbot.exdev.co.kr" +) + + +apply_hosts() { + local tmp + tmp="$(mktemp)" + cp "$HOSTS_FILE" "$tmp" + + # 기존 exdev 관련 항목·마커 제거 + grep -vE 'exdev\.co\.kr|GoldenChart local \(NAT hairpin bypass\)' "$tmp" > "${tmp}.clean" || true + mv "${tmp}.clean" "$tmp" + + { + cat "$tmp" + echo "" + echo "$MARKER" + for line in "${ENTRIES[@]}"; do + echo "$line" + done + } > "${tmp}.new" + + cp "$HOSTS_FILE" "${HOSTS_FILE}.backup.$(date +%Y%m%d_%H%M%S)" + mv "${tmp}.new" "$HOSTS_FILE" + rm -f "$tmp" +} + +verify() { + local ok_count=0 + for host in exdev.co.kr stock.exdev.co.kr; do + if ping -c1 -t1 "$host" 2>/dev/null | grep -q '127.0.0.1'; then + ok "ping $host → 127.0.0.1" + ok_count=$((ok_count + 1)) + elif dscacheutil -q host -a name "$host" 2>/dev/null | grep -q '127.0.0.1'; then + ok "DNS $host → 127.0.0.1" + ok_count=$((ok_count + 1)) + else + fail "DNS 확인 실패: $host (127.0.0.1 이 아님)" + fi + done + + curl -sf --connect-timeout 8 -o /dev/null -w "" "https://stock.exdev.co.kr/" \ + || fail "https://stock.exdev.co.kr 접속 실패" + ok "https://stock.exdev.co.kr → 200" + + curl -sf --connect-timeout 8 -o /dev/null "https://stock.exdev.co.kr/desktop/updates/latest.json" \ + || fail "desktop latest.json HTTPS 실패" + ok "desktop updater latest.json HTTPS OK" +} + +main() { + log "서버 로컬 hosts 설정 (NAT hairpin 우회)" + + if [[ "${1:-}" == "--as-root" ]]; then + apply_hosts + dscacheutil -flushcache 2>/dev/null || true + killall -HUP mDNSResponder 2>/dev/null || true + verify + ok "설정 완료" + return + fi + + if [[ "$(id -u)" -eq 0 ]]; then + apply_hosts + dscacheutil -flushcache 2>/dev/null || true + killall -HUP mDNSResponder 2>/dev/null || true + verify + ok "설정 완료" + return + fi + + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -x "${SUDO_ASKPASS}" ]]; then + log "SUDO_ASKPASS 사용" + sudo -A bash "$0" --as-root + return + fi + + if [[ -t 0 ]]; then + log "관리자 암호 필요 (sudo)" + sudo bash "$0" --as-root + return + fi + + fail "sudo 권한 필요. 서버 콘솔에서: sudo bash $0" +} + +main "$@"