From 6ad6084ef29f002c2e0bb0cfa32faaff6c4d1bcd Mon Sep 17 00:00:00 2001 From: choibk Date: Sat, 24 Jan 2026 19:09:09 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=A6=88=20v0.2.6:=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=9E=90=EB=8F=99=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20=EB=B0=B0=ED=8F=AC=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/PRODUCTION_DEPLOYMENT.md | 95 +++++++++++++++++++ package.json | 2 +- server/package.json | 2 +- server/routes/system.js | 80 ++++++++++++++++ src/platform/pages/VersionPage.tsx | 146 ++++++++++++++++++++++++----- 5 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 docs/PRODUCTION_DEPLOYMENT.md diff --git a/docs/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md new file mode 100644 index 0000000..88a865a --- /dev/null +++ b/docs/PRODUCTION_DEPLOYMENT.md @@ -0,0 +1,95 @@ +# 운영 서버 배포 및 관리 가이드 (Production Deployment Guide) + +본 문서는 SmartIMS 솔루션을 운영 서버에 최초 설치하는 절차와 Git Tag를 이용한 효율적인 버전 관리 및 업데이트 방역에 대해 설명합니다. + +--- + +## 1. 개요 (Overview) + +SmartIMS 배포는 **Git Tag 기반 배포** 방식을 권장합니다. 이는 검증된 특정 시점의 소스 코드(Release)만을 운영 서버에 반영하여 시스템 안정성을 확보하고, 향후 시스템 관리 메뉴를 통해 손쉽게 업데이트할 수 있는 기반을 제공합니다. + +--- + +## 2. 최초 설치 절차 (Initial Installation) + +운영 서버(Linux/Synology 등)에 접속한 상태에서 다음 과정을 순차적으로 진행합니다. + +### 2.1. 사전 요구 사항 +* **Git**: 소스 코드 동기화용 +* **Node.js**: v20 이상 환경 +* **MySQL/MariaDB**: 서비스 데이터베이스 +* **PM2**: 서비스 프로세스 관리 (`npm install -g pm2`) + +### 2.2. 설치 단계 +1. **소스 클론**: + ```bash + git clone [저장소_URL] smartims + cd smartims + ``` +2. **안정 버전(Tag) 전환**: + ```bash + git fetch --all --tags + # 현재 배포 가능한 최신 태그로 전환 (예: v0.2.5) + git checkout v0.2.5 + ``` +3. **환경 설정**: + * `server/.env` 파일을 환경에 맞게 생성(DB 정보 등 입력). + * `server/public_key.pem` 파일이 존재하는지 확인 (라이선스 검증용). +4. **패키지 설치 및 빌드**: + ```bash + # 전체 의존성 설치 및 프론트엔드 빌드 + npm install + npm run build + + # 백엔드 의존성 설치 + cd server + npm install + ``` +5. **데이터베이스 초기화**: + ```bash + # 처음 설치 시에만 수행 + node migrate_db.js + ``` +6. **PM2 프로세스 등록**: + ```bash + pm2 start index.js --name "smartims-api" + pm2 save + pm2 startup + ``` + +--- + +## 3. 업데이트 관리 (Update Management) + +### 3.1. 기존 수동 업데이트 방법 +수동으로 업데이트가 필요할 경우 다음 명령어를 조합하여 실행합니다. +```bash +git fetch --tags +git checkout [새로운_태그] +npm install +npm run build +pm2 reload smartims-api +``` + +### 3.2. 시스템 관리 메뉴를 통한 업데이트 (검토 중) +시스템의 "버전 정보" 화면에서 신규 버전을 감지하고 업데이트하는 기능을 검토 중입니다. + +* **동작 원리**: + 1. 서버가 원격 저장소의 최신 Tag 리스트를 정기적으로 또는 요청 시 확인합니다. + 2. `package.json`의 현재 버전과 원격의 최신 Tag를 비교하여 업데이트 버튼을 활성화합니다. + 3. 업데이트 실행 시 서버 내부적으로 `git checkout` -> `npm install` -> `npm run build` -> `pm2 reload` 과정을 자동화된 스크립트로 수행합니다. + +* **기대 효과**: + * 터미널(SSH) 접속 없이 관리자 화면에서 즉시 최신 기능 반영 가능. + * 버전 불일치 문제 예방 및 운영 편의성 증대. + +--- + +## 4. 주의 사항 (Precautions) + +1. **보안**: `tools/` 폴더 및 개인키(`private_key.pem`)는 운영 서버에 절대로 포함되지 않도록 주의하십시오. +2. **권한**: 업데이트 자동화 스크립트 실행 시 파일 시스템 쓰기 권한 및 Git 접근 권한이 서버 프로세스에 부여되어 있어야 합니다. +3. **백업**: 업데이트 전에 데이터베이스 백업을 반드시 수행할 것을 권장합니다. + +--- +마지막 업데이트: 2026-01-24 diff --git a/package.json b/package.json index 2d26323..a587fdf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "smartims", "private": true, - "version": "0.2.5", + "version": "0.2.6", "type": "module", "scripts": { "dev": "vite", diff --git a/server/package.json b/server/package.json index af2e6c1..4585844 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.2.5", + "version": "0.2.6", "description": "", "main": "index.js", "scripts": { diff --git a/server/routes/system.js b/server/routes/system.js index 0cc8290..a9d911a 100644 --- a/server/routes/system.js +++ b/server/routes/system.js @@ -1,5 +1,6 @@ const express = require('express'); const router = express.Router(); +const { exec, execSync } = require('child_process'); const db = require('../db'); const path = require('path'); const fs = require('fs'); @@ -424,4 +425,83 @@ router.post('/modules/:code/deactivate', isAuthenticated, hasRole('admin'), asyn +// --- System Update Logic --- + +// 5. Get Version Info (Current & Remote) +router.get('/version/remote', isAuthenticated, hasRole('admin'), async (req, res) => { + try { + const packageJsonPath = path.join(__dirname, '../package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const currentVersion = packageJson.version; + + // Run git fetch to update tags from remote + exec('git fetch --tags', (err) => { + if (err) { + console.error('Git fetch failed:', err); + return res.json({ + current: currentVersion, + latest: null, + error: '원격 저장소에 연결할 수 없습니다. Git 설정을 확인하세요.' + }); + } + + // Get the latest tag + exec('git describe --tags $(git rev-list --tags --max-count=1)', (err, stdout) => { + const latestTag = stdout ? stdout.trim() : null; + res.json({ + current: currentVersion, + latest: latestTag, + needsUpdate: latestTag ? (latestTag.replace(/^v/, '') !== currentVersion.replace(/^v/, '')) : false + }); + }); + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: '버전 정보를 가져오는 중 오류가 발생했습니다.' }); + } +}); + +// 6. Execute System Update +// WARNING: This is a heavy operation and will reload the server +router.post('/version/update', isAuthenticated, hasRole('admin'), async (req, res) => { + const { targetTag } = req.body; + + if (!targetTag) { + return res.status(400).json({ error: '업데이트할 대상 태그가 지정되지 않았습니다.' }); + } + + // This operation is asynchronous. We start it and return a message. + // In a real production, we might want to log this to a terminal-like view. + + const updateScript = ` + git checkout ${targetTag} && + npm install && + npm run build && + cd server && + npm install && + pm2 reload smartims-api + `; + + // Note: On Windows, we might need a different script or use a shell + const isWindows = process.platform === 'win32'; + const shellCommand = isWindows ? `powershell.exe -Command "${updateScript.replace(/\n/g, '')}"` : updateScript; + + console.log(`🚀 Starting system update to ${targetTag}...`); + + exec(shellCommand, { cwd: path.join(__dirname, '../..') }, (err, stdout, stderr) => { + if (err) { + console.error('❌ Update Failed:', err); + console.error(stderr); + return; + } + console.log('✅ Update completed successfully.'); + console.log(stdout); + }); + + res.json({ + success: true, + message: '업데이트 프로세스가 백그라운드에서 시작되었습니다. 약 1~3분 후 시스템이 재시작됩니다.' + }); +}); + module.exports = router; diff --git a/src/platform/pages/VersionPage.tsx b/src/platform/pages/VersionPage.tsx index 8ff83fd..8e58abc 100644 --- a/src/platform/pages/VersionPage.tsx +++ b/src/platform/pages/VersionPage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Card } from '../../shared/ui/Card'; import { apiClient } from '../../shared/api/client'; -import { Info, Cpu, Database, Server, Hash, Calendar } from 'lucide-react'; +import { Info, Cpu, Database, Server, Hash, Calendar, RefreshCw, AlertTriangle, CheckCircle2 } from 'lucide-react'; interface VersionInfo { status: string; @@ -9,35 +9,124 @@ interface VersionInfo { timestamp: string; } +interface RemoteVersion { + current: string; + latest: string | null; + needsUpdate: boolean; + error?: string; +} + export function VersionPage() { const [healthIcon, setHealthInfo] = useState(null); + const [remoteInfo, setRemoteInfo] = useState(null); const [loading, setLoading] = useState(true); + const [checkingRemote, setCheckingRemote] = useState(false); + const [updating, setUpdating] = useState(false); + const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null); + + const fetchVersion = async () => { + setLoading(true); + try { + const res = await apiClient.get('/health'); + setHealthInfo(res.data); + } catch (err) { + console.error('Failed to fetch version info', err); + } finally { + setLoading(false); + } + }; + + const fetchRemoteVersion = async () => { + setCheckingRemote(true); + try { + const res = await apiClient.get('/system/version/remote'); + setRemoteInfo(res.data); + } catch (err) { + console.error('Failed to fetch remote version info', err); + } finally { + setCheckingRemote(false); + } + }; useEffect(() => { - const fetchVersion = async () => { - try { - const res = await apiClient.get('/health'); - setHealthInfo(res.data); - } catch (err) { - console.error('Failed to fetch version info', err); - } finally { - setLoading(false); - } - }; fetchVersion(); + fetchRemoteVersion(); }, []); - // Frontend version (aligned with package.json) - const frontendVersion = '0.2.5'; + const handleUpdate = async () => { + if (!remoteInfo?.latest) return; + + if (!confirm(`시스템을 ${remoteInfo.latest} 버전으로 업데이트하시겠습니까?\n업데이트 중에는 시스템이 일시적으로 중단될 수 있습니다.`)) { + return; + } + + setUpdating(true); + try { + const res = await apiClient.post('/system/version/update', { targetTag: remoteInfo.latest }); + setUpdateResult({ success: true, message: res.data.message }); + } catch (err: any) { + console.error('Update failed', err); + setUpdateResult({ + success: false, + message: err.response?.data?.error || '업데이트 요청 중 오류가 발생했습니다.' + }); + } finally { + setUpdating(false); + } + }; + + // Use values from healthIcon if available, otherwise fallback to local constants + const currentVersion = healthIcon?.version || '0.2.6'; const buildDate = '2026-01-24'; return (
-
-

시스템 관리 - 버전 정보

-

플랫폼 및 서버의 현재 릴리즈 버전을 확인합니다.

+
+
+

시스템 관리 - 버전 정보

+

플랫폼 및 서버의 현재 릴리즈 버전을 확인하고 업데이트를 관리합니다.

+
+
+ {/* Update Alert Banner */} + {remoteInfo?.needsUpdate && !updateResult && ( +
+
+
+ +
+
+

새로운 시스템 업데이트가 가능합니다!

+

현재 버전: v{currentVersion} → 최신 버전: {remoteInfo.latest}

+
+
+ +
+ )} + + {/* Update Result Message */} + {updateResult && ( +
+ {updateResult.success ? : } +

{updateResult.message}

+
+ )} +
{/* Frontend Platform Version */} @@ -57,7 +146,7 @@ export function VersionPage() {
현재 버전 - v{frontendVersion} + v{currentVersion}
빌드 일자 @@ -107,6 +196,19 @@ export function VersionPage() {
+ + {/* Remote Info Status (Debug/Info) */} + {remoteInfo && ( +
+
+
+ 원격 저장소 상태: {remoteInfo.error ? `오류 (${remoteInfo.error})` : '정상'} +
+ {!remoteInfo.error && ( + 최신 배포 태그: {remoteInfo.latest || '없음'} + )} +
+ )} {/* Release History Section */}

@@ -119,12 +221,12 @@ export function VersionPage() { { version: '0.2.5', date: '2026-01-24', - title: '플랫폼 보안 모듈 및 관리자 권한 체계 강화', + title: '플랫폼 보안 모듈 및 시스템 자동 업데이트 엔진 도입', changes: [ - '최고관리자(Supervisor) 전용 2중 보안 잠금 시스템 적용', - '데이터베이스 및 암호화 마스터 키 자가 관리 엔진 구축', - '사용자 관리 UI 디자인 및 권한 계층 로직 일원화', - '시스템 버전 관리 통합 (v0.2.5)' + 'Git Tag 기반 시스템 자동 업데이트 관리 모듈 신규 도입', + '최고관리자 전용 업데이트 실행 UI 구축', + '데이터베이스 및 암호화 마스터 키 자가 관리 엔진 고도화', + '사용자 관리 UI 디자인 및 권한 계층 로직 일원화' ], type: 'feature' },