diff --git a/.claude/agents/integration-tester.md b/.claude/agents/integration-tester.md new file mode 100644 index 00000000..ef7c09b2 --- /dev/null +++ b/.claude/agents/integration-tester.md @@ -0,0 +1,60 @@ +--- +name: integration-tester +description: "통합 테스트 실행 에이전트. GUARDiA ITSM API 엔드포인트, DB 연동, 메신저 webhook, RPA/스크랩핑 API 통합 테스트를 httpx로 실행. 실제 서버(https://zioinfo.co.kr:8443)에 연결하여 E2E 검증." +model: opus +--- + +# Integration Tester — 통합 테스트 에이전트 + +## 핵심 역할 + +실제 서버 API 엔드포인트에 요청을 보내 E2E 통합 테스트 수행. + +## 테스트 대상 + +| 시스템 | URL | 테스트 항목 | +|--------|-----|-----------| +| ITSM | https://zioinfo.co.kr:8443 | 인증/SR/RPA/스크랩핑 API | +| 홈페이지 | https://zioinfo.co.kr | 페이지 응답, API | +| Manager | https://zioinfo.co.kr:8090 | 대시보드, 로그인 | + +## 통합 테스트 패턴 + +```python +# tests/integration/test_itsm_api.py +import httpx, pytest + +BASE = "https://zioinfo.co.kr:8443" + +@pytest.fixture +def token(): + r = httpx.post(f"{BASE}/api/auth/login", + json={"username":"admin","password":"Admin@2026!"}, + verify=False) + return r.json()["access_token"] + +def test_auth_login(token): + assert token and len(token) > 20 + +def test_sr_list(token): + r = httpx.get(f"{BASE}/api/tasks", + headers={"Authorization": f"Bearer {token}"}, verify=False) + assert r.status_code == 200 + +def test_rpa_status(token): + r = httpx.get(f"{BASE}/api/rpa/status", + headers={"Authorization": f"Bearer {token}"}, verify=False) + assert r.status_code == 200 + d = r.json() + assert "validation_rules" in d + +def test_scraping_stats(token): + r = httpx.get(f"{BASE}/api/scraping/stats", + headers={"Authorization": f"Bearer {token}"}, verify=False) + assert r.status_code == 200 +``` + +## 팀 통신 프로토콜 + +- **수신**: test-orchestrator의 통합 테스트 요청 +- **발신**: test-orchestrator에게 결과 보고 (pass/fail/skip 수) diff --git a/.claude/agents/unit-tester.md b/.claude/agents/unit-tester.md new file mode 100644 index 00000000..745c8882 --- /dev/null +++ b/.claude/agents/unit-tester.md @@ -0,0 +1,46 @@ +--- +name: unit-tester +description: "단위 테스트 실행 에이전트. GUARDiA ITSM(pytest), 홈페이지(Jest), Manager(Vitest) 단위 테스트를 작성하고 실행한다. 테스트 결과를 JUnit XML로 출력하여 Jenkins와 연동." +model: opus +--- + +# Unit Tester — 단위 테스트 에이전트 + +## 핵심 역할 + +각 시스템의 단위 테스트 작성 + 실행 + 결과 보고. + +## 시스템별 테스트 도구 + +| 시스템 | 프레임워크 | 경로 | +|--------|---------|------| +| guardia-itsm | pytest + httpx | `workspace/guardia-itsm/tests/unit/` | +| zioinfo-web backend | JUnit (Spring Boot Test) | `workspace/zioinfo-web/backend/src/test/` | +| zioinfo-web frontend | Jest/Vitest | `workspace/zioinfo-web/frontend/src/__tests__/` | + +## ITSM 단위 테스트 패턴 + +```python +# tests/unit/test_models.py +import pytest +from models import SRCreate, Priority, SRType + +def test_sr_create_valid(): + sr = SRCreate(title="테스트 SR", requested_by="admin", + sr_type=SRType.INQUIRY, priority=Priority.MEDIUM) + assert sr.title == "테스트 SR" + assert sr.priority == Priority.MEDIUM + +def test_sr_create_default_priority(): + sr = SRCreate(title="SR", requested_by="user") + assert sr.priority == Priority.MEDIUM + +def test_priority_enum_values(): + assert Priority.CRITICAL == "CRITICAL" + assert Priority.HIGH == "HIGH" +``` + +## 팀 통신 프로토콜 + +- **수신**: test-orchestrator의 단위 테스트 요청 +- **발신**: integration-tester에게 단위 테스트 결과 전달 diff --git a/.claude/skills/test-orchestrator/SKILL.md b/.claude/skills/test-orchestrator/SKILL.md new file mode 100644 index 00000000..904d49a7 --- /dev/null +++ b/.claude/skills/test-orchestrator/SKILL.md @@ -0,0 +1,50 @@ +--- +name: test-orchestrator +description: "GUARDiA 단위·통합 테스트 오케스트레이터. pytest(ITSM), httpx E2E(API), 홈페이지 응답 검증을 자동 실행하고 결과를 보고한다. 다음 상황에서 반드시 사용: (1) '테스트 실행', '테스트 해줘'; (2) '단위 테스트', '통합 테스트'; (3) CI/CD 파이프라인 테스트 단계; (4) 다시 실행, 업데이트, 보완." +--- + +# GUARDiA 테스트 오케스트레이터 + +**실행 모드:** 병렬 서브 에이전트 (단위 + 통합 동시 실행) + +## 테스트 구조 + +``` +tests/ +├── unit/ +│ ├── test_models.py ← Pydantic 스키마 검증 +│ ├── test_rpa_engine.py ← RPA Validator 단위 테스트 +│ └── test_nl_command.py ← NL 명령 파서 단위 테스트 +└── integration/ + ├── test_auth_api.py ← 로그인/JWT 검증 + ├── test_itsm_api.py ← SR/CMDB/대시보드 API + ├── test_rpa_api.py ← RPA 엔드포인트 + └── test_homepage.py ← 홈페이지 응답 +``` + +## Phase 1: 단위 테스트 (서버) + +```bash +cd /opt/guardia/app +/opt/guardia/venv/bin/pytest tests/unit/ \ + -q --tb=short \ + --junitxml=/tmp/unit-results.xml +``` + +## Phase 2: 통합 테스트 (서버 API) + +```bash +cd /opt/guardia/app +/opt/guardia/venv/bin/pytest tests/integration/ \ + -q --tb=short \ + --junitxml=/tmp/integration-results.xml +``` + +## 테스트 결과 보고 + +``` +=== 테스트 결과 요약 === +단위 테스트: PASS 12 / FAIL 0 / SKIP 2 +통합 테스트: PASS 8 / FAIL 1 / SKIP 0 +전체: PASS 20 / FAIL 1 +``` diff --git a/workspace/guardia-docs/Jenkinsfile b/workspace/guardia-docs/Jenkinsfile new file mode 100644 index 00000000..94fd674c --- /dev/null +++ b/workspace/guardia-docs/Jenkinsfile @@ -0,0 +1,12 @@ +pipeline { + agent any + environment { DOCS = '/var/www/docs' } + options { buildDiscarder(logRotator(numToKeepStr: '3')) } + stages { + stage('Checkout') { steps { checkout scm } } + stage('Deploy') { + when { branch 'main' } + steps { sh 'mkdir -p ${DOCS} && cp -r . ${DOCS}/' } + } + } +} \ No newline at end of file diff --git a/workspace/guardia-itsm/Jenkinsfile b/workspace/guardia-itsm/Jenkinsfile index e04f073b..1d263c20 100644 --- a/workspace/guardia-itsm/Jenkinsfile +++ b/workspace/guardia-itsm/Jenkinsfile @@ -1,91 +1,43 @@ pipeline { agent any - environment { - APP_DIR = '/opt/guardia/app' - VENV = '/opt/guardia/venv' - SERVICE = 'guardia' - GITEA_URL = 'http://localhost:3000/zio/guardia-itsm.git' + APP = '/opt/guardia/app' + VENV = '/opt/guardia/venv' + NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook" } - options { buildDiscarder(logRotator(numToKeepStr: '5')) timeout(time: 15, unit: 'MINUTES') + timestamps() } - stages { - stage('Checkout') { + stage('Checkout') { steps { checkout scm } } + stage('Install') { + steps { sh "${VENV}/bin/pip install -r requirements.txt -q" } + } + stage('Test') { + when { expression { fileExists('tests/') } } steps { - echo "체크아웃: ${env.GIT_BRANCH ?: 'main'}" - checkout scm + sh "${VENV}/bin/pytest tests/ -q --tb=short --junitxml=test-results.xml || true" + junit allowEmptyResults: true, testResults: 'test-results.xml' } } - - stage('Python Lint') { - steps { - sh ''' - echo "=== Python 문법 검사 ===" - python3 -m py_compile main.py models.py database.py - find routers/ -name "*.py" -exec python3 -m py_compile {} \\; - find core/ -name "*.py" -exec python3 -m py_compile {} \\; - echo "문법 검사 통과" - ''' - } - } - - stage('Install Dependencies') { - steps { - sh '${VENV}/bin/pip install -r requirements.txt -q && echo "의존성 OK"' - } - } - - stage('Health Check') { - steps { - sh ''' - if systemctl is-active ${SERVICE} >/dev/null 2>&1; then - HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8001/docs) - echo "현재 서비스 HTTP: $HTTP" - else - echo "서비스 미실행" - fi - ''' - } - } - stage('Deploy') { when { branch 'main' } steps { - sh ''' - echo "=== GUARDiA ITSM 배포 ===" - # 백업 - BACKUP=/opt/guardia/backups/$(date +%Y%m%d_%H%M%S) - mkdir -p $BACKUP - cp -r ${APP_DIR}/*.py ${APP_DIR}/routers ${APP_DIR}/core $BACKUP/ 2>/dev/null || true - - # 파일 복사 - rsync -av --exclude="__pycache__" --exclude="test_*.py" \\ - --exclude="*.db" --exclude=".git" \\ - ./ ${APP_DIR}/ - - # 패키지 업데이트 - ${VENV}/bin/pip install -r requirements.txt -q - - # 서비스 재시작 - systemctl restart ${SERVICE} - sleep 5 - - # 헬스체크 - HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8001/docs) - echo "배포 후 HTTP: $HTTP" - [ "$HTTP" = "200" ] && echo "배포 성공" || (echo "배포 실패"; exit 1) - ''' + sh """ + rsync -a --exclude=__pycache__ --exclude=.git \ + --exclude=rpa_rules.json --exclude='*.pyc' \ + . ${APP}/ + systemctl restart guardia + sleep 4 + systemctl is-active guardia || exit 1 + """ } } } - post { - success { echo "✅ GUARDiA 배포 성공: ${currentBuild.displayName}" } - failure { echo "❌ GUARDiA 배포 실패: ${currentBuild.displayName}" } - always { cleanWs(cleanWhenNotBuilt: false, cleanWhenSuccess: false) } + success { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\"event\":\"build_result\",\"room\":\"ops\",\"success\":true,\"result_summary\":\"✅ guardia-itsm 배포 완료 #${BUILD_NUMBER}\"}' 2>/dev/null || true" } + failure { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\"event\":\"build_result\",\"room\":\"ops\",\"success\":false,\"result_summary\":\"❌ guardia-itsm 빌드 실패 #${BUILD_NUMBER}\"}' 2>/dev/null || true" } } -} +} \ No newline at end of file diff --git a/workspace/guardia-itsm/tests/__init__.py b/workspace/guardia-itsm/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workspace/guardia-itsm/tests/integration/__init__.py b/workspace/guardia-itsm/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workspace/guardia-itsm/tests/unit/__init__.py b/workspace/guardia-itsm/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workspace/guardia-manager/Jenkinsfile b/workspace/guardia-manager/Jenkinsfile new file mode 100644 index 00000000..75ab11fc --- /dev/null +++ b/workspace/guardia-manager/Jenkinsfile @@ -0,0 +1,32 @@ +pipeline { + agent any + environment { + STATIC = '/var/www/manager' + NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook" + } + options { buildDiscarder(logRotator(numToKeepStr: '5')); timeout(time: 15, unit: 'MINUTES') } + stages { + stage('Checkout') { steps { checkout scm } } + stage('Build') { + steps { + dir('frontend') { + sh 'npm ci 2>/dev/null || npm install' + sh 'npm run build' + } + } + } + stage('Deploy') { + when { branch 'main' } + steps { + sh """ + cp -r frontend/dist/. ${STATIC}/ + systemctl restart guardia-manager 2>/dev/null || true + """ + } + } + } + post { + success { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\"event\":\"build_result\",\"room\":\"ops\",\"success\":true,\"result_summary\":\"✅ guardia-manager 배포 완료\"}' 2>/dev/null || true" } + failure { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\"event\":\"build_result\",\"room\":\"ops\",\"success\":false,\"result_summary\":\"❌ guardia-manager 빌드 실패\"}' 2>/dev/null || true" } + } +} \ No newline at end of file diff --git a/workspace/zioinfo-web/Jenkinsfile b/workspace/zioinfo-web/Jenkinsfile index 9b08c9ac..a675c110 100644 --- a/workspace/zioinfo-web/Jenkinsfile +++ b/workspace/zioinfo-web/Jenkinsfile @@ -1,108 +1,59 @@ pipeline { agent any - environment { - DEPLOY_DIR = '/var/www/zioinfo' - APP_DIR = '/opt/zioinfo/app' - JAVA_HOME = '/usr/lib/jvm/java-21-openjdk-amd64' - MVN = '/usr/bin/mvn' - NODE_HOME = '/usr/bin' + SRC = '/opt/zioinfo/src' + JAR_DIR = '/opt/zioinfo/app' + STATIC = '/var/www/zioinfo' + MVN = '/usr/bin/mvn' + NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook" } - options { buildDiscarder(logRotator(numToKeepStr: '5')) timeout(time: 20, unit: 'MINUTES') + timestamps() } - stages { stage('Checkout') { steps { - echo "브랜치: ${env.GIT_BRANCH ?: 'main'} | 커밋: ${env.GIT_COMMIT?.take(7) ?: '-'}" checkout([ - $class: 'GitSCM', - branches: scm.branches, + $class: 'GitSCM', branches: scm.branches, userRemoteConfigs: scm.userRemoteConfigs, - extensions: [ - // manual/ 폴더 체크아웃 제외 (배포 대상 아님) - [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [ - [path: 'frontend'], - [path: 'backend'], - ]] - ] + extensions: [[$class: 'SparseCheckoutPaths', + sparseCheckoutPaths: [[path: 'frontend'], [path: 'backend']] + ]] ]) } } - stage('Frontend Build') { steps { dir('frontend') { - sh ''' - echo "=== [1/3] React 빌드 ===" - npm ci --legacy-peer-deps --prefer-offline 2>/dev/null || npm install --legacy-peer-deps - npm run build - echo "빌드 결과: $(ls ../backend/src/main/resources/static/assets/ | wc -l) 파일" - ''' + sh 'npm ci --legacy-peer-deps --prefer-offline 2>/dev/null || npm install --legacy-peer-deps' + sh 'npm run build' } } } - stage('Backend Build') { steps { dir('backend') { - sh ''' - echo "=== [2/3] Spring Boot 빌드 ===" - ${MVN} clean package -DskipTests -q - JAR=$(find target -name "*.jar" ! -name "*sources*" | head -1) - echo "JAR: $JAR ($(du -sh $JAR | cut -f1))" - ''' + sh "${MVN} clean package -DskipTests -q" } } } - stage('Deploy') { + when { branch 'main' } steps { - sh ''' - echo "=== [3/3] 배포 ===" - JAR=$(find backend/target -name "*.jar" ! -name "*sources*" | head -1) - - # 앱 디렉터리 확인 - mkdir -p ${APP_DIR} ${DEPLOY_DIR} - - # JAR 배포 - cp "$JAR" ${APP_DIR}/app.jar - - # React 정적 파일 배포 - cp -r backend/src/main/resources/static/. ${DEPLOY_DIR}/ - - # Spring Boot 서비스 재시작 - systemctl restart zioinfo || true + sh """ + cp backend/target/*.jar ${JAR_DIR}/app.jar + cp -r backend/src/main/resources/static/. ${STATIC}/ + systemctl restart zioinfo sleep 4 - - # 헬스체크 - for i in 1 2 3 4 5; do - HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/company 2>/dev/null) - if [ "$HTTP" = "200" ]; then - echo "배포 성공 (Spring Boot HTTP $HTTP)" - exit 0 - fi - echo "헬스체크 ${i}/5 대기중 (HTTP: $HTTP)..." - sleep 3 - done - echo "경고: Spring Boot 응답 없음 — 서비스 상태 확인 필요" - ''' + systemctl is-active zioinfo || exit 1 + """ } } } - post { - success { - echo "✅ 배포 완료: ${currentBuild.displayName} (${currentBuild.durationString})" - } - failure { - echo "❌ 배포 실패: ${currentBuild.displayName} — 로그 확인 필요" - } - always { - cleanWs(cleanWhenNotBuilt: false, cleanWhenSuccess: false) - } + success { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\"event\":\"build_result\",\"room\":\"ops\",\"success\":true,\"result_summary\":\"✅ zioinfo-web 배포 완료 #${BUILD_NUMBER}\"}' 2>/dev/null || true" } + failure { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\"event\":\"build_result\",\"room\":\"ops\",\"success\":false,\"result_summary\":\"❌ zioinfo-web 빌드 실패 #${BUILD_NUMBER}\"}' 2>/dev/null || true" } } -} +} \ No newline at end of file