이호영 이호영 07-27
config: 서비스 포트를 8877로 고정
application.yml에 server.port가 없어 기본 8080으로 뜨고, 실행 스크립트마다 --server.port를
따로 넘기고 있어서 환경별로 포트가 달랐다. 앱 자체 기본값을 8877로 두어 어디서 띄우든
같은 포트가 되게 한다. SERVER_PORT 환경변수로는 여전히 덮어쓸 수 있다.

함께 맞춘 곳:
- Dockerfile EXPOSE / HEALTHCHECK, docker-compose 포트 매핑
- deploy/start.sh 기본값, itnhub.env.example (SERVER_PORT·TZ 항목 추가)
- vite 개발 프록시 대상(:8080 -> :8877). 이걸 안 바꾸면 npm run dev 에서 /api 가 죽는다
- run-local.ps1 은 --server.port 인자를 떼고 앱 기본값을 따르게 함
- README / deploy/README 의 포트 표기

로컬에서 재빌드 후 확인: 8877 /login 200, 8080 닫힘.

Co-Authored-By: Claude Opus 5 (1M context) 
@5d0c7bcfdc2aaccb074d6e2ffe774a1ad7a3d67b
Dockerfile
--- Dockerfile
+++ Dockerfile
@@ -35,12 +35,12 @@
 WORKDIR /app
 COPY --from=build /build/app.jar /app/app.jar
 
-# 컨테이너 안에서는 스프링 기본 포트를 그대로 쓴다. 바깥 포트는 실행할 때 -p 로 정한다.
-EXPOSE 8080
+# 컨테이너 안에서도 앱 기본 포트(8877)를 그대로 쓴다. 바깥 포트는 실행할 때 -p 로 정한다.
+EXPOSE 8877
 
 # actuator를 넣지 않아서 헬스체크는 로그인 화면 200으로 확인한다(익명 접근 허용 경로).
 HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
-  CMD wget -q --spider http://127.0.0.1:8080/login || exit 1
+  CMD wget -q --spider http://127.0.0.1:8877/login || exit 1
 
 # 컨테이너 메모리 한도에 맞춰 힙을 잡는다.
 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+UseContainerSupport"
README.md
--- README.md
+++ README.md
@@ -93,7 +93,7 @@
 export APP_ADMIN_USERNAME=admin
 export APP_ADMIN_PASSWORD=...
 
-java -jar target/itnhub-0.1.0.jar --server.port=8091
+java -jar target/itnhub-0.1.0.jar
 ```
 
 기동하면 Flyway가 `itnhub` 스키마와 테이블(V1~V12)을 **자동으로 만든다.** 수동 DDL은 없다.
@@ -101,7 +101,7 @@
 ### 프런트만 반복 개발할 때
 
 ```bash
-cd frontend && npm install && npm run dev   # Vite가 /api 를 :8080 으로 프록시
+cd frontend && npm install && npm run dev   # Vite가 /api 를 :8877 로 프록시
 mvn spring-boot:run                          # 백엔드는 따로 띄워 둔다
 ```
 
deploy/README.md
--- deploy/README.md
+++ deploy/README.md
@@ -6,8 +6,8 @@
 | 항목 | 값 |
 |---|---|
 | 런타임 | JDK 21 (이미지에 포함) |
-| 컨테이너 내부 포트 | 8080 |
-| 서버에 노출할 포트 | 8091 (compose에서 변경 가능) |
+| 컨테이너 내부 포트 | 8877 |
+| 서버에 노출할 포트 | 8877 |
 | DB | 외부 PostgreSQL 16 `192.168.0.60:5432/itnhub` (컨테이너로 띄우지 않음) |
 | 영속 볼륨 | **불필요** — 업로드 파일은 DB(`org_report`)와 Mattermost에 저장된다 |
 | 스키마 생성 | 기동 시 Flyway가 `itnhub` 스키마와 V1~V12를 자동 적용 |
@@ -33,7 +33,7 @@
 sudo cp deploy/docker-compose.yml /opt/itnhub/
 
 # 방화벽
-sudo firewall-cmd --add-port=8091/tcp --permanent && sudo firewall-cmd --reload
+sudo firewall-cmd --add-port=8877/tcp --permanent && sudo firewall-cmd --reload
 ```
 
 서버 밖에서 챙길 것 두 가지:
@@ -72,7 +72,7 @@
 
 ```bash
 ssh deploy@192.168.0.176 "sudo docker compose -f /opt/itnhub/docker-compose.yml ps"
-curl -f http://192.168.0.176:8091/login && echo OK
+curl -f http://192.168.0.176:8877/login && echo OK
 ssh deploy@192.168.0.176 "sudo docker logs --tail 100 itnhub"
 ```
 
