diff --git a/backend/src/main/java/com/goldenchart/service/DesktopAppBuildService.java b/backend/src/main/java/com/goldenchart/service/DesktopAppBuildService.java index 6debe9f..432b4a3 100644 --- a/backend/src/main/java/com/goldenchart/service/DesktopAppBuildService.java +++ b/backend/src/main/java/com/goldenchart/service/DesktopAppBuildService.java @@ -78,25 +78,35 @@ public class DesktopAppBuildService { if (!jenkinsEnabled) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Jenkins 연동이 비활성화되어 있습니다."); } + try { + syncGitLatest(); - syncGitLatest(); + DesktopAppBuildStatusDto current = fetchLastBuildStatus(); + if (current.isBuilding()) { + return DesktopAppBuildTriggerDto.builder() + .accepted(false) + .buildNumber(current.getBuildNumber()) + .message("이미 빌드가 진행 중입니다 (#" + current.getBuildNumber() + ").") + .build(); + } - DesktopAppBuildStatusDto current = fetchLastBuildStatus(); - if (current.isBuilding()) { + triggerJenkinsBuild(); + DesktopAppBuildStatusDto after = fetchLastBuildStatus(); return DesktopAppBuildTriggerDto.builder() - .accepted(false) - .buildNumber(current.getBuildNumber()) - .message("이미 빌드가 진행 중입니다 (#" + current.getBuildNumber() + ").") + .accepted(true) + .buildNumber(after.getBuildNumber()) + .message("Jenkins에서 macOS·Windows 데스크톱 앱 빌드를 시작했습니다. 완료 후 설치 파일이 자동으로 반영됩니다.") .build(); + } catch (ResponseStatusException e) { + throw e; + } catch (WebClientResponseException e) { + log.warn("[desktop-build] Jenkins HTTP {}: {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, jenkinsHttpError(e)); + } catch (Exception e) { + log.warn("[desktop-build] trigger failed: {}", e.getMessage()); + throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, + "Jenkins 빌드 요청 실패: " + rootMessage(e)); } - - triggerJenkinsBuild(); - DesktopAppBuildStatusDto after = fetchLastBuildStatus(); - return DesktopAppBuildTriggerDto.builder() - .accepted(true) - .buildNumber(after.getBuildNumber()) - .message("Jenkins에서 macOS·Windows 데스크톱 앱 빌드를 시작했습니다. 완료 후 설치 파일이 자동으로 반영됩니다.") - .build(); } private DesktopAppBuildStatusDto fetchLastBuildStatus() { @@ -156,16 +166,28 @@ public class DesktopAppBuildService { private void triggerJenkinsBuild() { WebClient client = jenkinsClient(); String buildUrl = jobApiPath() + "/build"; + Map crumb = fetchCrumbOptional(client); try { - postBuild(client, buildUrl, null); - } catch (WebClientResponseException.Forbidden e) { - Map crumb = fetchCrumb(client); postBuild(client, buildUrl, crumb); + } catch (WebClientResponseException.Forbidden e) { + if (crumb != null) throw e; + postBuild(client, buildUrl, fetchCrumb(client)); + } + } + + private Map fetchCrumbOptional(WebClient client) { + try { + return fetchCrumb(client); + } catch (Exception e) { + log.debug("[desktop-build] crumb optional skip: {}", e.getMessage()); + return null; } } private void postBuild(WebClient client, String buildUrl, Map crumb) { - var spec = client.post().uri(buildUrl); + var spec = client.post() + .uri(buildUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED); if (crumb != null) { spec = spec.header(crumb.get("field"), crumb.get("value")); } @@ -215,6 +237,27 @@ public class DesktopAppBuildService { return t == null || t.isBlank() ? null : t; } + private static String jenkinsHttpError(WebClientResponseException e) { + int code = e.getStatusCode().value(); + if (code == 401 || code == 403) { + return "Jenkins 인증 실패 — GC_DESKTOP_JENKINS_USER·TOKEN을 확인하세요."; + } + if (code == 404) { + return "Jenkins Job을 찾을 수 없습니다 — goldenChart-Desktop-Pipeline Job을 등록하세요."; + } + return "Jenkins HTTP " + code + ": " + e.getStatusText(); + } + + private static String rootMessage(Throwable e) { + Throwable t = e; + while (t.getCause() != null) t = t.getCause(); + String msg = t.getMessage(); + if (msg != null && msg.contains("Connection refused")) { + return "Jenkins에 연결할 수 없습니다. Docker 환경이면 GC_DESKTOP_JENKINS_URL=http://host.docker.internal:8090 를 설정하세요."; + } + return msg != null ? msg : e.toString(); + } + private void syncGitLatest() { if (!StringUtils.hasText(gitBareDir)) { log.debug("[desktop-build] git-bare-dir 미설정 — Jenkins SCM checkout에 위임"); diff --git a/backend/src/main/resources/application-docker.yml b/backend/src/main/resources/application-docker.yml index acd17f5..b2e4b9a 100644 --- a/backend/src/main/resources/application-docker.yml +++ b/backend/src/main/resources/application-docker.yml @@ -22,6 +22,16 @@ goldenchart: cors: allowed-origins: - "*" + desktop-app: + release-dir: ${GC_DESKTOP_APP_RELEASE_DIR:/app/data/desktop-releases} + public-base-url: ${GC_DESKTOP_APP_PUBLIC_BASE_URL:http://exdev.co.kr} + jenkins: + enabled: ${GC_DESKTOP_JENKINS_ENABLED:true} + # Docker 컨테이너 → 호스트 Jenkins (127.0.0.1:8090 은 컨테이너 자신을 가리킴) + url: ${GC_DESKTOP_JENKINS_URL:http://host.docker.internal:8090} + job: ${GC_DESKTOP_JENKINS_JOB:goldenChart-Desktop-Pipeline} + user: ${GC_DESKTOP_JENKINS_USER:} + token: ${GC_DESKTOP_JENKINS_TOKEN:} # FCM — exdev: FIREBASE_ENABLED=true + 서비스 계정 JSON 마운트 firebase: diff --git a/docker-compose.yml b/docker-compose.yml index c288557..2fe703b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,13 @@ services: GC_MOBILE_APP_RELEASE_DIR: /app/data/mobile-releases GC_MOBILE_APP_VERSION: ${GC_MOBILE_APP_VERSION:-} GC_MOBILE_APP_PUBLIC_BASE_URL: ${GC_MOBILE_APP_PUBLIC_BASE_URL:-http://exdev.co.kr} + GC_DESKTOP_APP_RELEASE_DIR: /app/data/desktop-releases + GC_DESKTOP_APP_PUBLIC_BASE_URL: ${GC_DESKTOP_APP_PUBLIC_BASE_URL:-http://exdev.co.kr} + GC_DESKTOP_JENKINS_ENABLED: ${GC_DESKTOP_JENKINS_ENABLED:-true} + GC_DESKTOP_JENKINS_URL: ${GC_DESKTOP_JENKINS_URL:-http://host.docker.internal:8090} + GC_DESKTOP_JENKINS_JOB: ${GC_DESKTOP_JENKINS_JOB:-goldenChart-Desktop-Pipeline} + GC_DESKTOP_JENKINS_USER: ${GC_DESKTOP_JENKINS_USER:-} + GC_DESKTOP_JENKINS_TOKEN: ${GC_DESKTOP_JENKINS_TOKEN:-} FIREBASE_ENABLED: ${FIREBASE_ENABLED:-false} FIREBASE_SERVICE_ACCOUNT: ${FIREBASE_SERVICE_ACCOUNT:-firebase-service-account.json} ports: @@ -76,6 +83,8 @@ services: volumes: - verification-images:/app/data/verification-images - ./data/mobile-releases:/app/data/mobile-releases:ro + - ./data/desktop-releases:/app/data/desktop-releases:ro + - ./frontend/public/desktop/updates:/app/data/desktop-updates:ro # FCM: .env 에 FIREBASE_SERVICE_ACCOUNT_PATH=/path/to/firebase-service-account.json 설정 시 주석 해제 # - ${FIREBASE_SERVICE_ACCOUNT_PATH}:/app/firebase-service-account.json:ro healthcheck: @@ -86,6 +95,8 @@ services: start_period: 60s networks: - goldenchart-net + extra_hosts: + - "host.docker.internal:host-gateway" # ── React Frontend (Nginx) ─────────────────────────────────────────────── frontend: