diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 2596cb7..52fa3a9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: - name: Test solution run: dotnet test TaxBaik.sln -c Release --no-build - - name: Publish Web (통합 앱) + - name: Publish Web run: dotnet publish TaxBaik.Web/ -c Release -o ./publish --no-restore - name: Write production secrets @@ -38,135 +38,119 @@ jobs: JWT_SECRET_KEY="${{ secrets.TAXBAIK_JWT_SECRET_KEY }}" TELEGRAM_BOT_TOKEN="${{ secrets.TAXBAIK_TELEGRAM_BOT_TOKEN }}" TELEGRAM_CHAT_ID="${{ secrets.TAXBAIK_TELEGRAM_CHAT_ID }}" - if [ -z "$JWT_SECRET_KEY" ]; then - echo "Missing TAXBAIK_JWT_SECRET_KEY secret" >&2 - exit 1 - fi - if [ -z "$TELEGRAM_BOT_TOKEN" ]; then - echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN secret" >&2 - exit 1 - fi - if [ -z "$TELEGRAM_CHAT_ID" ]; then - echo "Missing TAXBAIK_TELEGRAM_CHAT_ID secret" >&2 - exit 1 - fi - JWT_SECRET_KEY="$JWT_SECRET_KEY" TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" python3 -c 'import json, os, pathlib; pathlib.Path("./publish/appsettings.Production.json").write_text(json.dumps({"Jwt":{"SecretKey":os.environ["JWT_SECRET_KEY"]},"Telegram":{"BotToken":os.environ["TELEGRAM_BOT_TOKEN"],"ChatId":os.environ["TELEGRAM_CHAT_ID"]}}, ensure_ascii=False, indent=2), encoding="utf-8")' - test -s ./publish/appsettings.Production.json + [ -z "$JWT_SECRET_KEY" ] && { echo "Missing TAXBAIK_JWT_SECRET_KEY" >&2; exit 1; } + [ -z "$TELEGRAM_BOT_TOKEN" ] && { echo "Missing TAXBAIK_TELEGRAM_BOT_TOKEN" >&2; exit 1; } + [ -z "$TELEGRAM_CHAT_ID" ] && { echo "Missing TAXBAIK_TELEGRAM_CHAT_ID" >&2; exit 1; } + JWT_SECRET_KEY="$JWT_SECRET_KEY" \ + TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \ + TELEGRAM_CHAT_ID="$TELEGRAM_CHAT_ID" \ + python3 -c ' + import json, os, pathlib + pathlib.Path("./publish/appsettings.Production.json").write_text( + json.dumps({ + "Jwt": {"SecretKey": os.environ["JWT_SECRET_KEY"]}, + "Telegram": {"BotToken": os.environ["TELEGRAM_BOT_TOKEN"], "ChatId": os.environ["TELEGRAM_CHAT_ID"]} + }, ensure_ascii=False, indent=2), + encoding="utf-8" + )' + test -s ./publish/appsettings.Production.json || { echo "appsettings.Production.json is empty" >&2; exit 1; } - - name: Copy migrations to publish - run: | - cp -r db/migrations ./publish/migrations || true + - name: Copy migrations + run: cp -r db/migrations ./publish/migrations || true - name: Generate build info run: | - mkdir -p ./publish/wwwroot COMMIT_HASH=$(git rev-parse --short HEAD) BUILD_TIME=$(date -u +'%Y-%m-%d %H:%M:%S UTC') - echo "Version: $COMMIT_HASH" > ./publish/wwwroot/version.txt - echo "Built: $BUILD_TIME" >> ./publish/wwwroot/version.txt - echo "✓ Version: $COMMIT_HASH" + mkdir -p ./publish/wwwroot + printf 'Version: %s\nBuilt: %s\n' "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.txt + echo "✓ Build: $COMMIT_HASH @ $BUILD_TIME" - - name: Deploy (CI only, 통합 Web) + - name: Setup SSH + run: | + mkdir -p ~/.ssh + SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}" + SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}" + if [ -n "$SSH_KEY_B64" ]; then + printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519 + elif [ -n "$SSH_KEY_RAW" ]; then + if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then + printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519 + else + printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519 + fi + else + echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY" >&2; exit 1 + fi + sed -i 's/\r$//' ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || true + + - name: Package artifact + run: | + tar -czf taxbaik_deploy.tgz -C ./publish . + echo "✓ Package: $(du -sh taxbaik_deploy.tgz | cut -f1)" + + - name: Deploy & verify on server run: | set -e TIMESTAMP=$(date +%Y%m%d_%H%M%S) - DEPLOY_HOME="/home/kjh2064" - DEPLOY_DIR="$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}" + COMMIT=$(git rev-parse --short HEAD) DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}" DEPLOY_USER="${{ secrets.DEPLOY_USER }}" - echo "=== Deploying TaxBaik v$(git rev-parse --short HEAD) ===" - mkdir -p ~/.ssh - SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}" - SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}" - if [ -n "$SSH_KEY_B64" ]; then - printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519 - elif [ -n "$SSH_KEY_RAW" ]; then - if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then - printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519 - else - printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519 - fi - else - echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY secret" >&2 - exit 1 - fi - sed -i 's/\r$//' ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true + echo "=== Deploying TaxBaik $COMMIT ($TIMESTAMP) ===" - tar -czf taxbaik_publish.tgz -C ./publish . - scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes taxbaik_publish.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_publish_${TIMESTAMP}.tgz" - ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" " - set -e - mkdir -p '$DEPLOY_DIR' - tar -xzf '/tmp/taxbaik_publish_${TIMESTAMP}.tgz' -C '$DEPLOY_DIR' - rm -f '/tmp/taxbaik_publish_${TIMESTAMP}.tgz' - ln -sfn '$DEPLOY_DIR' '$DEPLOY_HOME/taxbaik_active' - sudo systemctl restart taxbaik - " - sleep 5 - echo "✓ Deployed to $DEPLOY_HOST:$DEPLOY_DIR" + # 1. 아티팩트 업로드 + scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \ + taxbaik_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/taxbaik_${TIMESTAMP}.tgz" - - name: Verify deployment - run: | + # 2. 서버에서 배포 + 헬스 체크 (SSH 1회 연결로 처리) + ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes \ + -o ServerAliveInterval=10 \ + "$DEPLOY_USER@$DEPLOY_HOST" bash << REMOTE set -e - DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}" - DEPLOY_USER="${{ secrets.DEPLOY_USER }}" - mkdir -p ~/.ssh - SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}" - SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}" - if [ -n "$SSH_KEY_B64" ]; then - printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519 - elif [ -n "$SSH_KEY_RAW" ]; then - if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN .*PRIVATE KEY'; then - printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/id_ed25519 - else - printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/id_ed25519 + DEPLOY_HOME="/home/kjh2064" + DEPLOY_DIR="\$DEPLOY_HOME/deployments/taxbaik_${TIMESTAMP}" + TIMESTAMP="${TIMESTAMP}" + + echo "--- [1/5] 압축 해제 ---" + mkdir -p "\$DEPLOY_DIR" + tar -xzf "/tmp/taxbaik_\${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR" + rm -f "/tmp/taxbaik_\${TIMESTAMP}.tgz" + + echo "--- [2/5] 운영 설정 검증 ---" + test -s "\$DEPLOY_DIR/appsettings.Production.json" \ + || { echo "FATAL: appsettings.Production.json 없음" >&2; exit 1; } + + echo "--- [3/5] 심볼릭 링크 전환 ---" + ln -sfn "\$DEPLOY_DIR" "\$DEPLOY_HOME/taxbaik_active" + + echo "--- [4/5] 서비스 재시작 ---" + sudo /usr/bin/systemctl restart taxbaik + + echo "--- [5/5] 헬스 체크 (최대 120초) ---" + ATTEMPTS=40 + for i in \$(seq 1 \$ATTEMPTS); do + STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/ 2>/dev/null || echo "000") + if [ "\$STATUS" = "200" ]; then + echo "✓ 서비스 정상 (시도 \$i/\$ATTEMPTS)" + # 구 배포 디렉토리 정리 (최근 5개 보존) + ls -1dt \$DEPLOY_HOME/deployments/taxbaik_* 2>/dev/null \ + | tail -n +6 | xargs rm -rf 2>/dev/null || true + exit 0 fi - else - echo "Missing DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY secret" >&2 - exit 1 - fi - sed -i 's/\r$//' ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true - ADMIN_TEST_PASSWORD="${{ secrets.TAXBAIK_ADMIN_TEST_PASSWORD }}" - HOME_STATUS="000" - LOGIN_STATUS="000" - BLOG_STATUS="000" - BLOG_HEADERS="" - BLOG_BODY="" - BLOG_FINAL_URL="" - AUTH_BODY="" - for i in $(seq 1 12); do - HOME_STATUS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/" || echo "000") - LOGIN_STATUS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5001/taxbaik/admin/login" || echo "000") - BLOG_STATUS_AND_URL=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -L -D /tmp/taxbaik_blog_check.headers -o /tmp/taxbaik_blog_check.html -w '%{http_code} %{url_effective}' http://127.0.0.1:5001/taxbaik/blog/accountant-mistakes-5" || echo "000") - BLOG_STATUS=$(printf '%s' "$BLOG_STATUS_AND_URL" | awk '{print $1}') - BLOG_FINAL_URL=$(printf '%s' "$BLOG_STATUS_AND_URL" | awk '{print $2}') - BLOG_HEADERS=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "sed -n '1,12p' /tmp/taxbaik_blog_check.headers | tr '\n' ' '" || echo "") - BLOG_BODY=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "sed -n '1,12p' /tmp/taxbaik_blog_check.html | tr '\n' ' '" || echo "") - if [ "$HOME_STATUS" = "200" ] && [ "$LOGIN_STATUS" = "200" ] && [ "$BLOG_STATUS" = "200" ]; then - AUTH_BODY=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "python3 -c \"import json, urllib.request; req = urllib.request.Request('http://127.0.0.1:5001/taxbaik/api/auth/login', data=json.dumps({'username':'admin','password':'${ADMIN_TEST_PASSWORD}'}).encode(), headers={'Content-Type':'application/json'}, method='POST'); print(urllib.request.urlopen(req, timeout=20).read().decode())\"" || echo "") - if echo "$AUTH_BODY" | grep -q '"token"'; then - echo "Home Status: $HOME_STATUS" - echo "Login Status: $LOGIN_STATUS" - echo "Blog Status: $BLOG_STATUS" - echo "Auth Body: $AUTH_BODY" - echo "✓ Service is running" - exit 0 - fi + if [ "\$i" -eq "\$ATTEMPTS" ]; then + echo "=== FATAL: 서비스가 \$ATTEMPTS회 시도 후에도 응답하지 않음 ===" >&2 + echo "--- systemd 상태 ---" >&2 + systemctl is-active taxbaik >&2 || true + echo "--- 최근 로그 50줄 ---" >&2 + journalctl -u taxbaik --no-pager -n 50 >&2 + exit 1 fi - sleep 5 + echo " 대기 중... (\$i/\$ATTEMPTS, HTTP \$STATUS)" + sleep 3 done - echo "Home Status: $HOME_STATUS" - echo "Login Status: $LOGIN_STATUS" - echo "Blog Status: $BLOG_STATUS" - echo "Blog Final URL: $BLOG_FINAL_URL" - echo "Blog Headers: $BLOG_HEADERS" - echo "Blog Body: $BLOG_BODY" - echo "Auth Body: $AUTH_BODY" - echo "Service verification failed; collecting remote service diagnostics..." >&2 - ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes "$DEPLOY_USER@$DEPLOY_HOST" "systemctl is-active taxbaik; systemctl status taxbaik --no-pager -l | sed -n '1,120p'; journalctl -u taxbaik --no-pager -n 120" || true - exit 1 + REMOTE + + echo "✓ 배포 완료: taxbaik_${TIMESTAMP} @ $DEPLOY_HOST" diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index b0bf4af..ea39953 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -142,6 +142,10 @@ if (!app.Environment.IsDevelopment()) app.MapControllers(); app.MapHealthChecks("/healthz"); app.MapRazorPages(); -app.MapRazorComponents().AddInteractiveServerRenderMode(); +// AllowAnonymous: JWT 미들웨어가 Blazor 셸 요청을 401로 차단하지 않도록 한다. +// 인증은 Blazor AuthorizeRouteView → RedirectToLogin 에서 처리한다. +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AllowAnonymous(); app.Run(); diff --git a/TaxBaik.Web/wwwroot/maintenance.html b/TaxBaik.Web/wwwroot/maintenance.html new file mode 100644 index 0000000..48ed5bd --- /dev/null +++ b/TaxBaik.Web/wwwroot/maintenance.html @@ -0,0 +1,76 @@ + + + + + + + 잠시 점검 중 — 백원숙 세무회계 + + + +
+
🔧
+
서비스 업데이트 중
+

잠시 후 다시 접속해 주세요

+

+ 더 나은 서비스를 위해 업데이트 작업을 진행하고 있습니다.
+ 보통 1~2분 이내에 완료됩니다. +

+
+

급하신 세무 문의는 카카오 채널로 연락해 주세요.

+ + 💬 카카오 채널 상담 + +

이 페이지는 15초 후 자동으로 새로고침됩니다.

+ +
+ +