diff --git a/deploy-home.sh b/deploy-home.sh
index abe6f89..2709251 100755
--- a/deploy-home.sh
+++ b/deploy-home.sh
@@ -17,7 +17,7 @@ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BO
REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}"
REMOTE_USER="${REMOTE_USER:-aidev}"
REMOTE_PORT="${REMOTE_PORT:-22}"
-REMOTE_PROJECT_DIR="${REMOTE_PROJECT_DIR:-/Users/aidev/dev/goldenChart}"
+REMOTE_PROJECT_DIR="${REMOTE_PROJECT_DIR:-/Users/aidev/apps/goldenChart}"
FRONTEND_PORT="${FRONTEND_PORT:-80}"
BACKEND_PORT="${BACKEND_PORT:-8080}"
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index f34c9d1..12d2739 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -13,8 +13,11 @@ DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${WORK_TREE}/docker-compose.yml}"
DEPLOY_LOG="${DEPLOY_LOG:-/Volumes/ADATA/logs/deploy/deploy.log}"
LOG_TAG="[goldenChart-deploy]"
SERVICE_NAME="${SERVICE_NAME:-all}"
+DOCKER_NO_CACHE="${DOCKER_NO_CACHE:-0}"
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://localhost:8080/api/indicators/list}"
FRONTEND_URL="${FRONTEND_URL:-http://localhost:80}"
+# 단일 배포 경로 (다른 work-tree 사용 금지)
+LEGACY_WORK_TREE="/Users/aidev/dev/goldenChart"
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
@@ -51,23 +54,41 @@ info "[Phase 1] Git checkout"
[ ! -d "$WORK_TREE" ] && mkdir -p "$WORK_TREE"
[ ! -d "$GIT_DIR" ] && err "GIT_DIR 없음: $GIT_DIR"
git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
+COMMIT="$(git --git-dir="$GIT_DIR" rev-parse HEAD)"
info "체크아웃 완료: $(git --git-dir="$GIT_DIR" rev-parse --short HEAD)"
+# work-tree 에 중첩 .git 금지 (bare checkout 과 혼선 방지)
+if [ -d "$WORK_TREE/.git" ]; then
+ err "work-tree에 중첩 .git 존재 — 제거 후 재배포: rm -rf $WORK_TREE/.git"
+fi
+WT_HEAD="$(git --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" rev-parse HEAD)"
+[ "$WT_HEAD" = "$COMMIT" ] || err "work-tree HEAD 불일치: $WT_HEAD != $COMMIT"
+if [ -f "$WORK_TREE/frontend/src/utils/ChartManager.ts" ]; then
+ info "소스 검증 ChartManager.sha256=$(shasum -a 256 "$WORK_TREE/frontend/src/utils/ChartManager.ts" | awk '{print $1}')"
+fi
+if [ -e "$LEGACY_WORK_TREE" ] && [ ! -L "$LEGACY_WORK_TREE" ]; then
+ warn "레거시 경로 존재(단일화 권장): $LEGACY_WORK_TREE — 배포는 $WORK_TREE 만 사용"
+fi
+
[ ! -f "$DOCKER_COMPOSE_FILE" ] && err "docker-compose.yml 없음: $DOCKER_COMPOSE_FILE"
cd "$WORK_TREE"
# Phase 2 — Docker 이미지 빌드 (호스트 npm 사용 안 함 → postcss 홈 디렉터리 간섭 방지)
info "[Phase 2] Docker 이미지 빌드 ($SERVICE_NAME)"
-build_frontend() {
- info "docker compose build frontend"
- docker compose -f "$DOCKER_COMPOSE_FILE" build frontend
+_dc_build() {
+ local svc="$1"
+ if [ "$DOCKER_NO_CACHE" = "1" ]; then
+ info "docker compose build --no-cache $svc"
+ docker compose -f "$DOCKER_COMPOSE_FILE" build --no-cache "$svc"
+ else
+ info "docker compose build $svc"
+ docker compose -f "$DOCKER_COMPOSE_FILE" build "$svc"
+ fi
}
-build_backend() {
- info "docker compose build backend"
- docker compose -f "$DOCKER_COMPOSE_FILE" build backend
-}
+build_frontend() { _dc_build frontend; }
+build_backend() { _dc_build backend; }
case "$SERVICE_NAME" in
frontend) build_frontend ;;
diff --git a/scripts/post-receive b/scripts/post-receive
new file mode 100755
index 0000000..4c300ed
--- /dev/null
+++ b/scripts/post-receive
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# GoldenChart post-receive — Track A (git push main → deploy.sh)
+# 설치: cp scripts/post-receive /Volumes/ADATA/git/goldenChart.git/hooks/post-receive
+set -euo pipefail
+
+GIT_DIR="/Volumes/ADATA/git/goldenChart.git"
+DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy.sh"
+SUMMARY_SCRIPT="/Volumes/ADATA/git/git-push-summary.sh"
+DEPLOY_LOG="/Volumes/ADATA/logs/deploy/deploy.log"
+SUMMARY_DIR="/Volumes/ADATA/logs/deploy/push-summaries"
+HOOK_TAG="[post-receive]"
+
+mkdir -p "$(dirname "$DEPLOY_LOG")" "$SUMMARY_DIR"
+
+echo ""
+echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
+echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Track A — git push 수신"
+echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
+
+while read -r OLDREV NEWREV REFNAME; do
+ BRANCH_NAME="${REFNAME#refs/heads/}"
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 브랜치: $BRANCH_NAME ($OLDREV → $NEWREV)"
+
+ if [ -x "$SUMMARY_SCRIPT" ]; then
+ SUMMARY_FILE="$("$SUMMARY_SCRIPT" "$GIT_DIR" "$OLDREV" "$NEWREV" "$REFNAME" "$SUMMARY_DIR" || true)"
+ if [ -n "${SUMMARY_FILE:-}" ] && [ -f "$SUMMARY_FILE" ]; then
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 변경 요약: $SUMMARY_FILE"
+ head -20 "$SUMMARY_FILE" | sed "s/^/$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG /"
+ fi
+ else
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] 요약 스크립트 없음: $SUMMARY_SCRIPT"
+ fi
+
+ if [ "$BRANCH_NAME" = "main" ]; then
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG main → deploy.sh 실행"
+ export SERVICE_NAME="${SERVICE_NAME:-all}"
+ "$DEPLOY_SCRIPT" 2>&1 | tee -a "$DEPLOY_LOG"
+ EXIT_CODE=${PIPESTATUS[0]}
+ if [ "$EXIT_CODE" -ne 0 ]; then
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [ERROR] 배포 실패 (exit: $EXIT_CODE)"
+ exit "$EXIT_CODE"
+ fi
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG 배포 완료"
+ else
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG '$BRANCH_NAME' — 배포 스킵 (main만)"
+ fi
+done
+
+echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG =============================="
+echo ""
diff --git a/scripts/server-rebuild-exdev.sh b/scripts/server-rebuild-exdev.sh
new file mode 100755
index 0000000..ab11c87
--- /dev/null
+++ b/scripts/server-rebuild-exdev.sh
@@ -0,0 +1,172 @@
+#!/usr/bin/env bash
+# ==============================================================================
+# exdev.co.kr — Git bare + work-tree + Jenkins 완전 재구축
+# 실행 위치: 서버 (aidev@exdev.co.kr) 또는 로컬에서 ssh로 원격 실행
+#
+# 유지 경로 (현재 운영과 동일):
+# GIT_DIR = /Volumes/ADATA/git/goldenChart.git
+# WORK_TREE = /Users/aidev/apps/goldenChart
+# DEPLOY_SCRIPT= /Volumes/ADATA/git/deploy.sh
+# DEPLOY_LOG = /Volumes/ADATA/logs/deploy/deploy.log
+# Jenkins = http://127.0.0.1:8090 job: goldenChart-Pipeline
+# ==============================================================================
+set -euo pipefail
+
+export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
+
+GIT_DIR="/Volumes/ADATA/git/goldenChart.git"
+WORK_TREE="/Users/aidev/apps/goldenChart"
+DEPLOY_SCRIPT="/Volumes/ADATA/git/deploy.sh"
+DEPLOY_LOG="/Volumes/ADATA/logs/deploy/deploy.log"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+JENKINS_HOME="${JENKINS_HOME:-$HOME/.jenkins}"
+JENKINS_JOB="goldenChart-Pipeline"
+STAMP="$(date +%Y%m%d-%H%M%S)"
+BACKUP_ROOT="/Volumes/ADATA/backup/goldenChart-rebuild-${STAMP}"
+
+log() { echo "[rebuild $(date '+%H:%M:%S')] $*"; }
+
+log "백업 디렉터리: $BACKUP_ROOT"
+mkdir -p "$BACKUP_ROOT" "$(dirname "$DEPLOY_LOG")" /Volumes/ADATA/logs/deploy/push-summaries
+
+# ── 1. Jenkins Job 제거 (데이터는 유지, job만 초기화) ─────────────────────
+log "Jenkins job 제거: $JENKINS_JOB"
+rm -rf "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
+rm -f "${JENKINS_HOME}/jobs/${JENKINS_JOB}.xml" 2>/dev/null || true
+
+# ── 2. work-tree 백업 (.env·데이터 보존) ───────────────────────────────────
+if [ -d "$WORK_TREE" ]; then
+ log "work-tree 백업 → $BACKUP_ROOT/work-tree-snapshot"
+ mkdir -p "$BACKUP_ROOT/work-tree-snapshot"
+ [ -f "$WORK_TREE/.env" ] && cp -a "$WORK_TREE/.env" "$BACKUP_ROOT/.env"
+ # embedded .git 제거 대상 — 백업만
+ if [ -d "$WORK_TREE/.git" ]; then
+ log "work-tree 내 중첩 .git 백업 (배포 혼선 원인)"
+ cp -a "$WORK_TREE/.git" "$BACKUP_ROOT/work-tree-snapshot/dot-git-nested"
+ fi
+fi
+
+# ── 3. bare 저장소 제거 후 재생성 ───────────────────────────────────────────
+if [ -d "$GIT_DIR" ]; then
+ log "bare 저장소 백업 → $BACKUP_ROOT/goldenChart.git.tgz"
+ tar -czf "$BACKUP_ROOT/goldenChart.git.tgz" -C "$(dirname "$GIT_DIR")" "$(basename "$GIT_DIR")" 2>/dev/null || true
+ log "bare 저장소 제거: $GIT_DIR"
+ rm -rf "$GIT_DIR"
+fi
+
+log "bare 저장소 초기화"
+git init --bare "$GIT_DIR"
+git --git-dir="$GIT_DIR" symbolic-ref HEAD refs/heads/main
+
+# ── 4. 레거시 work-tree 제거·단일 경로 고정 ─────────────────────────────────
+LEGACY_WORK_TREE="/Users/aidev/dev/goldenChart"
+if [ -e "$LEGACY_WORK_TREE" ] || [ -L "$LEGACY_WORK_TREE" ]; then
+ log "레거시 경로 백업·이동: $LEGACY_WORK_TREE"
+ if [ -d "$LEGACY_WORK_TREE" ] && [ ! -L "$LEGACY_WORK_TREE" ]; then
+ tar -czf "$BACKUP_ROOT/legacy-dev-goldenChart.tgz" -C "$(dirname "$LEGACY_WORK_TREE")" "$(basename "$LEGACY_WORK_TREE")" 2>/dev/null || true
+ mv "$LEGACY_WORK_TREE" "$BACKUP_ROOT/legacy-dev-goldenChart-dir" 2>/dev/null || true
+ else
+ rm -f "$LEGACY_WORK_TREE" 2>/dev/null || true
+ fi
+fi
+ln -sfn "$WORK_TREE" "$LEGACY_WORK_TREE"
+log " → $LEGACY_WORK_TREE → $WORK_TREE (심볼릭 링크)"
+
+# ── 5. work-tree 정리 (중첩 .git 제거, 소스만 비움) ───────────────────────
+log "work-tree 정리: $WORK_TREE"
+mkdir -p "$WORK_TREE"
+if [ -d "$WORK_TREE/.git" ]; then
+ rm -rf "$WORK_TREE/.git"
+ log " → 중첩 .git 제거 완료"
+fi
+# 소스 파일만 삭제 (.env·data·docker 볼륨 경로 유지)
+find "$WORK_TREE" -mindepth 1 -maxdepth 1 \
+ ! -name '.env' \
+ ! -name '.env.*' \
+ ! -name 'data' \
+ -exec rm -rf {} + 2>/dev/null || true
+
+[ -f "$BACKUP_ROOT/.env" ] && cp -a "$BACKUP_ROOT/.env" "$WORK_TREE/.env" && log " → .env 복원"
+
+# ── 6. deploy.sh · post-receive 설치 ───────────────────────────────────────
+log "deploy.sh 설치"
+cp -f "$REPO_ROOT/scripts/deploy.sh" "$DEPLOY_SCRIPT"
+chmod +x "$DEPLOY_SCRIPT"
+
+log "post-receive 훅 설치"
+cp -f "$REPO_ROOT/scripts/post-receive" "$GIT_DIR/hooks/post-receive"
+chmod +x "$GIT_DIR/hooks/post-receive"
+
+if [ -x /Volumes/ADATA/git/git-push-summary.sh ]; then
+ log "git-push-summary.sh 유지"
+else
+ log "[WARN] /Volumes/ADATA/git/git-push-summary.sh 없음 — push 요약 생략"
+fi
+
+# ── 7. Jenkins Job 재생성 ──────────────────────────────────────────────────
+log "Jenkins job 생성: $JENKINS_JOB"
+mkdir -p "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
+cat > "${JENKINS_HOME}/jobs/${JENKINS_JOB}/config.xml" << 'JENKINS_XML'
+
+
+ GoldenChart — Track B (deploy.sh, file:// bare repo)
+ false
+
+
+ 2
+
+
+ file:///Volumes/ADATA/git/goldenChart.git
+
+
+
+
+ */main
+
+
+ false
+
+
+
+ true
+ false
+ false
+ false
+
+ false
+
+
+ #!/bin/bash
+set -euo pipefail
+export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH"
+echo "=== goldenChart Jenkins Build #${BUILD_NUMBER} ==="
+echo "시각: $(date)"
+SERVICE_NAME="${SERVICE_NAME:-all}" /Volumes/ADATA/git/deploy.sh
+
+
+
+
+
+
+JENKINS_XML
+
+# Jenkins 설정 리로드 (실패해도 job 파일은 생성됨)
+if command -v jenkins-cli >/dev/null 2>&1; then
+ jenkins-cli reload-configuration 2>/dev/null || true
+fi
+if curl -sf -o /dev/null "http://127.0.0.1:8090/login" 2>/dev/null; then
+ curl -sf -X POST "http://127.0.0.1:8090/reload" 2>/dev/null || true
+fi
+
+rm -f /tmp/goldenchart_deploy.lock 2>/dev/null || true
+
+log "============================================================"
+log "서버 Git/Jenkins 재구축 완료 (소스는 아직 비어 있음)"
+log " GIT_DIR : $GIT_DIR"
+log " WORK_TREE : $WORK_TREE (중첩 .git 없음)"
+log " Jenkins : $JENKINS_JOB → file://$GIT_DIR"
+log ""
+log "다음: 로컬에서 실행"
+log " cd && git push origin main"
+log "============================================================"
diff --git a/scripts/verify-source-sync.sh b/scripts/verify-source-sync.sh
new file mode 100755
index 0000000..9161315
--- /dev/null
+++ b/scripts/verify-source-sync.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# 로컬 ↔ 서버 work-tree 소스 동일성 검증
+set -euo pipefail
+
+REMOTE="${REMOTE:-aidev@exdev.co.kr}"
+WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
+LOCAL_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+
+FILES=(
+ frontend/src/utils/ChartManager.ts
+ frontend/src/utils/IchimokuCloudPlugin.ts
+ frontend/package-lock.json
+ scripts/deploy.sh
+)
+
+echo "=== 로컬 ↔ 서버 체크섬 ($REMOTE:$WORK_TREE) ==="
+FAIL=0
+for f in "${FILES[@]}"; do
+ L="$LOCAL_ROOT/$f"
+ [ -f "$L" ] || { echo "MISSING local $f"; FAIL=1; continue; }
+ LHASH=$(shasum -a 256 "$L" | awk '{print $1}')
+ RHASH=$(ssh -o BatchMode=yes "$REMOTE" "shasum -a 256 '$WORK_TREE/$f' 2>/dev/null" | awk '{print $1}')
+ if [ "$LHASH" = "$RHASH" ]; then
+ echo "OK $f"
+ else
+ echo "DIFF $f"
+ echo " local=$LHASH"
+ echo " remote=$RHASH"
+ FAIL=1
+ fi
+done
+
+echo "=== Git HEAD ==="
+echo -n "local: "; git -C "$LOCAL_ROOT" rev-parse HEAD
+echo -n "remote: "; ssh -o BatchMode=yes "$REMOTE" "git --git-dir=/Volumes/ADATA/git/goldenChart.git rev-parse HEAD"
+echo ""
+[ "$FAIL" -eq 0 ] && echo "검증 통과" || { echo "검증 실패"; exit 1; }