frontend 성능 개선
This commit is contained in:
@@ -590,9 +590,9 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: useUpbit,
|
||||
enabled: useUpbit && chartVisible,
|
||||
source: chartRealtimeSource,
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartRealtimeSource]),
|
||||
);
|
||||
|
||||
const bars = useUpbit ? upbitBars : simBars;
|
||||
|
||||
@@ -161,17 +161,22 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
const lastPriceKeyRef = useRef('');
|
||||
useEffect(() => {
|
||||
if (!priceKey || initialLoading || priceKey === lastPriceKeyRef.current) return;
|
||||
lastPriceKeyRef.current = priceKey;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const marks = buildMarkPrices(summaryRef.current, tickersRef.current);
|
||||
if (!Object.keys(marks).length) return;
|
||||
try {
|
||||
const full = await loadPaperSummary(marks);
|
||||
if (!cancelled) setSummary(full);
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
const timer = setTimeout(() => {
|
||||
lastPriceKeyRef.current = priceKey;
|
||||
void (async () => {
|
||||
const marks = buildMarkPrices(summaryRef.current, tickersRef.current);
|
||||
if (!Object.keys(marks).length) return;
|
||||
try {
|
||||
const full = await loadPaperSummary(marks);
|
||||
if (!cancelled) setSummary(full);
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
}, 400);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [priceKey, initialLoading]);
|
||||
|
||||
const strategyNames = useMemo(
|
||||
|
||||
@@ -466,7 +466,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
onTrade={() => handleTradeFromAlert(item)}
|
||||
onReport={() => void handleReportFromAlert(item)}
|
||||
reportLoading={reportLoadingId === item.id}
|
||||
tickers={tickers}
|
||||
ticker={tickers?.get(normalizeMarketCode(item.market)) ?? tickers?.get(item.market)}
|
||||
/>
|
||||
), [
|
||||
theme,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect, useLayoutEffect, useState, useCallback } from 'react';
|
||||
import React, { useRef, useEffect, useLayoutEffect, useState, useCallback, memo } from 'react';
|
||||
import type { MouseEventParams, Time } from 'lightweight-charts';
|
||||
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing, ChartMode, Timeframe } from '../types';
|
||||
import { ChartManager } from '../utils/ChartManager';
|
||||
@@ -1996,4 +1996,4 @@ function indKey(inds: IndicatorConfig[]): string {
|
||||
return paramKey(inds) + '@@' + styleKey(inds);
|
||||
}
|
||||
|
||||
export default TradingChart;
|
||||
export default memo(TradingChart);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 매매 시그널 알림 목록 행 — 4칸이 목록 너비를 꽉 채움, 보조지표 2개↑ 시 우측 슬롯만 스크롤
|
||||
*/
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
|
||||
import type { Theme } from '../../types';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
||||
@@ -59,7 +59,8 @@ interface Props {
|
||||
layoutMode?: TradeNotificationRowLayout;
|
||||
chartsEnabled?: boolean;
|
||||
chartLiveReceiveHighlight?: boolean;
|
||||
tickers?: Map<string, TickerData>;
|
||||
/** 해당 종목 시세 (전체 tickers Map 대신 행 단위 전달 — memo·리렌더 최소화) */
|
||||
ticker?: TickerData;
|
||||
onSelect: () => void;
|
||||
onDelete: (e: React.MouseEvent) => void;
|
||||
onDetail: () => void;
|
||||
@@ -84,7 +85,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
layoutMode = 'list',
|
||||
chartsEnabled = true,
|
||||
chartLiveReceiveHighlight = true,
|
||||
tickers,
|
||||
ticker,
|
||||
onSelect,
|
||||
onDelete,
|
||||
onDetail,
|
||||
@@ -185,7 +186,6 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
const chartExpanded = isListLayout && expandedChartKey != null;
|
||||
const chartActive = isListLayout && chartsEnabled && panelActive;
|
||||
const signalCardEnabled = isListLayout && panelActive && !chartExpanded;
|
||||
const ticker = tickers?.get(marketCode) ?? tickers?.get(item.market);
|
||||
const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey);
|
||||
|
||||
const signalSnapshotEnabled = item.strategyId != null && (
|
||||
@@ -488,4 +488,35 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeNotificationListRow;
|
||||
function listRowPropsEqual(prev: Props, next: Props): boolean {
|
||||
if (prev.item !== next.item) {
|
||||
if (
|
||||
prev.item.id !== next.item.id
|
||||
|| prev.item.isRead !== next.item.isRead
|
||||
|| prev.item.receivedAt !== next.item.receivedAt
|
||||
|| prev.item.strategyId !== next.item.strategyId
|
||||
|| prev.item.strategyName !== next.item.strategyName
|
||||
) return false;
|
||||
}
|
||||
return (
|
||||
prev.theme === next.theme
|
||||
&& prev.isSelected === next.isSelected
|
||||
&& prev.layoutMode === next.layoutMode
|
||||
&& prev.chartsEnabled === next.chartsEnabled
|
||||
&& prev.chartLiveReceiveHighlight === next.chartLiveReceiveHighlight
|
||||
&& prev.reportLoading === next.reportLoading
|
||||
&& prev.itemTag === next.itemTag
|
||||
&& prev.prefetchedStrategies === next.prefetchedStrategies
|
||||
&& prev.ticker?.tradePrice === next.ticker?.tradePrice
|
||||
&& prev.ticker?.changeRate === next.ticker?.changeRate
|
||||
&& prev.ticker?.accTradePrice24 === next.ticker?.accTradePrice24
|
||||
&& prev.onSelect === next.onSelect
|
||||
&& prev.onDelete === next.onDelete
|
||||
&& prev.onDetail === next.onDetail
|
||||
&& prev.onGoToChart === next.onGoToChart
|
||||
&& prev.onTrade === next.onTrade
|
||||
&& prev.onReport === next.onReport
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(TradeNotificationListRow, listRowPropsEqual);
|
||||
|
||||
@@ -26,6 +26,8 @@ interface Props {
|
||||
chartCandleAreaPriceLabels?: boolean;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
||||
/** false — 뷰포트 밖: STOMP·히스토리 중지 (기본 true) */
|
||||
chartStreamEnabled?: boolean;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
@@ -46,6 +48,7 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
||||
chartCandleAreaPriceLabels = true,
|
||||
chartSeriesPriceLabels = true,
|
||||
chartPaneSeparator,
|
||||
chartStreamEnabled = true,
|
||||
}) => {
|
||||
const chartTf = toChartTimeframe(timeframe);
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
@@ -90,6 +93,7 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
||||
const handleNewCandle = useCallback((bar: OHLCVBar) => applyRealtimeBar(bar, true), [applyRealtimeBar]);
|
||||
|
||||
const useUpbit = isUpbitMarket(market);
|
||||
const streamActive = useUpbit && chartStreamEnabled;
|
||||
|
||||
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
||||
market,
|
||||
@@ -97,15 +101,15 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: useUpbit,
|
||||
enabled: streamActive,
|
||||
source: chartRealtimeSource,
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
||||
}), [handleTickUpdate, handleNewCandle, streamActive, chartRealtimeSource]),
|
||||
);
|
||||
|
||||
const { isLoadingMore } = useHistoryLoader({
|
||||
symbol: market,
|
||||
timeframe: chartTf,
|
||||
isUpbit: useUpbit,
|
||||
isUpbit: streamActive,
|
||||
managerRef,
|
||||
});
|
||||
|
||||
@@ -151,9 +155,9 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
||||
}, [market, chartTf, bars.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useUpbit) return;
|
||||
if (!streamActive) return;
|
||||
void pinCandleWatch(market, timeframeToCandleType(chartTf));
|
||||
}, [market, chartTf, useUpbit]);
|
||||
}, [market, chartTf, streamActive]);
|
||||
|
||||
return (
|
||||
<div className="vtd-card-chart-panel vtd-card-chart-panel--fill">
|
||||
@@ -170,6 +174,7 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
||||
)}
|
||||
<TradingChart
|
||||
key={`${market}-${chartTf}-${chartReloadTick}`}
|
||||
chartVisible={chartStreamEnabled}
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={market}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { tfLabelShort } from '../../utils/trendSearchMetrics';
|
||||
import type { Theme } from '../../types';
|
||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import { useIntersectionVisible } from '../../hooks/useIntersectionVisible';
|
||||
import VirtualTargetQuote from '../virtual/VirtualTargetQuote';
|
||||
import VirtualLiveBadge from '../virtual/VirtualLiveBadge';
|
||||
import TrendSearchCardSignalPanel from './TrendSearchCardSignalPanel';
|
||||
@@ -75,6 +76,8 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
||||
const quoteTicker = ticker ?? resultToTicker(result);
|
||||
const ko = quoteTicker.koreanName || getKoreanName(result.market) || en;
|
||||
const isChart = displayMode === 'chart';
|
||||
const { ref: cardRef, visible: cardInView } = useIntersectionVisible();
|
||||
const chartStreamEnabled = isChart && cardInView;
|
||||
|
||||
const flashCls = useMemo(() => {
|
||||
if (!flash) return '';
|
||||
@@ -83,6 +86,7 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={[
|
||||
'vtd-card',
|
||||
'vtd-card--signal',
|
||||
@@ -145,6 +149,7 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
||||
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
|
||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||
chartPaneSeparator={chartPaneSeparator}
|
||||
chartStreamEnabled={chartStreamEnabled}
|
||||
/>
|
||||
) : (
|
||||
<TrendSearchCardSignalPanel
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import {
|
||||
resolveVirtualTargetNames,
|
||||
@@ -11,6 +11,7 @@ import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
|
||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||
import type { Timeframe } from '../../types';
|
||||
import { useIntersectionVisible } from '../../hooks/useIntersectionVisible';
|
||||
import VirtualLiveBadge from './VirtualLiveBadge';
|
||||
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
||||
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||
@@ -128,9 +129,12 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
}, [running, lastTickAt, snapshot?.updatedAt]);
|
||||
const receiving = useLiveReceiveFlash(receiveSignal, running);
|
||||
const highlightReceiving = chartLiveReceiveHighlight && receiving;
|
||||
const { ref: cardRef, visible: cardInView } = useIntersectionVisible();
|
||||
const chartStreamEnabled = isChart && cardInView;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={[
|
||||
'vtd-card',
|
||||
'vtd-card--signal',
|
||||
@@ -234,6 +238,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||
chartPaneSeparator={chartPaneSeparator}
|
||||
chartTimeframe={chartTimeframe}
|
||||
chartStreamEnabled={chartStreamEnabled}
|
||||
/>
|
||||
) : (
|
||||
<VirtualTargetSignalPanel
|
||||
@@ -257,4 +262,4 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default VirtualTargetCard;
|
||||
export default memo(VirtualTargetCard);
|
||||
|
||||
@@ -37,6 +37,8 @@ interface Props {
|
||||
fillHeight?: boolean;
|
||||
/** 미지정 시 전략 DSL 대표 분봉 사용 */
|
||||
chartTimeframe?: Timeframe;
|
||||
/** false — 뷰포트 밖 등: STOMP·히스토리 구독 중지 (기본 true) */
|
||||
chartStreamEnabled?: boolean;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
@@ -52,6 +54,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
||||
chartPaneSeparator,
|
||||
fillHeight = false,
|
||||
chartTimeframe,
|
||||
chartStreamEnabled = true,
|
||||
}) => {
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
|
||||
@@ -120,6 +123,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
||||
}, [applyRealtimeBar]);
|
||||
|
||||
const useUpbit = isUpbitMarket(market);
|
||||
const streamActive = useUpbit && chartStreamEnabled;
|
||||
|
||||
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
||||
market,
|
||||
@@ -127,15 +131,15 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: useUpbit,
|
||||
enabled: streamActive,
|
||||
source: chartRealtimeSource,
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
||||
}), [handleTickUpdate, handleNewCandle, streamActive, chartRealtimeSource]),
|
||||
);
|
||||
|
||||
const { isLoadingMore } = useHistoryLoader({
|
||||
symbol: market,
|
||||
timeframe,
|
||||
isUpbit: useUpbit,
|
||||
isUpbit: streamActive,
|
||||
managerRef,
|
||||
});
|
||||
|
||||
@@ -199,12 +203,12 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
||||
}, [barsReady, applyPaneHeights, requestChartReload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useUpbit) return;
|
||||
if (!streamActive) return;
|
||||
void pinCandleWatch(market, timeframeToCandleType(timeframe));
|
||||
if (timeframeToCandleType(timeframe) !== '1m') {
|
||||
void pinCandleWatch(market, '1m');
|
||||
}
|
||||
}, [market, timeframe, useUpbit, running]);
|
||||
}, [market, timeframe, streamActive, running]);
|
||||
|
||||
return (
|
||||
<div className={`vtd-card-chart-panel${fillHeight ? ' vtd-card-chart-panel--fill' : ''}`}>
|
||||
@@ -239,7 +243,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
||||
)}
|
||||
<TradingChart
|
||||
key={`${market}-${timeframe}-${chartReloadTick}`}
|
||||
chartVisible
|
||||
chartVisible={chartStreamEnabled}
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={market}
|
||||
|
||||
@@ -74,8 +74,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
}, [viewMode, globalDisplayMode]);
|
||||
|
||||
const gridRemeasureKey = useMemo(
|
||||
() => `${viewMode}:${globalDisplayMode}:${targets.map(t => t.market).join(',')}:${Object.values(snapshots).map(s => s?.updatedAt ?? 0).join('|')}`,
|
||||
[viewMode, globalDisplayMode, targets, snapshots],
|
||||
() => `${viewMode}:${globalDisplayMode}:${targets.map(t => t.market).join(',')}`,
|
||||
[viewMode, globalDisplayMode, targets],
|
||||
);
|
||||
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
|
||||
const [focusMarket, setFocusMarket] = useState<string | null>(null);
|
||||
|
||||
@@ -638,7 +638,7 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
setDetailCentered(false);
|
||||
}, []);
|
||||
|
||||
const value: TradeNotificationContextValue = {
|
||||
const value = useMemo<TradeNotificationContextValue>(() => ({
|
||||
toastNotifications,
|
||||
allNotifications,
|
||||
unreadCount,
|
||||
@@ -657,7 +657,26 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
detailCentered,
|
||||
openDetail,
|
||||
closeDetail,
|
||||
};
|
||||
}), [
|
||||
toastNotifications,
|
||||
allNotifications,
|
||||
unreadCount,
|
||||
addNotification,
|
||||
dismissToast,
|
||||
dismissToasts,
|
||||
dismissAllToasts,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
deleteNotification,
|
||||
deleteNotifications,
|
||||
deleteUnreadNotifications,
|
||||
deleteAllNotifications,
|
||||
refreshHistory,
|
||||
detailSignal,
|
||||
detailCentered,
|
||||
openDetail,
|
||||
closeDetail,
|
||||
]);
|
||||
|
||||
return (
|
||||
<TradeNotificationContext.Provider value={value}>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export interface UseIntersectionVisibleOptions {
|
||||
root?: Element | Document | null;
|
||||
rootMargin?: string;
|
||||
threshold?: number | number[];
|
||||
/** true — 포커스·전체화면 등 항상 활성 */
|
||||
initialVisible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요소가 뷰포트(또는 root)에 들어왔는지 — 차트·폴링 gating 용
|
||||
*/
|
||||
export function useIntersectionVisible(
|
||||
options: UseIntersectionVisibleOptions = {},
|
||||
): { ref: (node: HTMLElement | null) => void; visible: boolean } {
|
||||
const {
|
||||
root = null,
|
||||
rootMargin = '100px 0px',
|
||||
threshold = 0.05,
|
||||
initialVisible = false,
|
||||
} = options;
|
||||
const [element, setElement] = useState<HTMLElement | null>(null);
|
||||
const [visible, setVisible] = useState(initialVisible);
|
||||
|
||||
const ref = useCallback((node: HTMLElement | null) => {
|
||||
setElement(node);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) return;
|
||||
const io = new IntersectionObserver(
|
||||
entries => {
|
||||
setVisible(entries[0]?.isIntersecting ?? false);
|
||||
},
|
||||
{ root, rootMargin, threshold },
|
||||
);
|
||||
io.observe(element);
|
||||
return () => io.disconnect();
|
||||
}, [element, root, rootMargin, threshold]);
|
||||
|
||||
return { ref, visible };
|
||||
}
|
||||
@@ -19,6 +19,18 @@ export default defineConfig({
|
||||
optimizeDeps: {
|
||||
include: ['react', 'react-dom'],
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules/lightweight-charts')) return 'vendor-charts';
|
||||
if (id.includes('node_modules/@xyflow')) return 'vendor-flow';
|
||||
if (id.includes('node_modules/@stomp') || id.includes('node_modules/sockjs-client')) return 'vendor-stomp';
|
||||
if (id.includes('node_modules/@tanstack/react-virtual')) return 'vendor-virtual';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
// ── 모바일 APK 정보/다운로드 → exdev (로컬 QR·PC 다운로드가 exdev APK 사용) ──
|
||||
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# GoldenChart — 소스 push 없이 서버 deploy.sh만 실행 (재배포)
|
||||
#
|
||||
# 사용:
|
||||
# ./server-restart.sh
|
||||
# ./server-restart.sh --frontend
|
||||
# ./server-restart.sh --backend
|
||||
# SERVICE_NAME=docker-only ./server-restart.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT/scripts/deploy-target.defaults"
|
||||
if [[ -f "$ROOT/scripts/deploy-target.local.env" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT/scripts/deploy-target.local.env"
|
||||
fi
|
||||
|
||||
REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}"
|
||||
REMOTE_USER="${REMOTE_USER:-aidev}"
|
||||
REMOTE_PORT="${REMOTE_PORT:-22}"
|
||||
REMOTE_DEPLOY_SCRIPT="${REMOTE_DEPLOY_SCRIPT:-/Volumes/ADATA/git/deploy.sh}"
|
||||
SERVICE_NAME="${SERVICE_NAME:-all}"
|
||||
SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"
|
||||
|
||||
SSH_CMD=(ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -p "$REMOTE_PORT")
|
||||
|
||||
print_usage() {
|
||||
cat <<EOF
|
||||
사용법: $0 [옵션]
|
||||
|
||||
(기본) git push 없이 서버 deploy.sh 실행 (전체 재배포)
|
||||
--frontend SERVICE_NAME=frontend
|
||||
--backend SERVICE_NAME=backend
|
||||
--docker-only SERVICE_NAME=docker-only (이미지 빌드 생략, compose up)
|
||||
--service NAME SERVICE_NAME=NAME
|
||||
--host HOST SSH 호스트 (기본: $REMOTE_HOST)
|
||||
--user USER SSH 사용자 (기본: $REMOTE_USER)
|
||||
--script PATH 서버 deploy.sh 경로 (기본: $REMOTE_DEPLOY_SCRIPT)
|
||||
-h, --help 도움말
|
||||
|
||||
실행 명령 (내부):
|
||||
ssh ${SSH_TARGET} 'SERVICE_NAME=... ${REMOTE_DEPLOY_SCRIPT}'
|
||||
|
||||
설정: scripts/deploy-target.defaults
|
||||
scripts/deploy-target.local.env (선택)
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--frontend) SERVICE_NAME=frontend; shift ;;
|
||||
--backend) SERVICE_NAME=backend; shift ;;
|
||||
--docker-only) SERVICE_NAME=docker-only; shift ;;
|
||||
--service) SERVICE_NAME="$2"; shift 2 ;;
|
||||
--host) REMOTE_HOST="$2"; SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"; shift 2 ;;
|
||||
--user) REMOTE_USER="$2"; SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"; shift 2 ;;
|
||||
--script) REMOTE_DEPLOY_SCRIPT="$2"; shift 2 ;;
|
||||
-h|--help) print_usage; exit 0 ;;
|
||||
*)
|
||||
echo -e "${RED}알 수 없는 옵션: $1${NC}" >&2
|
||||
print_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${CYAN}▶ 서버 재배포 (push 없음)${NC}"
|
||||
echo -e " 대상 : ${SSH_TARGET}"
|
||||
echo -e " 스크립트: ${REMOTE_DEPLOY_SCRIPT}"
|
||||
echo -e " 서비스 : ${SERVICE_NAME}"
|
||||
echo ""
|
||||
|
||||
REMOTE_CMD="SERVICE_NAME=${SERVICE_NAME} ${REMOTE_DEPLOY_SCRIPT}"
|
||||
if "${SSH_CMD[@]}" "$SSH_TARGET" "$REMOTE_CMD"; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ deploy.sh 완료${NC}"
|
||||
echo -e "${YELLOW}로그: ssh ${SSH_TARGET} 'tail -50 /Volumes/ADATA/logs/deploy/deploy.log'${NC}"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${RED}✗ deploy.sh 실패${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user