Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a884e53e3 | |||
| 7de00ecad2 | |||
| df5bbb162a | |||
| 3eec267769 | |||
| 6ab7e1fe16 | |||
| 58fe176715 | |||
| 6b6e2863cd | |||
| cfafccc34d | |||
| fe98ac45c4 | |||
| 5f9de93d35 | |||
| 48dfcf7714 | |||
| 6e3906878a | |||
| a0743465ab | |||
| d3e05256b7 | |||
| 4d5572f880 | |||
| e21014f992 | |||
| 43fe63964f | |||
| e51331986d | |||
| 23ba7993c3 | |||
| 90c43ea7b0 |
@@ -0,0 +1,140 @@
|
||||
name: eapim-admin CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- stage
|
||||
- feats/ci-test
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, linux, eapim-admin]
|
||||
env:
|
||||
JAVA_HOME: /apps/opts/jdk8
|
||||
JAVA_HOME_TOMCAT: /apps/opts/jdk17
|
||||
CATALINA_BASE: /prod/eapim/adminportal
|
||||
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
|
||||
CATALINA_PID: /prod/eapim/adminportal/temp/catalina.pid
|
||||
DEPLOY_HTTP_PORT: "39120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: eapim-admin
|
||||
|
||||
steps:
|
||||
- name: Checkout eapim-admin (trigger SHA)
|
||||
uses: http://172.30.1.50:3000/actions/checkout@v4
|
||||
with:
|
||||
path: eapim-admin
|
||||
|
||||
- name: Checkout sibling modules (master)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p eapim-online
|
||||
|
||||
# eapim-online/* — 5 modules referenced from settings.gradle
|
||||
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
|
||||
rm -rf "eapim-online/$REPO"
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
|
||||
"eapim-online/$REPO"
|
||||
done
|
||||
|
||||
# standalone siblings
|
||||
for REPO in elink-portal-common eapim-admin-djb; do
|
||||
rm -rf "$REPO"
|
||||
git clone --depth=1 --branch master \
|
||||
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
|
||||
"$REPO"
|
||||
done
|
||||
|
||||
- name: Verify toolchain
|
||||
run: |
|
||||
java -version
|
||||
gradle --version
|
||||
|
||||
- name: Gradle build (no tests)
|
||||
run: gradle clean build -x test --no-daemon
|
||||
|
||||
- name: Upload WAR artifact
|
||||
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
|
||||
with:
|
||||
name: eapim-admin-war-${{ github.sha }}
|
||||
path: eapim-admin/build/libs/eapim-admin.war
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Stop tomcat (pre-clean)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# 1) systemd 관리 인스턴스 정지 (이번 워크플로가 띄운 것)
|
||||
systemctl --user stop eapim-admin 2>/dev/null || true
|
||||
|
||||
# 2) systemd 외부 detached 인스턴스 정리
|
||||
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null; then
|
||||
JAVA_HOME=$JAVA_HOME_TOMCAT "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
|
||||
for i in $(seq 1 30); do
|
||||
[ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
rm -f "$CATALINA_PID"
|
||||
|
||||
- name: Deploy war as /monitoring
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
DEPLOY_DIR="$CATALINA_BASE/webapps"
|
||||
rm -rf "$DEPLOY_DIR/monitoring" "$DEPLOY_DIR/monitoring.war"
|
||||
cp eapim-admin/build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
|
||||
ls -la "$DEPLOY_DIR"
|
||||
|
||||
- name: Start tomcat and readiness probe (timeout 300s)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
|
||||
BEFORE=0
|
||||
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
|
||||
|
||||
# systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음
|
||||
systemctl --user start eapim-admin
|
||||
|
||||
# /loginForm.do 는 GET 매핑(MainController#loginForm) → tomcat·webapp 양쪽 살아있으면 200
|
||||
# /login.do 는 POST 처리 — GET 으로 probe 시 405/잘못된 응답 가능성 있어 회피
|
||||
PROBE_URL="http://localhost:$DEPLOY_HTTP_PORT/monitoring/loginForm.do"
|
||||
DEADLINE=$(($(date +%s) + 300))
|
||||
STATUS=000
|
||||
while [ $(date +%s) -lt $DEADLINE ]; do
|
||||
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
"$PROBE_URL" 2>/dev/null || echo "000")
|
||||
case "$STATUS" in
|
||||
200|302|401)
|
||||
echo "Readiness OK ($PROBE_URL HTTP $STATUS)"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
|
||||
echo "::error::Startup error detected in log"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | tail -80
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
case "$STATUS" in
|
||||
200|302|401) ;;
|
||||
*)
|
||||
echo "::error::Readiness probe failed within 300s (last HTTP status: $STATUS)"
|
||||
echo "--- last 300 lines of catalina log ---"
|
||||
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -300
|
||||
echo "--- systemd unit status ---"
|
||||
systemctl --user status eapim-admin --no-pager || true
|
||||
echo "--- listening ports ---"
|
||||
ss -tlnp 2>/dev/null | grep -E ":$DEPLOY_HTTP_PORT\b" || echo "(port $DEPLOY_HTTP_PORT 미점유)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "--- key boot log lines ---"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
|
||||
@@ -246,3 +246,6 @@ eapim-admin-kjb/
|
||||
*.report.md
|
||||
*.claude.md
|
||||
tmpclaude-*-cwd
|
||||
logs
|
||||
rinjae_
|
||||
tomcat-base
|
||||
@@ -10,7 +10,7 @@ AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할
|
||||
- **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다.
|
||||
- **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다.
|
||||
- **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다.
|
||||
- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다.
|
||||
- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `./gradlew compileJava` 를 먼저 실행하여 코드를 생성해야 합니다.
|
||||
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
|
||||
|
||||
## 프로젝트 개요
|
||||
@@ -39,50 +39,48 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
|
||||
|
||||
### 빌드 명령어
|
||||
|
||||
**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다.
|
||||
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
|
||||
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
|
||||
프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조).
|
||||
|
||||
```bash
|
||||
# 표준 빌드
|
||||
kjb-gradle.sh build
|
||||
./gradlew build
|
||||
|
||||
# Weblogic 배포용 빌드 (테스트 제외)
|
||||
kjb-gradle.sh build -x test -Pprofile=weblogic
|
||||
./gradlew build -x test -Pprofile=weblogic
|
||||
|
||||
# WAR 파일 빌드
|
||||
kjb-gradle.sh war
|
||||
./gradlew war
|
||||
|
||||
# 클린 빌드
|
||||
kjb-gradle.sh clean build
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
### 테스트 실행
|
||||
```bash
|
||||
# 모든 테스트 실행
|
||||
kjb-gradle.sh test
|
||||
./gradlew test
|
||||
|
||||
# 특정 테스트 클래스 실행
|
||||
kjb-gradle.sh test --tests "com.example.ClassName"
|
||||
./gradlew test --tests "com.example.ClassName"
|
||||
|
||||
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
|
||||
kjb-gradle.sh test --tests "com.eactive.eai.rms.*"
|
||||
./gradlew test --tests "com.eactive.eai.rms.*"
|
||||
```
|
||||
|
||||
### 개발 태스크
|
||||
|
||||
```bash
|
||||
# 패키징 없이 클래스만 컴파일
|
||||
kjb-gradle.sh classes
|
||||
./gradlew classes
|
||||
|
||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
||||
kjb-gradle.sh compileJava
|
||||
./gradlew compileJava
|
||||
|
||||
# 의존성 트리 보기
|
||||
kjb-gradle.sh dependencies
|
||||
./gradlew dependencies
|
||||
|
||||
# 사용 가능한 모든 태스크 목록
|
||||
kjb-gradle.sh tasks --all
|
||||
./gradlew tasks --all
|
||||
```
|
||||
|
||||
## 로컬 개발 빠른 시작
|
||||
@@ -92,26 +90,26 @@ kjb-gradle.sh tasks --all
|
||||
1. **IDE 설정 파일 생성 (최초 1회)**
|
||||
```bash
|
||||
# Eclipse 사용 시 (기본)
|
||||
kjb-gradle.sh eclipse
|
||||
./gradlew eclipse
|
||||
|
||||
# IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요)
|
||||
# 1. build.gradle을 build.gradle.eclipse로 백업
|
||||
# 2. build.gradle.intellij를 build.gradle로 복사
|
||||
# 3. kjb-gradle.sh idea 실행
|
||||
# 3. ./gradlew idea 실행
|
||||
```
|
||||
|
||||
2. **QueryDSL 등 소스 코드 생성**
|
||||
```bash
|
||||
kjb-gradle.sh compileJava
|
||||
./gradlew compileJava
|
||||
```
|
||||
|
||||
3. **전체 빌드 및 테스트 실행**
|
||||
```bash
|
||||
kjb-gradle.sh build
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
4. **IDE에서 프로젝트 열기**
|
||||
- Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
|
||||
- Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
|
||||
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
|
||||
|
||||
5. **실행 구성(Run Configuration) 설정**
|
||||
@@ -162,14 +160,14 @@ eapim-admin (root project)
|
||||
#### Q-Class 생성 위치
|
||||
|
||||
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
|
||||
- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성
|
||||
- `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성
|
||||
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
|
||||
|
||||
#### 생성된 코드 작업하기
|
||||
|
||||
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
|
||||
- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다
|
||||
- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다
|
||||
- **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다
|
||||
- **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다
|
||||
|
||||
#### IDE별 빌드 설정 파일
|
||||
|
||||
@@ -255,12 +253,21 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체
|
||||
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
|
||||
|
||||
```bash
|
||||
kjb-gradle.sh build -x test -Pprofile=weblogic
|
||||
./gradlew build -x test -Pprofile=weblogic
|
||||
```
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **Java 8**: Gradle toolchain을 통해 강제
|
||||
- **Java 8**: Gradle toolchain을 통해 강제 (`JavaLanguageVersion.of(8)`). 프로젝트의 `gradle.properties` 에는 JDK 경로를 기입하지 않습니다 — 개인 환경별로 `.envrc`(direnv) 또는 셸 rc 파일에서 `JAVA_HOME` 을 JDK 8 위치로 export 하세요. Gradle 8 의 toolchain auto-detect 가 `JAVA_HOME` 도 후보 경로로 인식합니다.
|
||||
|
||||
```bash
|
||||
# 예: 프로젝트 루트의 .envrc (Zulu 8 / macOS Apple Silicon)
|
||||
JAVA_HOME=${HOME}/opts/jdks/zulu8.94.0.17-ca-jdk8.0.492-macosx_aarch64
|
||||
GRADLE_USER_HOME=${HOME}/eactive/djb-eapim/gradle-home
|
||||
export PATH=${JAVA_HOME}/bin:${PATH}
|
||||
```
|
||||
|
||||
Gradle 표준 위치(`/Library/Java/JavaVirtualMachines/...`, Homebrew, SDKMAN 등)에 설치한 경우는 별도 설정 없이도 auto-detect 됩니다.
|
||||
- **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA)
|
||||
- **Spring Data JPA 2.5.2**: 리포지토리 추상화
|
||||
- **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM)
|
||||
@@ -271,9 +278,10 @@ kjb-gradle.sh build -x test -Pprofile=weblogic
|
||||
- **Jackson 2.13.1**: JSON 처리
|
||||
- **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께)
|
||||
- **AWS SDK 2.20.142**: S3 연동
|
||||
- **Kubernetes Client 18.0.1**: K8s 관리
|
||||
- **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑
|
||||
|
||||
> Kubernetes Client 의존성은 JDK 8 호환 문제로 제거됨(`io.kubernetes:client-java:18.0.1` 은 JDK 11+ 필요). K8s 모드 기능이 필요하면 `≤13.0.2` 로 다운그레이드 후 재도입 검토.
|
||||
|
||||
## 프로젝트 규칙
|
||||
|
||||
### 패키지 구조
|
||||
@@ -376,7 +384,7 @@ com.eactive.eai
|
||||
### IntelliJ IDEA (권장)
|
||||
|
||||
```bash
|
||||
kjb-gradle.sh idea
|
||||
./gradlew idea
|
||||
```
|
||||
|
||||
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
|
||||
@@ -395,7 +403,7 @@ cp build.gradle build.gradle.intellij
|
||||
cp build.gradle.eclipse build.gradle
|
||||
|
||||
# 3. Eclipse 프로젝트 생성
|
||||
kjb-gradle.sh eclipse
|
||||
./gradlew eclipse
|
||||
```
|
||||
|
||||
Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다:
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
</bean>
|
||||
|
||||
<bean id="ioAcceptor" class="com.eactive.eai.rms.onl.server.UdpServer" init-method="init" destroy-method="destory">
|
||||
<property name="port" value="10800" />
|
||||
<property name="port" value="39126" />
|
||||
<property name="handler" ref="handler"/>
|
||||
</bean>
|
||||
|
||||
|
||||
@@ -44,14 +44,16 @@
|
||||
'<%=localeMessage.getString("stdDefaultMan.columDesc")%>',
|
||||
'<%=localeMessage.getString("stdDefaultMan.columLen")%>',
|
||||
'<%=localeMessage.getString("stdDefaultMan.keyOr")%>',
|
||||
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>'
|
||||
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>',
|
||||
'<%=localeMessage.getString("stdDefaultMan.orderSeq")%>'
|
||||
],
|
||||
colModel:[
|
||||
{ name : 'COLUMNNAME' , align:'left' , sortable:false },
|
||||
{ name : 'COLUMNDESC' , align:'left' },
|
||||
{ name : 'COLUMNLENGTH' , align:'right' },
|
||||
{ name : 'ISKEY' , align:'center' },
|
||||
{ name : 'ISINTERFACE' , align:'center' }
|
||||
{ name : 'COLUMNNAME' , align:'left' , width: '200px', sortable:false },
|
||||
{ name : 'COLUMNDESC' , align:'left' , width: '200px' },
|
||||
{ name : 'COLUMNLENGTH' , align:'center' , width: '100px' },
|
||||
{ name : 'ISKEY' , align:'center', width: '100px' },
|
||||
{ name : 'ISINTERFACE' , align:'center', width: '100px' },
|
||||
{ name : 'ORDERSEQ' , align:'center', width: '100px' }
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems:false
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
|
||||
var hardDeleteEnabled = ${hardDeleteEnabled};
|
||||
const APPROVAL_TYPE_DISPLAY = {
|
||||
'USER': '법인사용자',
|
||||
'APP': 'API 사용'
|
||||
@@ -30,6 +31,46 @@
|
||||
cellvalue.substr(6, 2) ;
|
||||
}
|
||||
|
||||
function deleteSelectedApprovals() {
|
||||
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
showAlert("삭제할 승인 요청을 선택해주세요.", {type: 'warning'});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hardDeleteEnabled) {
|
||||
showAlert("완전삭제 기능이 비활성화되어 있습니다.", {type: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmMsg = "선택된 " + selectedIds.length + "건을 완전삭제(DB 영구 삭제)하시겠습니까?";
|
||||
showConfirm(confirmMsg, {
|
||||
type: 'error',
|
||||
onConfirm: function() {
|
||||
showConfirm("최종 확인: 완전삭제는 되돌릴 수 없습니다. 정말 실행하시겠습니까?", {
|
||||
type: 'error',
|
||||
title: '최종 확인',
|
||||
onConfirm: function() {
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: { cmd: 'HARD_DELETE_MULTIPLE', ids: selectedIds.join(',') },
|
||||
success: function(data) {
|
||||
showAlert(data.message, {
|
||||
type: data.status === 'success' ? 'success' : 'error',
|
||||
onClose: function() { $("#grid").trigger("reloadGrid"); }
|
||||
});
|
||||
},
|
||||
error: function(e) {
|
||||
showAlert("완전삭제 오류: " + e.responseText, {type: 'error'});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
@@ -62,6 +103,8 @@
|
||||
viewrecords: true,
|
||||
autowidth: true,
|
||||
height: 'auto',
|
||||
multiselect: true,
|
||||
multiboxonly: true,
|
||||
ondblClickRow: function(rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
@@ -102,6 +145,10 @@
|
||||
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#btn_delete_selected").click(function() {
|
||||
deleteSelectedApprovals();
|
||||
});
|
||||
|
||||
$("select[name^=search], input[name^=search]").keydown(function(key) {
|
||||
if (key.keyCode == 13) {
|
||||
$("#btn_search").click();
|
||||
@@ -121,9 +168,17 @@
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<c:if test="${hardDeleteEnabled}">
|
||||
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
|
||||
</c:if>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div>
|
||||
<c:if test="${hardDeleteEnabled}">
|
||||
<div style="background:#fff3cd; border:1px solid #ffc107; padding:8px 15px; margin:5px 0; border-radius:4px; color:#856404;">
|
||||
<strong>[주의]</strong> 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
|
||||
</div>
|
||||
</c:if>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tbody>
|
||||
|
||||
@@ -19,10 +19,23 @@
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
||||
var apiPopupUrl = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />';
|
||||
var file_download = '<c:url value="/file/download.do" />';
|
||||
var isDetail = false;
|
||||
var fileInfo = null;
|
||||
|
||||
// 코드 상수 — PortalNoticeUI 와 동기화
|
||||
var NOTICE_TYPE_NORMAL = '1';
|
||||
var NOTICE_TYPE_MAINTENANCE = '2';
|
||||
var NOTICE_TYPE_INCIDENT = '3';
|
||||
|
||||
// 영향 API 목록 (UI 상태)
|
||||
var affectedApis = [];
|
||||
|
||||
function isIncidentType(t) {
|
||||
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
|
||||
}
|
||||
|
||||
function init() {
|
||||
var key = "${param.id}";
|
||||
isDetail = key != "" && key != "null";
|
||||
@@ -36,7 +49,6 @@
|
||||
buttonControl(false);
|
||||
}
|
||||
|
||||
// 초기화 로직
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
@@ -45,6 +57,7 @@
|
||||
success:function(json){
|
||||
combo = json;
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
|
||||
toggleIncidentFields();
|
||||
|
||||
if (key) {
|
||||
detail(key);
|
||||
@@ -56,6 +69,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
function toggleIncidentFields() {
|
||||
var t = $('#noticeType').val();
|
||||
if (isIncidentType(t)) {
|
||||
$('.incident-row').show();
|
||||
// INCIDENT 만 상태 영역 표시
|
||||
if (t === NOTICE_TYPE_INCIDENT) {
|
||||
$('.incident-state-row').show();
|
||||
} else {
|
||||
$('.incident-state-row').hide();
|
||||
}
|
||||
} else {
|
||||
$('.incident-row').hide();
|
||||
$('.incident-state-row').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.innerHTML = text;
|
||||
@@ -73,57 +102,100 @@
|
||||
function displayAttachFile(file) {
|
||||
var $attachFiles = $('#attachFiles');
|
||||
$attachFiles.empty();
|
||||
|
||||
if (file) {
|
||||
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
|
||||
var fileItem = $('<div>').addClass('file-item');
|
||||
|
||||
$('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem);
|
||||
|
||||
var fileNameSpan = $('<span>')
|
||||
.addClass('fileName')
|
||||
var fileNameSpan = $('<span>').addClass('fileName')
|
||||
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
|
||||
.css('cursor', 'pointer')
|
||||
.on('click', function() {
|
||||
if (file.fileId) {
|
||||
downloadFile(file.fileId);
|
||||
}
|
||||
});
|
||||
|
||||
.on('click', function() { if (file.fileId) downloadFile(file.fileId); });
|
||||
fileNameSpan.appendTo(fileItem);
|
||||
$('<button>').text('X').addClass('delete-file').css('marginLeft', '15px')
|
||||
.on('click', function() {
|
||||
fileInfo = null;
|
||||
displayAttachFile(null);
|
||||
// 폼에 삭제 플래그 추가
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'fileDeleted',
|
||||
value: 'true'
|
||||
}).appendTo('#ajaxForm');
|
||||
})
|
||||
.appendTo(fileItem);
|
||||
|
||||
$('<input>').attr({type: 'hidden', name: 'fileDeleted', value: 'true'}).appendTo('#ajaxForm');
|
||||
}).appendTo(fileItem);
|
||||
fileItem.appendTo($attachFiles);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(fileId) {
|
||||
if (fileId) {
|
||||
var fileSn = 1; // 단일 파일이므로 시리얼 넘버를 1로 고정
|
||||
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=' + fileSn;
|
||||
} else {
|
||||
console.log("File not yet uploaded or doesn't have an ID");
|
||||
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=1';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAffectedApis() {
|
||||
var $tb = $('#affectedApiTbody');
|
||||
$tb.empty();
|
||||
if (!affectedApis.length) {
|
||||
$tb.append('<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>');
|
||||
return;
|
||||
}
|
||||
$.each(affectedApis, function(idx, api) {
|
||||
var $tr = $('<tr>');
|
||||
$tr.append('<td style="text-align:center;width:40px;"><input type="checkbox" class="affected-api-check" data-idx="' + idx + '"></td>');
|
||||
$tr.append('<td>' + (api.apiId || '') + '</td>');
|
||||
$tr.append('<td>' + (api.apiName || '') + '</td>');
|
||||
$tb.append($tr);
|
||||
});
|
||||
}
|
||||
|
||||
function openApiPopup() {
|
||||
// showModal 헬퍼는 urlAddServiceType + &menuId=getMenuId() + &pop=true 를 자동 부여 →
|
||||
// AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생.
|
||||
// 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어
|
||||
// frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별.
|
||||
var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(',');
|
||||
var url2 = apiPopupUrl + '?cmd=POPUP&selectedApiIds=' + encodeURIComponent(selectedIds);
|
||||
var args = {};
|
||||
showModal(url2, args, 1280, 860, function () {
|
||||
var doc = args.self && args.self.document;
|
||||
if (!doc) return;
|
||||
var popupFrame = null;
|
||||
var iframes = doc.querySelectorAll('iframe');
|
||||
for (var i = 0; i < iframes.length; i++) {
|
||||
var src = iframes[i].getAttribute('src') || '';
|
||||
if (src.indexOf('apiGroupMan') >= 0) {
|
||||
popupFrame = iframes[i].contentWindow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!popupFrame) return;
|
||||
var rv = popupFrame.returnValue;
|
||||
if (!rv) return;
|
||||
var rows = Array.isArray(rv) ? rv : [rv];
|
||||
$.each(rows, function(_, item) {
|
||||
if (!item || !item.apiId) return;
|
||||
var exists = affectedApis.some(function(a){ return a.apiId === item.apiId; });
|
||||
if (!exists) {
|
||||
affectedApis.push({apiId: item.apiId, apiName: item.apiDesc || item.apiName || ''});
|
||||
}
|
||||
});
|
||||
renderAffectedApis();
|
||||
});
|
||||
}
|
||||
|
||||
function removeSelectedAffectedApis() {
|
||||
var idxes = [];
|
||||
$('.affected-api-check:checked').each(function() {
|
||||
idxes.push(parseInt($(this).data('idx'), 10));
|
||||
});
|
||||
if (!idxes.length) {
|
||||
alert('삭제할 항목을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
// 역순 삭제
|
||||
idxes.sort(function(a,b){return b-a;}).forEach(function(i){ affectedApis.splice(i, 1); });
|
||||
renderAffectedApis();
|
||||
}
|
||||
|
||||
function detail(key) {
|
||||
if (!isDetail) return;
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
$("#id").val(key);
|
||||
@@ -135,7 +207,18 @@
|
||||
var decodedContent = decodeHTMLEntities(data.noticeDetail);
|
||||
$('#contents').summernote('code', decodedContent);
|
||||
|
||||
// 파일 정보 표시
|
||||
// 장애/점검 필드
|
||||
$('#summary').val(data.summary || '');
|
||||
$('#startedAt').val(data.startedAt || '');
|
||||
$('#endAt').val(data.endAt || '');
|
||||
$('#state').val(data.state || 'INVESTIGATING');
|
||||
$('#previousState').text(data.previousState || '없음');
|
||||
affectedApis = (data.affectedApis || []).map(function(a){
|
||||
return {apiId: a.apiId, apiName: a.apiName || ''};
|
||||
});
|
||||
renderAffectedApis();
|
||||
toggleIncidentFields();
|
||||
|
||||
if (data.fileInfo) {
|
||||
fileInfo = {
|
||||
originalFileName: data.fileInfo.fileDetails[0].originalFileName,
|
||||
@@ -146,9 +229,7 @@
|
||||
displayAttachFile(fileInfo);
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,13 +238,12 @@
|
||||
|
||||
init();
|
||||
|
||||
initSummernote('#contents', { placeholder: '여기에 내용을 입력하세요.', height: 300 });
|
||||
|
||||
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||
initSummernote('#contents', {
|
||||
placeholder: '여기에 내용을 입력하세요.',
|
||||
height: 300
|
||||
});
|
||||
$('#noticeType').on('change', toggleIncidentFields);
|
||||
|
||||
$('#btn_addAffectedApi').click(openApiPopup);
|
||||
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
|
||||
|
||||
$('#fileInput').change(function(e) {
|
||||
var file = e.target.files[0];
|
||||
@@ -171,52 +251,66 @@
|
||||
fileInfo = {
|
||||
originalFileName: file.name.split('.').slice(0, -1).join('.'),
|
||||
fileExtension: file.name.split('.').pop(),
|
||||
file: file,
|
||||
size: file.size
|
||||
file: file, size: file.size
|
||||
};
|
||||
displayAttachFile(fileInfo);
|
||||
}
|
||||
this.value = ''; // 입력 필드 초기화
|
||||
});
|
||||
|
||||
$('#addFile').click(function() {
|
||||
$('#fileInput').click();
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
$('#addFile').click(function() { $('#fileInput').click(); });
|
||||
|
||||
$("#btn_modify").click(function () {
|
||||
if (!checkRequired("ajaxForm")) return;
|
||||
|
||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true)
|
||||
var noticeType = $('#noticeType').val();
|
||||
if (isIncidentType(noticeType)) {
|
||||
if (!$('#startedAt').val()) {
|
||||
alert('시작 시각을 입력해 주십시오.');
|
||||
return;
|
||||
}
|
||||
if (noticeType === NOTICE_TYPE_MAINTENANCE && !$('#endAt').val()) {
|
||||
alert('점검 종료 시각을 입력해 주십시오.');
|
||||
return;
|
||||
}
|
||||
if (affectedApis.length === 0) {
|
||||
alert('영향 API를 1건 이상 선택해 주십시오.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
|
||||
|
||||
var formData = new FormData($("#ajaxForm")[0]);
|
||||
|
||||
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
||||
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
|
||||
formData.set("noticeDetail", $('#contents').summernote('code'));
|
||||
|
||||
// 시작/종료 시각이 비어 있으면 전송하지 않음 (서버 파싱 오류 방지)
|
||||
if (!$('#startedAt').val()) formData.delete('startedAt');
|
||||
if (!$('#endAt').val()) formData.delete('endAt');
|
||||
|
||||
// 영향 API → affectedApis[i].apiId/apiName 으로 전송
|
||||
$.each(affectedApis, function(i, api) {
|
||||
formData.set('affectedApis[' + i + '].apiId', api.apiId);
|
||||
formData.set('affectedApis[' + i + '].apiName', api.apiName || '');
|
||||
});
|
||||
|
||||
formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
|
||||
|
||||
// 파일 처리
|
||||
if (fileInfo && fileInfo.file) {
|
||||
formData.set("files", fileInfo.file);
|
||||
formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function (json) {
|
||||
type: "POST", url: url, data: formData,
|
||||
processData: false, contentType: false,
|
||||
success: function () {
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,25 +318,18 @@
|
||||
if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) {
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({name: "cmd", value: "DELETE"});
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: postData,
|
||||
success: function (args) {
|
||||
type: "POST", url: url, data: postData,
|
||||
success: function () {
|
||||
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_previous").click(function () {
|
||||
goNav(returnUrl);
|
||||
});
|
||||
$("#btn_previous").click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
@@ -252,7 +339,7 @@
|
||||
<ul class="path">
|
||||
<li><a href="#">${rmsMenuPath}</a></li>
|
||||
</ul>
|
||||
</div><!-- end content_top -->
|
||||
</div>
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||
@@ -270,7 +357,7 @@
|
||||
<col style="width: 40%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>게시유형</th>
|
||||
<th>게시유형 <font color="red">*</font></th>
|
||||
<td colspan="3">
|
||||
<div class="select-style">
|
||||
<select name="noticeType" id="noticeType"></select>
|
||||
@@ -286,7 +373,7 @@
|
||||
<td>
|
||||
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
|
||||
</td>
|
||||
<th>고정여부</th>
|
||||
<th>상단고정</th>
|
||||
<td>
|
||||
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
|
||||
</td>
|
||||
@@ -297,16 +384,55 @@
|
||||
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<th>첨부파일</th>
|
||||
<td colspan="3">
|
||||
<div style="margin: 5px 0px; display: inline-block;">
|
||||
<input type="file" id="fileInput" name="files" style="display:none;" />
|
||||
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button>
|
||||
</div>
|
||||
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div>
|
||||
|
||||
<!-- ───── 장애/점검 전용 영역 ───── -->
|
||||
<tr class="incident-row" style="display:none;">
|
||||
<th>시작 <font color="red">*</font></th>
|
||||
<td>
|
||||
<input type="datetime-local" id="startedAt" name="startedAt" style="width:90%">
|
||||
</td>
|
||||
</tr> -->
|
||||
<th>종료</th>
|
||||
<td>
|
||||
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="incident-row incident-state-row" style="display:none;">
|
||||
<th>상태</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select id="state" name="state">
|
||||
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
|
||||
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
|
||||
<option value="MONITORING">MONITORING - 모니터링</option>
|
||||
<option value="RESOLVED">RESOLVED - 해소</option>
|
||||
<option value="CANCELED">CANCELED - 취소</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<th>이전 상태</th>
|
||||
<td><span id="previousState">없음</span></td>
|
||||
</tr>
|
||||
<tr class="incident-row" style="display:none;">
|
||||
<th>영향 API 선택</th>
|
||||
<td colspan="3">
|
||||
<div style="margin-bottom:5px;">
|
||||
<button type="button" class="cssbtn smallBtn" id="btn_removeAffectedApi"><i class="material-icons">delete</i> 삭제</button>
|
||||
<button type="button" class="cssbtn smallBtn" id="btn_addAffectedApi"><i class="material-icons">add</i> 추가</button>
|
||||
</div>
|
||||
<table class="table_row" cellspacing="0" style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;text-align:center;"><input type="checkbox" id="affectedApiAll" onclick="$('.affected-api-check').prop('checked', this.checked);"></th>
|
||||
<th>API ID</th>
|
||||
<th>API 명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="affectedApiTbody">
|
||||
<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1686,7 +1686,7 @@
|
||||
<option value="none" selected>NONE</option>
|
||||
<option value="oauth">OAUTH</option>
|
||||
<option value="api_key">API_KEY</option>
|
||||
<option value="ca">CA</option>
|
||||
<!-- <option value="ca">CA</option> -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+9
-5
@@ -202,13 +202,12 @@ dependencies {
|
||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||
|
||||
implementation ('io.kubernetes:client-java:18.0.1') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
||||
}
|
||||
|
||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||
implementation 'xml-apis:xml-apis:1.4.01'
|
||||
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
@@ -228,6 +227,11 @@ configurations.all {
|
||||
cacheDynamicVersionsFor 10, 'minutes'
|
||||
// Do not cache changing modules
|
||||
cacheChangingModulesFor 0, 'seconds'
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없음.
|
||||
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
|
||||
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
|
||||
force 'xml-apis:xml-apis:1.4.01'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# DJBank EAPIM Admin — user-scope systemd unit
|
||||
#
|
||||
# Install (one-time, on the host as `djb`):
|
||||
# sudo loginctl enable-linger djb # allow services to outlive ssh logout
|
||||
# mkdir -p ~/.config/systemd/user
|
||||
# # workflow copies this file in on each deploy; for first boot you can also do:
|
||||
# # cp /prod/.../deploy/systemd/eapim-admin.service ~/.config/systemd/user/
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable eapim-admin
|
||||
# systemctl --user start eapim-admin
|
||||
#
|
||||
# Why this exists:
|
||||
# - `catalina.sh start` + `nohup &` does NOT escape the Gitea Actions runner
|
||||
# process group. When the workflow step finishes, the runner sends SIGTERM
|
||||
# to the process tree and Tomcat dies seconds after boot.
|
||||
# - Running Tomcat as a systemd user service detaches lifecycle from the
|
||||
# runner entirely. The workflow just calls `systemctl --user restart` and
|
||||
# systemd owns the process.
|
||||
|
||||
[Unit]
|
||||
Description=DJBank EAPIM Admin (Tomcat 9)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
Environment=JAVA_HOME=/apps/opts/jdk17
|
||||
Environment=CATALINA_BASE=/prod/eapim/adminportal
|
||||
Environment=CATALINA_HOME=/prod/eapim/apache-tomcat-9.0.116
|
||||
Environment=CATALINA_PID=/prod/eapim/adminportal/temp/catalina.pid
|
||||
|
||||
# `catalina.sh run` keeps Tomcat in the foreground so systemd can supervise it.
|
||||
ExecStart=/prod/eapim/apache-tomcat-9.0.116/bin/catalina.sh run
|
||||
|
||||
# Graceful shutdown via Tomcat's shutdown port, 30s window.
|
||||
ExecStop=/prod/eapim/apache-tomcat-9.0.116/bin/catalina.sh stop 30
|
||||
|
||||
# Tomcat returns 143 on SIGTERM during normal stop — treat as success.
|
||||
SuccessExitStatus=143
|
||||
TimeoutStartSec=180
|
||||
TimeoutStopSec=60
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Memory / fd limits — adjust as needed.
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -9,7 +9,7 @@
|
||||
## 기술 스택
|
||||
|
||||
- Spring Framework 5.3.27
|
||||
- Java 8
|
||||
- Java 17
|
||||
- Gradle 8.7
|
||||
- Oracle 19c
|
||||
- Spring Data JPA 2.5.2
|
||||
@@ -23,251 +23,61 @@
|
||||
|
||||
### 사전 요구사항
|
||||
|
||||
- JDK 8 이상
|
||||
- JDK 17 이상
|
||||
- Gradle 8.7
|
||||
- Git
|
||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
||||
|
||||
### Git Bash에서 초기 세팅
|
||||
|
||||
```bash
|
||||
# ====================================
|
||||
# 1. 변수 설정 (프로젝트 경로 및 브랜치)
|
||||
# ====================================
|
||||
export WORKSPACE_DIR="/c/eactive/workspaces/kjb-eapim"
|
||||
export PROJECT_BRANCH="jenkins_with_weblogic"
|
||||
|
||||
# Git Repository URL 설정
|
||||
# 기본: ssh://git@192.168.240.178:18081/eapim
|
||||
# 대체: https://git.eactive.synology.me:8090/kjb-eapim
|
||||
export GIT_REPO_BASE="ssh://git@192.168.240.178:18081/eapim"
|
||||
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
|
||||
|
||||
# Gradle 명령어 설정
|
||||
# 일반 환경: gradle
|
||||
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
|
||||
export GRADLE_CMD="gradle"
|
||||
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
||||
|
||||
# ====================================
|
||||
# 2. 작업 디렉토리 생성
|
||||
# ====================================
|
||||
mkdir -p $WORKSPACE_DIR
|
||||
cd $WORKSPACE_DIR
|
||||
|
||||
# ====================================
|
||||
# 3. eapim-admin 프로젝트 클론
|
||||
# ====================================
|
||||
git clone ${GIT_REPO_BASE}/eapim-admin.git
|
||||
cd eapim-admin
|
||||
|
||||
# 프로젝트 브랜치로 체크아웃
|
||||
git checkout $PROJECT_BRANCH
|
||||
|
||||
# ====================================
|
||||
# 4. eapim-online 프로젝트 클론 (코어 모듈 포함)
|
||||
# ====================================
|
||||
cd $WORKSPACE_DIR
|
||||
git clone ${GIT_REPO_BASE}/eapim-online.git
|
||||
cd eapim-online
|
||||
git checkout $PROJECT_BRANCH
|
||||
|
||||
# ====================================
|
||||
# 5. 의존 모듈 클론
|
||||
# ====================================
|
||||
cd $WORKSPACE_DIR
|
||||
|
||||
# elink-portal-common 클론
|
||||
git clone ${GIT_REPO_BASE}/elink-portal-common.git
|
||||
cd elink-portal-common
|
||||
git checkout master # 또는 필요한 브랜치
|
||||
|
||||
# kjb-safedb 클론
|
||||
cd $WORKSPACE_DIR
|
||||
git clone ${GIT_REPO_BASE}/kjb-safedb.git
|
||||
cd kjb-safedb
|
||||
git checkout master # 또는 필요한 브랜치
|
||||
|
||||
# ====================================
|
||||
# 6. 최종 폴더 구조 확인
|
||||
# ====================================
|
||||
# kjb-eapim/
|
||||
# ├── eapim-admin/ # Admin 관리 콘솔 (현재 프로젝트)
|
||||
# ├── eapim-online/ # Online 게이트웨이 코어 모듈
|
||||
# │ ├── elink-online-core/
|
||||
# │ ├── elink-online-core-jpa/
|
||||
# │ ├── elink-online-transformer/
|
||||
# │ ├── elink-online-common/
|
||||
# │ ├── elink-online-emsclient/
|
||||
# │ └── elink-online-adapter/
|
||||
# ├── elink-portal-common/ # 포털 공통 컴포넌트 (JPA 엔티티)
|
||||
# └── kjb-safedb/ # SafeDB 암호화 라이브러리
|
||||
|
||||
# ====================================
|
||||
# 7. eapim-admin 프로젝트로 이동 및 디펜던시 다운로드
|
||||
# ====================================
|
||||
cd $WORKSPACE_DIR/eapim-admin
|
||||
|
||||
# Gradle 디펜던시 다운로드
|
||||
$GRADLE_CMD dependencies
|
||||
```
|
||||
|
||||
### Windows CMD에서 초기 세팅
|
||||
|
||||
```cmd
|
||||
REM ====================================
|
||||
REM 1. 변수 설정 (프로젝트 경로 및 브랜치)
|
||||
REM ====================================
|
||||
set WORKSPACE_DIR=C:\eactive\workspaces\kjb-eapim
|
||||
set PROJECT_BRANCH=jenkins_with_weblogic
|
||||
|
||||
REM Git Repository URL 설정
|
||||
REM 기본: ssh://git@192.168.240.178:18081/eapim
|
||||
REM 대체: https://git.eactive.synology.me:8090/kjb-eapim
|
||||
set GIT_REPO_BASE=ssh://git@192.168.240.178:18081/eapim
|
||||
REM set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
|
||||
|
||||
REM Gradle 명령어 설정
|
||||
REM 일반 환경: gradle
|
||||
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
set GRADLE_CMD=gradle
|
||||
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
|
||||
REM ====================================
|
||||
REM 2. 작업 디렉토리 생성
|
||||
REM ====================================
|
||||
if not exist "%WORKSPACE_DIR%" mkdir "%WORKSPACE_DIR%"
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
|
||||
REM ====================================
|
||||
REM 3. eapim-admin 프로젝트 클론
|
||||
REM ====================================
|
||||
git clone %GIT_REPO_BASE%/eapim-admin.git
|
||||
cd eapim-admin
|
||||
|
||||
REM 프로젝트 브랜치로 체크아웃
|
||||
git checkout %PROJECT_BRANCH%
|
||||
|
||||
REM ====================================
|
||||
REM 4. eapim-online 프로젝트 클론 (코어 모듈 포함)
|
||||
REM ====================================
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
git clone %GIT_REPO_BASE%/eapim-online.git
|
||||
cd eapim-online
|
||||
git checkout %PROJECT_BRANCH%
|
||||
|
||||
REM ====================================
|
||||
REM 5. 의존 모듈 클론
|
||||
REM ====================================
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
|
||||
REM elink-portal-common 클론
|
||||
git clone %GIT_REPO_BASE%/elink-portal-common.git
|
||||
cd elink-portal-common
|
||||
git checkout master
|
||||
|
||||
REM kjb-safedb 클론
|
||||
cd /d "%WORKSPACE_DIR%"
|
||||
git clone %GIT_REPO_BASE%/kjb-safedb.git
|
||||
cd kjb-safedb
|
||||
git checkout master
|
||||
|
||||
REM ====================================
|
||||
REM 6. 최종 폴더 구조 확인
|
||||
REM ====================================
|
||||
REM kjb-eapim\
|
||||
REM ├── eapim-admin\ # Admin 관리 콘솔 (현재 프로젝트)
|
||||
REM ├── eapim-online\ # Online 게이트웨이 코어 모듈
|
||||
REM │ ├── elink-online-core\
|
||||
REM │ ├── elink-online-core-jpa\
|
||||
REM │ ├── elink-online-transformer\
|
||||
REM │ ├── elink-online-common\
|
||||
REM │ ├── elink-online-emsclient\
|
||||
REM │ └── elink-online-adapter\
|
||||
REM ├── elink-portal-common\ # 포털 공통 컴포넌트 (JPA 엔티티)
|
||||
REM └── kjb-safedb\ # SafeDB 암호화 라이브러리
|
||||
|
||||
REM ====================================
|
||||
REM 7. eapim-admin 프로젝트로 이동 및 디펜던시 다운로드
|
||||
REM ====================================
|
||||
cd /d "%WORKSPACE_DIR%\eapim-admin"
|
||||
|
||||
REM Gradle 디펜던시 다운로드
|
||||
%GRADLE_CMD% dependencies
|
||||
```
|
||||
|
||||
### 환경별 설정 파일
|
||||
|
||||
프로젝트 클론 후 필요한 환경 설정을 수정하세요:
|
||||
|
||||
- `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경
|
||||
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
|
||||
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
|
||||
|
||||
### 주의사항
|
||||
|
||||
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
|
||||
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
|
||||
### 주의사항
|
||||
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
|
||||
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
|
||||
|
||||
|
||||
## 빌드 및 실행
|
||||
|
||||
### Gradle 명령어 설정
|
||||
### Gradle 명령어
|
||||
|
||||
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
|
||||
|
||||
**Git Bash:**
|
||||
```bash
|
||||
# 일반 환경
|
||||
export GRADLE_CMD="gradle"
|
||||
|
||||
# msi-gf63 환경
|
||||
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
||||
```
|
||||
|
||||
**Windows CMD:**
|
||||
```cmd
|
||||
REM 일반 환경
|
||||
set GRADLE_CMD=gradle
|
||||
|
||||
REM msi-gf63 환경
|
||||
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
||||
```
|
||||
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
|
||||
|
||||
### 빌드 및 실행 명령어
|
||||
|
||||
```bash
|
||||
# 표준 빌드
|
||||
$GRADLE_CMD build
|
||||
./gradlew build
|
||||
|
||||
# Weblogic 배포용 빌드 (테스트 제외)
|
||||
$GRADLE_CMD build -x test -Pprofile=weblogic
|
||||
./gradlew build -x test -Pprofile=weblogic
|
||||
|
||||
# WAR 파일 빌드
|
||||
$GRADLE_CMD war
|
||||
# WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log)
|
||||
./gradlew war
|
||||
|
||||
# 클린 빌드
|
||||
$GRADLE_CMD clean build
|
||||
./gradlew clean build
|
||||
|
||||
# 테스트 실행
|
||||
$GRADLE_CMD test
|
||||
./gradlew test
|
||||
|
||||
# 패키징 없이 클래스만 컴파일
|
||||
$GRADLE_CMD classes
|
||||
./gradlew classes
|
||||
|
||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
||||
$GRADLE_CMD compileJava
|
||||
./gradlew compileJava
|
||||
|
||||
# 의존성 트리 보기
|
||||
$GRADLE_CMD dependencies
|
||||
./gradlew dependencies
|
||||
|
||||
# 사용 가능한 모든 태스크 목록
|
||||
$GRADLE_CMD tasks --all
|
||||
./gradlew tasks --all
|
||||
```
|
||||
|
||||
**주의**: gradlew 사용 금지 - offline gradle 단독 실행
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
### 전체 프로젝트 구조
|
||||
|
||||
+8
-1
@@ -5,7 +5,9 @@ import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -42,7 +44,12 @@ public class ExtendedColumnDefinitionService
|
||||
predicate = predicate.and(qExtendedColumnDefinition.columnDesc.containsIgnoreCase(searchColumnDesc));
|
||||
}
|
||||
|
||||
return repository.findAll(predicate, pageable);
|
||||
Pageable orderedPageable = PageRequest.of(
|
||||
pageable.getPageNumber(),
|
||||
pageable.getPageSize(),
|
||||
Sort.by(Sort.Order.asc("orderSeq"), Sort.Order.asc("columnName")));
|
||||
|
||||
return repository.findAll(predicate, orderedPageable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
|
||||
return approval.getApprovers().stream()
|
||||
.filter(this::isCurrentApprover)
|
||||
.map(this::getApproverId)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.anyMatch(id -> id.equals(approverId));
|
||||
}
|
||||
|
||||
@@ -22,6 +23,6 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
|
||||
}
|
||||
|
||||
private String getApproverId(Approver approver) {
|
||||
return approver.getUser().getId();
|
||||
return approver.getUser() != null ? approver.getUser().getId() : null;
|
||||
}
|
||||
}
|
||||
|
||||
+50
-2
@@ -13,10 +13,16 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class PortalApprovalManController extends BaseAnnotationController {
|
||||
@@ -24,8 +30,8 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
||||
private final PortalApprovalManService portalApprovalManService;
|
||||
|
||||
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view")
|
||||
public void view() {
|
||||
// view
|
||||
public void view(Model model) {
|
||||
model.addAttribute("hardDeleteEnabled", portalApprovalManService.isHardDeleteEnabled());
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL")
|
||||
@@ -77,6 +83,48 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=HARD_DELETE_MULTIPLE")
|
||||
public ResponseEntity<Map<String, Object>> hardDeleteMultiple(String ids) {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
try {
|
||||
if (StringUtils.isBlank(ids)) {
|
||||
resultMap.put("status", "fail");
|
||||
resultMap.put("message", "삭제할 승인 요청을 선택해주세요.");
|
||||
return ResponseEntity.ok(resultMap);
|
||||
}
|
||||
|
||||
String[] idArray = ids.split(",");
|
||||
int successCount = 0;
|
||||
List<String> failMessages = new ArrayList<>();
|
||||
|
||||
for (String id : idArray) {
|
||||
if (StringUtils.isNotBlank(id.trim())) {
|
||||
try {
|
||||
portalApprovalManService.hardDelete(id.trim());
|
||||
successCount++;
|
||||
} catch (BizException e) {
|
||||
failMessages.add(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
failMessages.add(id.trim() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failMessages.isEmpty()) {
|
||||
resultMap.put("status", "success");
|
||||
resultMap.put("message", successCount + "건의 승인 요청이 완전삭제되었습니다.");
|
||||
} else {
|
||||
resultMap.put("status", "fail");
|
||||
resultMap.put("message", "완전삭제 실패: " + String.join(", ", failMessages)
|
||||
+ (successCount > 0 ? " (" + successCount + "건 성공)" : ""));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
resultMap.put("status", "fail");
|
||||
resultMap.put("message", "완전삭제 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
return ResponseEntity.ok(resultMap);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=UPDATE")
|
||||
public ResponseEntity<Void> update(String id, String expectEndDate) {
|
||||
portalApprovalManService.update(id, expectEndDate);
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.Approval;
|
||||
import com.eactive.apim.portal.approval.entity.ApprovalType;
|
||||
import com.eactive.apim.portal.approval.statemachine.*;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||
@@ -25,6 +26,7 @@ import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgManService;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
|
||||
import com.eactive.eai.rms.common.acl.user.ui.UserUI;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||
@@ -36,6 +38,8 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@@ -43,6 +47,9 @@ import java.util.*;
|
||||
@RequiredArgsConstructor
|
||||
public class PortalApprovalManService extends BaseService {
|
||||
|
||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
||||
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
|
||||
|
||||
private final ApprovalService approvalService;
|
||||
private final ApprovalStateMachine approvalStateMachine;
|
||||
private final AppRequestRepository appRequestRepository;
|
||||
@@ -59,6 +66,10 @@ public class PortalApprovalManService extends BaseService {
|
||||
private final ApiGroupUIMapper apiGroupUIMapper;
|
||||
private final PortalApprovalAuthorizer portalApprovalAuthorizer;
|
||||
private final PortalOrgManService portalOrgManService;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||
private EntityManager entityManager;
|
||||
|
||||
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
|
||||
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
|
||||
@@ -347,6 +358,28 @@ public class PortalApprovalManService extends BaseService {
|
||||
|
||||
}
|
||||
|
||||
public boolean isHardDeleteEnabled() {
|
||||
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
||||
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
|
||||
}
|
||||
|
||||
public void hardDelete(String id) {
|
||||
if (!isHardDeleteEnabled()) {
|
||||
throw new BizException("승인 요청 완전삭제 기능이 비활성화되어 있습니다. (PTL_PROPERTY: " + HARD_DELETE_FLAG_KEY + ")");
|
||||
}
|
||||
// Approval.approvers 가 EAGER + Approver.user 가 NotFound 처리 없음 → 사라진 UserInfo 로 인해
|
||||
// findById/deleteById 가 hydration 단계에서 실패하는 row 가 존재. 네이티브 SQL 로 우회.
|
||||
entityManager.createNativeQuery("DELETE FROM PTL_APPROVER WHERE APPROVAL_ID = :id")
|
||||
.setParameter("id", id)
|
||||
.executeUpdate();
|
||||
int affected = entityManager.createNativeQuery("DELETE FROM PTL_APPROVAL WHERE ID = :id")
|
||||
.setParameter("id", id)
|
||||
.executeUpdate();
|
||||
if (affected == 0) {
|
||||
throw new BizException("승인 정보를 찾을 수 없습니다. ID: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
|
||||
approvalService.findById(id).ifPresent(approval -> {
|
||||
options.put("authorizer", portalApprovalAuthorizer);
|
||||
|
||||
+10
-17
@@ -6,7 +6,6 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.Approval;
|
||||
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
|
||||
import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import com.eactive.apim.portal.template.service.MessageSendService;
|
||||
@@ -19,10 +18,8 @@ import com.eactive.eai.rms.onl.apim.approval.RandomStringGenerator;
|
||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
|
||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
|
||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
|
||||
import com.eactive.eai.rms.onl.common.service.AgentUtilService;
|
||||
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
|
||||
import com.eactive.eai.rms.onl.manage.authserver.client.ClientUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -35,22 +32,12 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -116,9 +103,15 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
}
|
||||
|
||||
private void sendApprovalResult(AppRequest appRequest) {
|
||||
com.eactive.apim.portal.portaluser.entity.PortalUser requester =
|
||||
appRequest.getApproval() != null ? appRequest.getApproval().getRequester() : null;
|
||||
if (requester == null) {
|
||||
logger.warn("승인 결과 통지 생략 - 요청자 정보 없음. appRequestId: {}", appRequest.getId());
|
||||
return;
|
||||
}
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setPhone(appRequest.getApproval().getRequester().getMobileNumber());
|
||||
recipient.setUserId(appRequest.getApproval().getRequester().getEmailAddr());
|
||||
recipient.setPhone(requester.getMobileNumber());
|
||||
recipient.setUserId(requester.getEmailAddr());
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("apiKey", appRequest.getClientName());
|
||||
messageSendService.sendMessage(MessageCode.APP_REGISTER_APPROVED, recipient, params);
|
||||
@@ -165,8 +158,8 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
credentialUI.setClientname(appRequest.getClientName());
|
||||
credentialUI.setScope("api");
|
||||
credentialUI.setGranttypes("client_credentials");
|
||||
credentialUI.setOrgid(appRequest.getOrg().getId());
|
||||
credentialUI.setOrgname(appRequest.getOrg().getOrgName());
|
||||
credentialUI.setOrgid(appRequest.getOrg() != null ? appRequest.getOrg().getId() : null);
|
||||
credentialUI.setOrgname(appRequest.getOrg() != null ? appRequest.getOrg().getOrgName() : null);
|
||||
credentialUI.setAllowedips(appRequest.getIpWhitelist());
|
||||
credentialUI.setRedirecturi(appRequest.getCallbackUrl());
|
||||
credentialUI.setAppIconFileId(appRequest.getAppIconFileId());
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class IncidentAffectedApiUI {
|
||||
private String apiId;
|
||||
private String apiName;
|
||||
}
|
||||
+136
-1
@@ -1,5 +1,13 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApiId;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentState;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository;
|
||||
import com.eactive.apim.portal.file.entity.FileDetail;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
@@ -23,7 +31,12 @@ import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
@@ -32,6 +45,9 @@ public class PortalNoticeManService extends BaseService {
|
||||
private final PortalNoticeService portalNoticeService;
|
||||
private final PortalNoticeUIMapper portalNoticeUIMapper;
|
||||
private final FileService fileService;
|
||||
private final DjbApistatusIncidentRepository incidentRepository;
|
||||
private final DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||
private final DjbApistatusIncidentTimelineRepository incidentTimelineRepository;
|
||||
|
||||
private String decodeString(String value) {
|
||||
if (ContainerUtil.get() == ContainerUtil.TOMCAT) {
|
||||
@@ -48,10 +64,27 @@ public class PortalNoticeManService extends BaseService {
|
||||
@Autowired
|
||||
public PortalNoticeManService(PortalNoticeService portalNoticeService,
|
||||
PortalNoticeUIMapper portalNoticeUIMapper,
|
||||
FileService fileService){
|
||||
FileService fileService,
|
||||
DjbApistatusIncidentRepository incidentRepository,
|
||||
DjbApistatusIncidentApiRepository incidentApiRepository,
|
||||
DjbApistatusIncidentTimelineRepository incidentTimelineRepository){
|
||||
this.portalNoticeService = portalNoticeService;
|
||||
this.portalNoticeUIMapper = portalNoticeUIMapper;
|
||||
this.fileService = fileService;
|
||||
this.incidentRepository = incidentRepository;
|
||||
this.incidentApiRepository = incidentApiRepository;
|
||||
this.incidentTimelineRepository = incidentTimelineRepository;
|
||||
}
|
||||
|
||||
private static boolean isIncidentKind(String noticeType) {
|
||||
return PortalNoticeUI.NOTICE_TYPE_INCIDENT.equals(noticeType)
|
||||
|| PortalNoticeUI.NOTICE_TYPE_MAINTENANCE.equals(noticeType);
|
||||
}
|
||||
|
||||
private static IncidentKind toKind(String noticeType) {
|
||||
if (PortalNoticeUI.NOTICE_TYPE_INCIDENT.equals(noticeType)) return IncidentKind.INCIDENT;
|
||||
if (PortalNoticeUI.NOTICE_TYPE_MAINTENANCE.equals(noticeType)) return IncidentKind.MAINTENANCE;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +122,39 @@ public class PortalNoticeManService extends BaseService {
|
||||
|
||||
PortalNoticeUI ui = portalNoticeUIMapper.toVo(portalNotice);
|
||||
setFileInfo(portalNotice, ui);
|
||||
populateIncident(portalNotice, ui);
|
||||
return ui;
|
||||
}
|
||||
|
||||
private void populateIncident(PortalNotice portalNotice, PortalNoticeUI ui) {
|
||||
if (!isIncidentKind(portalNotice.getNoticeType())) {
|
||||
ui.setAffectedApis(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
Optional<DjbApistatusIncident> incidentOpt = incidentRepository.findByNoticeId(portalNotice.getId());
|
||||
if (!incidentOpt.isPresent()) {
|
||||
ui.setAffectedApis(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
DjbApistatusIncident incident = incidentOpt.get();
|
||||
ui.setIncidentId(incident.getIncidentId());
|
||||
ui.setSummary(incident.getSummary());
|
||||
ui.setStartedAt(incident.getStartedAt());
|
||||
ui.setEndAt(incident.getEndAt());
|
||||
ui.setState(incident.getState() == null ? null : incident.getState().name());
|
||||
ui.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name());
|
||||
|
||||
List<IncidentAffectedApiUI> apis = incidentApiRepository
|
||||
.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream()
|
||||
.map(api -> {
|
||||
IncidentAffectedApiUI vo = new IncidentAffectedApiUI();
|
||||
vo.setApiId(api.getApiId());
|
||||
vo.setApiName(api.getApiName());
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
ui.setAffectedApis(apis);
|
||||
}
|
||||
|
||||
public void insert(PortalNoticeUI portalNoticeUI) throws IOException {
|
||||
//portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject()));
|
||||
//portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())));
|
||||
@@ -106,6 +169,61 @@ public class PortalNoticeManService extends BaseService {
|
||||
}
|
||||
|
||||
portalNoticeService.save(portalNotice);
|
||||
|
||||
if (isIncidentKind(portalNoticeUI.getNoticeType())) {
|
||||
persistIncident(portalNoticeUI, portalNotice, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistIncident(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice, boolean isCreate) {
|
||||
IncidentKind kind = toKind(portalNoticeUI.getNoticeType());
|
||||
|
||||
DjbApistatusIncident incident = incidentRepository.findByNoticeId(portalNotice.getId())
|
||||
.orElseGet(DjbApistatusIncident::new);
|
||||
|
||||
incident.setKind(kind);
|
||||
incident.setTitle(portalNotice.getNoticeSubject());
|
||||
incident.setSummary(portalNoticeUI.getSummary());
|
||||
incident.setStartedAt(portalNoticeUI.getStartedAt() != null
|
||||
? portalNoticeUI.getStartedAt() : LocalDateTime.now());
|
||||
incident.setEndAt(portalNoticeUI.getEndAt());
|
||||
incident.setDetectedBy("MANUAL");
|
||||
incident.setNoticeId(portalNotice.getId());
|
||||
incident.setDraftYn("N");
|
||||
incident.setFixYn(StringUtils.defaultIfBlank(portalNotice.getFixYn(), "N"));
|
||||
|
||||
if (kind == IncidentKind.INCIDENT) {
|
||||
IncidentState newState = StringUtils.isNotBlank(portalNoticeUI.getState())
|
||||
? IncidentState.valueOf(portalNoticeUI.getState())
|
||||
: IncidentState.INVESTIGATING;
|
||||
if (!isCreate && incident.getState() != newState) {
|
||||
incident.setPreviousState(incident.getState());
|
||||
}
|
||||
incident.setState(newState);
|
||||
} else {
|
||||
incident.setState(null);
|
||||
incident.setPreviousState(null);
|
||||
}
|
||||
|
||||
DjbApistatusIncident saved = incidentRepository.save(incident);
|
||||
|
||||
// 영향 API 동기화 — 단순 전체 삭제 후 재삽입
|
||||
incidentApiRepository.deleteByIncidentId(saved.getIncidentId());
|
||||
List<IncidentAffectedApiUI> affected = portalNoticeUI.getAffectedApis();
|
||||
if (affected != null && !affected.isEmpty()) {
|
||||
List<DjbApistatusIncidentApi> rows = new ArrayList<>();
|
||||
for (IncidentAffectedApiUI src : affected) {
|
||||
if (StringUtils.isBlank(src.getApiId())) continue;
|
||||
DjbApistatusIncidentApi api = new DjbApistatusIncidentApi();
|
||||
api.setIncidentId(saved.getIncidentId());
|
||||
api.setApiId(src.getApiId());
|
||||
api.setApiName(src.getApiName());
|
||||
rows.add(api);
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
incidentApiRepository.saveAll(rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFileUpload(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice) throws IOException {
|
||||
@@ -129,6 +247,7 @@ public class PortalNoticeManService extends BaseService {
|
||||
portalNoticeUI.setNoticeDetail(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail()));
|
||||
|
||||
PortalNotice portalNotice = portalNoticeService.getById(portalNoticeUI.getId());
|
||||
String previousNoticeType = portalNotice.getNoticeType();
|
||||
|
||||
if (portalNoticeUI.getFiles() != null && !portalNoticeUI.getFiles().isEmpty()) {
|
||||
handleFileUpload(portalNoticeUI, portalNotice);
|
||||
@@ -139,11 +258,27 @@ public class PortalNoticeManService extends BaseService {
|
||||
|
||||
portalNoticeUIMapper.updateToEntity(portalNoticeUI, portalNotice);
|
||||
portalNoticeService.save(portalNotice);
|
||||
|
||||
if (isIncidentKind(portalNotice.getNoticeType())) {
|
||||
persistIncident(portalNoticeUI, portalNotice, false);
|
||||
} else if (isIncidentKind(previousNoticeType)) {
|
||||
// 일반 공지로 유형 변경 → 기존 incident 정리
|
||||
deleteIncidentIfExists(portalNotice.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteIncidentIfExists(String noticeId) {
|
||||
incidentRepository.findByNoticeId(noticeId).ifPresent(incident -> {
|
||||
incidentApiRepository.deleteByIncidentId(incident.getIncidentId());
|
||||
incidentTimelineRepository.deleteByIncidentId(incident.getIncidentId());
|
||||
incidentRepository.delete(incident);
|
||||
});
|
||||
}
|
||||
|
||||
public void delete(String id) {
|
||||
PortalNotice portalNotice = portalNoticeService.getById(id);
|
||||
Optional.ofNullable(portalNotice.getFileId()).ifPresent(fileService::deleteFile);
|
||||
deleteIncidentIfExists(id);
|
||||
portalNoticeService.deleteById(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,16 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Data
|
||||
public class PortalNoticeUI {
|
||||
|
||||
public static final String NOTICE_TYPE_NORMAL = "1";
|
||||
public static final String NOTICE_TYPE_MAINTENANCE = "2";
|
||||
public static final String NOTICE_TYPE_INCIDENT = "3";
|
||||
|
||||
private String id;
|
||||
|
||||
private String noticeSubject;
|
||||
@@ -53,4 +58,24 @@ public class PortalNoticeUI {
|
||||
private boolean hasAttachment; // 첨부파일 여부
|
||||
|
||||
private boolean fileDeleted; // 파일 삭제 여부
|
||||
|
||||
// ───── 장애/점검(djb_apistatus_incident) 연동 필드 ─────
|
||||
private Long incidentId;
|
||||
|
||||
private String summary;
|
||||
|
||||
// HTML <input type="datetime-local"> 는 'T' 구분자 형식 (예: 2026-06-16T10:29).
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
|
||||
private LocalDateTime endAt;
|
||||
|
||||
private String state; // INCIDENT 한정 (INVESTIGATING/IDENTIFIED/MONITORING/RESOLVED/CANCELED)
|
||||
|
||||
private String previousState; // 직전 상태
|
||||
|
||||
private List<IncidentAffectedApiUI> affectedApis;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
|
||||
import org.mapstruct.*;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
@Mapper(componentModel = "spring",
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE,
|
||||
unmappedSourcePolicy = ReportingPolicy.IGNORE)
|
||||
public interface PortalNoticeUIMapper {
|
||||
|
||||
PortalNoticeUI toVo(PortalNotice entity);
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -48,6 +50,9 @@ public class PortalOrgManService extends BaseService {
|
||||
private final PortalUserCorpManagerUIMapper portalUserCorpManagerUIMapper;
|
||||
private final FileService fileService;
|
||||
|
||||
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||
private EntityManager entityManager;
|
||||
|
||||
public Page<PortalOrgUI> selectList(Pageable pageable, PortalOrgUISearch portalOrgUISearch) {
|
||||
Page<PortalOrg> portalOrg = portalOrgService.findAll(pageable, portalOrgUISearch);
|
||||
return portalOrg.map(entity -> convertToUIWithMaskOption(entity, true));
|
||||
@@ -146,19 +151,37 @@ public class PortalOrgManService extends BaseService {
|
||||
throw new BizException("삭제(탈퇴) 상태인 법인만 완전삭제할 수 있습니다.");
|
||||
}
|
||||
|
||||
List<String> dependencies = new ArrayList<>();
|
||||
// 사용자(PortalUser)는 별도 화면에서 정리해야 하므로 cascade 하지 않고 차단
|
||||
long userCount = portalUserService.countByOrgId(id);
|
||||
if (userCount > 0) dependencies.add("사용자 " + userCount + "건");
|
||||
long appRequestCount = portalOrgService.countAppRequestByOrgId(id);
|
||||
if (appRequestCount > 0) dependencies.add("앱 신청 " + appRequestCount + "건");
|
||||
|
||||
if (!dependencies.isEmpty()) {
|
||||
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: " + String.join(", ", dependencies));
|
||||
if (userCount > 0) {
|
||||
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: 사용자 " + userCount + "건");
|
||||
}
|
||||
|
||||
// 앱 신청(AppRequest) + 연결된 승인(Approval/Approver) 은 cascade 삭제
|
||||
cascadeDeleteAppRequestsByOrg(id);
|
||||
|
||||
portalOrgService.deleteById(id);
|
||||
}
|
||||
|
||||
private void cascadeDeleteAppRequestsByOrg(String orgId) {
|
||||
// 1) 해당 org 의 app_request 가 참조하는 approval 의 approver 제거
|
||||
entityManager.createNativeQuery(
|
||||
"DELETE FROM PTL_APPROVER WHERE APPROVAL_ID IN " +
|
||||
"(SELECT APPROVAL_ID FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId AND APPROVAL_ID IS NOT NULL)"
|
||||
).setParameter("orgId", orgId).executeUpdate();
|
||||
|
||||
// 2) approval 제거
|
||||
entityManager.createNativeQuery(
|
||||
"DELETE FROM PTL_APPROVAL WHERE ID IN " +
|
||||
"(SELECT APPROVAL_ID FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId AND APPROVAL_ID IS NOT NULL)"
|
||||
).setParameter("orgId", orgId).executeUpdate();
|
||||
|
||||
// 3) app_request 제거
|
||||
entityManager.createNativeQuery(
|
||||
"DELETE FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId"
|
||||
).setParameter("orgId", orgId).executeUpdate();
|
||||
}
|
||||
|
||||
public Map<String, Object> selectDetailWithUsers(String id) {
|
||||
return getDetailWithUsers(id, true);
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.eactive.eai.rms.onl.common.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.eactive.eai.rms.onl.common.service.OnlAgentUtilServiceImpl;
|
||||
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointAddress;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointSubset;
|
||||
import io.kubernetes.client.openapi.models.V1Endpoints;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
|
||||
public class K8sUtil {
|
||||
private static final Logger logger = Logger.getLogger(K8sUtil.class);
|
||||
|
||||
public static List<Map<String, String>> getServerInfoByK8sUrl(String namespace, String serviceName, int port, String extUrl) throws Exception {
|
||||
|
||||
List<Map<String, String>> resultList = getServerInfoByK8sApi(namespace,serviceName);
|
||||
|
||||
for (Map<String,String> map : resultList) {
|
||||
String EAISEVRINSTNCNAME = map.get("EAISEVRINSTNCNAME");
|
||||
String EAISEVRIP = map.get("EAISEVRIP");
|
||||
Map<String, String> serverInfo = new HashMap<>();
|
||||
|
||||
serverInfo.put(OnlAgentUtilServiceImpl.FIELD_URL, new StringBuilder().append("http://" + EAISEVRIP + ":"+String.valueOf(port) + extUrl).toString());
|
||||
serverInfo.put(OnlAgentUtilServiceImpl.FIELD_INST_NAME, EAISEVRINSTNCNAME);
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
public static List<Map<String, String>> getServerInfoByK8sApi(String namespace, String serviceName) throws Exception {
|
||||
|
||||
logger.info("K8sAgentUtilService getServerInfoByK8sApi start.");
|
||||
|
||||
List<Map<String, String>> resultList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
ApiClient client = ClientBuilder.cluster().build();
|
||||
Configuration.setDefaultApiClient(client);
|
||||
|
||||
if (StringUtils.isBlank(namespace)) {
|
||||
new RuntimeException("Can't find kubernetes_namespace in Engine Property");
|
||||
}
|
||||
if (StringUtils.isBlank(serviceName)) {
|
||||
new RuntimeException("Can't find kubernetes_servicename in Engine Property");
|
||||
}
|
||||
|
||||
logger.info("getServerInfoByK8sApi Codes [ namespace: " + namespace + ", serviceName:" + serviceName + " ]");
|
||||
|
||||
V1Endpoints endpoints = new CoreV1Api().readNamespacedEndpoints(serviceName, namespace, null);
|
||||
|
||||
for (V1EndpointSubset subset : endpoints.getSubsets()) {
|
||||
for (V1EndpointAddress address : subset.getAddresses()) {
|
||||
Map<String, String> serverInfo = new HashMap<>();
|
||||
serverInfo.put("EAISEVRINSTNCNAME", address.getTargetRef().getName());
|
||||
serverInfo.put("EAISEVRIP", address.getIp());
|
||||
resultList.add(serverInfo);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("getServerInfoByK8sApi resultList [ " + resultList.toString() + " ]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e);
|
||||
throw new RuntimeException("K8S Mode로 AgentUtilService를 기동중 실패하였습니다.", e);
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,13 +5,20 @@
|
||||
# \uC790\uB3D9 \uD56B\uC2A4\uC651 \uD65C\uC131\uD654
|
||||
autoHotswap=true
|
||||
|
||||
# \uB85C\uAE45 \uB808\uBCA8 (TRACE, DEBUG, INFO, WARNING, ERROR)
|
||||
LOGGER.level=INFO
|
||||
# \uB85C\uADF8 \uB808\uBCA8 (TRACE, DEBUG, INFO, WARNING, ERROR)
|
||||
LOGGER=INFO
|
||||
|
||||
# \uB85C\uADF8 \uD30C\uC77C \uACBD\uB85C (\uC9C0\uC815 \uC2DC \uCF58\uC194 \uB300\uC2E0 \uD30C\uC77C\uB85C \uCD9C\uB825)
|
||||
LOGFILE=/Users/rinjae/eactive/djb-eapim/djb-eapim/eapim-admin/logs/hotswap-agent.log
|
||||
LOGFILE.append=true
|
||||
|
||||
# \uC2DC\uAC01 \uD3EC\uB9F7
|
||||
LOGGER_DATETIME_FORMAT=yyyy-MM-dd HH:mm:ss.SSS
|
||||
|
||||
# ============================================================
|
||||
# \uBA85\uC2DC\uC801 \uD50C\uB7EC\uADF8\uC778 \uBE44\uD65C\uC131\uD654 (\uC27C\uD45C \uAD6C\uBD84)
|
||||
# ============================================================
|
||||
disabledPlugins=org.hotswap.agent.plugin.ibatis.IBatisPlugin,org.hotswap.agent.plugin.mybatis.MyBatisPlugin
|
||||
disabledPlugins=IBatis,MyBatis
|
||||
|
||||
# ============================================================
|
||||
# Core Plugin \uD65C\uC131\uD654
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/Log/App/eapim/${inst.Name:-devSvr00}" />
|
||||
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-emsSvr00}" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/Log/App/eapim/${inst.Name:-devSvr00}" />
|
||||
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-devSvr00}" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/Log/App/eapim/${inst.Name:-emsSvr00}" />
|
||||
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-emsSvr00}" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-INFO}" />
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ JSP `detail()` 함수에서 제네릭 루프 대신 명시적 필드 매핑으
|
||||
|
||||
## 미완료 / 다음 작업
|
||||
|
||||
- [ ] `kjb-gradle.sh compileJava` 실행 → `QCryptoModuleConfig` Q-class 생성 확인
|
||||
- [ ] `./gradlew compileJava` 실행 → `QCryptoModuleConfig` Q-class 생성 확인
|
||||
- [ ] 관리자 메뉴 테이블에 경로 등록 (`/onl/admin/security/cryptoModuleMan.view`)
|
||||
- [ ] 화면 접근 권한(ACL) 등록
|
||||
- [ ] 전체 파일 커밋 (eapim-admin)
|
||||
|
||||
Reference in New Issue
Block a user