@@ -103,4 +103,4 @@
 - 테스트(`mvn test`)는 Testcontainers로 PostgreSQL을 띄우므로 **에이전트에 Docker 소켓이 필요**하다.
   이미지 빌드 안에서는 테스트를 돌리지 않는다(Docker-in-Docker가 되므로).
 - 권장 순서: `mvn -B verify`(테스트) → `docker build` → 레지스트리 push → 176에서 `compose up -d`
-  → `curl -f http://192.168.0.176:8091/login` 로 확인.
+  → `curl -f http://192.168.0.176:8877/login` 로 확인.
deploy/docker-compose.yml
--- deploy/docker-compose.yml
+++ deploy/docker-compose.yml
@@ -12,9 +12,9 @@
     env_file:
       - /etc/itnhub/itnhub.env
 
-    # 왼쪽이 서버에서 열리는 포트. 로컬 개발과 같은 8091로 맞춰 둔다.
+    # 앱 포트는 8877로 고정되어 있다(application.yml).
     ports:
-      - "8091:8080"
+      - "8877:8877"
 
     # 컨테이너 로그가 무한정 쌓이지 않게 한다.
     logging:
deploy/itnhub.env.example
--- deploy/itnhub.env.example
+++ deploy/itnhub.env.example
@@ -27,5 +27,11 @@
 APP_ADMIN_PASSWORD=
 
 # --- 선택 ---
+# 앱 포트. application.yml에 8877로 고정돼 있으니 보통 건드릴 일이 없다.
+# SERVER_PORT=8877
+
+# 서버 시간대. UTC로 두면 업무단계 도달 일시와 칸반 D+n이 9시간 어긋난다.
+TZ=Asia/Seoul
+
 # nginx 등 TLS 종단 프록시를 앞에 두면 켠다(리다이렉트·쿠키 Secure 판정이 정상화된다).
 # SERVER_FORWARD_HEADERS_STRATEGY=framework
 
deploy/start.sh (added)
+++ deploy/start.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+#
+# ITN-HUB 기동 스크립트 (nohup 방식).
+#
+# application.yml의 ${DB_URL} 같은 자리는 Spring이 "같은 이름의 환경변수"에서 찾아 채운다.
+# 그래서 java를 띄우기 전에 env 파일을 읽어 export 해두기만 하면 된다.
+# 비밀값을 java 명령 인자로 넘기면 `ps` 에 그대로 보이므로 반드시 환경변수로 준다.
+set -euo pipefail
+
+# ── 경로 (환경에 맞게 수정) ──────────────────────────────────────────
+JAVA_HOME_BIN=/home/hubAdmin/jdk-21.0.11/bin
+DEPLOY_PATH=/home/hubAdmin/deploy
+FILE_NM=itnhub-0.1.0.jar
+LOG_FILE_PATH=/home/hubAdmin/logs
+LOG_FILE_NM=itnhub.log
+PID_FILE=/home/hubAdmin/itnhub.pid
+
+# 값이 든 파일. 소유자만 읽게 두고(chmod 600) 저장소에는 올리지 않는다.
+ENV_FILE=/home/hubAdmin/itnhub.env
+
+# ── 환경변수 주입 ────────────────────────────────────────────────────
+if [ ! -f "$ENV_FILE" ]; then
+  echo "환경변수 파일이 없습니다: $ENV_FILE" >&2
+  echo "deploy/itnhub.env.example 을 복사해 값을 채우세요." >&2
+  exit 1
+fi
+
+# set -a 를 켜면 이후 대입되는 변수가 모두 export 된다(파일에 export를 안 써도 된다).
+set -a
+# shellcheck disable=SC1090
+. "$ENV_FILE"
+set +a
+
+# 필수 8개가 비어 있으면 기동 도중에 죽으므로 여기서 먼저 걸러낸다.
+for v in DB_URL DB_USERNAME DB_PASSWORD MATTERMOST_URL MATTERMOST_TOKEN \
+         MATTERMOST_TEAM_ID APP_ADMIN_USERNAME APP_ADMIN_PASSWORD; do
+  if [ -z "${!v:-}" ]; then
+    echo "필수 환경변수가 비어 있습니다: $v  ($ENV_FILE 확인)" >&2
+    exit 1
+  fi
+done
+
+# 서버 시간대가 UTC면 업무단계 도달 일시, 칸반의 D+n, 연락 이력 날짜가 9시간 어긋난다.
+export TZ="${TZ:-Asia/Seoul}"
+# 앱 기본 포트는 application.yml에 8877로 고정되어 있다. 필요하면 env 파일에서 바꾼다.
+export SERVER_PORT="${SERVER_PORT:-8877}"
+
+JAVA_OPTS="${JAVA_OPTS:--Xms512m -Xmx1g -Duser.timezone=Asia/Seoul -Dfile.encoding=UTF-8}"
+
+# ── 중복 기동 방지 ───────────────────────────────────────────────────
+if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
+  echo "이미 실행 중입니다 (PID $(cat "$PID_FILE")). 먼저 stop.sh 를 실행하세요." >&2
+  exit 1
+fi
+
+mkdir -p "$LOG_FILE_PATH"
+
+# ── 기동 ─────────────────────────────────────────────────────────────
+nohup "$JAVA_HOME_BIN/java" $JAVA_OPTS -jar "$DEPLOY_PATH/$FILE_NM" \
+  >> "$LOG_FILE_PATH/$LOG_FILE_NM" 2>&1 &
+
+echo $! > "$PID_FILE"
+echo "기동 요청 완료 (PID $(cat "$PID_FILE"), 포트 $SERVER_PORT)"
+
+# ── 기동 확인 ────────────────────────────────────────────────────────
+# actuator를 넣지 않아서 로그인 화면 200으로 확인한다.
+for _ in $(seq 1 60); do
+  if curl -fs -o /dev/null "http://127.0.0.1:${SERVER_PORT}/login"; then
+    echo "정상 기동 확인: http://127.0.0.1:${SERVER_PORT}/login"
+    exit 0
+  fi
+  sleep 1
+done
+
+echo "60초 안에 응답이 없습니다. 로그를 확인하세요: $LOG_FILE_PATH/$LOG_FILE_NM" >&2
+exit 1
 
