diff --git a/.gitea/workflows/deploy-wise.yml b/.gitea/workflows/deploy-wise.yml
new file mode 100644
index 0000000..b632df5
--- /dev/null
+++ b/.gitea/workflows/deploy-wise.yml
@@ -0,0 +1,132 @@
+# arko_renual — csv.wise.ai.kr 자동 배포 파이프라인
+#
+# [개발 PC] git push → 이 워크플로가 자동 실행
+# [웹서버] 빌드(JDK8/Maven) → WAR 을 WAS 로 전송
+# [WAS] ROOT 재전개 + 환경설정 주입 + Tomcat 재기동 + 헬스체크(실패 시 자동 롤백)
+# [웹서버] SSH 터널 경유로 공개 확인 (https://csv.wise.ai.kr)
+#
+# 그 세 걸음이 곧 지정된 경로다: PC → git → web → was(db)
+#
+# ┌ 왜 웹서버에서 빌드하나 ────────────────────────────────────────────────┐
+# │ WAS(27.255.76.2)는 64코어지만 llama-server 가 200G+ 를 점유해 가용 │
+# │ 메모리가 3G 안팎이다. 거기서 Maven(힙 1G)을 돌리면 LLM 서비스가 OOM 될 │
+# │ 수 있다. 웹서버(4코어/여유 4G)는 빌드 외 부하가 없어 여기서 빌드한다. │
+# └────────────────────────────────────────────────────────────────────────┘
+#
+# ※ Gitea 1.22.3 은 workflow_dispatch(수동 실행 버튼)를 지원하지 않는다 —
+# 넣으면 워크플로가 Actions 목록에 아예 뜨지 않는다. 트리거는 push 하나만 둔다.
+# 수동 재배포는 WAS 에서 직접:
+# ssh -J <웹서버> claudeops@27.255.76.2 \
+# 'bash /opt/arko-app/deploy/arko-deploy.sh /opt/arko-app/incoming/arko.war'
+name: deploy-wise
+
+on:
+ push:
+ branches:
+ - master
+ # 현재 배포 대상은 이 브랜치다. master 는 7커밋 뒤처져 있고 이관된 DB 스키마와
+ # 맞지 않는다. master 로 병합한 뒤에는 아래 줄을 지워도 된다.
+ - feature/privacy-access-control-policy
+ paths-ignore: # 문서만 바뀐 커밋으로 재배포하지 않는다
+ - '**.md'
+ - '**.docx'
+ - '**.pptx'
+ - '**.hwp'
+ - 'docs/**'
+ - '_workspace/**'
+ - '_workspace_privacy/**'
+
+concurrency:
+ group: arko-wise-deploy
+ cancel-in-progress: false # 배포 도중 취소되면 ROOT 가 반쪽 상태로 남는다
+
+env:
+ JAVA_HOME: /usr/lib/jvm/java-1.8.0-openjdk
+ MVN: /opt/maven/bin/mvn
+ WAS_HOST: 27.255.76.2
+ WAS_USER: claudeops
+ WAS_KEY: /root/.ssh/id_ed25519
+ APP_HOME: /opt/arko-app
+ SITE_URL: https://csv.wise.ai.kr/
+
+jobs:
+ deploy:
+ runs-on: arko-web
+ timeout-minutes: 25
+
+ steps:
+ # ⚠ lfs: true 를 주지 않는다 — 러너에 git-lfs 가 없으면 checkout 자체가 죽는다.
+ - uses: actions/checkout@v4
+
+ - name: 사전 점검
+ run: |
+ set -e
+ df -h /opt | tail -1
+ free -h | head -2
+ # 터널이 살아 있어야 배포 후 확인이 가능하다
+ systemctl is-active arko-was-tunnel || echo "::warning::터널이 죽어 있다 — 배포 후 확인이 실패할 수 있다"
+
+ - name: 빌드 (JDK 8)
+ run: |
+ set -e
+ export JAVA_HOME="$JAVA_HOME"
+ export MAVEN_OPTS="-Xmx1024m"
+ # Maven 3.8+ 는 HTTP 저장소를 차단한다. eGov 2.7 은 HTTP 저장소(terracotta 등)를
+ # 쓰므로 3.6.3 을 고정해서 쓴다. 3.8 로 올리면 의존성 해석부터 실패한다.
+ "$MVN" -B clean package -DskipTests
+ ls -lh target/arko.war
+
+ - name: 산출물 검증
+ run: |
+ set -e
+ # 바이트코드 major=51(Java 7). JDK 11/17 로 잘못 빌드되면 WAS 에서 못 뜬다.
+ CLS=$(find target/classes -name '*.class' | head -1)
+ VER=$(od -An -t d1 -j 7 -N 1 "$CLS" | tr -d ' ')
+ echo "바이트코드 major=$VER (51이어야 정상)"
+ [ "$VER" = "51" ] || { echo "::error::JDK 8 이 아닌 컴파일러로 빌드됐다"; exit 1; }
+ # 파이프 없이 한 번만 읽어 검사한다(파이프+pipefail 조합이 정상 WAR 을 오판한 전례가 있다)
+ WAR_LIST=$(unzip -l target/arko.war)
+ grep -q "WEB-INF/web.xml" <<< "$WAR_LIST" || { echo "::error::정상 WAR 이 아니다"; exit 1; }
+
+ - name: WAS 로 전송
+ run: |
+ set -e
+ SSH="ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i $WAS_KEY"
+ $SSH "$WAS_USER@$WAS_HOST" "mkdir -p $APP_HOME/incoming $APP_HOME/deploy"
+ scp -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$WAS_KEY" \
+ target/arko.war "$WAS_USER@$WAS_HOST:$APP_HOME/incoming/arko.war"
+ # 배포 스크립트도 매번 저장소본으로 덮어쓴다.
+ # (이전 파이프라인에서 서버 사본이 저장소본과 갈라져 고친 내용이 반영되지 않는
+ # 사고가 있었다. 저장소를 유일한 원본으로 둔다.)
+ scp -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$WAS_KEY" \
+ deploy/wise/arko-deploy.sh "$WAS_USER@$WAS_HOST:$APP_HOME/deploy/arko-deploy.sh"
+ # 잘린 전송을 잡는다 — 전례가 있어 크기를 반드시 대조한다
+ LOCAL_MD5=$(md5sum target/arko.war | cut -d' ' -f1)
+ REMOTE_MD5=$($SSH "$WAS_USER@$WAS_HOST" "md5sum $APP_HOME/incoming/arko.war | cut -d' ' -f1")
+ echo "local=$LOCAL_MD5 remote=$REMOTE_MD5"
+ [ "$LOCAL_MD5" = "$REMOTE_MD5" ] || { echo "::error::전송된 WAR 이 손상됐다"; exit 1; }
+
+ - name: WAS 배포
+ run: |
+ set -e
+ ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -i "$WAS_KEY" \
+ "$WAS_USER@$WAS_HOST" "chmod +x $APP_HOME/deploy/arko-deploy.sh && bash $APP_HOME/deploy/arko-deploy.sh $APP_HOME/incoming/arko.war"
+
+ - name: 공개 확인
+ run: |
+ set -e
+ # 터널 경유(웹서버 → WAS)
+ TUN=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 http://127.0.0.1:11060/ || echo 000)
+ echo "터널 127.0.0.1:11060 → HTTP $TUN"
+ # 공개 URL. 첫 요청은 JSP 컴파일로 15초 이상 걸릴 수 있다.
+ CODE=000
+ for _ in $(seq 1 6); do
+ CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 "$SITE_URL" || echo 000)
+ case "$CODE" in 200|302) break ;; esac
+ sleep 5
+ done
+ echo "$SITE_URL → HTTP $CODE"
+ case "$CODE" in
+ 200|302) echo "배포 정상" ;;
+ *) echo "::error::공개 URL 응답 없음 (HTTP $CODE) — 터널(arko-was-tunnel) 상태를 확인할 것"; exit 1 ;;
+ esac
diff --git a/deploy/wise/01-was-setup.sh b/deploy/wise/01-was-setup.sh
new file mode 100644
index 0000000..ae856fd
--- /dev/null
+++ b/deploy/wise/01-was-setup.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# arko WAS(27.255.76.2) 최초 구축 — 컨테이너 2기(MariaDB, Tomcat) 생성
+#
+# 실행 위치: WAS 에서 claudeops 계정으로. bash 01-was-setup.sh
+# 재실행 안전: 컨테이너를 지우고 다시 만든다. 단 db-data 는 건드리지 않는다.
+#
+# ┌ 이 서버의 제약 ────────────────────────────────────────────────────────┐
+# │ ① claudeops 는 sudo 를 쓸 수 없다. docker 그룹 소속이라 컨테이너로만 │
+# │ 설치할 수 있다. /opt 쓰기도 컨테이너를 거쳐야 한다. │
+# │ ② llama-server 가 200G+ 를 점유해 가용 메모리가 3G 안팎이다. │
+# │ 두 컨테이너 합쳐 1.75G 로 묶어 둔다. 올리기 전 반드시 free -g 확인. │
+# │ ③ 11000~11050 은 다른 서비스가 쓰고 있다. arko 는 11060/11061 을 쓴다. │
+# └────────────────────────────────────────────────────────────────────────┘
+set -euo pipefail
+
+APP_HOME=/opt/arko-app
+MARIADB_ROOT_PW=${MARIADB_ROOT_PW:?"MARIADB_ROOT_PW 를 지정할 것"}
+
+# ---------------------------------------------------------- 0. 디렉토리
+# /opt 는 root 소유라 컨테이너를 거쳐 만든다(claudeops 는 sudo 불가).
+docker run --rm -v /opt:/host-opt alpine:3 sh -c \
+ 'for d in db-data incoming webapps logs deploy conf backup lib upload privacy-download; do mkdir -p /host-opt/arko-app/$d; done; chown -R 1005:1006 /host-opt/arko-app'
+mkdir -p "$APP_HOME"/upload/{thumbNail,preView,Synch,bbsTemp} "$APP_HOME"/logs/{mysql,tomcat}
+
+docker network inspect arko-net >/dev/null 2>&1 || docker network create arko-net
+
+# ------------------------------------------------------ 1. JDBC 드라이버
+# MariaDB 로 가므로 MariaDB 커넥터를 쓴다. WAR 안의 mysql-connector 는 쓰지 않는다
+# (pom 은 5.1.17 고정인데 properties 는 8.x 전용 com.mysql.cj.jdbc.Driver 를 가리켜
+# 그대로는 맞지 않는다). Tomcat 의 lib 에 두면 WAR 을 건드리지 않고 교체할 수 있다.
+[ -f "$APP_HOME/lib/mariadb-java-client-2.7.12.jar" ] || \
+ curl -fsSL -o "$APP_HOME/lib/mariadb-java-client-2.7.12.jar" \
+ https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.7.12/mariadb-java-client-2.7.12.jar
+
+# ---------------------------------------------------------- 2. MariaDB
+# lower_case_table_names=1 은 initdb 시점에만 반영된다(mariadb.cnf 주석 참고).
+docker rm -f arko-mariadb >/dev/null 2>&1 || true
+docker run -d --name arko-mariadb --restart unless-stopped --network arko-net \
+ -p 127.0.0.1:11061:3306 \
+ -m 768m --memory-swap 768m \
+ -e MARIADB_ROOT_PASSWORD="$MARIADB_ROOT_PW" \
+ -e MARIADB_DATABASE=csv -e MARIADB_USER=csv -e MARIADB_PASSWORD=csv \
+ -e TZ=Asia/Seoul \
+ -v "$APP_HOME/db-data":/var/lib/mysql \
+ -v "$APP_HOME/conf/mariadb.cnf":/etc/mysql/conf.d/arko.cnf:ro \
+ -v "$APP_HOME/logs/mysql":/var/log/mysql \
+ mariadb:11.4
+
+# ----------------------------------------------------------- 3. Tomcat
+# arko 는 source/target 1.7 이라 JDK 8 로만 뜬다(11/17 불가).
+# 컨테이너는 만들어만 두고 기동은 arko-deploy.sh 가 한다(ROOT 가 없으면 의미가 없다).
+docker rm -f arko-tomcat >/dev/null 2>&1 || true
+docker create --name arko-tomcat --restart unless-stopped --network arko-net \
+ -p 127.0.0.1:11060:8080 \
+ -m 1g --memory-swap 1g \
+ -e TZ=Asia/Seoul \
+ -e JAVA_OPTS="-Xms256m -Xmx768m -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Seoul -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom" \
+ -v "$APP_HOME/webapps":/usr/local/tomcat/webapps \
+ -v "$APP_HOME/lib/mariadb-java-client-2.7.12.jar":/usr/local/tomcat/lib/mariadb-java-client-2.7.12.jar:ro \
+ -v "$APP_HOME/logs/tomcat":/usr/local/tomcat/logs \
+ -v "$APP_HOME/upload":/app/upload \
+ -v "$APP_HOME/privacy-download":/app/privacy-download \
+ tomcat:9.0-jdk8-temurin
+
+echo "완료. 다음: config/globals-{dev,live}.properties 를 두고 arko-deploy.sh 를 실행할 것."
diff --git a/deploy/wise/README.md b/deploy/wise/README.md
new file mode 100644
index 0000000..ebd8c5a
--- /dev/null
+++ b/deploy/wise/README.md
@@ -0,0 +1,123 @@
+# csv.wise.ai.kr 배포 구성
+
+## 전체 구조
+
+```
+ [개발 PC]
+ │ git push
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ 웹서버 175.118.124.176 (utimsurpai.cafe24.com, Rocky 8, SSH :9271) │
+│ Gitea git.wise.ai.kr (127.0.0.1:9003) │
+│ act_runner `arko-web:host` → JDK8+Maven 3.6.3 으로 WAR 빌드 │
+│ Apache httpd → https://csv.wise.ai.kr │
+└───────────┬──────────────────────────────────┬──────────────────────┘
+ │ ① scp WAR + 배포 스크립트 (SSH) │ ② SSH 터널 :11060
+ ▼ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ WAS 27.255.76.2 (Ubuntu 22.04, 64코어) │
+│ arko-tomcat 컨테이너 127.0.0.1:11060 ← ROOT 전개 │
+│ arko-mariadb 컨테이너 127.0.0.1:11061 DB `csv` │
+│ /opt/arko-app (webapps · config · upload · backup · logs) │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+WAS 는 27 → 인터넷 방향으로만 열려 있고, 웹서버를 거쳐야 닿는다.
+Tomcat 은 127.0.0.1 에만 바인딩되며 **인터넷에 직접 노출되지 않는다** — 웹서버가
+아웃바운드 SSH 터널로 자기 쪽 11060 을 WAS 의 Tomcat 에 잇는다.
+
+## 배포 흐름
+
+```bash
+git push # 이하 전부 자동
+```
+
+```
+push → 웹서버에서 빌드(JDK8) → 바이트코드 major=51 검증 → WAR md5 대조 전송
+ → WAS 에서 ROOT 재전개 + 환경설정 주입 + Tomcat 재기동 + 헬스체크
+ → 실패 시 직전 백업으로 자동 롤백
+ → https://csv.wise.ai.kr 공개 확인
+```
+
+## 왜 이런 구조인가
+
+**빌드를 WAS 에서 하지 않는 이유** — WAS 는 64코어지만 llama-server 가 200G+ 를
+점유해 **가용 메모리가 3G 안팎**이다. 여기서 Maven(힙 1G)을 돌리면 LLM 서비스가
+OOM 될 수 있다. 웹서버(4코어 / 여유 4G)는 빌드 외 부하가 없다.
+
+**컨테이너로 설치한 이유** — WAS 의 `claudeops` 계정은 **sudo 를 쓸 수 없고**
+docker 그룹에만 속한다. 패키지 설치가 불가능해 Tomcat·MariaDB 를 컨테이너로 올렸다.
+같은 서버의 다른 서비스(urpai 등)와 같은 관행이다.
+
+**배포 스크립트를 매번 덮어쓰는 이유** — 이전 파이프라인에서 서버 사본과 저장소본이
+갈라져, 저장소에서 고친 내용이 배포에 반영되지 않는 사고가 있었다. 워크플로가 매 실행마다
+`deploy/wise/arko-deploy.sh` 를 WAS 로 복사하므로 **저장소가 유일한 원본**이다.
+
+## 포트
+
+| 포트 | 위치 | 용도 |
+|---|---|---|
+| 9271 | 웹서버 | SSH |
+| 9003 | 웹서버 127.0.0.1 | Gitea |
+| 11060 | WAS 127.0.0.1 | Tomcat (웹서버는 터널로 접근) |
+| 11061 | WAS 127.0.0.1 | MariaDB |
+
+11000~11050 은 같은 서버의 다른 서비스가 쓰고 있다. 포트를 새로 잡을 때 겹치지 않게 할 것.
+
+## 파일
+
+| 파일 | 설치 위치 | 역할 |
+|---|---|---|
+| `01-was-setup.sh` | WAS | 컨테이너 2기 최초 생성 (재실행 안전) |
+| `arko-deploy.sh` | WAS `/opt/arko-app/deploy/` | 재전개·설정주입·기동·헬스체크·롤백 |
+| `mariadb.cnf` | WAS `/opt/arko-app/conf/` | MariaDB 설정 (`lower_case_table_names=1` 주의) |
+| `csv.wise.ai.kr.conf` | 웹 `/etc/httpd/conf.d/` | Apache vhost (80→443, 터널로 프록시) |
+| `arko-was-tunnel.service` | 웹 `/etc/systemd/system/` | SSH 터널 (자동 재연결) |
+| `arko-runner.service` | 웹 `/etc/systemd/system/` | arko 전용 act_runner |
+| `arko-runner-config.yaml` | 웹 `/opt/arko-runner/config.yaml` | 러너 설정 |
+
+## 환경설정 (저장소에 없음)
+
+`globals-dev.properties` / `globals-live.properties` 는 `.gitignore` 대상이라
+저장소에도 WAR 에도 들어가지 않는다. **WAS 의 `/opt/arko-app/config/` 에만 두고**
+배포 때 `WEB-INF/classes/egovframework/egovProps/` 로 주입한다.
+`context-datasource.xml` 이 `globals-dev.properties` 를 읽으므로 이 주입이 없으면 기동 실패한다.
+
+WAS 값에서 로컬과 다른 부분:
+
+```properties
+Globals.OsType =UNIX
+Globals.DriverClassName =org.mariadb.jdbc.Driver
+Globals.Url =jdbc:mariadb://arko-mariadb:3306/csv?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+Globals.fileStorePath =/app/upload/
+Globals.home.url =https://csv.wise.ai.kr
+Globals.home.base =/usr/local/tomcat/webapps/ROOT
+Globals.privacyDownloadPath=/app/privacy-download/
+```
+
+## 운영 명령
+
+```bash
+# 웹서버 (ssh -p 9271 root@175.118.124.176)
+systemctl status arko-runner arko-was-tunnel # 502 가 나면 터널부터 확인
+tail -f /var/log/httpd/csv_error.log
+
+# WAS (ssh 는 웹서버를 경유한다)
+docker ps --filter name=arko-
+docker logs -f --tail 100 arko-tomcat
+tail -f /opt/arko-app/logs/deploy.log
+docker exec -it arko-mariadb mariadb -ucsv -pcsv csv
+
+# 수동 재배포 (Gitea 1.22 는 수동 실행 버튼이 없다)
+bash /opt/arko-app/deploy/arko-deploy.sh /opt/arko-app/incoming/arko.war
+```
+
+## 알려진 제약
+
+- **Gitea 1.22.3 은 `workflow_dispatch` 를 지원하지 않는다.** 넣으면 워크플로가 Actions
+ 목록에 아예 뜨지 않는다. 재배포는 실제 파일을 바꿔 push 하거나 WAS 에서 직접 실행한다.
+- **Maven 은 3.6.3 을 쓴다.** 3.8+ 는 HTTP 저장소를 차단하는데 eGov 2.7 은 HTTP 저장소
+ (terracotta 등)에 의존해 의존성 해석부터 실패한다.
+- **메모리가 빠듯하다.** 두 컨테이너 합쳐 1.75G 상한이다. 올리기 전 `free -g` 로 확인할 것.
+- **DB 는 MariaDB 다.** 원본(MySQL 8)의 `utf8mb4_0900_ai_ci` 는 MariaDB 에 없어
+ `utf8mb4_general_ci` 로 변환해 이관했다. 한글 정렬 결과는 같다(코드포인트 순).
diff --git a/deploy/wise/arko-deploy.sh b/deploy/wise/arko-deploy.sh
new file mode 100644
index 0000000..1d3d4b9
--- /dev/null
+++ b/deploy/wise/arko-deploy.sh
@@ -0,0 +1,115 @@
+#!/usr/bin/env bash
+# arko(문화품앗이) WAS 배포 스크립트 — 27.255.76.2
+#
+# 설치 위치: /opt/arko-app/deploy/arko-deploy.sh
+# 호출 주체: Gitea Actions(웹서버 러너)가 ssh 로, 또는 수동 실행
+# 사용법 : bash arko-deploy.sh /opt/arko-app/incoming/arko.war
+#
+# ┌ 이 서버의 제약 ────────────────────────────────────────────────────────┐
+# │ claudeops 는 sudo 를 쓸 수 없다. 대신 docker 그룹에 속한다. │
+# │ 그래서 Tomcat·MariaDB 는 패키지 설치가 아니라 컨테이너로 돌린다. │
+# │ 또한 llama-server 가 200G+ 를 점유해 가용 메모리가 3G 안팎이다. │
+# │ 컨테이너에 메모리 상한을 걸어 두었으니 함부로 올리지 말 것. │
+# └────────────────────────────────────────────────────────────────────────┘
+#
+# 동작: WAR 검증 → 현재 ROOT 백업(3개 유지) → 컨테이너 정지 → 재전개
+# → 환경설정 주입 → 기동 → 헬스체크 (실패 시 직전 백업으로 자동 롤백)
+set -euo pipefail
+
+APP_HOME=/opt/arko-app
+WEBAPPS=$APP_HOME/webapps
+ROOT_DIR=$WEBAPPS/ROOT
+CONFIG_DIR=$APP_HOME/config
+BACKUP_DIR=$APP_HOME/backup
+LOG_DIR=$APP_HOME/logs
+CONTAINER=arko-tomcat
+HEALTH_URL=http://127.0.0.1:11060/
+HEALTH_TIMEOUT=240 # 초. arko 는 첫 기동에서 JSP 컴파일로 60~120초 걸린다
+KEEP_BACKUPS=3
+
+WAR_SRC=${1:-$APP_HOME/incoming/arko.war}
+TS=$(date +%Y%m%d_%H%M%S)
+
+mkdir -p "$BACKUP_DIR" "$LOG_DIR" "$WEBAPPS"
+log() { echo "[$(date +'%F %T')] $*" | tee -a "$LOG_DIR/deploy.log"; }
+die() { log "ERROR: $*"; exit 1; }
+
+# ------------------------------------------------------------- 1. WAR 검증
+[ -f "$WAR_SRC" ] || die "WAR 파일 없음: $WAR_SRC"
+# unzip -l 을 파이프로 grep 하면 set -o pipefail 과 맞물려 정상 WAR 을 오판한다
+# (2026-07-30 개발 파이프라인에서 실제로 배포가 멈춘 적이 있다). 한 번만 읽어 변수에 담는다.
+WAR_LIST=$(unzip -l "$WAR_SRC") || die "WAR 손상: $WAR_SRC"
+grep -q "WEB-INF/web.xml" <<< "$WAR_LIST" || die "WEB-INF/web.xml 없음 — 정상 WAR 아님"
+log "배포 시작: $WAR_SRC ($(du -h "$WAR_SRC" | cut -f1))"
+
+# 전개본 + 백업까지 WAR 3배의 여유가 필요하다
+NEED_KB=$(( $(du -k "$WAR_SRC" | cut -f1) * 3 ))
+FREE_KB=$(df -k "$APP_HOME" | awk 'NR==2 {print $4}')
+[ "$FREE_KB" -gt "$NEED_KB" ] || die "디스크 부족: 여유 $((FREE_KB/1024))MB, 필요 $((NEED_KB/1024))MB"
+
+# --------------------------------------------------------- 2. 현재본 백업
+BACKUP_FILE=""
+if [ -d "$ROOT_DIR" ]; then
+ BACKUP_FILE="$BACKUP_DIR/ROOT_$TS.tar.gz"
+ log "현재 ROOT 백업 → $BACKUP_FILE"
+ tar -czf "$BACKUP_FILE" -C "$WEBAPPS" ROOT
+ ls -1t "$BACKUP_DIR"/ROOT_*.tar.gz 2>/dev/null | tail -n +$((KEEP_BACKUPS+1)) | xargs -r rm -f
+fi
+
+# --------------------------------------------------------- 3. 컨테이너 정지
+if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
+ log "Tomcat 정지"
+ docker stop "$CONTAINER" >/dev/null 2>&1 || true
+fi
+
+# ------------------------------------------------------------- 4. 재전개
+log "ROOT 재전개"
+rm -rf "$ROOT_DIR" "$WEBAPPS/ROOT.tmp"
+mkdir -p "$WEBAPPS/ROOT.tmp"
+unzip -q "$WAR_SRC" -d "$WEBAPPS/ROOT.tmp"
+mv "$WEBAPPS/ROOT.tmp" "$ROOT_DIR"
+
+# ------------------------------------------------- 5. 환경설정 주입 (필수)
+# globals-{dev,live}.properties 는 .gitignore 대상이라 저장소에도 WAR 에도 없다.
+# context-datasource.xml 이 globals-dev.properties 를 읽으므로 이 주입이 없으면 기동 실패한다.
+PROPS_DEST="$ROOT_DIR/WEB-INF/classes/egovframework/egovProps"
+[ -d "$PROPS_DEST" ] || die "egovProps 경로 없음: $PROPS_DEST"
+for f in globals-dev.properties globals-live.properties; do
+ [ -f "$CONFIG_DIR/$f" ] || die "서버 보관 환경설정 없음: $CONFIG_DIR/$f"
+ cp -f "$CONFIG_DIR/$f" "$PROPS_DEST/$f"
+ log "환경설정 주입: $f"
+done
+chmod 640 "$PROPS_DEST"/globals-*.properties
+
+# --------------------------------------------------------- 6. 컨테이너 기동
+log "Tomcat 기동"
+docker start "$CONTAINER" >/dev/null || die "컨테이너 기동 실패 — 'docker logs $CONTAINER' 확인"
+
+# ------------------------------------------------------------- 7. 헬스체크
+log "헬스체크 (최대 ${HEALTH_TIMEOUT}초)"
+CODE=000
+for _ in $(seq 1 $((HEALTH_TIMEOUT/5))); do
+ CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$HEALTH_URL" || echo 000)
+ case "$CODE" in 200|302) break ;; esac
+ sleep 5
+done
+
+case "$CODE" in
+ 200|302)
+ log "배포 완료 — HTTP $CODE"
+ ;;
+ *)
+ log "헬스체크 실패 (HTTP $CODE)"
+ if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then
+ log "롤백: $BACKUP_FILE 복구"
+ docker stop "$CONTAINER" >/dev/null 2>&1 || true
+ rm -rf "$ROOT_DIR"
+ tar -xzf "$BACKUP_FILE" -C "$WEBAPPS"
+ docker start "$CONTAINER" >/dev/null 2>&1 || true
+ log "롤백 완료 — 기동 로그: docker logs --tail 100 $CONTAINER"
+ else
+ log "복구할 백업이 없다(최초 배포). 컨테이너 로그를 확인할 것."
+ fi
+ exit 1
+ ;;
+esac
diff --git a/deploy/wise/arko-runner-config.yaml b/deploy/wise/arko-runner-config.yaml
new file mode 100644
index 0000000..be86e52
--- /dev/null
+++ b/deploy/wise/arko-runner-config.yaml
@@ -0,0 +1,14 @@
+# arko 전용 act_runner 설정 — /opt/arko-runner/config.yaml
+# 이 서버에는 러너가 셋(wise/urpai/arko) 상주한다. arko 빌드는 1~3분이 걸리고
+# WAR 이 192MB 라, 다른 서비스 배포를 막지 않도록 러너를 분리해 둔다.
+log:
+ level: info
+runner:
+ capacity: 1 # arko 배포는 ROOT 를 교체하므로 절대 병렬로 돌리지 않는다
+ timeout: 30m
+ labels:
+ - arko-web:host # host 모드 — 러너가 웹서버에서 직접 명령을 실행한다
+cache:
+ enabled: true
+container:
+ network: host
diff --git a/deploy/wise/arko-runner.service b/deploy/wise/arko-runner.service
new file mode 100644
index 0000000..169dd68
--- /dev/null
+++ b/deploy/wise/arko-runner.service
@@ -0,0 +1,20 @@
+# arko 전용 Gitea Actions 러너 — /etc/systemd/system/arko-runner.service
+# 라벨 arko-web:host 로 .gitea/workflows/deploy-wise.yml 을 받는다.
+[Unit]
+Description=Gitea Actions runner (arko / csv.wise.ai.kr)
+After=network-online.target gitea.service
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/opt/arko-runner
+# nginx/Apache 를 거치지 않고 Gitea 에 직결한다(127.0.0.1:9003).
+ExecStart=/opt/arko-runner/act_runner daemon --config /opt/arko-runner/config.yaml
+Environment=HOME=/root
+Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
diff --git a/deploy/wise/arko-was-tunnel.service b/deploy/wise/arko-was-tunnel.service
new file mode 100644
index 0000000..403a83c
--- /dev/null
+++ b/deploy/wise/arko-was-tunnel.service
@@ -0,0 +1,40 @@
+# arko(문화품앗이) WAS 터널 — 운영 웹(175.118.124.176) → WAS(27.255.76.2)
+# 설치 위치: /etc/systemd/system/arko-was-tunnel.service
+#
+# 왜 터널인가:
+# WAS 의 Tomcat 은 컨테이너 포트를 127.0.0.1:11060 에만 바인딩한다.
+# 같은 서버의 urpai 처럼 공인 고포트로 열 수도 있지만, arko 는 개인정보보호
+# 과제 대상 시스템이라 WAS 를 인터넷에 직접 노출하지 않는다.
+# 웹서버가 아웃바운드 SSH 로 자기 쪽 127.0.0.1:11060 을 WAS 의 Tomcat 에 잇는다.
+#
+# 키 제한: WAS 의 authorized_keys 에서
+# restrict,port-forwarding,permitopen="127.0.0.1:11060"
+# 으로 묶여 있어 셸 접근이 불가능하고 이 포트 외에는 열 수 없다.
+#
+# 502 가 나면 여기부터 확인한다: systemctl status arko-was-tunnel
+[Unit]
+Description=SSH tunnel to arko WAS (27.255.76.2:11060)
+After=network-online.target
+Wants=network-online.target
+Before=httpd.service
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/ssh -N -T \
+ -o ExitOnForwardFailure=yes \
+ -o ServerAliveInterval=30 \
+ -o ServerAliveCountMax=3 \
+ -o StrictHostKeyChecking=accept-new \
+ -o UserKnownHostsFile=/root/.ssh/known_hosts \
+ -o IdentitiesOnly=yes \
+ -i /root/.ssh/id_arko_tunnel \
+ -L 127.0.0.1:11060:127.0.0.1:11060 \
+ claudeops@27.255.76.2
+
+Restart=always
+RestartSec=10
+User=root
+Group=root
+
+[Install]
+WantedBy=multi-user.target
diff --git a/deploy/wise/csv.wise.ai.kr.conf b/deploy/wise/csv.wise.ai.kr.conf
new file mode 100644
index 0000000..6acc07c
--- /dev/null
+++ b/deploy/wise/csv.wise.ai.kr.conf
@@ -0,0 +1,58 @@
+# csv.wise.ai.kr — arko(문화품앗이) 웹 티어
+# 설치 위치: /etc/httpd/conf.d/csv.wise.ai.kr.conf (이 서버는 Apache httpd 를 쓴다)
+#
+# 경로: 브라우저 → httpd 443 → [SSH 터널] 127.0.0.1:11060 → WAS(27.255.76.2) Tomcat
+# DB(MariaDB)는 WAS 안에서만 접근한다. 이 서버는 DB 에 붙지 않는다.
+#
+# 127.0.0.1:11060 은 arko-was-tunnel.service 가 만들어 준다.
+# 502 가 나면 `systemctl status arko-was-tunnel` 부터 확인할 것.
+
+
+ ServerName csv.wise.ai.kr
+
+ # 인증서 갱신(webroot)은 리다이렉트보다 먼저 처리되어야 한다
+ Alias /.well-known/acme-challenge/ /var/www/html/.well-known/acme-challenge/
+
+ Require all granted
+ Options None
+
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
+ RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
+
+
+
+ ServerName csv.wise.ai.kr
+
+ SSLEngine on
+ SSLCertificateFile /etc/letsencrypt/live/csv.wise.ai.kr/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/live/csv.wise.ai.kr/privkey.pem
+ SSLProtocol -all +TLSv1.2 +TLSv1.3
+ SSLCipherSuite HIGH:!aNULL:!MD5
+
+ # 자원봉사 활동사진·첨부파일 업로드가 있어 기본값으로는 부족하다
+ LimitRequestBody 104857600
+
+ Header always set X-Frame-Options "SAMEORIGIN"
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
+ # 운영(csv.culture.go.kr)이 따로 있는 검증 환경이므로 색인을 막는다
+ Header always set X-Robots-Tag "noindex, nofollow"
+
+ # Host 를 그대로 넘겨야 WAS 가 csv.wise.ai.kr 기준으로 링크를 만든다
+ ProxyPreserveHost On
+ # arko 는 첫 요청에서 JSP 를 컴파일하고, 통계·엑셀 다운로드가 오래 걸린다
+ ProxyTimeout 300
+
+ # 접속기록(개인정보보호 SFR-004)이 실제 접속자 IP 를 남겨야 한다.
+ # 프록시를 한 단 거치므로 원본 IP 를 헤더로 전달한다.
+ RequestHeader set X-Forwarded-Proto "https"
+
+ ProxyPass /.well-known/acme-challenge/ !
+ ProxyPass / http://127.0.0.1:11060/
+ ProxyPassReverse / http://127.0.0.1:11060/
+
+ ErrorLog /var/log/httpd/csv_error.log
+ CustomLog /var/log/httpd/csv_access.log combined
+
diff --git a/deploy/wise/mariadb.cnf b/deploy/wise/mariadb.cnf
new file mode 100644
index 0000000..9c592bf
--- /dev/null
+++ b/deploy/wise/mariadb.cnf
@@ -0,0 +1,33 @@
+# arko(문화품앗이) MariaDB — /opt/arko-app/conf/mariadb.cnf
+#
+# 이 서버(27.255.76.2)는 llama-server가 200G+를 쓰는 GPU 추론 서버다.
+# 가용 메모리가 3G 안팎이라 버퍼풀을 384M로 묶고 컨테이너에도 768m 상한을 건다.
+# 늘리기 전에 반드시 `free -g` 로 여유를 확인할 것.
+[mysqld]
+# 원본은 Windows MySQL 8 (lower_case_table_names=1). Linux 기본값 0으로 두면
+# iBatis SQL의 테이블명 대소문자가 어긋나 "Table doesn't exist"가 난다.
+# 이 값은 초기화(initdb) 시점에만 반영되므로 db-data를 비우고 다시 만들어야 바뀐다.
+lower_case_table_names = 1
+
+character-set-server = utf8mb4
+collation-server = utf8mb4_general_ci
+
+innodb_buffer_pool_size = 384M
+innodb_log_file_size = 128M
+innodb_flush_log_at_trx_commit = 2
+max_connections = 150
+max_allowed_packet = 64M
+table_open_cache = 400
+tmp_table_size = 32M
+max_heap_table_size = 32M
+
+# 원본 MySQL 8은 ONLY_FULL_GROUP_BY가 켜진 상태로 운영된다. 동작을 맞춰 둔다.
+sql_mode = ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
+
+default_time_zone = '+09:00'
+slow_query_log = 1
+slow_query_log_file = /var/log/mysql/slow.log
+long_query_time = 3
+
+[client]
+default-character-set = utf8mb4