deploy/stop.sh (added)
+++ deploy/stop.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+#
+# ITN-HUB 종료 스크립트. start.sh가 남긴 PID 파일을 쓴다.
+set -euo pipefail
+
+PID_FILE=/home/hubAdmin/itnhub.pid
+
+if [ ! -f "$PID_FILE" ]; then
+  echo "PID 파일이 없습니다: $PID_FILE"
+  exit 0
+fi
+
+PID=$(cat "$PID_FILE")
+
+if ! kill -0 "$PID" 2>/dev/null; then
+  echo "이미 종료된 프로세스입니다 (PID $PID)."
+  rm -f "$PID_FILE"
+  exit 0
+fi
+
+# 먼저 정상 종료를 요청하고, 20초를 기다렸다가 그래도 살아 있으면 강제 종료한다.
+kill "$PID"
+for _ in $(seq 1 20); do
+  if ! kill -0 "$PID" 2>/dev/null; then
+    rm -f "$PID_FILE"
+    echo "종료 완료 (PID $PID)"
+    exit 0
+  fi
+  sleep 1
+done
+
+echo "정상 종료되지 않아 강제 종료합니다 (PID $PID)" >&2
+kill -9 "$PID"
+rm -f "$PID_FILE"
frontend/vite.config.ts
--- frontend/vite.config.ts
+++ frontend/vite.config.ts
@@ -9,7 +9,7 @@
   },
   server: {
     proxy: {
-      '/api': 'http://localhost:8080',
+      '/api': 'http://localhost:8877',
     },
   },
   test: {
run-local.ps1
--- run-local.ps1
+++ run-local.ps1
@@ -29,5 +29,5 @@
 $env:APP_ADMIN_PASSWORD = 'itntest123'
 
 Set-Location 'D:\Development\InteliJ_repository\itnhub'
-Write-Host 'Starting ITN-HUB with the REAL Mattermost token. Port 8091. Stop: Ctrl+C'
-java -jar target\itnhub-0.1.0.jar --server.port=8091
+Write-Host 'Starting ITN-HUB with the REAL Mattermost token. Port 8877. Stop: Ctrl+C'
+java -jar target\itnhub-0.1.0.jar
src/main/resources/application.yml
--- src/main/resources/application.yml
+++ src/main/resources/application.yml
@@ -1,3 +1,7 @@
+server:
+  # 운영·개발 모두 8877을 쓴다. 필요하면 SERVER_PORT 환경변수로 덮어쓸 수 있다.
+  port: ${SERVER_PORT:8877}
+
 spring:
   application:
     name: itnhub
Add a comment
List