97 Commits

Author SHA1 Message Date
Rinjae 1ac84f2f92 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-09-01 18:26:47 +09:00
Rinjae 60d1dcea96 관리자 수동 등록 장애 공지 양식 추가
- 영향을 받는 API 표시 문구 재활용 및 본문 자동 생성 지원
- yml 및 DB 프로퍼티 병합 처리로 문구 동기화 자동화
2026-09-01 18:26:41 +09:00
eastargh bb3fb5e111 1000건 이상 경고
eapim-admin CI / build (push) Has been cancelled
2026-09-01 15:57:13 +09:00
eastargh 1608d29726 UI 개선
eapim-admin CI / build (push) Has been cancelled
2026-09-01 14:23:19 +09:00
eastargh d08c17bb13 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-09-01 13:23:56 +09:00
eastargh 5fd8334ea8 UI 조정
eapim-admin CI / build (push) Has been cancelled
2026-09-01 10:07:32 +09:00
eastargh a1d2058bca 웹훅 인증 우회 로그 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-31 11:12:24 +09:00
eastargh 927ef830ac API 통계 화면 정리
eapim-admin CI / build (push) Has been cancelled
2026-08-31 10:47:15 +09:00
eastargh 9cdfad1317 세션타임아웃 1시간으로 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-31 09:45:18 +09:00
eastargh bc40362c47 SMS인증창 오픈시 인증번호 focus
eapim-admin CI / build (push) Has been cancelled
2026-08-28 15:25:17 +09:00
eastargh 74f7aa5cf9 유량제어토큰획득실패 감시 스케쥴 프라퍼티 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-28 13:58:40 +09:00
eastargh 258bb70a93 eCams 연동시 로그 저장 2026-08-28 13:57:41 +09:00
eastargh a2cda26909 거래현황 900 제거
eapim-admin CI / build (push) Has been cancelled
2026-08-28 09:48:34 +09:00
eastargh 598dc5b1ee 웹훅 발송 인증 우회(내부망만 사용)
eapim-admin CI / build (push) Has been cancelled
2026-08-27 16:54:27 +09:00
eastargh 28ab6863da 웹훅 발송 2026-08-27 16:54:01 +09:00
eastargh 6e4abf9f27 비밀번호 변경시 SMS 인증 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-27 16:05:22 +09:00
eastargh 95ce7f9150 취약점 점검
eapim-admin CI / build (push) Has been cancelled
2026-08-27 13:36:02 +09:00
eastargh 8a7ceacb32 기능 개선
eapim-admin CI / build (push) Has been cancelled
2026-08-27 11:35:13 +09:00
eastargh 8b22f8e1f8 소스 정리
eapim-admin CI / build (push) Has been cancelled
2026-08-26 17:08:24 +09:00
eastargh 497b72c52a 자동 로그아웃 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-26 14:59:29 +09:00
eastargh e69e1a1031 업무오류는 오류건수 집계에서 제외 & 로그조회에서 하늘색 배경색 표시
eapim-admin CI / build (push) Has been cancelled
2026-08-26 13:49:12 +09:00
eastargh 9df39e74bd 취약점 점검 수정 2026-08-26 13:47:32 +09:00
eastargh 37817b15a9 웹훅 발송내역 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-26 10:58:55 +09:00
eastargh b3f797b726 웹훅 신청내역 조회화면 2026-08-26 10:58:32 +09:00
eastargh 929ec1f62d 웹훅 발송 정보항목 추가 2026-08-26 10:56:41 +09:00
eastargh 720c49295c js/css 캐시 허용
eapim-admin CI / build (push) Has been cancelled
2026-08-25 13:24:35 +09:00
eastargh b3b315cd6a UMS 메신저 전용 다수인 발송 추가 2026-08-25 09:53:40 +09:00
eastargh e0b7ab1129 API 사용현황 footer
eapim-admin CI / build (push) Has been cancelled
2026-08-24 13:57:43 +09:00
Rinjae ac90d171d3 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-08-24 13:16:45 +09:00
Rinjae 0f36718534 삭제 요청 처리 기능 추가 및 오류 메시지 처리 강화
- 승인 요청 삭제 버튼 추가 및 확인 경고문 삽입
- 삭제 가능 여부 검증 로직 추가 및 네이티브 쿼리 대체 처리
- 오류 메시지 파싱 로직 및 상세 조회 실패 시 UI 처리 개선
2026-08-24 13:16:39 +09:00
eastargh 314eb87ea4 API 통계 footer에 합계 표시
eapim-admin CI / build (push) Has been cancelled
2026-08-24 13:01:43 +09:00
eastargh a6e88f2281 IIM연동 오류 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-24 09:41:23 +09:00
eastargh 38f89fcd7a IIM연동 이력 조회
eapim-admin CI / build (push) Has been cancelled
2026-08-21 17:25:17 +09:00
eastargh 48073d5d91 메뉴명 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-21 15:08:40 +09:00
eastargh 79653a2887 IIM 연동이력 조회 화면 2026-08-21 15:05:18 +09:00
eastargh 97d6489bf7 IIM 연동시 interface 로그 저장
eapim-admin CI / build (push) Has been cancelled
2026-08-21 13:59:17 +09:00
eastargh 1bf3a950e3 IIM연동시 dev가 삭제된경우, 추가로 만들지 않는다
eapim-admin CI / build (push) Has been cancelled
2026-08-20 14:03:47 +09:00
eastargh 12e4213fa1 소스취약점 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-20 10:48:38 +09:00
eastargh e60d5443a3 스케쥴러 clustered 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-19 15:19:55 +09:00
Rinjae a15fa5ad66 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-08-19 14:31:19 +09:00
Rinjae 7b8646ab94 자동 탐지 미게시 포함 설정 추가
- 설정 키 확인 로직 추가 및 미게시 인터페이스 처리 적용
- 게시/미게시 API 필터링 로직 분리 및 오류 처리 강화
2026-08-19 14:31:12 +09:00
eastargh 26f85b6f11 동시실행 방지 @DisallowConcurrentExecution 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-19 14:15:48 +09:00
eastargh de6f7e5ff2 스케쥴러 CLUSTERED config 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-19 13:37:17 +09:00
eastargh e2206aedc4 메세지발송내역 - apistatus_changed 마스킹 예외 적용
eapim-admin CI / build (push) Has been cancelled
2026-08-19 11:11:28 +09:00
eastargh 2e30239a90 UMS messenger api-monitor 발송여부 체크
eapim-admin CI / build (push) Has been cancelled
2026-08-18 15:03:45 +09:00
eastargh e28d6e54c5 api관리 검색조건 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-18 14:29:47 +09:00
eastargh eb5e289a4e 헤더복사 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-18 13:52:10 +09:00
eastargh ca9c3240bf API상태변화 직원 스윙챗발송여부 환경변수 처리
eapim-admin CI / build (push) Has been cancelled
2026-08-18 09:26:12 +09:00
eastargh 289f8e2f2f 인터페이스 배포버튼
eapim-admin CI / build (push) Has been cancelled
2026-08-15 10:47:45 +09:00
eastargh a0a1954a58 damo-manager lib에 포함 & log-path 환경변수 설정 2026-08-15 10:47:22 +09:00
eastargh 0149da372e 인터페이스관리 - 배포 버튼(uploadfull) 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-14 18:04:19 +09:00
eastargh 3bb0325bf7 대시보드 api 검색조건 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-14 17:30:40 +09:00
eastargh 579cb61a75 @DisallowConcurrentExecution 제거
eapim-admin CI / build (push) Has been cancelled
2026-08-14 17:04:03 +09:00
eastargh 6c67e3f1d8 ... 2026-08-14 17:02:41 +09:00
eastargh a19f65a660 불필요한 로그 제거 2026-08-14 17:01:40 +09:00
eastargh 0e616bc995 UMS 발송 핸드폰번호 null 체크
eapim-admin CI / build (push) Has been cancelled
2026-08-14 16:59:19 +09:00
eastargh 7ac9c05f62 UMS 스윙챗 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-14 10:02:27 +09:00
eastargh 7c9f87db73 팝업오픈시 매핑코드 전달
eapim-admin CI / build (push) Has been cancelled
2026-08-13 17:28:32 +09:00
eastargh 8448d99fe8 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git 2026-08-13 17:28:14 +09:00
eastargh ec9cc19821 검색조건 유지 2026-08-13 17:28:06 +09:00
Rinjae c2d6eb4c34 레거시 암호화 컨트롤러 제거 - 내부 API 토큰 초기화 로직 추가 - 정규식 성능 최적화
eapim-admin CI / build (push) Has been cancelled
2026-08-13 16:52:04 +09:00
Rinjae e364a35b66 - 소수 자릿수 처리 개선 - DECIMAL/BIGDECIMAL 정수/숫자 구분
eapim-admin CI / build (push) Has been cancelled
- 샘플 생성 로직 리팩토링 - 길이 및 소수 자릿수 기준 상세화
- 공통 스키마 처리 함수 추가 - 코드 중복 제거 및 가독성 향상
2026-08-13 14:42:44 +09:00
Rinjae 760000070d APP 승인 시 GW 반영 로직 개선
eapim-admin CI / build (push) Has been cancelled
- GwClientSyncService 분리 및 트랜잭션 관리 강화
- 승인 화면에 GW 반영 필수 정보 노출 추가
- JSP 동적 행 노출 및 승인 데이터 검증 로직 적용
2026-08-13 14:37:31 +09:00
eastargh 71e433a2a5 IIM연동시 삭제 오류 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-13 10:50:42 +09:00
eastargh 5830031f32 전문레이아웃 매핑 화면 오픈시 validation
eapim-admin CI / build (push) Has been cancelled
2026-08-13 10:35:26 +09:00
eastargh c7be9760a0 IIM 연동시 삭제 오류 수정 2026-08-13 10:34:34 +09:00
eastargh ee0b882608 변환명령에 함수가 적용된 경우, 복제시 함수명 잘리는 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-12 17:46:10 +09:00
eastargh 2cdd321c39 전문레이아웃매핑 변환명령에 아이템 멀티선택 콤보 추가
eapim-admin CI / build (push) Has been cancelled
2026-08-12 17:04:04 +09:00
Rinjae d1eecb4ee7 v1 API 스펙 버튼 노출 설정 추가
eapim-admin CI / build (push) Has been cancelled
- PortalProperty 기반 노출 여부 제어 로직 추가
- JSP 뷰모델에 apiSpecV1Enabled 속성 연동
2026-08-11 19:14:28 +09:00
Rinjae 91a156c6b0 - Swagger spec 서버 주소 GW 기준 치환 통합 - UI/다운로드 동일 기준
eapim-admin CI / build (push) Has been cancelled
- ApiTesterFilter: gw 호출 시 base-url 재조립 처리 추가
- admin 스펙 저장 시 실주소 고정 제거 - 재치환 유연성 확보
2026-08-11 19:00:18 +09:00
Rinjae c19c12df70 응답유형별 서버 실주소 처리 개선
eapim-admin CI / build (push) Has been cancelled
- 저장본에 sentinel 유지 및 편집기 사용 시 실주소로 치환
- 응답유형별 서버 실주소 계산 유틸 함수 추가
2026-08-11 16:56:33 +09:00
eastargh 347cca2d7c IIM연동시 개발 인터페이스를 추가로 만들어 준다
eapim-admin CI / build (push) Has been cancelled
2026-08-11 15:17:19 +09:00
eastargh d8a1ca84e2 전문레이아웃 매핑 선연결 제외 2026-08-11 15:15:58 +09:00
Rinjae bf4f953d56 변경/해지 신청 개선 및 사전검사 추가
eapim-admin CI / build (push) Has been cancelled
- 변경/해지 상태 표시 및 취소 기능 추가
- 해지 신청 사전검사 및 경고 메시지 처리 구현
- 관리자 승인 이전 허용되는 취소 정책 지원
2026-08-11 14:15:34 +09:00
eastargh 814571eeba rms.webhook.enabled 프라퍼티에 따라 발송
eapim-admin CI / build (push) Has been cancelled
2026-08-11 09:37:14 +09:00
eastargh 13850ba329 IIM연동 로그 저장시 시퀀스 적용
eapim-admin CI / build (push) Has been cancelled
2026-08-10 15:55:55 +09:00
eastargh 04b4d415a9 인터페이스 미확인 오류 로그- 업무데이타 내용 복호화
eapim-admin CI / build (push) Has been cancelled
2026-08-10 13:40:19 +09:00
eastargh 38902cf6b3 로그 조회 GUID 입력시 다른 조건 무시
eapim-admin CI / build (push) Has been cancelled
2026-08-10 13:39:46 +09:00
eastargh 7ebbd7d9e4 로그조회시 GUID입력시 다른 조건 무시
eapim-admin CI / build (push) Has been cancelled
2026-08-10 10:42:39 +09:00
eastargh f59de66d2c UMS email 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-10 09:23:50 +09:00
eastargh c179e50047 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-08-10 09:14:16 +09:00
eastargh 39f0baf5ef 인터페이스 세부설정관리 - 그리드/검색조건 재구성 2026-08-10 09:14:08 +09:00
Rinjae 08354837b1 OpenAPI Spec·포탈 노출 필터 추가 - EAIMessage 검색 조건 추가 - API Group 팝업 검색 개선 - JSP 체크박스 연동 처리
eapim-admin CI / build (push) Has been cancelled
2026-08-07 13:40:49 +09:00
Rinjae a31b991e59 API 판정 로직 다중 스키마 지원 - 게시 조건 분리 처리
eapim-admin CI / build (push) Has been cancelled
- API 그룹/스펙 조건 분리 및 추가 Repository 적용
- ORA-00942 방지 위해 EMSAPP 조회 로직 수정
2026-08-06 17:59:38 +09:00
Rinjae 615e56687a Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-08-06 17:05:50 +09:00
Rinjae ba1161be7b Quartz 스케줄러 상태 JSP 추가
- Quartz 스케줄러 상태 표시용 quartzStatus.jsp 신규 작성
- 스케줄러/잡/트리거 정보 표시 및 자동 새로고침 기능 구현
2026-08-06 16:57:39 +09:00
eastargh 7430b03323 다시 적용
eapim-admin CI / build (push) Has been cancelled
2026-08-06 11:33:52 +09:00
eastargh 5b05489924 일단 원래데로
eapim-admin CI / build (push) Has been cancelled
2026-08-06 11:05:45 +09:00
eastargh 3c386b780b Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-08-06 10:47:01 +09:00
eastargh f928a0b1e4 전문매핑 단일 연결/해제 버튼 추가 2026-08-06 10:46:53 +09:00
Rinjae dc9e737aa9 IP 매칭 유틸리티 추가 - CIDR/와일드카드 포함(#194).
eapim-admin CI / build (push) Has been cancelled
포탈 캐시 리로드 다중 서버 지원 - UI 개선/동시 호출 추가.
내부 API 유틸/테스트 및 JSP UI 연계 최적화.
2026-08-06 09:59:23 +09:00
eastargh db57e95233 이벤트유형 CONTROL_ > CHECK_ 로 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-06 09:15:57 +09:00
Rinjae 643e7fcdf2 damo-manager.jar 라이브러리 업데이트
eapim-admin CI / build (push) Has been cancelled
- eapim-admin, eapim-online, elink-portal 공통 반영
2026-08-05 17:46:55 +09:00
Rinjae 36994a57b6 속성 관리 화면 수정
eapim-admin CI / build (push) Has been cancelled
- 다중라인 값 편집 및 줄바꿈 지원 PRPTY2VAL 속성 수정
- prpty2Val 입력 필드 텍스트 영역으로 변경 및 스타일 추가
- jQuery 선택자 간소화 `<input>` → `<textarea>` for consistency
2026-08-05 15:23:46 +09:00
Rinjae 4a087e576b Merge commit '9f22d4fa4d6720aabb06a5541449ebb120397987'
eapim-admin CI / build (push) Has been cancelled
2026-08-05 14:22:34 +09:00
eastargh 9f22d4fa4d 로그상세에서 정렬클릭시 지수형 풀리지 않도록
eapim-admin CI / build (push) Has been cancelled
2026-08-05 11:36:51 +09:00
eastargh 66c01029cd UMS 설정 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-05 11:12:46 +09:00
151 changed files with 7875 additions and 2572 deletions
@@ -15,4 +15,4 @@ eai.tableowner=EMSAPP
eai.systemmode=L eai.systemmode=L
eai.drmode=N eai.drmode=N
# black / blue / green / orange / yellow # black / blue / green / orange / yellow
theme.color=yellow theme.color=blue
+1 -1
View File
@@ -3,7 +3,7 @@
<context-root>monitoring</context-root> <context-root>monitoring</context-root>
<session-descriptor> <session-descriptor>
<timeout-secs>1800</timeout-secs> <timeout-secs>3600</timeout-secs>
<cookie-name>JSESSIONID_EMS</cookie-name> <cookie-name>JSESSIONID_EMS</cookie-name>
<persistent-store-type>replicated_if_clustered</persistent-store-type> <persistent-store-type>replicated_if_clustered</persistent-store-type>
</session-descriptor> </session-descriptor>
+45 -33
View File
@@ -191,25 +191,12 @@
} }
}); });
// 경로(op.path.value)에는 어댑터경로가 이미 baking 됨(백엔드 applyAutoRules). 서버는 응답유형별 호스트만 붙인다. // spec 의 서버는 응답유형과 무관하게 항상 GW 주소로 고정한다 — spec 은 "이 API 를 실제로 어디로
// - gw : djb.gateway.base-url (PortalProperty) → [gw호스트 + 어댑터경로 + 경로] // 호출하는가"의 공개 계약이고, 그 기준은 게이트웨이다. sample/mock 은 테스트베드 동작(포탈
// - mock : Mock Server URL(전체 URL) → [mock URL 그대로] (어댑터/오퍼레이션 경로 미부착) // ApiTesterFilter 가 응답유형에 따라 sampleResponse 반환 / mockUrl forward)에서만 갈린다.
// - 그 외 : 호스트 없음 → [어댑터경로 + 경로] // 경로(op.path.value)에는 어댑터경로가 이미 baking 됨(백엔드 applyAutoRules).
var _rt = (d.portal && d.portal.responseType) || 'sample'; spec.servers = [{ url: gatewayAddress() }];
var _host; spec.paths[op.path.value] = { [op.method.value.toLowerCase()]: operation };
var _pathKey = op.path.value;
if (_rt === 'gw') {
_host = (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
} else if (_rt === 'mock') {
// Mock 은 전체 호출 URL. origin=서버, pathname(+query)=경로로 분해해 경로가 뒤에 덧붙지 않도록 한다.
var _mk = splitFullUrl((d.portal && d.portal.mockUrl) || '');
_host = _mk.origin;
_pathKey = _mk.path;
} else {
_host = '';
}
if (_host) spec.servers = [{ url: _host }];
spec.paths[_pathKey] = { [op.method.value.toLowerCase()]: operation };
// 보안 스킴 // 보안 스킴
d.securitySchemes.forEach(s => { d.securitySchemes.forEach(s => {
@@ -237,6 +224,8 @@
const s = { type: row.type.value }; const s = { type: row.type.value };
if (row.format && row.format.value) s.format = row.format.value; if (row.format && row.format.value) s.format = row.format.value;
if (row.maxLength && row.maxLength.value) s.maxLength = Number(row.maxLength.value); if (row.maxLength && row.maxLength.value) s.maxLength = Number(row.maxLength.value);
// 소수 자릿수(GW 레이아웃 유래) 보존 — 예제값 생성 시 '.' 포함 길이 계산에 사용
if (row.decimals && row.decimals.value !== '' && row.decimals.value != null) s['x-djb-decimal'] = Number(row.decimals.value);
if (row.pattern && row.pattern.value) s.pattern = row.pattern.value; if (row.pattern && row.pattern.value) s.pattern = row.pattern.value;
if (row.description && row.description.value) s.description = row.description.value; if (row.description && row.description.value) s.description = row.description.value;
if (row.example && row.example.value !== '') s.example = coerceExample(row.type.value, row.example.value); if (row.example && row.example.value !== '') s.example = coerceExample(row.type.value, row.example.value);
@@ -981,6 +970,11 @@
} }
} }
// spec servers 에 넣을 게이트웨이 주소(단일 기준). 응답유형과 무관하게 항상 이 값을 쓴다.
function gatewayAddress() {
return (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
}
// 전체 URL → { origin, path }. Mock Server URL 을 서버+경로로 분해(경로를 별도로 덧붙이지 않기 위함). // 전체 URL → { origin, path }. Mock Server URL 을 서버+경로로 분해(경로를 별도로 덧붙이지 않기 위함).
function splitFullUrl(u) { function splitFullUrl(u) {
u = (u || '').trim(); u = (u || '').trim();
@@ -1474,6 +1468,7 @@
required: { value: false, locked: false }, required: { value: false, locked: false },
format: { value: '', locked: false }, format: { value: '', locked: false },
maxLength: { value: '', locked: false }, maxLength: { value: '', locked: false },
decimals: { value: '', locked: false },
pattern: { value: '', locked: false }, pattern: { value: '', locked: false },
description: { value: '', locked: false }, description: { value: '', locked: false },
example: { value: '', locked: false }, example: { value: '', locked: false },
@@ -1488,6 +1483,7 @@
required: { value: false, locked: false }, required: { value: false, locked: false },
format: { value: '', locked: false }, format: { value: '', locked: false },
maxLength: { value: '', locked: false }, maxLength: { value: '', locked: false },
decimals: { value: '', locked: false },
pattern: { value: '', locked: false }, pattern: { value: '', locked: false },
description: { value: '', locked: false }, description: { value: '', locked: false },
example: { value: '', locked: false } example: { value: '', locked: false }
@@ -1741,6 +1737,7 @@
var row = { var row = {
name: wrap(name, gw), type: wrap(t, gw), required: wrap(!!required, false), name: wrap(name, gw), type: wrap(t, gw), required: wrap(!!required, false),
format: wrap(p.format || ''), maxLength: wrap(p.maxLength == null ? '' : p.maxLength), format: wrap(p.format || ''), maxLength: wrap(p.maxLength == null ? '' : p.maxLength),
decimals: wrap(p['x-djb-decimal'] == null ? '' : p['x-djb-decimal']),
pattern: wrap(p.pattern || ''), description: wrap(p.description || ''), pattern: wrap(p.pattern || ''), description: wrap(p.description || ''),
example: wrap(p.example == null ? '' : p.example), children: null example: wrap(p.example == null ? '' : p.example), children: null
}; };
@@ -1790,7 +1787,7 @@
}; };
d.parameters = (op.parameters || []).map(function (p) { d.parameters = (op.parameters || []).map(function (p) {
var s = p.schema || {}; var s = p.schema || {};
return { in: p.in || 'query', name: wrap(p.name), type: wrap(s.type || 'string'), required: wrap(!!p.required), format: wrap(s.format || ''), maxLength: wrap(s.maxLength == null ? '' : s.maxLength), pattern: wrap(s.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example) }; return { in: p.in || 'query', name: wrap(p.name), type: wrap(s.type || 'string'), required: wrap(!!p.required), format: wrap(s.format || ''), maxLength: wrap(s.maxLength == null ? '' : s.maxLength), decimals: wrap(s['x-djb-decimal'] == null ? '' : s['x-djb-decimal']), pattern: wrap(s.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example) };
}); });
var rb = op.requestBody || {}; var rbc = rb.content || {}; var mt = Object.keys(rbc)[0] || 'application/json'; var rb = op.requestBody || {}; var rbc = rb.content || {}; var mt = Object.keys(rbc)[0] || 'application/json';
d.requestBody = { mediaType: mt, required: wrap(!!rb.required), schema: schemaToRows((rbc[mt] || {}).schema) }; d.requestBody = { mediaType: mt, required: wrap(!!rb.required), schema: schemaToRows((rbc[mt] || {}).schema) };
@@ -1835,19 +1832,34 @@
// 스키마 기반 샘플 JSON 생성 (자동생성 예제) // 스키마 기반 샘플 JSON 생성 (자동생성 예제)
function _coerce(t, v) { if (t === 'integer') return parseInt(v, 10) || 0; if (t === 'number') return parseFloat(v) || 0; if (t === 'boolean') return v === true || v === 'true'; return v; } function _coerce(t, v) { if (t === 'integer') return parseInt(v, 10) || 0; if (t === 'number') return parseFloat(v) || 0; if (t === 'boolean') return v === true || v === 'true'; return v; }
// 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123456.123, boolean→false // 자릿수 문자열: start 위치부터 1~9 를 순환('123456789123...')
// - 숫자는 길이 지정 시 정수부를 그 길이에 맞춰 자름(실수는 소수 .123 유지) function _digits(start, count) { var s = ''; for (var i = 0; i < count; i++) s += String(((start + i) % 9) + 1); return s; }
function _defScalar(t, name, maxLen) { // 숫자 예제값. 길이는 소수점(.)까지 포함한 문자열 전체 길이로 계산한다.
var ml = parseInt(maxLen, 10); // 예) 길이 9 / 소수 5 → 123.45678 (정수부 3 + '.' 1 + 소수부 5 = 9자)
if (t === 'integer') { // - 길이가 소수부+2 보다 짧으면 정수부 1자리를 확보하고 소수부를 줄인다.
var iv = '123456'; // - 길이 미지정이면 정수부 6자리(기존 기본값)를 사용한다.
if (ml && ml > 0 && iv.length > ml) iv = iv.substring(0, ml); function _numSample(maxLen, dec) {
return parseInt(iv, 10); var ml = parseInt(maxLen, 10) || 0, d = parseInt(dec, 10) || 0;
if (d <= 0) return _digits(0, ml > 0 ? Math.min(ml, 6) : 6);
var il;
if (ml > 0) {
il = ml - d - 1;
if (il < 1) { il = 1; d = Math.max(1, ml - 2); }
} else {
il = 6;
} }
return _digits(0, il) + '.' + _digits(il, d);
}
// 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123.45678, boolean→false
// - 숫자는 지정 길이를 넘지 않도록 자릿수를 맞춘다(소수는 '.' 포함 길이 기준).
// - number 인데 소수 자릿수 정보가 없으면 3자리로 가정(소수 0자리는 백엔드가 integer 로 매핑).
function _defScalar(t, name, maxLen, decimals) {
var ml = parseInt(maxLen, 10);
if (t === 'integer') return parseInt(_numSample(ml, 0), 10);
if (t === 'number') { if (t === 'number') {
var ip = '123456'; var d = parseInt(decimals, 10);
if (ml && ml > 0 && ip.length > ml) ip = ip.substring(0, ml); if (!(d > 0)) d = 3;
return parseFloat(ip + '.123'); return parseFloat(_numSample(ml, d));
} }
if (t === 'boolean') return false; if (t === 'boolean') return false;
var v = 'sample_' + (name || ''); var v = 'sample_' + (name || '');
@@ -1859,7 +1871,7 @@
if (t === 'object') return _sampleObj(row.children || []); if (t === 'object') return _sampleObj(row.children || []);
if (t === 'array') { var it = (row.itemsType && row.itemsType.value) || 'string'; return [it === 'object' ? _sampleObj(row.children || []) : _defScalar(it, (row.name && row.name.value) || 'item', '')]; } if (t === 'array') { var it = (row.itemsType && row.itemsType.value) || 'string'; return [it === 'object' ? _sampleObj(row.children || []) : _defScalar(it, (row.name && row.name.value) || 'item', '')]; }
if (ex !== undefined && ex !== '') return _coerce(t, ex); if (ex !== undefined && ex !== '') return _coerce(t, ex);
return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value); return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value, row.decimals && row.decimals.value);
} }
function _sampleObj(rows) { var o = {}; (rows || []).forEach(function (r) { if (r && r.name && r.name.value) o[r.name.value] = _sampleVal(r); }); return o; } function _sampleObj(rows) { var o = {}; (rows || []).forEach(function (r) { if (r && r.name && r.name.value) o[r.name.value] = _sampleVal(r); }); return o; }
function ensureExamplesFromSchema(force) { function ensureExamplesFromSchema(force) {
@@ -1905,7 +1917,7 @@
if (r.children && r.children.length) fillSchemaExamples(r.children); if (r.children && r.children.length) fillSchemaExamples(r.children);
} else { } else {
r.example = r.example || { value: '', locked: false }; r.example = r.example || { value: '', locked: false };
r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value)); r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value, r.decimals && r.decimals.value));
} }
}); });
} }
+15 -11
View File
@@ -45,9 +45,13 @@ $(document).ready(function() {
} }
}); });
//검색조건 달력
$("#startDatepicker").datepicker();
$("#endDatepicker").datepicker();
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
gridPostData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val() + $("input[name=searchStartHHMM]").val(); gridPostData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchStartHHMM]").val();
gridPostData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val() + $("input[name=searchEndHHMM]").val(); gridPostData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchEndHHMM]").val();
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
@@ -107,8 +111,8 @@ $(document).ready(function() {
$("#btn_search").click(function(){ $("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
postData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val() + $("input[name=searchStartHHMM]").val(); postData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchStartHHMM]").val();
postData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val() + $("input[name=searchEndHHMM]").val(); postData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchEndHHMM]").val();
$("#grid").setGridParam({ postData: postData ,page : "1" }).trigger("reloadGrid"); $("#grid").setGridParam({ postData: postData ,page : "1" }).trigger("reloadGrid");
}); });
@@ -154,11 +158,11 @@ function getEndTime(){
<tr> <tr>
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.logamndHMS") %></th> <th style="width:180px;"><%= localeMessage.getString("auditLogMan.logamndHMS") %></th>
<td> <td>
<input type="text" name="searchStartYYYYMMDD" value="" size="10" style="width:100px; border:1px solid #ebebec;"> <input type="text" name="searchStartYYYYMMDD" id="startDatepicker" value="${param.searchStartYYYYMMDD}" readonly="readonly" size="10" style="width:100px; border:1px solid #ebebec;">
<input type="text" name="searchStartHHMM" value="" size="8" style="width:100px; border:1px solid #ebebec;"> <input type="text" name="searchStartHHMM" value="${param.searchStartHHMM}" size="8" style="width:100px; border:1px solid #ebebec;">
~ ~
<input type="text" name="searchEndYYYYMMDD" value="" size="10" style="width:100px; border:1px solid #ebebec;"> <input type="text" name="searchEndYYYYMMDD" id="endDatepicker" value="${param.searchEndYYYYMMDD}" readonly="readonly" size="10" style="width:100px; border:1px solid #ebebec;">
<input type="text" name="searchEndHHMM" value="" size="8" style="width:100px; border:1px solid #ebebec;"> <input type="text" name="searchEndHHMM" value="${param.searchEndHHMM}" size="8" style="width:100px; border:1px solid #ebebec;">
<input type="hidden" name="searchStartDateTime" /> <input type="hidden" name="searchStartDateTime" />
<input type="hidden" name="searchEndDateTime" /> <input type="hidden" name="searchEndDateTime" />
</td> </td>
@@ -168,9 +172,9 @@ function getEndTime(){
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.systemCode") %></th> <th style="width:180px;"><%= localeMessage.getString("auditLogMan.systemCode") %></th>
<td><div class = "select-style"> <td><div class = "select-style">
<select id="searchSystemCode" name="searchSystemCode"> <select id="searchSystemCode" name="searchSystemCode">
<option value=""><%= localeMessage.getString("combo.all")%></option> <option value="" <c:if test="${empty param.searchSystemCode}">selected</c:if>><%= localeMessage.getString("combo.all")%></option>
<c:forEach items="${serviceTypeCombos }" var="serviceTypeCombo"> <c:forEach items="${serviceTypeCombos }" var="serviceTypeCombo">
<option value="${serviceTypeCombo.code }">${serviceTypeCombo.codename }</option> <option value="${serviceTypeCombo.code }" <c:if test="${serviceTypeCombo.code == param.searchSystemCode}">selected</c:if>>${serviceTypeCombo.codename }</option>
</c:forEach> </c:forEach>
</select> </select>
</div> </div>
@@ -180,7 +184,7 @@ function getEndTime(){
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.logTypeText") %></th> <th style="width:180px;"><%= localeMessage.getString("auditLogMan.logTypeText") %></th>
<td> <td>
<input type="text" value="" name="searchLogTypeText" checked="checked"/> <input type="text" value="${param.searchLogTypeText}" name="searchLogTypeText" checked="checked"/>
</td> </td>
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.remoteAddress") %></th> <th style="width:180px;"><%= localeMessage.getString("auditLogMan.remoteAddress") %></th>
<td><input type="text" name="searchRemoteAddress" value="${param.searchRemoteAddress}" style="width:100%"></td> <td><input type="text" name="searchRemoteAddress" value="${param.searchRemoteAddress}" style="width:100%"></td>
+6 -2
View File
@@ -31,8 +31,12 @@
<script language="javascript" src="<c:url value="/js/custom.js"/>"></script> <script language="javascript" src="<c:url value="/js/custom.js"/>"></script>
<script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script> <script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script>
<script type="text/javascript"> <script type="text/javascript">
/* 자동 로그아웃: iframe 내 활동을 sessionStorage에 기록 (main.jsp 타이머와 공유) */ /* 자동 로그아웃: 서버로 실제 요청이 발생한 경우에만 활동으로 기록 (main.jsp 타이머와 sessionStorage 공유) */
(function() { var m = function() { sessionStorage.setItem('lastActivity', String(Date.now())); }; ['mousemove','keydown','click','touchstart'].forEach(function(e){ document.addEventListener(e, m, true); }); })(); (function() {
var mark = function() { sessionStorage.setItem('lastActivity', String(Date.now())); };
mark(); // 화면(프레임) 로드 자체가 서버 요청이므로 활동으로 기록
$(document).ajaxSend(mark); // 이후 이 프레임에서 발생하는 모든 AJAX 요청을 활동으로 기록
})();
</script> </script>
<script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script>
<script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script>
@@ -31,6 +31,8 @@ function init(){
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("", new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
"<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering(); "<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
$("select[name=searchJobUseYn]").val("Y");
putSelectFromParam(); putSelectFromParam();
initGrid(); initGrid();
+87 -10
View File
@@ -65,6 +65,7 @@
// SMS 인증 관련 변수 // SMS 인증 관련 변수
var smsAuthTimer = null; var smsAuthTimer = null;
var resendTimer = null; var resendTimer = null;
var smsAuthPurpose = 'LOGIN'; // 'LOGIN' | 'PASSWORD_CHANGE' - SMS 인증 모달을 어느 흐름에서 띄웠는지 구분
function fncPreMain() { function fncPreMain() {
if ($("input[name=userId]").val() ==null || $("input[name=userId]").val().length == 0 ){ if ($("input[name=userId]").val() ==null || $("input[name=userId]").val().length == 0 ){
@@ -96,13 +97,14 @@
} }
else if (response.smsAuthRequired) { else if (response.smsAuthRequired) {
// SMS 인증 필요 // SMS 인증 필요
smsAuthPurpose = 'LOGIN';
showSmsAuthModal(response); showSmsAuthModal(response);
} else if (response.success) { } else if (response.success) {
// 직접 로그인 성공 // 직접 로그인 성공
window.location.href = response.redirectUrl; window.location.href = response.redirectUrl;
} else { } else {
// 로그인 실패 // 로그인 실패
alert(response.errorMessage || '로그인에 실패했습니다.'); alert('로그인에 실패했습니다');
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
@@ -123,11 +125,21 @@
function showSmsAuthModal(data) { function showSmsAuthModal(data) {
$('#smsAuthMaskedPhone').text(data.maskedPhone); $('#smsAuthMaskedPhone').text(data.maskedPhone);
$('#smsAuthCode').val(''); $('#smsAuthCode').val('');
updateSmsTestHint(data.smsTestHint);
startExpiryTimer(data.expiresAt); startExpiryTimer(data.expiresAt);
startResendTimer(data.resendableAt); startResendTimer(data.resendableAt);
$('#smsAuthModal').modal('show'); $('#smsAuthModal').modal('show');
} }
// fixed 모드(djb.sms_auth.mode=fixed)일 때만 서버가 내려주는 테스트용 인증번호 힌트 표시
function updateSmsTestHint(hint) {
if (hint) {
$('#smsAuthTestHint').text(hint).show();
} else {
$('#smsAuthTestHint').text('').hide();
}
}
// 유효시간 타이머 // 유효시간 타이머
function startExpiryTimer(expiresAt) { function startExpiryTimer(expiresAt) {
clearInterval(smsAuthTimer); clearInterval(smsAuthTimer);
@@ -181,6 +193,7 @@
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
if (data.sent) { if (data.sent) {
updateSmsTestHint(data.smsTestHint);
startExpiryTimer(data.expiresAt); startExpiryTimer(data.expiresAt);
startResendTimer(data.resendableAt); startResendTimer(data.resendableAt);
alert('인증번호가 재발송되었습니다.'); alert('인증번호가 재발송되었습니다.');
@@ -194,7 +207,7 @@
}); });
} }
// SMS 인증번호 확인 // SMS 인증번호 확인 (로그인 / 비밀번호 변경 공용 - smsAuthPurpose로 분기)
function verifySmsAuthCode() { function verifySmsAuthCode() {
var code = $('#smsAuthCode').val(); var code = $('#smsAuthCode').val();
if (!code || code.length !== 6) { if (!code || code.length !== 6) {
@@ -203,17 +216,25 @@
return; return;
} }
var isPasswordChange = (smsAuthPurpose === 'PASSWORD_CHANGE');
var verifyUrl = isPasswordChange
? '<c:url value="/sms-auth/verify-password-change.json"/>'
: '<c:url value="/sms-auth/verify.json"/>';
$.ajax({ $.ajax({
url: '<c:url value="/sms-auth/verify.json"/>', url: verifyUrl,
type: 'POST', type: 'POST',
data: { authCode: code }, data: { authCode: code },
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
if (data.success) { if (data.success) {
$('#smsAuthModal').modal('hide'); $('#smsAuthModal').modal('hide');
if (isPasswordChange) {
alert(data.message);
}
window.location.href = data.redirectUrl; window.location.href = data.redirectUrl;
} else { } else {
alert(data.errorMessage || '인증에 실패했습니다.'); alert(data.errorMessage || data.message || '인증에 실패했습니다.');
} }
}, },
error: function() { error: function() {
@@ -263,6 +284,46 @@
return true; return true;
} }
// 비밀번호 변경 요청 (AJAX) - 서버 룰 검증 실패 시 모달을 닫지 않고 비밀번호 입력값만 초기화
function submitChangePassword() {
if (!checkPwd()) {
return false;
}
$.ajax({
url: '<c:url value="/changePassword.do"/>',
type: 'POST',
data: $('#modalLoginForm').serialize(),
dataType: 'json',
success: function(response) {
if (response.smsAuthRequired) {
// 비밀번호는 아직 반영되지 않음 - SMS 인증 완료 시 최종 반영됨
smsAuthPurpose = 'PASSWORD_CHANGE';
// 두 모달이 겹치면 이전 모달의 enforceFocus/backdrop 이 남아 입력이 막히므로
// #pwdChgModal 이 완전히 닫힌 뒤 SMS 인증 모달을 연다
$('#pwdChgModal').one('hidden.bs.modal', function() {
showSmsAuthModal(response);
});
$('#pwdChgModal').modal('hide');
return;
}
alert(response.message);
if (response.success) {
window.location.href = response.redirectUrl || '<c:url value="/emergency.jsp"/>';
} else {
$("input[name=changePassword]").val('');
$("input[name=confirmPassword]").val('');
$("input[name=changePassword]").trigger("focus");
}
},
error: function() {
alert("<%= localeMessage.getString("login.pwdchangefail") %>");
}
});
return false;
}
$(document).ready(function() { $(document).ready(function() {
$("input[name=userId]").keydown(function(event){ $("input[name=userId]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
@@ -276,12 +337,14 @@
}); });
$("input[name=changePassword]").keydown(function(event){ $("input[name=changePassword]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
$('#modalLoginForm').submit(); event.preventDefault();
submitChangePassword();
} }
}); });
$("input[name=confirmPassword]").keydown(function(event){ $("input[name=confirmPassword]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
$('#modalLoginForm').submit(); event.preventDefault();
submitChangePassword();
} }
}); });
// Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음) // Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음)
@@ -310,6 +373,19 @@
verifySmsAuthCode(); verifySmsAuthCode();
} }
}); });
// 인증번호는 숫자만 허용 (타이핑/붙여넣기/드래그 공통, 백스페이스·방향키는 방해하지 않음)
$("#smsAuthCode").on("input", function(){
var digits = this.value.replace(/[^0-9]/g, '').slice(0, 6);
if (this.value !== digits) {
this.value = digits;
}
});
// SMS 인증 모달이 완전히 열린 뒤 인증번호 입력창에 포커스
$('#smsAuthModal').on('shown.bs.modal', function() {
$('#smsAuthCode').trigger('focus');
});
}); });
function fncSsoLogin() { function fncSsoLogin() {
@@ -387,7 +463,7 @@
<span aria-hidden="true">X</span> <span aria-hidden="true">X</span>
</button> </button>
</div> </div>
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return checkPwd()" novalidate> <form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return submitChangePassword()" novalidate>
<div class="modal-body"> <div class="modal-body">
<div id="changeInitPassword"> <div id="changeInitPassword">
<div class="form-group d-flex"> <div class="form-group d-flex">
@@ -405,7 +481,8 @@
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required> <input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
</div> </div>
<span style="font-size:0.8em; color:red"> <span style="font-size:0.8em; color:red">
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상 ※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상<br/>
※ 연속된 숫자/문자(예: 123, abc, qwer), 동일 문자 3자 이상 반복 사용 불가
</span> </span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -427,9 +504,9 @@
<p>인증번호가 <strong><span id="smsAuthMaskedPhone"></span></strong>로 발송되었습니다.</p> <p>인증번호가 <strong><span id="smsAuthMaskedPhone"></span></strong>로 발송되었습니다.</p>
<div class="form-group"> <div class="form-group">
<input type="text" id="smsAuthCode" class="form-control" <input type="text" id="smsAuthCode" class="form-control"
placeholder="인증번호 6자리" maxlength="6" placeholder="인증번호 6자리" maxlength="6" inputmode="numeric" autocomplete="off">
onkeypress="return event.charCode >= 48 && event.charCode <= 57">
</div> </div>
<p id="smsAuthTestHint" class="text-warning" style="display:none;"></p>
<p class="text-muted">남은 시간: <span id="smsAuthRemainingTime" class="text-danger font-weight-bold">60</span>초</p> <p class="text-muted">남은 시간: <span id="smsAuthRemainingTime" class="text-danger font-weight-bold">60</span>초</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
+2 -4
View File
@@ -166,10 +166,8 @@
location.href = '<%=request.getContextPath()%>/rms/logout.do'; location.href = '<%=request.getContextPath()%>/rms/logout.do';
} }
/* 상위 프레임(main.jsp) 활동 감지 */ /* 상위 프레임(main.jsp)에서 발생하는 AJAX 요청만 활동으로 기록 (마우스 이동/클릭만으로는 갱신되지 않음) */
['mousemove', 'keydown', 'click', 'touchstart'].forEach(function (e) { $(document).ajaxSend(markActivity);
document.addEventListener(e, markActivity, true);
});
/* 주기적 유휴 시간 체크 */ /* 주기적 유휴 시간 체크 */
setInterval(function () { setInterval(function () {
+31 -2
View File
@@ -557,6 +557,34 @@
}); });
// 세션(자동로그아웃) 타임아웃 카운트다운 - main.jsp의 유휴시간 판단과 동일한 sessionStorage 값을 공유한다.
(function () {
var IDLE_MS = ${autoLogoutTimeout} * 60 * 1000;
function getLastActivity() {
var t = sessionStorage.getItem('lastActivity');
return t ? parseInt(t, 10) : Date.now();
}
function formatRemain(ms) {
if (ms < 0) ms = 0;
var totalSec = Math.floor(ms / 1000);
var mm = Math.floor(totalSec / 60);
var ss = totalSec % 60;
return (mm < 10 ? '0' + mm : mm) + ':' + (ss < 10 ? '0' + ss : ss);
}
function updateSessionTimeoutTimer() {
var remain = IDLE_MS - (Date.now() - getLastActivity());
$('#sessionTimeoutTimer').text('세션 만료까지 ' + formatRemain(remain));
}
$(function () {
updateSessionTimeoutTimer();
setInterval(updateSessionTimeoutTimer, 1000);
});
})();
</script> </script>
</head> </head>
<body onload="menuRender();"> <body onload="menuRender();">
@@ -568,11 +596,12 @@
<ul> <ul>
<li style="width:240px;"> <li style="width:240px;">
<a style="width:240px; cursor: default" <a style="width:240px; cursor: default"
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>-<%=System.getProperty("inst.Name") %>)</span> href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span> <span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
<% if (!"".equals(LastLoginYms.trim())) { %> <% if (!"".equals(LastLoginYms.trim())) { %>
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%> [ <%=LastLoginIp%> ]</span> <span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%></span>
<% } %> <% } %>
<span id="sessionTimeoutTimer" style="display:block;font-size:10px;"></span>
</a></li> </a></li>
<li style="width:100px; line-height:80px; text-align:center;"> <li style="width:100px; line-height:80px; text-align:center;">
<select id="selectLocale" name="locale" style="width:60px; padding: 2px; text-align:center;"> <select id="selectLocale" name="locale" style="width:60px; padding: 2px; text-align:center;">
@@ -190,6 +190,8 @@
var key = "${param.clientId}"; var key = "${param.clientId}";
if (key != "" && key != "null") { if (key != "" && key != "null") {
isDetail = true; isDetail = true;
} else {
$('input[name="grantTypes_"]').prop('checked', true);
} }
init(key, detail); init(key, detail);
@@ -506,18 +508,17 @@
<td><input type="text" name="scope"/></td> <td><input type="text" name="scope"/></td>
</tr> </tr>
<tr> <tr>
<th><%= localeMessage.getString("clntManDtl.grntTp") %> <th><%= localeMessage.getString("clntManDtl.grntTp") %> <small>(*)</small></th>
(<%= localeMessage.getString("clntManDtl.sprt") %> ,) <small>(*)</small></th>
<td> <td>
<input type="checkbox" name="grantTypes_" value="client_credentials"> client_credentials <input type="checkbox" name="grantTypes_" value="client_credentials"> client_credentials
<%-- <input type="checkbox" name="grantTypes_" value="authorization_code"> authorization_code--%> <%-- <input type="checkbox" name="grantTypes_" value="authorization_code"> authorization_code--%>
<%-- <input type="checkbox" name="grantTypes_" value="password"> password--%> <%-- <input type="checkbox" name="grantTypes_" value="password"> password--%>
<input type="checkbox" name="grantTypes_" value="refresh_token"> refresh_token <!-- <input type="checkbox" name="grantTypes_" value="refresh_token"> refresh_token -->
</td> </td>
<input type="hidden" name="grantTypes"/> <input type="hidden" name="grantTypes"/>
</td> </td>
</tr> </tr>
<tr> <tr style="display: none;">
<th>REDIRECT URI</th> <th>REDIRECT URI</th>
<td><input type="text" name="redirectUri"/></td> <td><input type="text" name="redirectUri"/></td>
</tr> </tr>
@@ -538,7 +539,7 @@
</th> </th>
<td><input type="text" name="accessTokenValiditySeconds"/></td> <td><input type="text" name="accessTokenValiditySeconds"/></td>
</tr> </tr>
<tr> <tr style="display: none;">
<th><%= localeMessage.getString("clntManDtl.rfrshTknExprtnDt") %> <th><%= localeMessage.getString("clntManDtl.rfrshTknExprtnDt") %>
</th> </th>
<td><input type="text" name="refreshTokenValiditySeconds"/></td> <td><input type="text" name="refreshTokenValiditySeconds"/></td>
@@ -555,7 +556,7 @@
</th> </th>
<td><input type="text" name="allowedIps"/></td> <td><input type="text" name="allowedIps"/></td>
</tr> </tr>
<tr> <tr style="display: none;">
<th>Security Key</th> <th>Security Key</th>
<td><textarea name="securityKey" rows="3" style="width:100%;"></textarea></td> <td><textarea name="securityKey" rows="3" style="width:100%;"></textarea></td>
</tr> </tr>
@@ -0,0 +1,173 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/rule/layoutSyncHistoryMan.json" />';
var url_view = '<c:url value="/onl/admin/rule/layoutSyncHistoryMan.view" />';
function formatPrcssRslt(cellvalue, options, rowObject) {
if (cellvalue === 'S') return '<span style="color:#1a73e8;font-weight:bold;">성공</span>';
if (cellvalue === 'F') return '<span style="color:#d93025;font-weight:bold;">실패</span>';
return cellvalue || '';
}
function init() {
$("#startDatepicker").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#endDatepicker").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
var today = getToday();
$("input[name=searchStartYYYYMMDD]").val(today.substring(0, 6) + "01");
$("input[name=searchEndYYYYMMDD]").val(today);
$("input[name=searchStartDate]").val(today.substring(0, 6) + "01");
$("input[name=searchEndDate]").val(today);
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
putSelectFromParam();
buildGrid();
}
function buildGrid() {
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
url: url,
postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['No', '서비스명', 'Command', '연동구분명', '연동시간', '결과', '결과메세지'],
colModel: [
{name: 'seqno', align: 'center', width: 50, sortable: false},
{name: 'serviceName', align: 'center', width: 100, sortable: false},
{name: 'command', align: 'center', width: 80, sortable: false},
{name: 'loutName', align: 'left', width: 180, sortable: false},
{name: 'recvAmndHms', align: 'center', width: 140, sortable: false},
{name: 'prcssRslt', align: 'center', width: 60, sortable: false, formatter: formatPrcssRslt},
{name: 'prcssRsltCmnt', align: 'left', width: 300, sortable: false}
],
jsonReader: {repeatitems: false},
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
ondblClickRow: function (rowId) {
var rowData = $(this).getRowData(rowId);
var seqno = rowData['seqno'];
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $(this).getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
url2 += '&seqno=' + seqno;
url2 += '&' + getSearchUrl();
goNav(url2);
},
loadError: function (jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/';
comloadError(jqXHR, textStatus, errorThrown, location);
}
});
resizeJqGridWidth('grid', 'content_middle', '1000');
}
$(document).ready(function () {
init();
$("#btn_search").click(function () {
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "");
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "");
if (start && end && start > end) {
alert("조회기간 종료일은 시작일보다 커야합니다.");
$("input[name=searchEndYYYYMMDD]").focus();
return false;
}
$("input[name=searchStartDate]").val(start);
$("input[name=searchEndDate]").val(end);
var postData = getSearchForJqgrid("cmd", "LIST");
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
});
$("input[name^=search]").keydown(function (key) {
if (key.keyCode == 13) {
$("#btn_search").click();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<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">IIM 레이아웃 연동 이력과 eCams 인터페이스 배포 이력을 조회합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:120px;"><col style="width:260px;">
<col style="width:120px;"><col style="width:260px;">
<col style="width:120px;"><col style="width:260px;">
</colgroup>
<tbody>
<tr>
<th>연동시간</th>
<td>
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
~
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:100px; border:1px solid #ebebec;">
<input type="hidden" name="searchStartDate" value="">
<input type="hidden" name="searchEndDate" value="">
</th>
<th>서비스명</th>
<td><input type="text" name="searchServiceName" autocomplete="off" style="width:95%;"></td>
<th>Command</th>
<td><input type="text" name="searchCommand" autocomplete="off" style="width:95%;"></td>
</tr>
<tr>
<th>연동구분명</th>
<td><input type="text" name="searchLoutName" autocomplete="off" style="width:95%;"></td>
<th>결과</th>
<td>
<div class="select-style" style="display:inline-block; margin-left:6px;">
<select name="searchPrcssRslt">
<option value="">전체</option>
<option value="S">성공</option>
<option value="F">실패</option>
</select>
</div>
</td>
<th></th>
<td></td>
</tr>
</tbody>
</table>
</form>
<table id="grid"></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,141 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
.succ-y { color:#1a73e8; font-weight:bold; }
.succ-n { color:#d93025; font-weight:bold; }
.pre-wrap { white-space:pre-wrap; word-break:break-all; height:500px; overflow: auto; line-height:1.5; }
.recvdata-toolbar { text-align:right; margin-top: 5px; margin-bottom:6px; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/admin/rule/layoutSyncHistoryMan.json" />';
var key = '${param.seqno}';
var rawRecvData = '';
var isRecvDataPretty = false;
function detail() {
if (!key) return;
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'DETAIL', seqno: key},
success: function (data) {
$('#seqno').text(data.seqno != null ? data.seqno : '');
$('#serviceName').text(data.serviceName || '');
$('#command').text(data.command || '');
$('#loutName').text(data.loutName || '');
$('#recvAmndHms').text(data.recvAmndHms || '');
$('#prcssRsltCmnt').text(data.prcssRsltCmnt || '');
rawRecvData = data.recvData || '';
isRecvDataPretty = false;
$('#recvData').text(rawRecvData);
if (data.prcssRslt === 'S') {
$('#prcssRslt').html('<span class="succ-y">성공</span>');
} else if (data.prcssRslt === 'F') {
$('#prcssRslt').html('<span class="succ-n">실패</span>');
} else {
$('#prcssRslt').text(data.prcssRslt || '');
}
},
error: function (e) { alert(e.responseText); }
});
}
// JSON 지수표기(예: 1e+21) 값이 vkbeautify 처리 중 깨지지 않도록 문자열로 감쌌다가 되돌린다.
function preserveExponential(jsonString) {
var protectedJson = jsonString.replace(/:\s*([+-]?\d+\.?\d*e[+-]?\d+)/gi, ': "$1"');
var prettyJson = vkbeautify.json(protectedJson);
prettyJson = prettyJson.replace(/:\s*"([+-]?\d+\.?\d*e[+-]?\d+)"/gi, ': $1');
return prettyJson;
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
buttonControl();
detail();
$('#btn_previous').click(function () { goNav(returnUrl); });
$('#btn_pretty_recvdata').click(function () {
if (!rawRecvData) return;
if (isRecvDataPretty) {
$('#recvData').text(rawRecvData);
isRecvDataPretty = false;
return;
}
try {
$('#recvData').text(preserveExponential(rawRecvData));
isRecvDataPretty = true;
} catch (e) {
alert('JSON 형식이 아니거나 정렬할 수 없습니다.');
}
});
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
</div>
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title" id="title">전문레이아웃 연동 이력 상세</div>
<table class="table_row" cellspacing="0">
<colgroup>
<col style="width:12%"/><col style="width:38%"/>
<col style="width:12%"/><col style="width:38%"/>
</colgroup>
<tr>
<th>No</th>
<td><span id="seqno"></span></td>
<th>Service Name</th>
<td><span id="serviceName"></span></td>
</tr>
<tr>
<th>레이아웃명</th>
<td><span id="loutName"></span></td>
<th>Command</th>
<td><span id="command"></span></td>
</tr>
<tr>
<th>연동시간</th>
<td><span id="recvAmndHms"></span></td>
<th>결과</th>
<td><span id="prcssRslt"></span></td>
</tr>
<tr>
<th>결과메세지</th>
<td colspan="3"><span id="prcssRsltCmnt"></span></td>
</tr>
<tr>
<th>수신데이타</th>
<td colspan="3">
<div class="recvdata-toolbar">
<button type="button" class="cssbtn" id="btn_pretty_recvdata"><i class="material-icons">format_align_left</i> 정렬</button>
</div>
<div id="recvData" class="pre-wrap"></div>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
@@ -45,6 +45,9 @@
background-color: #EDD5DD background-color: #EDD5DD
} }
.btn_move { width:32px; min-width: 32px; height: 32px; background-color: #fff; border:1px solid #999; border-radius:4px; margin-bottom: 4px; }
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" src="<c:url value="/js/jsplumb.js"/>"></script> <script language="javascript" src="<c:url value="/js/jsplumb.js"/>"></script>
@@ -888,7 +891,7 @@
} }
}); });
$("input[name=auto]").click(function(){ $("#auto").click(function(){
//debugger; //debugger;
jsPlumbInstance.deleteEveryConnection(); jsPlumbInstance.deleteEveryConnection();
@@ -915,7 +918,7 @@
} }
}); });
$("input[name=addAll]").click(function() { $("#addAll").click(function() {
console.debug('addAll'); console.debug('addAll');
var srcRowId = sourceGrid.getGridParam( "selrow" ); var srcRowId = sourceGrid.getGridParam( "selrow" );
var tgtRowId = targetGrid.getGridParam( "selrow" ); var tgtRowId = targetGrid.getGridParam( "selrow" );
@@ -966,7 +969,7 @@
} }
}); });
$("input[name=removeAll]").click(function(){ $("#removeAll").click(function(){
var tgtRowId = targetGrid.getGridParam( "selrow" ); var tgtRowId = targetGrid.getGridParam( "selrow" );
if (tgtRowId == null) {alert("타겟레이아웃을 선택하여 주십시요.");return;} if (tgtRowId == null) {alert("타겟레이아웃을 선택하여 주십시요.");return;}
//grid data //grid data
@@ -986,6 +989,49 @@
} }
}); });
$("#add").click(function() {
var srcRowId = sourceGrid.getGridParam( "selrow" );
var tgtRowId = targetGrid.getGridParam( "selrow" );
if (srcRowId == null) {alert('<%=localeMessage.getString("transformManDetail.alert1")%>');return;}
if (tgtRowId == null) {alert('<%=localeMessage.getString("transformManDetail.alert2")%>');return;}
//field, ATTR 여부 판단
var sourceRow = sourceGrid.getRowData( srcRowId );
var targetRow = targetGrid.getRowData( tgtRowId );
if ( (sourceRow["loutItemType"] == 'Field' || sourceRow["loutItemType"] == 'Attr' || sourceRow["loutItemType"] == 'Array') &&
(targetRow["loutItemType"] == 'Field' || targetRow["loutItemType"] == 'Attr' || targetRow["loutItemType"] == 'Array') ){
;
}else{
alert('<%=localeMessage.getString("transformManDetail.alert3")%>');
return ;
}
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNCMDNAME',addArrayTag(sourceRow['loutItemType'], addGroupTag(srcGroup,sourceRow['LOUTITEMPATH'])));
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNRSULTITEMPATHNAME',addArrayTag(targetRow['loutItemType'], addGroupTag(tgtGroup,targetRow['LOUTITEMPATH'])));
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNITEMSERNO',targetRow['loutItemSerno']);
try {
jsPlumbConnect(srcRowId, tgtRowId);
} catch (e) {console.error(e);}
});
$("#remove").click(function(){
var tgtRowId = targetGrid.getGridParam( "selrow" );
if (tgtRowId == null) {alert("타겟레이아웃을 선택하여 주십시요.");return;}
//grid data
var targetData = targetGrid.getRowData();
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNCMDNAME', null);
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNRSULTITEMPATHNAME', null);
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNITEMSERNO', null);
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNITEMBASCVAL', null);
const targetEndpointElementId = targetGrid.getEndpointElementId(tgtRowId);
jsPlumbInstance.deleteConnectionsForElement(document.getElementById(targetEndpointElementId));
});
$("textarea[name=CNVSNRSULTITEMPATHNAME],textarea[name=CNVSNCMDNAME],input[name=CNVSNITEMBASCVAL],input[name=CNVSNITEMSERNO]").blur(function(){ $("textarea[name=CNVSNRSULTITEMPATHNAME],textarea[name=CNVSNCMDNAME],input[name=CNVSNITEMBASCVAL],input[name=CNVSNITEMSERNO]").blur(function(){
var rowId = targetGrid.getGridParam( "selrow" ); var rowId = targetGrid.getGridParam( "selrow" );
if (isNull(rowId)) return; if (isNull(rowId)) return;
@@ -2161,12 +2207,12 @@
<div id="sourcePage"></div> <div id="sourcePage"></div>
</div> </div>
</td> </td>
<td style="width:30%; text-align:center; vertical-align: top;"> <td style="width:30%; min-width:70px; text-align:center; vertical-align: top; ">
<button type=button id="remove" name="remove" class="cssbtn btn_move" title="remove"><</button>
<br /><!-- 이 br 이 없을 때 버튼이 제대로 동작 하지 않는다. --> <button type=button id="add" name="add" class="cssbtn btn_move" title="add"> > </button><br />
<input type=button id="removeAll" name="removeAll" class="btn_img btn_removeAll"/><br /> <button type=button id="removeAll" name="removeAll" class="cssbtn btn_move" title="remove all"> << </button>
<input type=button id="addAll" name="addAll" class="btn_img btn_addAll"/><br /> <button type=button id="addAll" name="addAll" class="cssbtn btn_move" title="add all"> >> </button><br />
<input type=button id="auto" name="auto" class="btn_img btn_auto" /> <button type=button id="auto" name="auto" class="cssbtn btn_move" style="width: 70px; " title="auto"> Auto </button>
</td> </td>
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
+110 -39
View File
@@ -4,11 +4,18 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %> <%@ include file="/jsp/common/include/localemessage.jsp" %>
<%@ page import="com.eactive.eai.common.util.SystemUtil"%> <%@ page import="com.eactive.eai.common.util.SystemUtil"%>
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
<% <%
response.setHeader("Pragma", "No-cache"); response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache"); response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0"); response.setHeader("Expires", "0");
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
String deployYnStr = monitoringContext.getStringProperty(MonitoringContext.IOMAP_DEPLOY_YN_BY_RMS, "N");
boolean deployYn = "Y".equalsIgnoreCase(deployYnStr);
String adminOnlyYnStr = monitoringContext.getStringProperty(MonitoringContext.IOMAP_DEPLOY_YN_BY_ADMIN_ONLY, "Y");
boolean adminOnly = "Y".equalsIgnoreCase(adminOnlyYnStr);
%> %>
<html> <html>
<head> <head>
@@ -17,61 +24,47 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <script>
var url = '<c:url value="/onl/admin/rule/transformMan.json" />'; var url = '<c:url value="/onl/admin/rule/transformMan.json" />';
var url_view = '<c:url value="/onl/admin/rule/transformMan.view" />'; var url_view = '<c:url value="/onl/admin/rule/transformMan.view" />';
var url_excel= '<c:url value="/onl/admin/rule/transformMan.excel"/>'; var url_excel= '<c:url value="/onl/admin/rule/transformMan.excel"/>';
var roleString = "<%=SessionManager.getRoleIdString(request)%>"; var roleString = "<%=SessionManager.getRoleIdString(request)%>";
var devSvr = <%=SystemUtil.isDevServer()%>; var devSvr = <%=SystemUtil.isDevServer()%>;
var stageSvr = <%=SystemUtil.isStagingServer()%>;
function dateFormater(cellvalue, options, rowObject){ var adminOnly = <%=adminOnly%>;
if (cellvalue==null|| cellvalue==undefined) return "";
if (cellvalue.trim().length < 14) return "";
var display = "";
display=display+cellvalue.substr(0,4)+"-";
display=display+cellvalue.substr(4,2)+"-";
display=display+cellvalue.substr(6,2)+" ";
display=display+cellvalue.substr(8,2)+":";
display=display+cellvalue.substr(10,2)+":";
display=display+cellvalue.substr(12,2);
return display;
}
$(document).ready(function() { $(document).ready(function() {
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST' postData : postData,
, searchBizCode : $('input[name=searchBizCode]').val()
, searchCnvsnName : $('input[name=searchCnvsnName]').val()
, searchCnvsnDesc : $('input[name=searchCnvsnDesc]').val()
},
colNames:[ colNames:[
'<%=localeMessage.getString("transformMan.eaibzwkdstcd")%>', '<%=localeMessage.getString("transformMan.eaibzwkdstcd")%>',
'<%=localeMessage.getString("transformMan.telCngMappCd")%>', '<%=localeMessage.getString("transformMan.telCngMappCd")%>',
'<%=localeMessage.getString("transformMan.telCngMappDesc")%>',
'<%=localeMessage.getString("transformMan.ifSvcCd")%>', '<%=localeMessage.getString("transformMan.ifSvcCd")%>',
'<%=localeMessage.getString("transformMan.ifSvcDesc")%>', '<%=localeMessage.getString("transformMan.ifSvcDesc")%>',
'<%=localeMessage.getString("transformMan.eaiSvrDiv")%>', '<%=localeMessage.getString("transformMan.eaiSvrDiv")%>',
'<%= localeMessage.getString("combo.useYn")%> ', '<%= localeMessage.getString("combo.useYn")%> ',
'<%=localeMessage.getString("transformMan.modifyDate")%>', '<%=localeMessage.getString("transformMan.modifyDate")%>',
'<%=localeMessage.getString("transformMan.verInfo")%>', '<%=localeMessage.getString("transformMan.verInfo")%>',
'타입', 'cnvsnType',
'flag' 'flag'
], ],
colModel:[ colModel:[
{ name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false , width : "40"}, { name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false , width:"30"},
{ name : 'CNVSNNAME' , align:'left' }, { name : 'CNVSNNAME' , align:'left' , width:"100" },
{ name : 'EAISVCNAME' , align:'left' }, { name : 'CNVSNDESC' , align:'left' , width:"100" },
{ name : 'EAISVCDESC' , align:'left' }, { name : 'EAISVCNAME' , align:'left' , width:"70" },
{ name : 'EAISVCDESC' , align:'left' , width:"100"},
{ name : 'EAISEVRDSTCD' , hidden : true }, { name : 'EAISEVRDSTCD' , hidden : true },
{ name : 'USEYN', align:'center', width:30,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"}, { name : 'USEYN', align:'center', width:30 ,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"},
{ name : 'LASTAMNDHMS' , align:'center' , formatter: dateFormater, width : "70" }, { name : 'LASTAMNDHMS' , align:'center' , width : "60" },
{ name : 'VERINFO' , align:'center' , width : "70" }, { name : 'VERINFO' , align:'center' , width : "30" },
{ name : 'CNVSNTYPE' , hidden : true }, { name : 'CNVSNTYPE' , hidden : true },
{ name : 'FLAG' , hidden : true } { name : 'FLAG' , hidden : true }
], ],
@@ -136,7 +129,8 @@ $(document).ready(function() {
//key값 //key값
url2 += '&cnvsnName='+cnvsnName; url2 += '&cnvsnName='+cnvsnName;
url2 += '&eaiSvcName='+eaiSvcName; url2 += '&eaiSvcName='+eaiSvcName;
url2 += '&eaiSvcDesc='+eaiSvcDesc; //url2 += '&eaiSvcDesc='+eaiSvcDesc;
url2 += '&eaiSvcDesc='+encodeURIComponent(eaiSvcDesc);
url2 += '&cnvsnDesc='+cnvsnDesc; url2 += '&cnvsnDesc='+cnvsnDesc;
url2 += '&eaiSevrDStcd='+eaiSevrDStcd; url2 += '&eaiSevrDStcd='+eaiSevrDStcd;
url2 += '&useYn='+useYn; url2 += '&useYn='+useYn;
@@ -161,7 +155,6 @@ $(document).ready(function() {
} }
}); });
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
@@ -217,7 +210,7 @@ $(document).ready(function() {
} }
return false; return false;
}else{ }else{
if(confirm("초기화 하시겠습니까?")){ if(confirm("<%=localeMessage.getString("eaiMessage.confirm2")%>")){
var postData = []; var postData = [];
postData.push({ name: "cmd" , value:"LIST_INITIALIZE"}); postData.push({ name: "cmd" , value:"LIST_INITIALIZE"});
postData.push({ name: "chkItems" , value : chkArr}); postData.push({ name: "chkItems" , value : chkArr});
@@ -242,8 +235,13 @@ $(document).ready(function() {
}); });
$('#btn_cmsave').click(onclickBtnCmSave); $('#btn_cmsave').click(onclickBtnCmSave);
$('#btn_clone').click(onclickBtnClone);
$("#btn_delete").click(function(){ $("#btn_delete").click(function(){
if(!confirm("삭제하시겠습니까?")){
return;
}
var id = "grid"; var id = "grid";
var cnvsnNameArr = new Array(); var cnvsnNameArr = new Array();
var eaiSvcNameArr = new Array(); var eaiSvcNameArr = new Array();
@@ -263,7 +261,7 @@ $(document).ready(function() {
} }
if(chkcnt == 0){ if(chkcnt == 0){
alert("변환매핑을 선택해 주세요"); alert("<%= localeMessage.getString("standardLayout.message4") %>");
return false; return false;
} }
@@ -303,10 +301,18 @@ $(document).ready(function() {
}); });
buttonControl(); buttonControl();
setBtnHide(roleString, "admin", "btn_initialize"); setBtnHide(roleString, "admin", "btn_initialize");
//setBtnControl(roleString, "admin", "btn_cmsave");
if(!devSvr){ if(devSvr || stageSvr){
$("#btn_cmsave").hide(); if (adminOnly) {
setBtnHide(roleString, "admin", "btn_clone");
} }
}else{
$("#btn_clone").hide();
}
//setBtnControl(roleString, "admin", "btn_cmsave");
//if(!devSvr){
// $("#btn_cmsave").hide();
//}
}); });
function setBtnControl(roleString, condiVal, compoId) function setBtnControl(roleString, condiVal, compoId)
@@ -430,6 +436,62 @@ function addCMResource(eaiBzwkDsdCd, obj, cnvsnNames) {
}); });
} }
function onclickBtnClone() {
var sel = $('#grid').jqGrid('getGridParam', 'selarrrow');
if (sel.length == 0) {
alert('<%= localeMessage.getString("eaiMessage.alert17")%>');
return;
}
var selectedTransformNames = [];
$(sel).each(function(index, i) {
var cnvsnName = $('#grid').getCell(i, 'CNVSNNAME');
selectedTransformNames.push(cnvsnName);
});
var url2 = url_view;
url2 += "?cmd=POPUP CALENDAR";
var args = new Object();
args['selectedCnvsnName'] = selectedTransformNames.join(',');
showModal(url2,args,540,270, function(arg){
var args = null;
if(arg == null || arg == undefined ) {
//chrome
args = this.dialogArguments;
args.returnValue = this.returnValue;
} else {
//ie
args = arg;
}
if( !args || !args.returnValue ) return;
deployTransforms(args.returnValue);
});
}
function deployTransforms(ret) {
var params = ret;
params['cmd'] = 'CLONETOPROD';
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:params,
success:function(json){
alert(json.message);
list($("#grid").getGridParam("page"));
},
error:function(xhr,status,error){
if (xhr.responseText != null) {
try {
alert(JSON.parse(xhr.responseText).errorMsg);
} catch (e) {
alert(error);
}
}
}
});
}
</script> </script>
</head> </head>
<body> <body>
@@ -441,6 +503,11 @@ function addCMResource(eaiBzwkDsdCd, obj, cnvsnNames) {
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle" id="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<% if (SystemUtil.isDevServer() && deployYn) { %>
<button type="button" class="cssbtn" id="btn_clone" level="W"><i class="material-icons">move_up</i> <%= localeMessage.getString("button.copyStage") %></button>
<% } else if (SystemUtil.isStagingServer() && deployYn) { %>
<button type="button" class="cssbtn" id="btn_clone" level="W"><i class="material-icons">move_up</i> <%= localeMessage.getString("button.copyProd") %></button>
<% } %>
<button type="button" class="cssbtn" id="btn_excel" level="W" status="DETAIL"><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button> <button type="button" class="cssbtn" id="btn_excel" level="W" status="DETAIL"><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button>
<button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button> <button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button>
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL,NEW"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL,NEW"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
@@ -469,9 +536,13 @@ function addCMResource(eaiBzwkDsdCd, obj, cnvsnNames) {
</tr> </tr>
<tr> <tr>
<th style="width:180px;"><%=localeMessage.getString("transformMan.telCngMappDesc")%> </th> <th style="width:180px;"><%=localeMessage.getString("transformMan.telCngMappDesc")%> </th>
<td colspan="3"> <td>
<input type="text" name="searchCnvsnDesc" value="${param.searchCnvsnDesc}"> <input type="text" name="searchCnvsnDesc" value="${param.searchCnvsnDesc}">
</td> </td>
<th style="width:180px;"><%=localeMessage.getString("transformMan.ifSvcCd")%> </th>
<td>
<input type="text" name="searchIfSvcCd" value="${param.searchIfSvcCd}">
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -0,0 +1,258 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
var url = '<c:url value="/onl/transaction/apim/apiInterfaceMan.json"/>';
var url_view = '<c:url value="/onl/transaction/apim/apiInterfaceMan.view"/>';
var selectName = "searchEaiBzwkDstcd"; // selectBox Name
function processSelectedData() {
var grid = $("#grid");
var selectedRowIds = grid.jqGrid('getGridParam', 'selarrrow');
var selectedData = [];
for(var i = 0; i < selectedRowIds.length; i++) {
var rowData = grid.jqGrid('getRowData', selectedRowIds[i]);
selectedData.push({
bizCode: rowData.eaiBzwkDstcd, // CLINET
apiId: rowData.eaiSvcName,
apiDesc: rowData.eaiSvcDesc, // SCOPE-API
APIFULLPATH: rowData.apiFullPath,
BZWKSVCKEYNAME: rowData.bzwksvckeyname
});
}
if(selectedData.length === 0) {
alert("선택된 항목이 없습니다.");
return;
}
alert("선택한 " + selectedData.length + "건의 API를 추가합니다.");
window.returnValue = selectedData.length === 1 ? selectedData[0] : selectedData;
window.close();
}
function updateSelectionInfo(gridId) {
var grid = $(gridId || "#grid");
var selectedRows = grid.jqGrid('getGridParam', 'selarrrow');
$("#selected-count").text("총: " + selectedRows.length + " 건");
}
function init( callback) {
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_COMBO'},
success:function(json){
new makeOptions("BIZCODE","BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("","전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
setSearchable(selectName); // 콤보에 searchable 설정
if (typeof callback === 'function') {
callback();
}
},
error:function(e){
alert(e.responseText);
}
});
}
function detail(){
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
$("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid");
}
function search(){
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
}
function ioFormatter(cellvalue,options,rowObject){
var serviceType = sessionStorage["serviceType"];
if(cellvalue == "I"){
return "타발";
}else if(cellvalue =="O"){
return "당발";
}
}
function adapterNameShortFormatter(cellvalue,options,rowObject){
if(cellvalue == null || cellvalue == '')
return ''
return cellvalue.substring(1, 4);
}
function list(){
detail()
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
// gridPostData.searchApiEnableYn = 'Y';
var urlParams = new URLSearchParams(window.location.search);
var selectedApiIds = urlParams.get("selectedApiIds");
$('#grid').jqGrid({
datatype : "json",
mtype : 'POST',
url : url,
postData : gridPostData,
colNames : [ '업무구분',
'<%= localeMessage.getString("eaiMessage.eaiSvcName")%>',
'<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%>',
'API FULL PATH',
'업무서비스명',
'요청',
'응답',
'작성자',
'가상응답여부',
'SyncAsyncType',
],
colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false},
{ name : 'eaiSvcName' , align : 'left' , width:'100'},
{ name : 'eaiSvcDesc' , align : 'left' },
{ name : 'apiFullPath' , align : 'left' , width:'100'},
{ name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true},
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
{ name : 'author' , align : 'center' , width:'60' },
{ name : 'simYn' , align : 'center' , width:'40' },
{ name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true},
],
jsonReader : {
repeatitems : false
},
pager : $('#pager'),
page : '${param.page}',
rowNum : '${rmsDefaultRowNum}',
autoheight : true,
height : $("#container").height(),
autowidth : true,
viewrecords : true,
multiselect: false,
multiboxonly: false,
rowList : eval('[${rmsDefaultRowList}]'),
loadComplete:function (d){
var $grid = $(this);
if(selectedApiIds) {
const selectedIds = selectedApiIds.split(',');
$.each($grid.getDataIDs(), function(_, id) {
var rowData = $grid.getRowData(id);
if(selectedIds.includes(rowData.eaiSvcName)) {
$grid.jqGrid('setSelection', id, true);
}
});
}
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
updateSelectionInfo("#grid");
},
onSelectRow: function(rowid, status, e) {
updateSelectionInfo("#grid");
},
onSelectAll: function(rowids, status) {
updateSelectionInfo("#grid");
},
ondblClickRow : function(rowId) {
const rowData = $(this).getRowData(rowId);
const returnValue = {
bizCode: rowData.eaiBzwkDstcd,
apiId: rowData.eaiSvcName,
apiDesc: rowData.eaiSvcDesc
};
//alert("선택한 API를 추가합니다.\nAPI ID: " + rowData.eaiSvcName);
window.returnValue = returnValue;
window.close();
}
});
}
$(document).ready(function() {
init(list);
resizeJqGridWidth('grid','content_middle','1000');
// 업무구분명 선택 시 자동 검색
$("select[name=searchEaiBzwkDstcd]").change(function() {
search();
});
$("#btn_search").click(function(){
search();
});
$("input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
}
});
buttonControl();
$("#btn_select").click(function(){
processSelectedData();
});
$("#btn_close").click(function () {
window.close();
});
});
</script>
</head>
<body>
<div>
<div class="content_middle" id="content_middle" style="margin-top: 0px">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
<button type="button" class="cssbtn" id="btn_select" level="R" ><i class="material-icons">check</i> <%= localeMessage.getString("button.check") %></button>
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i class="material-icons">close</i> <%= localeMessage.getString("button.close") %></button>
</div>
<div class="title" id="title" >${rmsMenuName}</div>
<!-- 선택 정보 표시 영역 추가 -->
<div style="margin-bottom: 5px;">
<span id="selected-count" style="color: #666; font-size: 12px;"></span>
</div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
<th style="width:180px;">업무구분명</th>
<td>
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}">
</select>
</td>
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th>
<td>
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
</td>
</tr>
<tr>
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
<td>
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
</td>
<th style="width:180px;">API FULL PATH</th>
<td>
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
</td>
</tr>
</tbody>
</table>
<table id="grid" ></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -46,10 +46,10 @@ $(document).ready(function() {
, src_layout : src_layout , src_layout : src_layout
}, },
colNames:[ colNames:[
'<%=localeMessage.getString("tranManDtPop.conRsItPtNm")%>', '변환결과 항목경로명',
'<%=localeMessage.getString("tranManDtPop.conOrd")%>', '변환명령',
'<%=localeMessage.getString("tranManDtPop.ItDft")%>', '항목 기본값',
'<%=localeMessage.getString("tranManDtPop.itSrlNmb")%>', '항목 일련번호',
'타겟Flag', '타겟Flag',
'소스Flag', '소스Flag',
'<%=localeMessage.getString("tranManDtPop.errMsg")%>' '<%=localeMessage.getString("tranManDtPop.errMsg")%>'
@@ -104,7 +104,6 @@ $(document).ready(function() {
resizeJqGridWidth('grid','title','1000'); resizeJqGridWidth('grid','title','1000');
$("#btn_close").click(function(){ $("#btn_close").click(function(){
//debugger;
window.close(); window.close();
}); });
$("body").css("overflow-x","hidden"); $("body").css("overflow-x","hidden");
@@ -115,13 +114,12 @@ $(document).ready(function() {
<body> <body>
<div class="popup_box" id="title"> <div class="popup_box" id="title">
<div class="search_wrap"> <div class="search_wrap">
<%-- <img id="btn_close" src="<c:url value="/img/btn_close.png"/>" level="R"/> --%> <img id="btn_close" src="<c:url value="/img/btn_close.png"/>" level="R"/>
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %></button>
</div> </div>
<div class="title"><%=localeMessage.getString("tranManDtPop.title")%></div> <div class="title">변환매핑항목 상세</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
<tr> <tr>
<th style="width:180px;"><%=localeMessage.getString("tranManDtPop.conMapCd")%></th> <th style="width:180px;">변환매핑 코드</th>
<td> <td>
<input type="text" name="cnvsnName" readonly> <input type="text" name="cnvsnName" readonly>
</td> </td>
@@ -20,6 +20,7 @@
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <script language="javascript" >
var url_view = '<c:url value="/onl/admin/rule/transformMan.view" />';
var $ = jQuery.noConflict(); var $ = jQuery.noConflict();
var bzwkDstCd = ""; var bzwkDstCd = "";
@@ -81,8 +82,9 @@ $(document).ready(function() {
var key = ""; var key = "";
var args = new Object(); var args = new Object();
args['eaiSvcName'] = $('input[name=eaiSvcName]').val(); args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
var url='<c:url value="/onl/transaction/extnl/interfaceMan.view"/>'; var url=url_view; //'<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
url = url + "?cmd=POPUP"; url = url + "?cmd=API_POPUP";
console.log('[new]', url);
var ret = showModal(url,args,1020,630, function(arg){ var ret = showModal(url,args,1020,630, function(arg){
var args = null; var args = null;
if(arg == null || arg == undefined ) {//chrome if(arg == null || arg == undefined ) {//chrome
@@ -96,15 +98,15 @@ $(document).ready(function() {
var ret = args.returnValue; var ret = args.returnValue;
console.log("ret",ret); console.log("ret",ret);
key = ret['key']; key = ret['apiId'];
$("input[name=eaiSvcName]").val(key); $("input[name=eaiSvcName]").val(key);
$("input[name=eaiSvcDesc]").val(ret['eaiSvcDesc']); $("input[name=eaiSvcDesc]").val(ret['apiDesc']);
bzwkDstCd = ret['eaiBzwkDstCd']; bzwkDstCd = ret['bizCode'];
$("input[name=eaiSvcName]").change(); $("input[name=eaiSvcName]").change();
$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']); //$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']);
$("select[name=eaiSevrDstcd]").attr("disabled",true); //$("select[name=eaiSevrDstcd]").attr("disabled",true);
}); });
}); });
@@ -159,25 +161,29 @@ $(document).ready(function() {
<form id="ajaxForm"> <form id="ajaxForm">
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<tr> <tr>
<th style="width:25%;"><%=localeMessage.getString("transformMan.ifSvcCd")%> </th> <th style="width:14%; min-width:130px;"><%=localeMessage.getString("transformMan.ifSvcCd")%> </th>
<td colspan="3"> <td style="width:31%;">
<input type="text" name="eaiSvcName" style="width:calc(100% - 60px);" readonly> <%-- <img id="btn_search" src="<c:url value="/img/btn_pop_search.png" />" level="R" class="btn_img" /> --%> <input type="text" name="eaiSvcName" style="width:60%;" readonly> <!-- <img id="btn_search" src="<c:url value="/img/btn_pop_search.png" />" level="R" class="btn_img" /> -->
<button type="button" class="cssbtn smallBtn2" id="btn_search" style="vertical-align:middle; font-weight:bold;" level="R"><i class="material-icons">search</i><%= localeMessage.getString("button.search") %></button> <button type="button" class="cssbtn smallBtn2" id="btn_search" style="vertical-align:middle; font-weight:bold;" level="R"><i class="material-icons">search</i><%= localeMessage.getString("button.search") %></button>
</td> </td>
</tr> <th style="width:14%; min-width:130px;"><%=localeMessage.getString("transformMan.ifSvcDesc")%></th>
<tr> <td style="width:41%;">
<th><%=localeMessage.getString("transformMan.ifSvcDesc")%></th>
<td colspan="3">
<input type="text" name="eaiSvcDesc" readOnly> <input type="text" name="eaiSvcDesc" readOnly>
</td> </td>
</tr> </tr>
<tr> <tr>
<th><%=localeMessage.getString("transformMan.eaiSvrDiv")%></th> <th><%=localeMessage.getString("transformMan.eaiSvrDiv")%></th>
<td colspan="3"> <td>
<div class="select-style"> <div class="select-style">
<select name="eaiSevrDstcd" /> <select name="eaiSevrDstcd" />
</div> </div>
</td> </td>
<th style="width:13%; min-width:130px;"><%=localeMessage.getString("combo.useYn")%></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr> </tr>
<tr> <tr>
<th><%=localeMessage.getString("transformMan.cnvsnType")%></th> <th><%=localeMessage.getString("transformMan.cnvsnType")%></th>
@@ -186,20 +192,13 @@ $(document).ready(function() {
<select name="cnvsnType" /> <select name="cnvsnType" />
</div> </div>
</td> </td>
<th><%=localeMessage.getString("combo.useYn")%></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
<tr>
<th><%=localeMessage.getString("transformMan.telCngMappCd")%></th> <th><%=localeMessage.getString("transformMan.telCngMappCd")%></th>
<td colspan="3"><input type="text" name="cnvsnName" readonly="readonly"/></td> <td><input type="text" name="cnvsnName" readonly="readonly"/></td>
</tr> </tr>
<tr height="100px"> <tr>
<th><%=localeMessage.getString("transformMan.cnvsnDesc")%></th><td colspan="3"><textarea name="cnvsnDesc" style="width:100%;height:100px"></textarea></td> <th><%=localeMessage.getString("transformMan.cnvsnDesc")%></th><td colspan="3"><textarea name="cnvsnDesc" style="width:100%;height:50px"></textarea></td>
</tr> </tr>
</table> </table>
</form> </form>
+12 -24
View File
@@ -47,9 +47,6 @@ function init(callback){
dataType:"json", dataType:"json",
data:{cmd: 'LIST_INIT', codeGroupArr : 'SVC_SAME_TIME_TYPE,<%= tranTypeCode %>'}, data:{cmd: 'LIST_INIT', codeGroupArr : 'SVC_SAME_TIME_TYPE,<%= tranTypeCode %>'},
success:function(json){ success:function(json){
new makeOptions("CODE","NAME").setObj($("select[name=searchSvcMotivUsedStcd]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].SVC_SAME_TIME_TYPE).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=searchPsvintfacdsticName]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].SVC_SAME_TIME_TYPE).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=searchFlowControl]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.listFlowControl).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=searchTranType]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].<%= tranTypeCode %>).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=searchTranType]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].<%= tranTypeCode %>).rendering();
svrInst = json.listInoutType; svrInst = json.listInoutType;
putSelectFromParam(); putSelectFromParam();
@@ -89,8 +86,6 @@ $(document).ready(function() {
gridPostData["rmsMenuAuth"]= auth; gridPostData["rmsMenuAuth"]= auth;
init(detail); init(detail);
gridPostData["searchSvcMotivUsedStcd"] = '${param.searchSvcMotivUsedStcd}';
gridPostData["searchPsvintfacdsticName"] = '${param.searchPsvintfacdsticName}';
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
@@ -100,10 +95,8 @@ $(document).ready(function() {
colNames:['<%= localeMessage.getString("eaiMsg.bizCode")%> ', colNames:['<%= localeMessage.getString("eaiMsg.bizCode")%> ',
'<%= localeMessage.getString("eaiMessage.eaiSvcName")%> ', '<%= localeMessage.getString("eaiMessage.eaiSvcName")%> ',
'<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%> ', '<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%> ',
'<%= localeMessage.getString("eaiMessage.svcMotivUsedStcd")%>', '<%= localeMessage.getString("eaiMessage.gstatSysAdptr")%>',
'<%= localeMessage.getString("eaiMessage.psvintfacdsticName")%>', '<%= localeMessage.getString("eaiMessage.psvSysAdptr")%>',
'<%= localeMessage.getString("eaiMessage.queueName")%> ',
'<%= localeMessage.getString("eaiMessage.flowCtrlRoutName")%> ',
'<%= localeMessage.getString("eaiMessage.tranType")%> ', '<%= localeMessage.getString("eaiMessage.tranType")%> ',
'<%= localeMessage.getString("combo.useYn")%> ', '<%= localeMessage.getString("combo.useYn")%> ',
'<%= localeMessage.getString("eaiMessage.sevrDstCd")%>', '<%= localeMessage.getString("eaiMessage.sevrDstCd")%>',
@@ -113,10 +106,8 @@ $(document).ready(function() {
{ name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false, width:20 }, { name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false, width:20 },
{ name : 'EAISVCNAME' , align:'left', width:50 }, { name : 'EAISVCNAME' , align:'left', width:50 },
{ name : 'EAISVCDESC' , align:'left' }, { name : 'EAISVCDESC' , align:'left' },
{ name : 'SVCMOTIVUSEDSTCD', align:'center', width:50 }, { name : 'GSTATSYSADPTRBZWKGROUPNAME', align:'left', width:80 },
{ name : 'PSVINTFACDSTICNAME', align:'center', width:50 }, { name : 'PSVSYSADPTRBZWKGROUPNAME', align:'left', width:80 },
{ name : 'NONMOTIVROUTURINAME' , align:'left', width:90 },
{ name : 'FLOWCTRLROUTNAME' , align:'center', width:40 },
{ name : 'TRANTYPE', align:'center', width:30 }, { name : 'TRANTYPE', align:'center', width:30 },
{ name : 'USEYN', align:'center', width:30,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"}, { name : 'USEYN', align:'center', width:30,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"},
{ name : 'EAISEVRDSTCD', align:'center', width:30 , formatter:formatter, hidden:true}, { name : 'EAISEVRDSTCD', align:'center', width:30 , formatter:formatter, hidden:true},
@@ -426,28 +417,25 @@ function deployInterfaces(ret) {
<td> <td>
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}"> <input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
</td> </td>
<th style="width:10%; min-width:120px;"><%= localeMessage.getString("eaiMessage.svcMotivUsedStcd")%></th>
<td><div class="select-style"><select name="searchSvcMotivUsedStcd" value="${param.searchSvcMotivUsedStcd}"></select></div></td>
<th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.tranType")%></th>
<td><div class="select-style"><select name="searchTranType" value="${param.searchTranType}"></select></div></td>
<th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th> <th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
<td> <td>
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}"> <input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
</td> </td>
<th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.tranType")%></th>
<td><div class="select-style"><select name="searchTranType" value="${param.searchTranType}"></select></div></td>
</tr> </tr>
<tr> <tr>
<th><%= localeMessage.getString("eaiMessage.searchBzwkDstCd")%></th> <th><%= localeMessage.getString("eaiMessage.searchBzwkDstCd")%></th>
<td> <td>
<input type="text" name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"> <input type="text" name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}">
</td> </td>
<th><%= localeMessage.getString("eaiMessage.psvintfacdsticName")%></th> <th><%= localeMessage.getString("eaiMessage.gstatSysAdptr")%></th>
<td><div class="select-style"><select name="searchPsvintfacdsticName" value="${param.searchPsvintfacdsticName}"></select></div></td>
<th><%= localeMessage.getString("eaiMessage.searchFlowControl")%></th>
<td><div class="select-style"><select name="searchFlowControl" value="${param.searchFlowControl}"></select></div></td>
<th><%= localeMessage.getString("eaiMessage.queueName")%></th>
<td> <td>
<input type="text" name="searchQueueName" value="${param.searchQueueName}"> <input type="text" name="searchGstatSysAdptrBzwkGroupName" value="${param.searchGstatSysAdptrBzwkGroupName}">
</td>
<th><%= localeMessage.getString("eaiMessage.psvSysAdptr")%></th>
<td>
<input type="text" name="searchPsvSysAdptrBzwkGroupName" value="${param.searchPsvSysAdptrBzwkGroupName}">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -72,13 +72,22 @@
}); });
} }
function detail(){ // 체크박스는 name 을 search* 로 두면 getSearchForJqgrid 가 해제 상태에서도 value 를 담아버린다
// (input[type!=radio][name^=search] 를 checked 여부와 무관하게 수집) → 여기서 직접 붙인다
function getPopupSearchData() {
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
postData.searchApiSpecYn = $("#chkApiSpec").is(":checked") ? "Y" : "";
postData.searchPortalDisplayYn = $("#chkPortalDisplay").is(":checked") ? "Y" : "";
return postData;
}
function detail(){
var postData = getPopupSearchData();
$("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid"); $("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid");
} }
function search(){ function search(){
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var postData = getPopupSearchData();
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid"); $("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
} }
function ioFormatter(cellvalue,options,rowObject){ function ioFormatter(cellvalue,options,rowObject){
@@ -97,7 +106,7 @@
function list(){ function list(){
detail() detail()
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var gridPostData = getPopupSearchData();
// gridPostData.searchApiEnableYn = 'Y'; // gridPostData.searchApiEnableYn = 'Y';
var urlParams = new URLSearchParams(window.location.search); var urlParams = new URLSearchParams(window.location.search);
var selectedApiIds = urlParams.get("selectedApiIds"); var selectedApiIds = urlParams.get("selectedApiIds");
@@ -116,21 +125,27 @@
'작성자', '작성자',
'가상응답여부', '가상응답여부',
'SyncAsyncType', 'SyncAsyncType',
'변경일시',
], ],
colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false}, colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false},
{ name : 'eaiSvcName' , align : 'left' , width:'100'}, { name : 'eaiSvcName' , align : 'left' , width:'100', sortable:true},
{ name : 'eaiSvcDesc' , align : 'left' }, { name : 'eaiSvcDesc' , align : 'left' , sortable:false},
{ name : 'apiFullPath' , align : 'left' , width:'100'}, { name : 'apiFullPath' , align : 'left' , width:'100', sortable:false},
{ name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true}, { name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true},
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter }, { name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter, sortable:false },
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter }, { name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter, sortable:false },
{ name : 'author' , align : 'center' , width:'60' }, { name : 'author' , align : 'center' , width:'60', sortable:false },
{ name : 'simYn' , align : 'center' , width:'40' }, { name : 'simYn' , align : 'center' , width:'40', sortable:false },
{ name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true}, { name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true},
{ name : 'lastModifiedDate' , align : 'center' , width:'130', sortable:true },
], ],
jsonReader : { jsonReader : {
repeatitems : false repeatitems : false
}, },
// 서버(ApiInterfaceController#selectList)가 sidx/sord 대신 sortName/sortOrder 파라미터로 정렬조건을 받는다
prmNames : { sort : 'sortName', order : 'sortOrder' },
sortname : 'lastModifiedDate',
sortorder : 'desc',
pager : $('#pager'), pager : $('#pager'),
page : '${param.page}', page : '${param.page}',
rowNum : '${rmsDefaultRowNum}', rowNum : '${rmsDefaultRowNum}',
@@ -152,10 +167,6 @@
} }
}); });
} }
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
updateSelectionInfo("#grid"); updateSelectionInfo("#grid");
}, },
onSelectRow: function(rowid, status, e) { onSelectRow: function(rowid, status, e) {
@@ -189,6 +200,9 @@
$("select[name=searchEaiBzwkDstcd]").change(function() { $("select[name=searchEaiBzwkDstcd]").change(function() {
search(); search();
}); });
$("#chkApiSpec, #chkPortalDisplay").change(function() {
search();
});
$("#btn_search").click(function(){ $("#btn_search").click(function(){
search(); search();
}); });
@@ -247,6 +261,18 @@
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}"> <input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
</td> </td>
</tr> </tr>
<tr>
<th style="width:180px;">포탈 노출</th>
<td colspan="3">
<label class="checkbox-inline">
<input type="checkbox" id="chkApiSpec" <c:if test="${param.searchApiSpecYn eq 'Y'}">checked</c:if>> OpenAPI Spec 등록
</label>
&nbsp;&nbsp;
<label class="checkbox-inline">
<input type="checkbox" id="chkPortalDisplay" <c:if test="${param.searchPortalDisplayYn eq 'Y'}">checked</c:if>> 포탈 게시
</label>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<table id="grid" ></table> <table id="grid" ></table>
@@ -176,13 +176,65 @@
} }
}); });
$("#btn_app_delete").click(function () {
if (!confirm("이 승인 요청을 삭제하시겠습니까?\n삭제 후에는 복구할 수 없습니다.")) {
return;
}
$.ajax({
type: "POST",
url: url,
data: {cmd: "DELETE", id: key},
success: function (args) {
alert("삭제되었습니다.");
if (window.dialogArguments && window.dialogArguments.self) {
window.dialogArguments.self.location.reload();
} else if (window.opener) {
window.opener.location.reload();
}
window.close();
},
error: function (e) {
alert(approvalErrorMessage(e));
}
});
});
$("#btn_close").click(function () { $("#btn_close").click(function () {
window.close(); window.close();
}); });
}); });
function approvalErrorMessage(e) {
try {
var json = JSON.parse(e.responseText);
return json.errorMsg || e.responseText;
} catch (ex) {
return e.responseText;
}
}
// 요청자(PTL_USER) row 가 삭제되면 상세 조회 자체가 불가능하다. 이 경우 삭제만 가능하도록 노출한다.
function showLoadFailure(message) {
$("#approvalSubject").text("상세 정보를 조회할 수 없는 승인 요청");
$("#approvalDetail").text(message);
$("#btn_modify, #btn_app_approve, #btn_app_reject, #btn_app_force_reject, #btn_app_redeploy").hide();
$("#gwActionSection, #gwActionResult").hide();
document.getElementById("btn_app_delete").style.display = "";
}
var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용) var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용)
// 값이 있을 때만 행을 노출한다. (해지 신청처럼 해당 항목이 없는 유형에서 빈 행이 남지 않도록)
function setOptionalRow(rowId, spanId, value) {
if (value) {
$("#" + spanId).text(value);
$("#" + rowId).show();
} else {
$("#" + spanId).text("");
$("#" + rowId).hide();
}
}
function detail(key) { function detail(key) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
@@ -264,6 +316,21 @@
$("#clientName").text(data.clientName); $("#clientName").text(data.clientName);
// 신청서에 담긴 Client 등록 정보(승인 시 GW 클라이언트로 반영되는 값). 값이 있을 때만 노출
setOptionalRow("appDescriptionRow", "appDescription", data.appDescription);
setOptionalRow("ipWhitelistRow", "ipWhitelist", data.ipWhitelist);
setOptionalRow("callbackUrlRow", "callbackUrl", data.callbackUrl);
// 신청 사유. 해지 신청은 사유 입력이 필수이므로 "해지 사유"로 표기하고, 그 외 유형은 "신청 사유"
if (data.requestReason) {
$("#requestReasonLabel").text(data.requestType === 'DELETE' ? "해지 사유" : "신청 사유");
$("#requestReason").text(data.requestReason);
$("#requestReasonRow").show();
} else {
$("#requestReason").text("");
$("#requestReasonRow").hide();
}
var apiServiceHtml = ""; var apiServiceHtml = "";
for (var i = 0; i < data.apiServices.length; i++) { for (var i = 0; i < data.apiServices.length; i++) {
var apiService = data.apiServices[i]; var apiService = data.apiServices[i];
@@ -318,7 +385,9 @@
$("#apisTable tbody").html(apisHtml); $("#apisTable tbody").html(apisHtml);
}, },
error: function (e) { error: function (e) {
alert(e.responseText); var message = approvalErrorMessage(e);
alert(message);
showLoadFailure(message);
} }
}); });
} }
@@ -365,6 +434,22 @@
<th style="width:20%;">이름</th> <th style="width:20%;">이름</th>
<td><span id="clientName"></span></td> <td><span id="clientName"></span></td>
</tr> </tr>
<tr id="appDescriptionRow" style="display:none;">
<th style="width:20%;">앱 설명</th>
<td><span id="appDescription" style="white-space: pre-wrap;"></span></td>
</tr>
<tr id="ipWhitelistRow" style="display:none;">
<th style="width:20%;">IP 화이트리스트</th>
<td><span id="ipWhitelist" style="white-space: pre-wrap;"></span></td>
</tr>
<tr id="callbackUrlRow" style="display:none;">
<th style="width:20%;">Callback URL</th>
<td><span id="callbackUrl" style="white-space: pre-wrap;"></span></td>
</tr>
<tr id="requestReasonRow" style="display:none;">
<th style="width:20%;"><span id="requestReasonLabel">신청 사유</span></th>
<td><span id="requestReason" style="white-space: pre-wrap;"></span></td>
</tr>
</table> </table>
<div class="title">API 목록</div> <div class="title">API 목록</div>
<table id="apisTable" class="table_row" cellspacing="0"> <table id="apisTable" class="table_row" cellspacing="0">
@@ -412,6 +497,9 @@
<button type="button" class="cssbtn" style="display: none;" id="btn_app_force_reject" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_force_reject" level="W"><i
class="material-icons">block</i>강제 반려 class="material-icons">block</i>강제 반려
</button> </button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_delete" level="W"><i
class="material-icons">delete</i>삭제
</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_redeploy" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_redeploy" level="W"><i
class="material-icons">close</i>재처리 class="material-icons">close</i>재처리
</button> </button>
@@ -181,7 +181,7 @@
}); });
$("#btn_user_delete").click(function() { $("#btn_user_delete").click(function() {
if (!confirm("삭제된 사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?")) { if (!confirm("삭제된 요청자·사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?\n삭제 후에는 복구할 수 없습니다.")) {
return; return;
} }
$.ajax({ $.ajax({
@@ -198,7 +198,7 @@
window.close(); window.close();
}, },
error: function(e) { error: function(e) {
alert(e.responseText); alert(approvalErrorMessage(e));
} }
}); });
}); });
@@ -362,10 +362,31 @@
} }
}, },
error: function(e) { error: function(e) {
alert(e.responseText); var message = approvalErrorMessage(e);
alert(message);
showLoadFailure(message);
} }
}); });
} }
function approvalErrorMessage(e) {
try {
var json = JSON.parse(e.responseText);
return json.errorMsg || e.responseText;
} catch (ex) {
return e.responseText;
}
}
// 요청자(PTL_USER) row 가 삭제되면 상세 조회 자체가 불가능하다. 이 경우 삭제만 가능하도록 노출한다.
function showLoadFailure(message) {
$("#approvalSubject").text("상세 정보를 조회할 수 없는 승인 요청");
$("#approvalDetail").text(message);
$("#approvalDetailRow").show();
$("#btn_unmask").hide();
$("#btn_user_approve, #btn_user_reject, #btn_user_force_reject, #btn_user_redeploy").hide();
document.getElementById("btn_user_delete").style.display = "";
}
function updateDetailView(data) { function updateDetailView(data) {
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용"); $("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
$("#approvalStatus").text(data.approvalStatus.description); $("#approvalStatus").text(data.approvalStatus.description);
@@ -62,7 +62,19 @@
return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>'; return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>';
} }
function toDashDate(yyyymmdd) {
return yyyymmdd.substring(0, 4) + "-" + yyyymmdd.substring(4, 6) + "-" + yyyymmdd.substring(6, 8);
}
function init() { function init() {
$("#startDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
$("#endDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
var today = getToday();
$("input[name=searchFromDate]").val(toDashDate(today.substring(0,6)+"01"));
$("input[name=searchToDate]").val(toDashDate(today));
$.ajax({ $.ajax({
type: "POST", url: url, dataType: "json", type: "POST", url: url, dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'}, data: {cmd: 'LIST_INIT_COMBO'},
@@ -145,6 +157,20 @@
init(); init();
$("#btn_search").click(function () { $("#btn_search").click(function () {
var start = $("input[name=searchFromDate]").val().replace(/-/gi, "");
var end = $("input[name=searchToDate]").val().replace(/-/gi, "");
if (start && start > getToday()) {
alert("시작일이 오늘 이후입니다. 시작일을 확인해주세요.");
$("input[name=searchFromDate]").focus();
return false;
}
if (start && end && start > end) {
alert("조회기간 종료일은 시작일보다 커야합니다.");
$("input[name=searchToDate]").focus();
return false;
}
var postData = getSearchForJqgrid("cmd", "LIST"); var postData = getSearchForJqgrid("cmd", "LIST");
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid"); $("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
}); });
@@ -198,7 +224,9 @@
</td> </td>
<th>요청기간</th> <th>요청기간</th>
<td colspan="3"> <td colspan="3">
<input type="date" name="searchFromDate"> ~ <input type="date" name="searchToDate"> <input type="text" name="searchFromDate" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
~
<input type="text" name="searchToDate" value="" id="endDatepicker" size="10" readonly="readonly" style="width:100px; border:1px solid #ebebec;">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -189,7 +189,7 @@
<td> <td>
<input type="text" id="keywordList" style="width:100%" placeholder="쉼표(,)로 구분하여 입력"> <input type="text" id="keywordList" style="width:100%" placeholder="쉼표(,)로 구분하여 입력">
<div class="help-text" style="color:#666; font-size:12px; margin-top:5px;"> <div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">
예시: #인증, #대출, #예금 예시: 인증, 대출, 예금
</div> </div>
</td> </td>
</tr> </tr>
@@ -327,14 +327,61 @@
}, 'json').fail(ajaxFail); }, 'json').fail(ajaxFail);
} }
// ── 캐시 Reload ──
var reloadModal;
var reloadBtnHtml;
function reloadPortalCache() { function reloadPortalCache() {
var $btn = $('#btnReloadCache');
if ($btn.prop('disabled')) return;
if (reloadBtnHtml === undefined) reloadBtnHtml = $btn.html();
$btn.prop('disabled', true)
.html('<span class="spinner-border spinner-border-sm"></span> 리로드 중...');
$.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) { $.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) {
if (result && result.success) { showReloadResult(result);
alert('포탈 메뉴 캐시 리로드 완료 (항목 ' + result.itemCount + '건, ' + result.reloadedAt + ')'); }, 'json').fail(ajaxFail).always(function () {
} else { $btn.prop('disabled', false).html(reloadBtnHtml);
alert('캐시 리로드 실패: ' + (result ? result.message : '알 수 없는 오류')); });
} }
}, 'json').fail(ajaxFail);
function showReloadResult(result) {
if (!result) {
alert('캐시 리로드 실패: 알 수 없는 오류');
return;
}
var servers = result.servers || [];
var $summary = $('#reloadSummary');
$summary.removeClass('alert-success alert-danger alert-warning')
.addClass(result.success ? 'alert-success'
: (result.successCount > 0 ? 'alert-warning' : 'alert-danger'))
.text(result.message || '');
var rows = '';
servers.forEach(function (s) {
var badge = s.success
? '<span class="badge text-bg-success">성공</span>'
: '<span class="badge text-bg-danger">실패</span>';
var detail = s.success
? ('항목 ' + (s.itemCount == null ? '-' : s.itemCount) + '건'
+ (s.reloadedAt ? ' / ' + escapeHtml(String(s.reloadedAt)) : ''))
: escapeHtml(String(s.message || '-'));
rows += '<tr>'
+ '<td class="text-center">' + badge + '</td>'
+ '<td style="word-break:break-all;">' + escapeHtml(String(s.url || '')) + '</td>'
+ '<td>' + detail + '</td>'
+ '<td class="text-end">' + (s.elapsedMs == null ? '-' : s.elapsedMs + 'ms') + '</td>'
+ '</tr>';
});
if (rows === '') {
rows = '<tr><td colspan="4" class="text-center text-muted">'
+ '호출 대상 서버가 없습니다. 아래 설정 안내를 확인하세요.</td></tr>';
}
$('#reloadResultBody').html(rows);
if (!reloadModal) reloadModal = new bootstrap.Modal(document.getElementById('reloadResultModal'));
reloadModal.show();
} }
function ajaxFail(xhr) { function ajaxFail(xhr) {
@@ -453,7 +500,7 @@
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()"> <button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button> <i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
<span class="spacer"></span> <span class="spacer"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" level="W" onclick="reloadPortalCache()"> <button type="button" class="btn btn-sm btn-outline-secondary" id="btnReloadCache" level="W" onclick="reloadPortalCache()">
<i class="bi bi-arrow-repeat"></i> 캐시 Reload</button> <i class="bi bi-arrow-repeat"></i> 캐시 Reload</button>
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()"> <button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
<i class="bi bi-save"></i> 저장</button> <i class="bi bi-save"></i> 저장</button>
@@ -463,6 +510,12 @@
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다. 드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다. 기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영). 저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영).
<br>
캐시 Reload 대상 서버는 포탈 프로퍼티 <code>Portal / portal.internal.menu-reload-url</code> 로 설정합니다.
포탈이 <strong>다중 서버(이중화)</strong> 로 기동된 경우 모든 서버의 리로드 URL 을
<strong>콤마(,) 또는 줄바꿈</strong> 으로 나열해야 전체 서버에 반영됩니다
(예: <code>http://10.0.0.11:39130/internal/menu/reload, http://10.0.0.12:39130/internal/menu/reload</code>).
서버별 요청 타임아웃은 <strong>3초</strong> 이며, 실행 결과는 서버별로 표시됩니다.
</div> </div>
<div class="menu-board"> <div class="menu-board">
@@ -544,6 +597,39 @@
</div> </div>
</div> </div>
<!-- 캐시 Reload 결과 -->
<div class="modal fade" id="reloadResultModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">포탈 메뉴 캐시 Reload 결과</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
</div>
<div class="modal-body">
<div class="alert py-2 px-3" id="reloadSummary" style="font-size:13px;"></div>
<table class="table table-sm table-bordered align-middle" style="font-size:12px;">
<thead class="table-light">
<tr>
<th style="width:70px;" class="text-center">결과</th>
<th>서버 URL</th>
<th style="width:38%;">상세</th>
<th style="width:80px;" class="text-end">소요</th>
</tr>
</thead>
<tbody id="reloadResultBody"></tbody>
</table>
<div class="text-muted" style="font-size:12px;">
대상 서버는 포탈 프로퍼티 <code>Portal / portal.internal.menu-reload-url</code> 에
콤마(,) 또는 줄바꿈으로 나열합니다. 서버별 타임아웃 3초, 전 서버 병렬 호출입니다.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>
</div><!-- end content_middle --> </div><!-- end content_middle -->
</div><!-- end right_box --> </div><!-- end right_box -->
</body> </body>
@@ -118,6 +118,9 @@
$('.incident-endat-hint').hide(); $('.incident-endat-hint').hide();
$('#endAt').prop('readonly', false); $('#endAt').prop('readonly', false);
} }
// 양식(템플릿)은 장애 공지에만 쓴다 - 점검은 문구 체계가 다르다
$('.template-row').toggle(t === NOTICE_TYPE_INCIDENT);
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다 // 타임라인은 등록된 장애(수정 모드)에서만 다룬다
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) { if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
$('#timelineSection').show(); $('#timelineSection').show();
@@ -126,6 +129,31 @@
} }
} }
/**
* 장애 공지 양식(제목·본문)을 서버에서 받아 채운다.
* 문구는 PTL_PROPERTY 'djb.apistatus.draft.*' 이며 자동 탐지 초안과 같은 양식이다.
*
* @param force true 면 이미 입력한 제목·본문도 (확인 후) 덮어쓴다
*/
function applyTemplate(force) {
if (force) {
var hasInput = $.trim($('#noticeSubject').val()) !== '' || !$('#contents').summernote('isEmpty');
if (hasInput && !confirm('현재 입력한 제목·본문을 양식으로 덮어씁니다. 계속하시겠습니까?')) {
return;
}
}
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'DRAFT_TEMPLATE', apisJson: JSON.stringify(affectedApis)},
success: function (data) {
if (!data) return;
$('#noticeSubject').val(data.subject || '');
$('#contents').summernote('code', data.detail || '');
},
error: function (e) { alert(e.responseText); }
});
}
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea'); var textArea = document.createElement('textarea');
textArea.innerHTML = text; textArea.innerHTML = text;
@@ -189,8 +217,11 @@
// AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생. // AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생.
// 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어 // 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어
// frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별. // frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별.
// 공지 영향 API 는 포탈에 실제 노출되는 것만 고르면 되므로 두 조건을 켠 채로 연다.
// (타 화면에서 여는 팝업은 파라미터가 없어 해제 상태 — 팝업 기본값은 건드리지 않는다)
var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(','); var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(',');
var url2 = apiPopupUrl + '?cmd=POPUP&selectedApiIds=' + encodeURIComponent(selectedIds); var url2 = apiPopupUrl + '?cmd=POPUP&searchApiSpecYn=Y&searchPortalDisplayYn=Y'
+ '&selectedApiIds=' + encodeURIComponent(selectedIds);
var args = {}; var args = {};
showModal(url2, args, 1280, 860, function () { showModal(url2, args, 1280, 860, function () {
var doc = args.self && args.self.document; var doc = args.self && args.self.document;
@@ -438,6 +469,15 @@
$('#noticeType').on('change', toggleIncidentFields); $('#noticeType').on('change', toggleIncidentFields);
// 신규 등록에서 장애를 고르면 빈 본문에 한해 양식을 자동으로 채운다.
// (수정 모드나 이미 쓴 본문은 건드리지 않는다 - 덮어쓰려면 '양식 적용' 버튼)
$('#noticeType').on('change', function () {
if (!isDetail && $(this).val() === NOTICE_TYPE_INCIDENT && $('#contents').summernote('isEmpty')) {
applyTemplate(false);
}
});
$('#btn_applyTemplate').click(function () { applyTemplate(true); });
$('#btn_addAffectedApi').click(openApiPopup); $('#btn_addAffectedApi').click(openApiPopup);
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis); $('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
@@ -587,7 +627,12 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>본문 <font color="red">*</font></th> <th>본문 <font color="red">*</font>
<div class="template-row" style="display:none;margin-top:5px;">
<button type="button" class="cssbtn smallBtn" id="btn_applyTemplate" level="W" status="DETAIL,NEW"
style="min-width:0;width:90%;" title="장애 공지 양식으로 제목과 본문을 채웁니다.">양식 적용</button>
</div>
</th>
<td colspan="3"> <td colspan="3">
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea> <textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td> </td>
@@ -40,7 +40,7 @@
if ($('input[name=prptyName]').val() == "") { if ($('input[name=prptyName]').val() == "") {
alert("<%= localeMessage.getString("propertyDetail.checkRequired2") %>"); alert("<%= localeMessage.getString("propertyDetail.checkRequired2") %>");
return false; return false;
} else if ($('input[name=prpty2Val]').val() == "") { } else if ($('[name=prpty2Val]').val() == "") {
alert("<%= localeMessage.getString("propertyDetail.checkRequired3") %>"); alert("<%= localeMessage.getString("propertyDetail.checkRequired3") %>");
return false; return false;
} }
@@ -90,7 +90,19 @@
'설명', '설명',
'<%= localeMessage.getString("propertyDetail.delYn") %>'], '<%= localeMessage.getString("propertyDetail.delYn") %>'],
colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true}, colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true},
{name: 'PRPTY2VAL', width: 150, align: 'left', editable: true}, {
name: 'PRPTY2VAL',
width: 150,
align: 'left',
editable: true,
//다행(멀티라인) 값 편집 — 관련 사이트 목록처럼 줄 단위 표기하는 값 지원
edittype: 'textarea',
editoptions: {rows: 3, cols: 60},
//줄바꿈이 그리드에서도 그대로 보이도록
cellattr: function () {
return ' style="white-space:pre-line; word-break:break-all;"';
}
},
{name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}}, {name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}},
{ {
name: 'DELETEYN', name: 'DELETEYN',
@@ -265,7 +277,7 @@
} }
var data = new Object(); var data = new Object();
data["PRPTYNAME"] = $("input[name=prptyName]").val(); data["PRPTYNAME"] = $("input[name=prptyName]").val();
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val(); data["PRPTY2VAL"] = $("[name=prpty2Val]").val();
data["PRPTYDESC"] = $("input[name=prptyDesc]").val(); data["PRPTYDESC"] = $("input[name=prptyDesc]").val();
var rows = $("#grid")[0].rows; var rows = $("#grid")[0].rows;
@@ -346,7 +358,8 @@
<tr> <tr>
<th><%= localeMessage.getString("propertyDetail.propertyValue") %> <th><%= localeMessage.getString("propertyDetail.propertyValue") %>
</th> </th>
<td colspan="3"><input type="text" name="prpty2Val"/> <td colspan="3"><textarea name="prpty2Val" rows="3"
style="width:100%; box-sizing:border-box; resize:vertical;"></textarea>
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -0,0 +1,149 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
.url-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/webhook/webhookReqMan.json" />';
var url_view = '<c:url value="/onl/apim/webhook/webhookReqMan.view" />';
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '&quot;'); }
function formatTargetUrl(cellvalue, options, rowObject) {
var v = cellvalue || '';
return '<span class="url-cell" title="' + escapeAttr(v) + '">' + escapeHtmlJs(v) + '</span>';
}
function init() {
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
putSelectFromParam();
buildGrid();
}
function buildGrid() {
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
url: url,
postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['id', 'No', '기관명', '리버스 프록시 경로', 'Target URL', '신청일시'],
colModel: [
{name: 'id', align: 'center', key: true, hidden: true},
{name: 'rowNum', align: 'center', width: 45, sortable: false},
{name: 'orgName', align: 'left', width: 160},
{name: 'reverseProxyPath', align: 'left', width: 160},
{name: 'targetUrl', align: 'left', width: 380, formatter: formatTargetUrl},
{name: 'createdDate', align: 'center', width: 140}
],
jsonReader: {repeatitems: false},
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
ondblClickRow: function (rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $(this).getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
url2 += '&id=' + id;
url2 += '&' + getSearchUrl();
goNav(url2);
},
loadComplete: function () {
var page = $(this).getGridParam('page');
var rowNum = $(this).getGridParam('rowNum');
var rows = $(this).getDataIDs();
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < rows.length; i++) {
var number = ((page - 1) * rowNum + i) + 1;
$(this).setCell(rows[i], 'rowNum', number);
}
// 서버 고정 정렬(신청일시 역순) — 컬럼 정렬 비활성
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, {sortable: false});
}
},
loadError: function (jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/';
comloadError(jqXHR, textStatus, errorThrown, location);
}
});
resizeJqGridWidth('grid', 'content_middle', '1000');
}
$(document).ready(function () {
init();
$("#btn_search").click(function () {
var postData = getSearchForJqgrid("cmd", "LIST");
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
});
$("select[name^=search]").change(function () { $("#btn_search").click(); });
$("input[name^=search]").keydown(function (key) {
if (key.keyCode == 13) { $("#btn_search").click(); }
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<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>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:120px;"><col style="width:200px;">
<col style="width:120px;"><col style="width:200px;">
</colgroup>
<tbody>
<tr>
<th>기관명</th>
<td><input type="text" name="searchOrgName" autocomplete="off"></td>
<th>리버스 프록시 경로</th>
<td><input type="text" name="searchReverseProxyPath" autocomplete="off"></td>
</tr>
<tr>
<th>Target URL</th>
<td colspan="3"><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
</tr>
</tbody>
</table>
</form>
<table id="grid"></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,154 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
.sub_list { width:100%; border-collapse:collapse; }
.sub_list th, .sub_list td { border:1px solid #ebebec; padding:6px 8px; text-align:left; }
.sub_list th { background:#f7f7f8; width:60px; }
.empty-msg { color:#999; padding:6px 2px; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/webhook/webhookReqMan.json" />';
var url_view = '<c:url value="/onl/apim/webhook/webhookReqMan.view" />';
var key = '${param.id}';
var combo;
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
// 이벤트유형 코드 → 모니터링 공통코드(EVENT_TYPE) 코드명
function eventTypeName(code) {
if (!combo || !combo.eventTypeList) { return code; }
for (var i = 0; i < combo.eventTypeList.length; i++) {
if (combo.eventTypeList[i].CODE == code) { return combo.eventTypeList[i].NAME; }
}
return code;
}
function renderApiList(apiIds) {
var $tbody = $('#apiListBody').empty();
if (!apiIds || apiIds.length === 0) {
$tbody.append('<tr><td class="empty-msg">등록된 API가 없습니다.</td></tr>');
return;
}
for (var i = 0; i < apiIds.length; i++) {
$tbody.append('<tr><td>' + escapeHtmlJs(apiIds[i]) + '</td></tr>');
}
}
function renderEventTypes(eventTypes) {
if (!eventTypes || eventTypes.length === 0) {
$('#eventTypes').text('');
return;
}
var names = eventTypes.map(function (code) { return eventTypeName(code); });
$('#eventTypes').text(names.join(', '));
}
function detail() {
if (!key) return;
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'DETAIL', id: key},
success: function (data) {
$('#id').text(data.id != null ? data.id : '');
$('#orgName').text(data.orgName || '');
$('#reverseProxyPath').text(data.reverseProxyPath || '');
$('#targetUrl').text(data.targetUrl || '');
$('#secret').text(data.secret || '');
$('#userSecret').text(data.userSecret || '');
$('#createdDate').text(data.createdDate || '');
renderApiList(data.apiIds);
renderEventTypes(data.eventTypes);
},
error: function (e) { alert(e.responseText); }
});
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
buttonControl();
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function (json) {
combo = json;
detail();
},
error: function (e) { alert(e.responseText); }
});
$('#btn_previous').click(function () { goNav(returnUrl); });
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
</div>
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title" id="title">웹훅 신청내역 상세</div>
<table class="table_row" cellspacing="0">
<colgroup>
<col style="width:12%"/><col style="width:38%"/>
<col style="width:12%"/><col style="width:38%"/>
</colgroup>
<tr>
<th>신청 ID</th>
<td colspan="3"><span id="id"></span></td>
</tr>
<tr>
<th>기관명</th>
<td><span id="orgName"></span></td>
<th>신청일시</th>
<td><span id="createdDate"></span></td>
</tr>
<tr>
<th>리버스 프록시 경로</th>
<td><span id="reverseProxyPath"></span></td>
<th>Target URL</th>
<td><span id="targetUrl"></span></td>
</tr>
<tr>
<th>Secret</th>
<td colspan="3"><span id="secret"></span></td>
</tr>
<tr>
<th>User Secret</th>
<td colspan="3"><span id="userSecret"></span></td>
</tr>
<tr>
<th>신청 이벤트</th>
<td colspan="3"><span id="eventTypes"></span></td>
</tr>
</table>
<div class="title" style="margin-top:20px;">API 목록</div>
<table class="table_row">
<colgroup><col style="width:100%"/></colgroup>
<thead><tr><th style="width:100%;">API ID</th></tr></thead>
<tbody id="apiListBody"></tbody>
</table>
</div>
</div>
</body>
</html>
@@ -89,7 +89,7 @@
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData: getSearchForJqgrid("cmd", "LIST"), postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '발송서버', '발송일시'], colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'],
colModel: [ colModel: [
{name: 'id', align: 'center', key: true, hidden: true}, {name: 'id', align: 'center', key: true, hidden: true},
{name: 'rowNum', align: 'center', width: 45, sortable: false}, {name: 'rowNum', align: 'center', width: 45, sortable: false},
@@ -99,6 +99,7 @@
{name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl}, {name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl},
{name: 'statusCode', align: 'center', width: 60}, {name: 'statusCode', align: 'center', width: 60},
{name: 'success', align: 'center', width: 70, formatter: formatSuccess}, {name: 'success', align: 'center', width: 70, formatter: formatSuccess},
{name: 'retryCount', align: 'center', width: 60},
{name: 'sendBy', align: 'center', width: 100}, {name: 'sendBy', align: 'center', width: 100},
{name: 'sentAt', align: 'center', width: 140} {name: 'sentAt', align: 'center', width: 140}
], ],
@@ -222,9 +223,9 @@
<td><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td> <td><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
<th>발송기간</th> <th>발송기간</th>
<td> <td>
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:40%; border:1px solid #ebebec;"> <input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
~ ~
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:40%; border:1px solid #ebebec;"> <input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:100px; border:1px solid #ebebec;">
<input type="hidden" name="searchStartDate" value=""> <input type="hidden" name="searchStartDate" value="">
<input type="hidden" name="searchEndDate" value=""> <input type="hidden" name="searchEndDate" value="">
</td> </td>
@@ -43,6 +43,7 @@
$('#sendBy').text(data.sendBy || ''); $('#sendBy').text(data.sendBy || '');
$('#sentAt').text(data.sentAt || ''); $('#sentAt').text(data.sentAt || '');
$('#signature').text(data.signature || ''); $('#signature').text(data.signature || '');
$('#userSecret').text(data.userSecret || '');
$('#errorMessage').text(data.errorMessage || ''); $('#errorMessage').text(data.errorMessage || '');
$('#payload').text(data.payload || ''); $('#payload').text(data.payload || '');
$('#responseBody').text(data.responseBody || ''); $('#responseBody').text(data.responseBody || '');
@@ -116,7 +117,9 @@
</tr> </tr>
<tr> <tr>
<th>Signature</th> <th>Signature</th>
<td colspan="3"><span id="signature"></span></td> <td><span id="signature"></span></td>
<th>User Secret</th>
<td><span id="userSecret"></span></td>
</tr> </tr>
<tr> <tr>
<th>Payload</th> <th>Payload</th>
@@ -33,6 +33,18 @@
width: 100%; width: 100%;
height: 250px; height: 250px;
} }
/* 달력 팝업이 파이차트 가운데 숫자(z-index:10) 위로 오도록 */
#ui-datepicker-div {
z-index: 9999 !important;
}
.cssbtn.small {
height: 24px;
border-radius: 2px;
min-width: 24px;
box-shadow: none;
border-color: #ebebec;
font-size: 11px;
}
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script> <script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
@@ -286,6 +298,28 @@
fetchChartData(); fetchChartData();
} }
// 조회기간을 시작일 기준 이전/다음 달의 1일~말일로 변경 후 조회 (direction: -1 이전달, 1 다음달)
function shiftMonthRange(direction) {
var v = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
if (!v || v.length < 6) return;
var year = parseInt(v.substring(0, 4));
var month = parseInt(v.substring(4, 6)) - 1; // 0-based
var first = new Date(year, month + direction, 1);
var last = new Date(year, month + direction + 1, 0); // 해당 달의 말일
function pad(n) { return String(n).padStart(2, '0'); }
function fmt(d) {
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
}
$("input[name=searchStartDateTime]").val(fmt(first));
$("input[name=searchEndDateTime]").val(fmt(last));
search();
}
function exportToExcel(isSummary) { function exportToExcel(isSummary) {
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT'; var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
console.log('[Excel Export] Starting... (type: ' + cmdType + ')'); console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
@@ -416,12 +450,13 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
{ name: 'apiName', align: 'left', width: '250', sortable: false }, { name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
@@ -458,7 +493,7 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'통계시간', 'API', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
@@ -505,6 +540,8 @@
$("input[name=searchStartDateTime]").inputmask("9999-99-99", { 'autoUnmask': true }); $("input[name=searchStartDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchEndDateTime]").inputmask("9999-99-99", { 'autoUnmask': true }); $("input[name=searchEndDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchStartDateTime], input[name=searchEndDateTime]").datepicker();
// 기본값 설정 (어제 기준 해당 월 1일 ~ 어제) // 기본값 설정 (어제 기준 해당 월 1일 ~ 어제)
var today = getToday(); var today = getToday();
var todayStr = today.replace(/-/g, ''); var todayStr = today.replace(/-/g, '');
@@ -544,6 +581,14 @@
search(); search();
}); });
$("#btnPrevMonth").click(function() {
shiftMonthRange(-1);
});
$("#btnNextMonth").click(function() {
shiftMonthRange(1);
});
$("#btn_excel_export_summary").click(function() { $("#btn_excel_export_summary").click(function() {
exportToExcel(true); exportToExcel(true);
}); });
@@ -598,11 +643,13 @@
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;"> <input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
~ ~
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;"> <input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
<button type="button" id="btnPrevMonth" class="cssbtn small">&lt;</button>
<button type="button" id="btnNextMonth" class="cssbtn small">&gt;</button>
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span> <span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>API</th> <th>API ID</th>
<td> <td>
<input type="text" name="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" value="${param.searchApiName}">
</td> </td>
@@ -643,7 +690,7 @@
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">API별 요약 통계</div> <div class="title" style="margin: 0;">API별 요약 통계</div>
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R"> <button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드 (요약)
</button> </button>
@@ -33,6 +33,18 @@
width: 100%; width: 100%;
height: 250px; height: 250px;
} }
/* 달력 팝업이 파이차트 가운데 숫자(z-index:10) 위로 오도록 */
#ui-datepicker-div {
z-index: 9999 !important;
}
.cssbtn.small {
height: 24px;
border-radius: 2px;
min-width: 24px;
box-shadow: none;
border-color: #ebebec;
font-size: 11px;
}
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script> <script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
@@ -292,6 +304,25 @@
fetchChartData(); fetchChartData();
} }
// 조회일자를 하루 이동 후 조회 (direction: -1 이전날, 1 이후날)
function shiftSearchDate(direction) {
var v = $("input[name=searchDate]").val().replace(/-/g, "");
if (!v || v.length !== 8) return;
var d = new Date(
parseInt(v.substring(0, 4)),
parseInt(v.substring(4, 6)) - 1,
parseInt(v.substring(6, 8))
);
d.setDate(d.getDate() + direction);
function pad(n) { return String(n).padStart(2, '0'); }
$("input[name=searchDate]").val(
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()));
search();
}
function exportToExcel(isSummary) { function exportToExcel(isSummary) {
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT'; var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
console.log('[Excel Export] Starting... (type: ' + cmdType + ')'); console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
@@ -418,12 +449,13 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API 명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
{ name: 'apiName', align: 'left', width: '250', sortable: false }, { name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
@@ -460,7 +492,7 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'통계시간', 'API', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
@@ -506,6 +538,8 @@
// 날짜 입력 마스크 // 날짜 입력 마스크
$("input[name=searchDate]").inputmask("9999-99-99", { 'autoUnmask': true }); $("input[name=searchDate]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchDate]").datepicker();
// 기본값 설정 (오늘) // 기본값 설정 (오늘)
var today = getToday(); var today = getToday();
if (!$("input[name=searchDate]").val()) { if (!$("input[name=searchDate]").val()) {
@@ -524,6 +558,14 @@
search(); search();
}); });
$("#btnPrevDate").click(function() {
shiftSearchDate(-1);
});
$("#btnNextDate").click(function() {
shiftSearchDate(1);
});
$("#btn_excel_export_summary").click(function() { $("#btn_excel_export_summary").click(function() {
exportToExcel(true); exportToExcel(true);
}); });
@@ -576,11 +618,13 @@
<th>조회일자</th> <th>조회일자</th>
<td colspan="5"> <td colspan="5">
<input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;"> <input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;">
<button type="button" id="btnPrevDate" class="cssbtn small">&lt;</button>
<button type="button" id="btnNextDate" class="cssbtn small">&gt;</button>
<span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span> <span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>API</th> <th>API ID</th>
<td> <td>
<input type="text" name="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" value="${param.searchApiName}">
</td> </td>
@@ -621,7 +665,7 @@
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">API별 요약 통계</div> <div class="title" style="margin: 0;">API별 요약 통계</div>
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R"> <button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드 (요약)
</button> </button>
@@ -26,6 +26,18 @@
border: 1px solid #ddd; border: 1px solid #ddd;
background: #fff; background: #fff;
} }
.cssbtn.small {
height: 24px;
border-radius: 2px;
min-width: 24px;
box-shadow: none;
border-color: #ebebec;
font-size: 11px;
}
/* 달력 팝업이 파이차트 가운데 숫자(z-index:10) 위로 오도록 */
#ui-datepicker-div {
z-index: 9999 !important;
}
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script> <script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
@@ -203,7 +215,6 @@
function validateAndAdjustDateRange() { function validateAndAdjustDateRange() {
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, ""); var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, ""); var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
var endDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, ""); var endTime = $("input[name=searchEndTime]").val().replace(/:/g, "");
if (!startDate || !startTime) return null; if (!startDate || !startTime) return null;
@@ -219,35 +230,43 @@
); );
} }
// UI 업데이트 헬퍼 // 종료일시(yyyyMMddHHmm) 포맷 헬퍼
function updateEndDateTime(dt) { function formatDateTime(dt) {
var d = dt.getFullYear() + return dt.getFullYear() +
String(dt.getMonth() + 1).padStart(2, '0') + String(dt.getMonth() + 1).padStart(2, '0') +
String(dt.getDate()).padStart(2, '0'); String(dt.getDate()).padStart(2, '0') +
var t = String(dt.getHours()).padStart(2, '0') + String(dt.getHours()).padStart(2, '0') +
String(dt.getMinutes()).padStart(2, '0'); String(dt.getMinutes()).padStart(2, '0');
$("input[name=searchEndDate]").val(d.substring(0, 4) + '-' + d.substring(4, 6) + '-' + d.substring(6, 8)); }
$("input[name=searchEndTime]").val(t.substring(0, 2) + ':' + t.substring(2, 4));
return d + t; // 종료시각 UI 업데이트 헬퍼
function updateEndTime(dt) {
$("input[name=searchEndTime]").val(
String(dt.getHours()).padStart(2, '0') + ':' + String(dt.getMinutes()).padStart(2, '0'));
return formatDateTime(dt);
} }
var start = parseDateTime(startDate, startTime); var start = parseDateTime(startDate, startTime);
var startDateTime = startDate + startTime; var startDateTime = startDate + startTime;
var endDateTime; var endDateTime;
if (endDate && endTime) { if (endTime) {
var end = parseDateTime(endDate, endTime); // 종료일은 시작일 기준. 종료시각이 시작시각보다 이르면 자정을 넘긴 것으로 보고 +1일
var end = parseDateTime(startDate, endTime);
if (end < start) {
end = new Date(end.getTime() + 24 * 60 * 60 * 1000);
}
var diffMinutes = (end - start) / (1000 * 60); var diffMinutes = (end - start) / (1000 * 60);
if (diffMinutes > 60) { if (diffMinutes > 60) {
// 1시간 초과 시 시작 + 1시간으로 조정 // 1시간 초과 시 시작 + 1시간으로 조정
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000)); endDateTime = updateEndTime(new Date(start.getTime() + 60 * 60 * 1000));
} else { } else {
endDateTime = endDate + endTime; endDateTime = formatDateTime(end);
} }
} else { } else {
// 종료시간 미입력 시 시작 + 1시간으로 설정 // 종료시간 미입력 시 시작 + 1시간으로 설정
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000)); endDateTime = updateEndTime(new Date(start.getTime() + 60 * 60 * 1000));
} }
return { start: startDateTime, end: endDateTime }; return { start: startDateTime, end: endDateTime };
@@ -302,6 +321,44 @@
fetchChartData(range); fetchChartData(range);
} }
// 조회기간을 정시 단위 1시간 구간으로 이동 (direction: -1 이전, 1 이후)
// ex) 시작 08:35, 이전 클릭 -> 08:00~09:00, 다시 -> 07:00~08:00
// ex) 시작 08:35, 이후 클릭 -> 09:00~10:00, 다시 -> 10:00~11:00
function shiftTimeRange(direction) {
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
if (!startDate || !startTime) return;
var start = new Date(
parseInt(startDate.substring(0, 4)),
parseInt(startDate.substring(4, 6)) - 1,
parseInt(startDate.substring(6, 8)),
parseInt(startTime.substring(0, 2)),
parseInt(startTime.substring(2, 4))
);
if (direction < 0) {
// 분 단위가 있으면 정시로 내림, 이미 정시면 1시간 전
if (start.getMinutes() === 0) start.setHours(start.getHours() - 1);
else start.setMinutes(0);
} else {
// 분 단위가 있으면 다음 정시로 올림, 이미 정시면 1시간 후
start.setMinutes(0);
start.setHours(start.getHours() + 1);
}
var end = new Date(start.getTime() + 60 * 60 * 1000);
function pad(n) { return String(n).padStart(2, '0'); }
$("input[name=searchStartDate]").val(
start.getFullYear() + '-' + pad(start.getMonth() + 1) + '-' + pad(start.getDate()));
$("input[name=searchStartTime]").val(pad(start.getHours()) + ':' + pad(start.getMinutes()));
$("input[name=searchEndTime]").val(pad(end.getHours()) + ':' + pad(end.getMinutes()));
search();
}
function exportToExcel(isSummary) { function exportToExcel(isSummary) {
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT'; var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
console.log('[Excel Export] Starting... (type: ' + cmdType + ')'); console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
@@ -317,13 +374,10 @@
serviceType: '${param.serviceType}' serviceType: '${param.serviceType}'
}; };
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, ""); var range = validateAndAdjustDateRange();
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, ""); if (range) {
var endDate = $("input[name=searchEndDate]").val().replace(/-/g, ""); postData.searchStartDateTime = range.start;
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, ""); postData.searchEndDateTime = range.end;
if (startDate && startTime) {
postData.searchStartDateTime = startDate + startTime;
postData.searchEndDateTime = endDate + endTime;
} }
console.log('[Excel Export] Request data:', postData); console.log('[Excel Export] Request data:', postData);
@@ -431,12 +485,13 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API 명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
{ name: 'apiName', align: 'left', width: '250', sortable: false }, { name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
@@ -473,7 +528,7 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'통계시간', 'API', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
@@ -517,9 +572,11 @@
$(document).ready(function() { $(document).ready(function() {
// 날짜/시간 입력 마스크 // 날짜/시간 입력 마스크
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true }); $("input[name=searchStartDate]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchStartTime], input[name=searchEndTime]").inputmask("99:99", { 'autoUnmask': true }); $("input[name=searchStartTime], input[name=searchEndTime]").inputmask("99:99", { 'autoUnmask': true });
$("input[name=searchStartDate]").datepicker();
// 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분)) // 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분))
var now = new Date(); var now = new Date();
var oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); var oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
@@ -537,8 +594,7 @@
$("input[name=searchStartDate]").val(formatDate(oneHourAgo)); $("input[name=searchStartDate]").val(formatDate(oneHourAgo));
$("input[name=searchStartTime]").val(formatTime(oneHourAgo)); $("input[name=searchStartTime]").val(formatTime(oneHourAgo));
} }
if (!$("input[name=searchEndDate]").val()) { if (!$("input[name=searchEndTime]").val()) {
$("input[name=searchEndDate]").val(formatDate(now));
$("input[name=searchEndTime]").val(formatTime(now)); $("input[name=searchEndTime]").val(formatTime(now));
} }
@@ -554,6 +610,14 @@
search(); search();
}); });
$("#btnPrevTime").click(function() {
shiftTimeRange(-1);
});
$("#btnNextTime").click(function() {
shiftTimeRange(1);
});
$("#btn_excel_export_summary").click(function() { $("#btn_excel_export_summary").click(function() {
exportToExcel(true); exportToExcel(true);
}); });
@@ -568,8 +632,8 @@
} }
}); });
// 종료일/시간 변경 시 1시간 초과 검증 // 종료시각 변경 시 1시간 초과 검증
$("input[name=searchEndDate], input[name=searchEndTime]").on('change blur', function() { $("input[name=searchEndTime]").on('change blur', function() {
validateAndAdjustDateRange(); validateAndAdjustDateRange();
}); });
@@ -601,14 +665,19 @@
<tbody> <tbody>
<tr> <tr>
<th style="width:100px;">조회기간</th> <th style="width:100px;">조회기간</th>
<td colspan="3"> <td style="width:400px">
<input type="text" name="searchStartDate" value="${param.searchStartDate}" style="width:100px;"> <input type="text" name="searchStartDate" value="${param.searchStartDate}" style="width:80px;">
<input type="text" name="searchStartTime" value="${param.searchStartTime}" style="width:60px;"> <input type="text" name="searchStartTime" value="${param.searchStartTime}" style="width:40px;">
~ ~
<input type="text" name="searchEndDate" value="${param.searchEndDate}" style="width:100px;"> <input type="text" name="searchEndTime" value="${param.searchEndTime}" style="width:40px;">
<input type="text" name="searchEndTime" value="${param.searchEndTime}" style="width:60px;"> <button type="button" id="btnPrevTime" class="cssbtn small">&lt;</button>
<button type="button" id="btnNextTime" class="cssbtn small">&gt;</button>
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span> <span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
</td> </td>
<th style="width:100px;">API ID</th>
<td style="width:400px">
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -421,12 +421,13 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API 명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
{ name: 'apiName', align: 'left', width: '250', sortable: false }, { name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
@@ -463,7 +464,7 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'통계시간', 'API', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
@@ -510,11 +511,21 @@
$("input[name=searchStartDateTime]").inputmask("9999-99", { 'autoUnmask': true }); $("input[name=searchStartDateTime]").inputmask("9999-99", { 'autoUnmask': true });
$("input[name=searchEndDateTime]").inputmask("9999-99", { 'autoUnmask': true }); $("input[name=searchEndDateTime]").inputmask("9999-99", { 'autoUnmask': true });
// 기본값 설정 (최근 12월) // 기본값 설정: 올해 1월 ~ 지난달 (현재가 1월이면 작년 1월 ~ 작년 12월)
var today = new Date(); var today = new Date();
var endMonth = today.getFullYear() + '-' + (today.getMonth() + 1 < 10 ? '0' : '') + (today.getMonth() + 1); var startDate, endDate;
var startDate = new Date(today.getFullYear(), today.getMonth() - 11, 1); if (today.getMonth() === 0) { // 1월
var startMonth = startDate.getFullYear() + '-' + (startDate.getMonth() + 1 < 10 ? '0' : '') + (startDate.getMonth() + 1); startDate = new Date(today.getFullYear() - 1, 0, 1);
endDate = new Date(today.getFullYear() - 1, 11, 1);
} else {
startDate = new Date(today.getFullYear(), 0, 1);
endDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
}
function toMonthStr(d) {
return d.getFullYear() + '-' + (d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1);
}
var startMonth = toMonthStr(startDate);
var endMonth = toMonthStr(endDate);
if (!$("input[name=searchStartDateTime]").val()) { if (!$("input[name=searchStartDateTime]").val()) {
$("input[name=searchStartDateTime]").val(startMonth); $("input[name=searchStartDateTime]").val(startMonth);
@@ -593,7 +604,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>API</th> <th>API ID</th>
<td> <td>
<input type="text" name="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" value="${param.searchApiName}">
</td> </td>
@@ -635,7 +646,7 @@
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">API별 요약 통계</div> <div class="title" style="margin: 0;">API별 요약 통계</div>
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R"> <button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드 (요약)
</button> </button>
@@ -397,12 +397,13 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API 명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
{ name: 'apiName', align: 'left', width: '250', sortable: false }, { name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
@@ -439,7 +440,7 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'통계시간', 'API', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
@@ -563,7 +564,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>API</th> <th>API ID</th>
<td> <td>
<input type="text" name="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" value="${param.searchApiName}">
</td> </td>
@@ -604,7 +605,7 @@
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">API별 요약 통계</div> <div class="title" style="margin: 0;">API별 요약 통계</div>
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R"> <button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드 (요약)
</button> </button>
@@ -15,6 +15,19 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script> <script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<style>
#gbox_grid .ui-jqgrid-ftable tr.footrow td {
height: 26px;
}
.cssbtn.small {
height: 24px;
border-radius: 2px;
min-width: 24px;
box-shadow: none;
border-color: #ebebec;
font-size: 11px;
}
</style>
<script language="javascript"> <script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>';
@@ -60,13 +73,53 @@
} }
// 조회구분(searchType)에 따라 '구분' / '구분명' 컬럼의 헤더명·폭·표시여부를 조정
function applyGridColumnsByType(type) {
var $g = $("#grid");
if (type === 'API') {
$g.jqGrid('setLabel', 'orgName', 'API ID');
$g.jqGrid('setLabel', 'apiDesc', 'API명');
$g.jqGrid('setColProp', 'orgName', { widthOrg: 150, width: 150 });
$g.jqGrid('setColProp', 'apiDesc', { widthOrg: 250, width: 250 });
$g.jqGrid('showCol', 'apiDesc');
} else {
$g.jqGrid('setLabel', 'orgName', (type === 'DATE') ? '사용일' : '제휴사');
$g.jqGrid('setColProp', 'orgName', { widthOrg: 250, width: 250 });
$g.jqGrid('hideCol', 'apiDesc');
}
$g.jqGrid('setGridWidth', $('#content_middle').width(), true);
}
function search() { function search() {
var postData = getPostData("cmd", "LIST"); var postData = getPostData("cmd", "LIST");
if (postData) { if (postData) {
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid"); applyGridColumnsByType($('input[name="searchType"]:checked').val());
$("#grid").setGridParam({ url: url, postData: postData }).trigger("reloadGrid");
} }
} }
// 조회기간을 시작일 기준 이전/다음 달의 1일~말일로 변경 후 조회 (direction: -1 이전달, 1 다음달)
function shiftMonthRange(direction) {
var v = $("input[name=searchStartDate]").val().replace(/-/g, "");
if (!v || v.length < 6) return;
var year = parseInt(v.substring(0, 4));
var month = parseInt(v.substring(4, 6)) - 1; // 0-based
var first = new Date(year, month + direction, 1);
var last = new Date(year, month + direction + 1, 0); // 해당 달의 말일
function pad(n) { return String(n).padStart(2, '0'); }
function fmt(d) {
return d.getFullYear() + '' + pad(d.getMonth() + 1) + pad(d.getDate());
}
$("input[name=searchStartDate]").val(fmt(first));
$("input[name=searchEndDate]").val(fmt(last));
search();
}
function exportToExcel() { function exportToExcel() {
var postData = getPostData("cmd", "EXCEL_EXPORT"); var postData = getPostData("cmd", "EXCEL_EXPORT");
@@ -181,12 +234,13 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'구분', '제휴사', 'API명',
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류', '총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
{ name: 'orgName', align: 'left', width: '250', sortable: false }, { name: 'orgName', align: 'left', width: '250', sortable: false },
{ name: 'apiDesc', align: 'left', width: '250', sortable: false, hidden: true },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
@@ -198,19 +252,18 @@
{ name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), height: 500,
page: '${param.page}', rowNum: 10000,
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: 'auto',
autowidth: true, autowidth: true,
viewrecords: true, footerrow: true,
rowList: eval('[${rmsDefaultRowList}]'), userDataOnFooter: true,
loadComplete: function(d) { loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel"); var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) { for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false }); $(this).setColProp(colModel[i].name, { sortable: false });
} }
$(".ui-jqgrid-ftable td").css({ "font-weight": "bold", "background-color": "#f5f5f5" });
} }
}); });
} }
@@ -221,9 +274,22 @@
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true }); $("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker(); $("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
// 기본값 설정: 이번달 1일 ~ 어제 (오늘이 1일이면 지난달 1일 ~ 지난달 말일)
var today = getToday(); var today = getToday();
var startDate = today.substring(0,6)+"01"; var now = new Date();
var endDate = today; function toYmd(d) {
var m = d.getMonth() + 1;
var day = d.getDate();
return d.getFullYear() + '' + (m < 10 ? '0' : '') + m + (day < 10 ? '0' : '') + day;
}
var startDate, endDate;
if (now.getDate() === 1) {
startDate = toYmd(new Date(now.getFullYear(), now.getMonth() - 1, 1));
endDate = toYmd(new Date(now.getFullYear(), now.getMonth(), 0));
} else {
startDate = today.substring(0,6) + "01";
endDate = toYmd(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1));
}
if (!$("input[name=searchStartDate]").val()) { if (!$("input[name=searchStartDate]").val()) {
@@ -242,6 +308,14 @@
search(); search();
}); });
$("#btnPrevMonth").click(function() {
shiftMonthRange(-1);
});
$("#btnNextMonth").click(function() {
shiftMonthRange(1);
});
$("input[name=searchType]").click(function() { $("input[name=searchType]").click(function() {
search(); search();
}); });
@@ -284,6 +358,8 @@
<input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;"> <input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
~ ~
<input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;"> <input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
<button type="button" id="btnPrevMonth" class="cssbtn small">&lt;</button>
<button type="button" id="btnNextMonth" class="cssbtn small">&gt;</button>
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span> <span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
</td> </td>
</tr> </tr>
@@ -298,7 +374,7 @@
<td> <td>
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}"> <input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
</td> </td>
<th style="width:120px;">API</th> <th style="width:120px;">API ID</th>
<td> <td>
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
</td> </td>
@@ -308,7 +384,6 @@
<div> <div>
<table id="grid"></table> <table id="grid"></table>
<div id="pager"></div>
</div> </div>
@@ -0,0 +1,140 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title>인터페이스 배포</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/transaction/apim/apiInterfaceMan.json"/>';
var url_loader = '<c:url value="/loader.do"/>';
function loadList() {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_DEPLOY_FILES'},
success: function (json) {
console.log('list', json)
if (json) {
$('#grid').jqGrid('clearGridData').jqGrid('setGridParam', {data: json.rows || []}).trigger('reloadGrid');
}
},
error: function (e) {
alert(e.responseText);
}
});
}
function callDeploy(filePath) {
var postData = [];
postData.push({name: "cmd", value: "interface"});
postData.push({name: "control", value: "uploadfull"});
postData.push({name: "filePath", value: filePath});
return new Promise(function (resolve, reject) {
$.ajax({
type: "POST",
url: url_loader,
data: postData,
success: function (response) {
resolve(response);
},
error: function (error) {
reject(error);
}
});
});
}
$(document).ready(function () {
$("#btn_close").click(function () {
window.close();
});
$('#grid').jqGrid({
datatype: "local",
colNames: ['업무그룹', '인터페이스', '생성일시', 'filePath'],
colModel: [
{name: 'bizCode', align: 'center', width: '80', sortable: true},
{name: 'interfaceId', align: 'left', sortable: true},
{name: 'createdDate', align: 'center', width: '130', sortable: true},
{name: 'filePath', hidden: true}
],
pager: $('#pager'),
rowNum: 20,
rowList: [10, 20, 50, 100],
autoheight: false,
height: 460,
width: 650,
autowidth: false,
viewrecords: true,
shrinkToFit: true,
multiselect: true,
multiboxonly: false
});
loadList();
$("#btn_deploy").click(async function () {
var rowIds = $('#grid').jqGrid('getGridParam', 'selarrrow');
if (rowIds.length === 0) {
alert('배포할 인터페이스를 선택하세요.');
return;
}
if (!confirm(rowIds.length + '건을 배포 하시겠습니까?')) {
return;
}
$("#btn_deploy").prop("disabled", true);
var successCount = 0;
var failedList = [];
for (var i = 0; i < rowIds.length; i++) {
var rowData = $("#grid").getRowData(rowIds[i]);
var target = rowData['bizCode'] + '/' + rowData['interfaceId'];
try {
var response = await callDeploy(rowData['filePath']);
if (response != null && response.includes('status=success')) {
successCount++;
} else {
failedList.push(target);
}
} catch (error) {
failedList.push(target);
}
}
$("#btn_deploy").prop("disabled", false);
var msg = '배포 완료 - 성공[' + successCount + ']건, 실패[' + failedList.length + ']건';
if (failedList.length > 0) {
msg += '\n실패목록 : ' + failedList.join(', ');
}
alert(msg);
});
});
</script>
</head>
<body>
<div class="popup_box">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_deploy" level="W"><i class="material-icons">cloud_upload</i> 배포
</button>
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
</button>
</div>
<div class="title">배포 대상 인터페이스 목록</div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</body>
</html>
@@ -1,5 +1,6 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %> <%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %> <%@ page import="java.io.*" %>
<%@ page import="com.eactive.eai.common.util.SystemUtil" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %> <%@ include file="/jsp/common/include/localemessage.jsp" %>
<% <%
@@ -246,6 +247,11 @@
window.alert('이관 대상 API 성공['+successCount+']건, 실패['+failedCount+']건 서버에 다운로드 완료'); window.alert('이관 대상 API 성공['+successCount+']건, 실패['+failedCount+']건 서버에 다운로드 완료');
}); });
$("#btn_deploy").click(function(){
var url2 = '<c:url value="/onl/transaction/apim/apiInterfaceMan.view"/>?cmd=DEPLOY_POPUP';
showModal2(url2, {}, 663, 600);
});
buttonControl(); buttonControl();
// 25.11.25 파일 가져오기 할 때 중복 검사 기능추가. // 25.11.25 파일 가져오기 할 때 중복 검사 기능추가.
@@ -381,6 +387,7 @@
<div class="content_middle" id="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_download" level="W" status="DETAIL"><i class="material-icons">cloud_download</i> 이관</button> <button type="button" class="cssbtn" id="btn_download" level="W" status="DETAIL"><i class="material-icons">cloud_download</i> 이관</button>
<!-- <button type="button" class="cssbtn" id="btn_deploy" level="W" status="DETAIL"><i class="material-icons">cloud_upload</i> 배포</button> -->
<button type="button" class="cssbtn" id="btn_import" level="R" data-bs-toggle="modal" <button type="button" class="cssbtn" id="btn_import" level="R" data-bs-toggle="modal"
data-bs-target="#uploadModal"><i data-bs-target="#uploadModal"><i
class="material-icons">upload</i> <%=localeMessage.getString("button.import")%> class="material-icons">upload</i> <%=localeMessage.getString("button.import")%>
@@ -396,16 +403,10 @@
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<tbody> <tbody>
<tr> <tr>
<th style="width:180px;">업무구분명</th>
<td>
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
</td>
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th> <th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th>
<td> <td>
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}"> <input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
</td> </td>
</tr>
<tr>
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th> <th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
<td> <td>
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}"> <input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
@@ -415,6 +416,23 @@
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}"> <input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
</td> </td>
</tr> </tr>
<tr>
<th style="width:180px;">업무구분명</th>
<td>
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
</td>
<th style="width:180px;">상태</th>
<td>
<select name="searchStatusCode" style="width: 100%">
<option value="">전체</option>
<option value="N">정상</option>
<option value="D">지연</option>
<option value="E">장애</option>
<option value="C">점검</option>
</select>
</td>
<th></th><td></td>
</tr>
</tbody> </tbody>
</table> </table>
<div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">(*) : 정렬가능항목</div> <div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">(*) : 정렬가능항목</div>
@@ -646,7 +646,7 @@
const svcDesc = encodeURI(encodeURIComponent($('#eaiSvcDesc').val())); const svcDesc = encodeURI(encodeURIComponent($('#eaiSvcDesc').val()));
const cnvsnDesc = encodeURI(encodeURIComponent(transformDesc)); const cnvsnDesc = encodeURI(encodeURIComponent(transformDesc));
var args = {title:transformName+' 수정', parent: true}; var args = {title:transformName+' 수정', parent: true};
var url='<c:url value="/onl/admin/rule/transform2/transform2Man.view"/>'; var url='<c:url value="/onl/admin/rule/transformMan.view"/>';
url += '?cmd=DETAIL'; url += '?cmd=DETAIL';
url += '&cnvsnName='+transformName; url += '&cnvsnName='+transformName;
url += '&eaiSvcName='+svcName; url += '&eaiSvcName='+svcName;
@@ -793,7 +793,7 @@
function updateFirstLayout(newTransformName, transformDesc){ function updateFirstLayout(newTransformName, transformDesc){
$.ajax({ $.ajax({
type : "POST", type : "POST",
url: '<c:url value="/onl/admin/rule/transform2/transform2Man.json" />', url: '<c:url value="/onl/admin/rule/transformMan.json" />',
data: { cmd: 'UPDATE', data: { cmd: 'UPDATE',
gridData: '[]', gridData: '[]',
eaiSvcName: svcName, eaiSvcName: svcName,
@@ -824,7 +824,7 @@
}; };
$.ajax({ $.ajax({
type : "POST", type : "POST",
url: '<c:url value="/onl/admin/rule/transform2/transform2Man.json" />', url: '<c:url value="/onl/admin/rule/transformMan.json" />',
data: { cmd: 'INSERT', data: { cmd: 'INSERT',
gridData: '[]', gridData: '[]',
cnvsnType, cnvsnType,
@@ -1608,8 +1608,10 @@
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button> <button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 --> <%-- v1: 기존 API 스펙 (모달). PTL_PROPERTY(Portal / djb.apispec.v1.enabled) 로 노출 제어. --%>
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL"><i class="material-icons">description</i> API 스펙(삭제예정)</button> <c:if test="${apiSpecV1Enabled}">
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL"><i class="material-icons">description</i> API 스펙</button>
</c:if>
<!-- v2: 신규 OpenAPI Spec (새 탭) --> <!-- v2: 신규 OpenAPI Spec (새 탭) -->
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL"><i class="material-icons">api</i> OpenAPI Spec</button> <button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL"><i class="material-icons">api</i> OpenAPI Spec</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button> <button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
@@ -55,10 +55,10 @@ $(document).ready(function() {
'200', '200',
'300', '300',
'400', '400',
'900', /* '900', */
'300', '300',
'400', '400',
'900', /* '900', */
'IF', 'IF',
'<%= localeMessage.getString("tranStat.recv") %>', '<%= localeMessage.getString("tranStat.recv") %>',
'<%= localeMessage.getString("common.total") %>', '<%= localeMessage.getString("common.total") %>',
@@ -75,11 +75,11 @@ $(document).ready(function() {
{ name : 'E200' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, { name : 'E200' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
{ name : 'E300' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, { name : 'E300' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
{ name : 'E400' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, { name : 'E400' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
{ name : 'E900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, /* { name : 'E900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, */
{ name : 'E9300' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, { name : 'E9300' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
{ name : 'E9400' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, { name : 'E9400' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
{ name : 'E9900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, /* { name : 'E9900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, */
{ name : 'AVGEAI' , align : 'center' , width:'46' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'avg'}, { name : 'AVGEAI' , align : 'center' , width:'46' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'avg'},
{ name : 'AVGPSV' , align : 'center' , width:'46' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'avg'}, { name : 'AVGPSV' , align : 'center' , width:'46' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'avg'},
@@ -152,8 +152,8 @@ $(document).ready(function() {
useColSpanStyle: true, useColSpanStyle: true,
groupHeaders:[ groupHeaders:[
{startColumnName: 'P100', numberOfColumns: 4, titleText: '<%= localeMessage.getString("tranStat.tranNo") %>'}, {startColumnName: 'P100', numberOfColumns: 4, titleText: '<%= localeMessage.getString("tranStat.tranNo") %>'},
{startColumnName: 'E100', numberOfColumns: 5, titleText: '<%= localeMessage.getString("tranStat.errNo") %>'}, {startColumnName: 'E100', numberOfColumns: 4, titleText: '<%= localeMessage.getString("tranStat.errNo") %>'},
{startColumnName: 'E9300', numberOfColumns: 3, titleText: '<%= localeMessage.getString("tranStat.timeout") %>'}, {startColumnName: 'E9300', numberOfColumns: 2, titleText: '<%= localeMessage.getString("tranStat.timeout") %>'},
{startColumnName: 'AVGEAI', numberOfColumns: 3, titleText: '<%= localeMessage.getString("tranStat.avgTime") %>'} {startColumnName: 'AVGEAI', numberOfColumns: 3, titleText: '<%= localeMessage.getString("tranStat.avgTime") %>'}
] ]
}); });
@@ -210,9 +210,9 @@ $(document).ready(function() {
var merge = new Array(); var merge = new Array();
merge.push(makeMerge("업무구분","0","1","0","0")); merge.push(makeMerge("업무구분","0","1","0","0"));
merge.push(makeMerge("구간별 처리건수","0","0","1","4")); merge.push(makeMerge("구간별 처리건수","0","0","1","4"));
merge.push(makeMerge("구간별 에러건수","0","0","5","9")); merge.push(makeMerge("구간별 에러건수","0","0","5","8"));
merge.push(makeMerge("타임아웃","0","0","10","12")); merge.push(makeMerge("타임아웃","0","0","9","11"));
merge.push(makeMerge("평균처리시간(초)","0","0","13","15")); merge.push(makeMerge("평균처리시간(초)","0","0","12","14"));
gridToExcelSubmit(url,"LIST_GRID_TO_EXCEL",$("#grid"),$("#ajaxForm"),"업무별 거래현황",merge); gridToExcelSubmit(url,"LIST_GRID_TO_EXCEL",$("#grid"),$("#ajaxForm"),"업무별 거래현황",merge);
return false; return false;
@@ -224,9 +224,9 @@ $(document).ready(function() {
var merge = new Array(); var merge = new Array();
merge.push(makeMerge("업무구분","0","1","0","0")); merge.push(makeMerge("업무구분","0","1","0","0"));
merge.push(makeMerge("구간별 처리건수","0","0","1","4")); merge.push(makeMerge("구간별 처리건수","0","0","1","4"));
merge.push(makeMerge("구간별 에러건수","0","0","5","9")); merge.push(makeMerge("구간별 에러건수","0","0","5","8"));
merge.push(makeMerge("타임아웃","0","0","10","12")); merge.push(makeMerge("타임아웃","0","0","9","11"));
merge.push(makeMerge("평균처리시간(초)","0","0","13","15")); merge.push(makeMerge("평균처리시간(초)","0","0","12","14"));
gridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",$("#grid"),$("#ajaxForm"),'업무별 거래현황',merge); gridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",$("#grid"),$("#ajaxForm"),'업무별 거래현황',merge);
return false; return false;
}); });
@@ -273,7 +273,6 @@ $(document).ready(function() {
<%-- <img src="<c:url value="/img/btn_10min.png" />" alt="" id="btn_10min" level="W" /> --%> <%-- <img src="<c:url value="/img/btn_10min.png" />" alt="" id="btn_10min" level="W" /> --%>
<button type="button" class="cssbtn" id="btn_detail" level="R" ><i class="material-icons">details</i> <%= localeMessage.getString("button.detail") %></button> <button type="button" class="cssbtn" id="btn_detail" level="R" ><i class="material-icons">details</i> <%= localeMessage.getString("button.detail") %></button>
<button type="button" class="cssbtn" id="btn_excel" level="W" ><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button> <button type="button" class="cssbtn" id="btn_excel" level="W" ><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button>
<button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button>
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button> <button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
<%-- <img src="<c:url value="/img/btn_detail.png" />" alt="" id="btn_detail" level="R" /> --%> <%-- <img src="<c:url value="/img/btn_detail.png" />" alt="" id="btn_detail" level="R" /> --%>
<%-- <img src="<c:url value="/img/btn_excel.png" />" alt="" id="btn_excel" level="W" /> --%> <%-- <img src="<c:url value="/img/btn_excel.png" />" alt="" id="btn_excel" level="W" /> --%>
@@ -282,6 +281,9 @@ $(document).ready(function() {
<%-- <img id="btn_excel" src="<c:url value="/img/btn_excel.png"/>" level="W" status="DETAIL"/> --%> <%-- <img id="btn_excel" src="<c:url value="/img/btn_excel.png"/>" level="W" status="DETAIL"/> --%>
</div> </div>
<div class="title"><%= localeMessage.getString("tranStat.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div> <div class="title"><%= localeMessage.getString("tranStat.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div>
<div style="padding-bottom: 10px; color: #555">
※ 100 : 송신>APIM &nbsp; 200 : APIM>수신 &nbsp; 300 : 수신>APIM &nbsp; 400 : APIM>송신
</div>
<ajax> <ajax>
<table id="grid" ></table> <table id="grid" ></table>
</ajax> </ajax>
@@ -59,10 +59,10 @@ function w2GridToExcelSubmit(url,cmd,gridObj,formObject,fileName,merge){
records[i]['E200'] = data[i]['E200'].toString(); records[i]['E200'] = data[i]['E200'].toString();
records[i]['E300'] = data[i]['E300'].toString(); records[i]['E300'] = data[i]['E300'].toString();
records[i]['E400'] = data[i]['E400'].toString(); records[i]['E400'] = data[i]['E400'].toString();
records[i]['E900'] = data[i]['E900'].toString(); /* records[i]['E900'] = data[i]['E900'].toString(); */
records[i]['E9300'] = data[i]['E9300'].toString(); records[i]['E9300'] = data[i]['E9300'].toString();
records[i]['E9400'] = data[i]['E9400'].toString(); records[i]['E9400'] = data[i]['E9400'].toString();
records[i]['E9900'] = data[i]['E9900'].toString(); /* records[i]['E9900'] = data[i]['E9900'].toString(); */
records[i]['BIZNAME'] = data[i]['BIZNAME']; records[i]['BIZNAME'] = data[i]['BIZNAME'];
records[i]['SVCCODE'] = data[i]['SVCCODE']; records[i]['SVCCODE'] = data[i]['SVCCODE'];
@@ -245,8 +245,8 @@ $( document ).ready(function() {
{span : 4, caption:'<%= localeMessage.getString("tranStat.interface") %>'}, {span : 4, caption:'<%= localeMessage.getString("tranStat.interface") %>'},
{span : 4, caption:'<%= localeMessage.getString("tranStat.tranNo") %>'}, {span : 4, caption:'<%= localeMessage.getString("tranStat.tranNo") %>'},
{span : 3, caption:'<%= localeMessage.getString("tranStat.avgTime") %>'}, {span : 3, caption:'<%= localeMessage.getString("tranStat.avgTime") %>'},
{span : 5, caption:'<%= localeMessage.getString("tranStat.errNo") %>'}, {span : 4, caption:'<%= localeMessage.getString("tranStat.errNo") %>'},
{span : 3, caption:'<%= localeMessage.getString("tranStat.timeout") %>'} {span : 2, caption:'<%= localeMessage.getString("tranStat.timeout") %>'}
], ],
columns: [ columns: [
@@ -269,11 +269,11 @@ $( document ).ready(function() {
{ field : 'E200' ,type:'text' ,caption : '200' , size: '65px' ,style:'text-align:right', render:thousandsSeparator }, { field : 'E200' ,type:'text' ,caption : '200' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
{ field : 'E300' ,type:'text' ,caption : '300' , size: '65px' ,style:'text-align:right', render:thousandsSeparator }, { field : 'E300' ,type:'text' ,caption : '300' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
{ field : 'E400' ,type:'text' ,caption : '400' , size: '65px' ,style:'text-align:right', render:thousandsSeparator }, { field : 'E400' ,type:'text' ,caption : '400' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
{ field : 'E900' ,type:'text' ,caption : '900' , size: '65px' ,style:'text-align:right', render:thousandsSeparator }, /* { field : 'E900' ,type:'text' ,caption : '900' , size: '65px' ,style:'text-align:right', render:thousandsSeparator }, */
{ field : 'E9300' ,type:'text' ,caption : '300' , size: '50px' ,style:'text-align:right', render:thousandsSeparator }, { field : 'E9300' ,type:'text' ,caption : '300' , size: '50px' ,style:'text-align:right', render:thousandsSeparator },
{ field : 'E9400' ,type:'text' ,caption : '400' , size: '50px' ,style:'text-align:right', render:thousandsSeparator }, { field : 'E9400' ,type:'text' ,caption : '400' , size: '50px' ,style:'text-align:right', render:thousandsSeparator },
{ field : 'E9900' ,type:'text' ,caption : '900' , size: '50px' ,style:'text-align:right', render:thousandsSeparator } /* { field : 'E9900' ,type:'text' ,caption : '900' , size: '50px' ,style:'text-align:right', render:thousandsSeparator } */
] ]
,summary:[ ,summary:[
@@ -346,8 +346,8 @@ $( document ).ready(function() {
merge.push(makeMerge("API","0","0","0","3")); merge.push(makeMerge("API","0","0","0","3"));
merge.push(makeMerge("전문구간별 처리건수","0","0","4","7")); merge.push(makeMerge("전문구간별 처리건수","0","0","4","7"));
merge.push(makeMerge("평균처리시간(초)","0","0","8","10")); merge.push(makeMerge("평균처리시간(초)","0","0","8","10"));
merge.push(makeMerge("전문구간별 에러건수","0","0","11","15")); merge.push(makeMerge("전문구간별 에러건수","0","0","11","14"));
merge.push(makeMerge("타임아웃","0","0","16","18")); merge.push(makeMerge("타임아웃","0","0","15","16"));
w2GridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",w2ui['myGrid'],$("#ajaxForm"),"전문별 거래현황",merge); w2GridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",w2ui['myGrid'],$("#ajaxForm"),"전문별 거래현황",merge);
return false; return false;
@@ -376,8 +376,8 @@ $( document ).ready(function() {
merge.push(makeMerge("API","0","0","0","3")); merge.push(makeMerge("API","0","0","0","3"));
merge.push(makeMerge("구간별 처리건수","0","0","4","7")); merge.push(makeMerge("구간별 처리건수","0","0","4","7"));
merge.push(makeMerge("평균처리시간(초)","0","0","8","10")); merge.push(makeMerge("평균처리시간(초)","0","0","8","10"));
merge.push(makeMerge("구간별 에러건수","0","0","11","15")); merge.push(makeMerge("구간별 에러건수","0","0","11","14"));
merge.push(makeMerge("타임아웃","0","0","16","18")); merge.push(makeMerge("타임아웃","0","0","15","16"));
w2GridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",w2ui['myGrid'],$("#ajaxForm"),"전문별 거래현황",merge); w2GridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",w2ui['myGrid'],$("#ajaxForm"),"전문별 거래현황",merge);
return false; return false;
@@ -429,7 +429,6 @@ $( document ).ready(function() {
<input type="checkbox" id="recentYnVal" value="N"/>&nbsp;<b><%= localeMessage.getString("tranStat.rec10Min") %></b>&nbsp;&nbsp; <input type="checkbox" id="recentYnVal" value="N"/>&nbsp;<b><%= localeMessage.getString("tranStat.rec10Min") %></b>&nbsp;&nbsp;
<button type="button" class="cssbtn" id="btn_detail" level="R" ><i class="material-icons">details</i> <%= localeMessage.getString("button.detail") %></button> <button type="button" class="cssbtn" id="btn_detail" level="R" ><i class="material-icons">details</i> <%= localeMessage.getString("button.detail") %></button>
<button type="button" class="cssbtn" id="btn_excel" level="W" ><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button> <button type="button" class="cssbtn" id="btn_excel" level="W" ><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button>
<button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button>
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button> <button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
<%-- <img src="<c:url value="/img/btn_10min.png"/>" alt="" id="btn_10min" level="W" /> --%> <%-- <img src="<c:url value="/img/btn_10min.png"/>" alt="" id="btn_10min" level="W" /> --%>
<%-- <img src="<c:url value="/img/btn_detail.png"/>" alt="" id="btn_detail" level="R" /> --%> <%-- <img src="<c:url value="/img/btn_detail.png"/>" alt="" id="btn_detail" level="R" /> --%>
@@ -438,6 +437,9 @@ $( document ).ready(function() {
<%-- <img src="<c:url value="/img/btn_search.png"/>" alt="" id="btn_search" level="R" /> --%> <%-- <img src="<c:url value="/img/btn_search.png"/>" alt="" id="btn_search" level="R" /> --%>
</div> </div>
<div class="title"><%= localeMessage.getString("tranStatDetail.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div> <div class="title"><%= localeMessage.getString("tranStatDetail.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div>
<div style="padding-bottom: 10px; color: #555">
※ 100 : 송신>APIM &nbsp; 200 : APIM>수신 &nbsp; 300 : 수신>APIM &nbsp; 400 : APIM>송신
</div>
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<tbody> <tbody>
<tr> <tr>
@@ -24,6 +24,7 @@
var roleString = "<%=SessionManager.getRoleIdString(request)%>"; var roleString = "<%=SessionManager.getRoleIdString(request)%>";
let totalLogs = 0; let totalLogs = 0;
let displayedLogs = 0; let displayedLogs = 0;
var bizErrCodeSet = new Set(); // ERRCODE_BIZ(업무 오류) 코드 목록 - 그리드 배경색 구분용
function adminChk() { function adminChk() {
var arrRoleID = roleString.split(','); var arrRoleID = roleString.split(',');
@@ -106,6 +107,8 @@
new makeOptions("CODE", "NAME").setObj($("select[name=searchExtBizCd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.extBizList).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE", "NAME").setObj($("select[name=searchExtBizCd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.extBizList).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE", "NAME").setObj($("select[name=searchLogPrcssSerno]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.logPrcssSernoList).rendering(); new makeOptions("CODE", "NAME").setObj($("select[name=searchLogPrcssSerno]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.logPrcssSernoList).rendering();
bizErrCodeSet = new Set((json.bizErrCodeList || []).map(function (c) { return c.CODE; }));
setSearchable("searchEaiBzwkDstcd"); // 콤보에 searchable 설정 setSearchable("searchEaiBzwkDstcd"); // 콤보에 searchable 설정
setSearchable("searchExtBizCd"); setSearchable("searchExtBizCd");
@@ -222,63 +225,6 @@
$("input[name=searchEndHHMM]").val(endHHMM); $("input[name=searchEndHHMM]").val(endHHMM);
} }
function guidEvent() {
var searchGuid = $("input[name=searchGuid]").val();
if (searchGuid.trim() == '')
return;
var searchTm = searchGuid.trim();
/*
var yy = searchTm.substr(13,4);
var mm = searchTm.substr(17,2);
var dd = searchTm.substr(19,2);
var hh = searchTm.substr(21,2);
var MM = searchTm.substr(23,2);
*/
var yy = searchTm.substr(3,4);
var mm = searchTm.substr(7,2);
var dd = searchTm.substr(9,2);
var hh = searchTm.substr(11,2);
var MM = searchTm.substr(13,2);
var ms = Date.UTC(yy,mm-1,dd,hh,MM,00);
var date = new Date(ms);
var startYYYYMMDD;
var startHHMM;
var endYYYYMMDD;
var endHHMM;
//Local Time Setting
date.setHours(date.getHours() - 9);
//5분 간격
date.setMinutes(date.getMinutes() - 5);
startYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
startHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
date.setMinutes(date.getMinutes() + 10);
endYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
endHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
//추가 코드 입력 2012.05.16 권세환 (일자가 바뀔경우 시간 간격을 +-5분 -> +-1분)
if (startYYYYMMDD.substr(6, 2) != endYYYYMMDD.substr(6, 2)) {
date.setMinutes(date.getMinutes() - 6);
startYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
startHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
date.setMinutes(date.getMinutes() + 2);
endYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
endHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
}
$("input[name=searchStartYYYYMMDD]").val(startYYYYMMDD);
$("input[name=searchEndYYYYMMDD]").val(endYYYYMMDD);
$("input[name=searchStartHHMM]").val(startHHMM);
$("input[name=searchEndHHMM]").val(endHHMM);
}
function timeStampFormat2(cellvalue, options, rowObject) { function timeStampFormat2(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue.length < 17) if (cellvalue == null || cellvalue.length < 17)
return ''; return '';
@@ -368,7 +314,8 @@
'<%=localeMessage.getString("tracking.companyitcontactname")%>', '<%=localeMessage.getString("tracking.companyitcontactname")%>',
'<%=localeMessage.getString("tracking.companyitcontacttelno")%>', '<%=localeMessage.getString("tracking.companyitcontacttelno")%>',
'<%=localeMessage.getString("tracking.itcontactpointperson1")%>', '<%=localeMessage.getString("tracking.itcontactpointperson1")%>',
'<%=localeMessage.getString("tracking.bzwkcontactperson")%>' '<%=localeMessage.getString("tracking.bzwkcontactperson")%>',
'EAIERRCD'
], ],
colModel: [ colModel: [
{name: 'EAISVCSERNO', hidden: true}, {name: 'EAISVCSERNO', hidden: true},
@@ -398,7 +345,8 @@
{name: 'COMPANYITCONTACTNAME', align: 'left', width: '110', hidden: true}, {name: 'COMPANYITCONTACTNAME', align: 'left', width: '110', hidden: true},
{name: 'COMPANYITCONTACTTELNO', align: 'left', width: '120', hidden: true}, {name: 'COMPANYITCONTACTTELNO', align: 'left', width: '120', hidden: true},
{name: 'ITCONTACTPOINTPERSON1', align: 'left', width: '120', hidden: true}, {name: 'ITCONTACTPOINTPERSON1', align: 'left', width: '120', hidden: true},
{name: 'BZWKCONTACTPERSON', align: 'left', width: '120', hidden: true} {name: 'BZWKCONTACTPERSON', align: 'left', width: '120', hidden: true},
{name: 'EAIERRCD', hidden: true}
], ],
jsonReader: { jsonReader: {
repeatitems: false repeatitems: false
@@ -435,7 +383,7 @@
}, },
loadComplete: function (d) { loadComplete: function (d) {
$("#grid").tuiTableRowSpan("7");
// 거래 건수 표시 // 거래 건수 표시
var uniqueCount = calculateUniqueLogCount("#grid"); var uniqueCount = calculateUniqueLogCount("#grid");
@@ -444,14 +392,22 @@
//에러 행 색상 표시 //에러 행 색상 표시
var rows = $(this).getDataIDs(); var rows = $(this).getDataIDs();
for (var i = 0; i < rows.length; i++) { for (var i = 0; i < rows.length; i++) {
var val = $("#grid").getCell(rows[i], 'ERRYN'); var errYn = $("#grid").getCell(rows[i], 'ERRYN');
if (val == "Y") { var eaiErrCd = $("#grid").getCell(rows[i], 'EAIERRCD');
var isBizErr = eaiErrCd != null && bizErrCodeSet.has(String(eaiErrCd).trim());
if (isBizErr) {
// 업무(BIZ) 오류코드(ERRCODE_BIZ) - 하늘색 배경
$("#" + rows[i]).find("td").css("background-color", "#BEEDFF");
} else if (errYn == "Y") {
$("#" + rows[i]).find("td").css("background-color", "#FFCC99"); $("#" + rows[i]).find("td").css("background-color", "#FFCC99");
} }
} }
$("#grid").tuiTableRowSpan("7");
//1000건이상 alert //1000건이상 alert
if (d.records == 1000) { if (d.records >= 1000) {
setTimeout(() => { setTimeout(() => {
alert("<%=localeMessage.getString("tracking.alert5")%>"); // 조회 결과 1,000건 초과 alert("<%=localeMessage.getString("tracking.alert5")%>"); // 조회 결과 1,000건 초과
}, 0); // Alert는 비동기적으로 처리 }, 0); // Alert는 비동기적으로 처리
@@ -481,7 +437,6 @@
//$("#grid").jqGrid('setFrozenColumns'); //$("#grid").jqGrid('setFrozenColumns');
$("#btn_search").click(function () { $("#btn_search").click(function () {
$("input[name=searchGuid]").blur();
detail(); detail();
getTransactionCounts(); getTransactionCounts();
}); });
@@ -551,12 +506,6 @@
return false; return false;
}); });
//GUID
$("input[name=searchGuid]").blur(function (event) {
guidEvent();
});
buttonControl(); buttonControl();
var local = localStorage.getItem("guid"); var local = localStorage.getItem("guid");
@@ -701,7 +650,7 @@
<td colspan="3"><input type="text" name="searchEaiSvcSerno" value="${param.searchEaiSvcSerno}"></td> <td colspan="3"><input type="text" name="searchEaiSvcSerno" value="${param.searchEaiSvcSerno}"></td>
<th><%=localeMessage.getString("tracking.keymgtmsgctnt")%></th> <th><%=localeMessage.getString("tracking.keymgtmsgctnt")%></th>
<td colspan="2"><input type="text" name="searchKeyMgtMsgCtnt" value="${param.searchKeyMgtMsgCtnt}"></td> <td colspan="2"><input type="text" name="searchGuid" value="${param.searchGuid}" ></td>
<%-- <th><%=localeMessage.getString("tracking.orgGUID")%></th> <%-- <th><%=localeMessage.getString("tracking.orgGUID")%></th>
<td colspan="4"><input type="text" name="searchGuid" value="" maxlength="<%=DbTrackingService.GUID_LENGTH%>"></td> --%> <td colspan="4"><input type="text" name="searchGuid" value="" maxlength="<%=DbTrackingService.GUID_LENGTH%>"></td> --%>
@@ -23,6 +23,9 @@
white-space: normal; white-space: normal;
} }
.table_row td { padding: 0 5px; }
.table_row input { box-sizing: border-box; }
</style> </style>
<script language="javascript" > <script language="javascript" >
// var eaiBzwkDstcd = window.dialogArguments["eaiBzwkDstcd"]; // var eaiBzwkDstcd = window.dialogArguments["eaiBzwkDstcd"];
@@ -253,7 +256,7 @@
var name = $(this).attr("name"); var name = $(this).attr("name");
if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정 if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정
$("#"+name).show(); $("#"+name).show();
$("input[name="+name+"]").css("width","185px"); $("input[name="+name+"]").css("width","calc(100% - 60px)");
} }
}); });
@@ -261,7 +264,7 @@
var name = $(this).attr("name"); var name = $(this).attr("name");
if($("input[name="+name+"]").val().trim() != ""){ if($("input[name="+name+"]").val().trim() != ""){
$("#"+name).show(); $("#"+name).show();
$("input[name="+name+"]").css("width","185px"); $("input[name="+name+"]").css("width","calc(100% - 60px)");
} }
}); });
@@ -376,7 +379,7 @@
const value = bodyCodeEditor.getValue(); const value = bodyCodeEditor.getValue();
let prettyValue = value; let prettyValue = value;
if(syntax === 'JSN'){ if(syntax === 'JSN'){
prettyValue = vkbeautify.json(value); prettyValue = preserveExponential(value); //vkbeautify.json(value);
}else if(syntax === 'XML'){ }else if(syntax === 'XML'){
prettyValue = vkbeautify.xml(value).trim(); prettyValue = vkbeautify.xml(value).trim();
} }
@@ -407,6 +410,19 @@
} }
}); });
} }
function preserveExponential(jsonString) {
// 1. 지수 형태의 숫자(예: 1e+21, 5.43e-7)를 찾아 문자열 ""로 감쌉니다.
// JSON의 key 뒤에 오는 value 위치의 지수 표기법을 매칭합니다.
let protectedJson = jsonString.replace(/:\s*([+-]?\d+\.?\d*e[+-]?\d+)/gi, ': "$1"');
// 2. 보호된 상태로 vkbeautify를 실행하여 예쁘게 정렬합니다.
let prettyJson = vkbeautify.json(protectedJson);
// 3. (선택사항) 만약 최종 결과물에서 반드시 큰따옴표("")를 다시 제거해야 한다면 아래 주석을 해제하세요.
prettyJson = prettyJson.replace(/:\s*"([+-]?\d+\.?\d*e[+-]?\d+)"/gi, ': $1');
return prettyJson;
}
function doChangeSyntax(syntax){ function doChangeSyntax(syntax){
if(syntax === 'JSN'){ if(syntax === 'JSN'){
$('#btn_pretty_body').css('display', 'block'); $('#btn_pretty_body').css('display', 'block');
@@ -789,6 +805,35 @@ $(document).ready(function() {
$("html, hbody").css("overflow-x","scroll"); $("html, hbody").css("overflow-x","scroll");
} }
function copyTextToClipboard(text, successMessage){
if (typeof(navigator.clipboard) == 'undefined'){
var textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
var successful = false;
try{
successful = document.execCommand('copy');
}catch(err){
}
document.body.removeChild(textArea);
if (successful){
alert(successMessage);
}else{
prompt("Ctrl+C 를 눌러 클립보드로 복사하세요.", text);
}
}else{
navigator.clipboard.writeText(text).then(function(){
alert(successMessage);
}).catch(function(){
prompt("클립보드 복사에 실패했습니다. Ctrl+C 를 눌러 복사하세요.", text);
});
}
}
$('#headerToClipboard').click(function(){ $('#headerToClipboard').click(function(){
if($('#grid1').get(0) == null || $('#grid1').get(0).jexcel == null){ if($('#grid1').get(0) == null || $('#grid1').get(0).jexcel == null){
return; return;
@@ -800,13 +845,11 @@ $(document).ready(function() {
paddedValues += row[5].padEnd(row[4], " "); paddedValues += row[5].padEnd(row[4], " ");
} }
navigator.clipboard.writeText(paddedValues);//클립보드로 복사 copyTextToClipboard(paddedValues, '헤더값이 복사되었습니다.');
alert('헤더값이 복사되었습니다.')
}); });
$('#messageToClipboard').click(function() { $('#messageToClipboard').click(function() {
navigator.clipboard.writeText(fullMessageData);//클립보드로 복사 copyTextToClipboard(fullMessageData, '전체 전문 복사되었습니다.');
alert('전체 전문 복사되었습니다.')
}); });
$('#btn_pretty_print').click(function () { $('#btn_pretty_print').click(function () {
@@ -937,15 +980,23 @@ html,hbody {
<input type=hidden name=realHeader> <input type=hidden name=realHeader>
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.common")%></div> <div class="table_row_title"><%=localeMessage.getString("trackingDetail.common")%></div>
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col style="width:180px"/>
<col style="width:300px" />
<col style="width:180px"/>
<col style="width:300px" />
</colgroup>
<tr> <tr>
<th><%=localeMessage.getString("trackingDetail.eaiSvcSerno")%></th> <th><%=localeMessage.getString("trackingDetail.eaiSvcSerno")%></th>
<td colspan="3"><input type="text" name="eaiSvcSerno" readonly="readonly"/> </td> <td><input type="text" name="eaiSvcSerno" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th>
<td><input type="text" name="rspnsErrcdName" readonly="readonly"/> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.eaiBzwkDstcd")%></th> <th><%=localeMessage.getString("trackingDetail.eaiBzwkDstcd")%></th>
<td style="width:30%;"><input type="text" name="eaiBzwkDstcd" readonly="readonly"/> </td> <td><input type="text" name="eaiBzwkDstcd" readonly="readonly"/> </td>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.msgDpstYMS")%></th> <th><%=localeMessage.getString("trackingDetail.msgDpstYMS")%></th>
<td style="width:30%;"><input type="text" name="msgDpstYMS" readonly="readonly"/> </td> <td><input type="text" name="msgDpstYMS" readonly="readonly"/> </td>
</tr> </tr>
<tr> <tr>
<th><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th><td><input type="text" name="eaiSvcName" readonly="readonly" /> <th><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th><td><input type="text" name="eaiSvcName" readonly="readonly" />
@@ -979,11 +1030,17 @@ html,hbody {
</table> </table>
<div class="table_row_title">API 정보</div> <div class="table_row_title">API 정보</div>
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col style="width:180px"/>
<col style="width:300px" />
<col style="width:180px"/>
<col style="width:300px" />
</colgroup>
<tr> <tr>
<th style="width:20%;">APP(Client) ID</th> <th>APP(Client) ID</th>
<td style="width:30%;"><input type="text" name="clientId" readonly="readonly"/> </td> <td><input type="text" name="clientId" readonly="readonly"/> </td>
<th style="width:20%;">APP 명</th> <th>APP 명</th>
<td style="width:30%;"><input type="text" name="clientName" readonly="readonly"/> </td> <td><input type="text" name="clientName" readonly="readonly"/> </td>
</tr> </tr>
<tr> <tr>
<th>법인 ID</th><td ><input type="text" name="orgId" readonly="readonly"/> </td> <th>법인 ID</th><td ><input type="text" name="orgId" readonly="readonly"/> </td>
@@ -1105,20 +1162,27 @@ html,hbody {
</div><!-- end.container-1 --> </div><!-- end.container-1 -->
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.inbound")%></div> <div class="table_row_title"><%=localeMessage.getString("trackingDetail.inbound")%></div>
<table class="table_row" cellspacing="0" > <table class="table_row" cellspacing="0" >
<colgroup>
<col style="width:180px"/>
<col style="width:300px" />
<col style="width:180px"/>
<col style="width:300px" />
</colgroup>
<tr> <tr>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.svcMotivUseDstcd")%></th> <%-- <th style="width:20%;"><%=localeMessage.getString("trackingDetail.svcMotivUseDstcd")%></th>
<td style="width:30%;"><input type="text" name="svcMotivUseDstcd" readonly="readonly"/> </td> <td style="width:30%;"><input type="text" name="svcMotivUseDstcd" readonly="readonly"/> </td> --%>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.gstatSysAdptrBzwkGroupName")%></th> <th style="width:20%;"><%=localeMessage.getString("trackingDetail.gstatSysAdptrBzwkGroupName")%></th>
<td style="width:30%;"><input type="text" name="gstatSysAdptrBzwkGroupName" readonly="readonly"/> <td style="width:30%;"><input type="text" name="gstatSysAdptrBzwkGroupName" readonly="readonly" style="width: calc(100% - 60px);"/>
<%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="gstatSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%> <%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="gstatSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%>
<button type="button" class="cssbtn smallBtn2" id="gstatSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button> <button type="button" class="cssbtn smallBtn2" id="gstatSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
</td> </td>
</tr>
<tr>
<th><%=localeMessage.getString("trackingDetail.flowCtrlRoutName")%></th><td><input type="text" name="flowCtrlRoutName" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.stndMsgUseYn")%></th><td><input type="text" name="stndMsgUseYn" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.stndMsgUseYn")%></th><td><input type="text" name="stndMsgUseYn" readonly="readonly"/> </td>
</tr> </tr>
<tr> <%-- <tr>
<th><%=localeMessage.getString("trackingDetail.flowCtrlRoutName")%></th><td><input type="text" name="flowCtrlRoutName" readonly="readonly"/> </td>
</tr> --%>
<%-- <tr>
<th><%=localeMessage.getString("trackingDetail.svcPrcssDsticName")%></th><td><input type="text" name="svcPrcssDsticName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.svcPrcssDsticName")%></th><td><input type="text" name="svcPrcssDsticName" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.svcBfClmnLogYn")%></th><td><input type="text" name="svcBfClmnLogYn" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.svcBfClmnLogYn")%></th><td><input type="text" name="svcBfClmnLogYn" readonly="readonly"/> </td>
</tr> </tr>
@@ -1129,39 +1193,52 @@ html,hbody {
<tr> <tr>
<th><%=localeMessage.getString("trackingDetail.gstatSvcDsticName")%></th><td><input type="text" name="gstatSvcDsticName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.gstatSvcDsticName")%></th><td><input type="text" name="gstatSvcDsticName" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.prsntMsgIdName")%></th><td><input type="text" name="prsntMsgIdName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.prsntMsgIdName")%></th><td><input type="text" name="prsntMsgIdName" readonly="readonly"/> </td>
</tr> </tr> --%>
</table> </table>
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.outbound")%></div> <div class="table_row_title"><%=localeMessage.getString("trackingDetail.outbound")%></div>
<table class="table_row" cellspacing="0" > <table class="table_row" cellspacing="0" >
<colgroup>
<col style="width:180px"/>
<col style="width:300px" />
<col style="width:180px"/>
<col style="width:300px" />
</colgroup>
<tr> <tr>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvIntfacDsticName")%></th> <%-- <th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvIntfacDsticName")%></th>--%>
<td style="width:30%;"><input type="text" name="psvIntfacDsticName" readonly="readonly"/> </td> <%-- <td style="width:30%;"><input type="text" name="psvIntfacDsticName" readonly="readonly"/> </td> --%>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvSysAdptrBzwkGroupName")%></th> <th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvSysAdptrBzwkGroupName")%></th>
<td style="width:30%;"><input type="text" name="psvSysAdptrBzwkGroupName" readonly="readonly"/> <td style="width:30%;"><input type="text" name="psvSysAdptrBzwkGroupName" readonly="readonly" style="width: calc(100% - 60px);"/>
<%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="psvSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%> <%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="psvSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%>
<button type="button" class="cssbtn smallBtn2" id="psvSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button> <button type="button" class="cssbtn smallBtn2" id="psvSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
</td> </td>
<th><%=localeMessage.getString("trackingDetail.psvBzwkSysName")%></th><td><input type="text" name="psvBzwkSysName" readonly="readonly"/> </td>
</tr> </tr>
<tr> <tr>
<th><%=localeMessage.getString("trackingDetail.outbndRoutName")%></th><td><input type="text" name="outbndRoutName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.outbndRoutName")%></th><td><input type="text" name="outbndRoutName" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.toutVal")%></th><td><input type="text" name="toutVal" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.toutVal")%></th><td><input type="text" name="toutVal" readonly="readonly"/> </td>
</tr> </tr>
<tr> <%-- <tr>
<th><%=localeMessage.getString("trackingDetail.psvSysSvcDsticName")%></th><td><input type="text" name="psvSysSvcDsticName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.psvSysSvcDsticName")%></th><td><input type="text" name="psvSysSvcDsticName" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.psvBzwkSysName")%></th><td><input type="text" name="psvBzwkSysName" readonly="readonly"/> </td>
</tr> </tr>--%>
<tr> <%-- <tr>
<th><%=localeMessage.getString("trackingDetail.rspnsErrFldName")%></th><td><input type="text" name="rspnsErrFldName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.rspnsErrFldName")%></th><td><input type="text" name="rspnsErrFldName" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.psvSysIdName")%></th><td><input type="text" name="psvSysIdName" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.psvSysIdName")%></th><td><input type="text" name="psvSysIdName" readonly="readonly"/> </td>
</tr> </tr> --%>
</table> </table>
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.MAPPING")%></div> <div class="table_row_title"><%=localeMessage.getString("trackingDetail.MAPPING")%></div>
<table class="table_row" cellspacing="0" > <table class="table_row" cellspacing="0" >
<colgroup>
<col style="width:180px"/>
<col style="width:300px" />
<col style="width:180px"/>
<col style="width:300px" />
</colgroup>
<tr> <tr>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.chngYn")%></th> <th><%=localeMessage.getString("trackingDetail.chngYn")%></th>
<td style="width:30%;"><input type="text" name="chngYn" readonly="readonly"/> </td> <td><input type="text" name="chngYn" readonly="readonly"/> </td>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.chngMsgIdName")%></th> <th><%=localeMessage.getString("trackingDetail.chngMsgIdName")%></th>
<td style="width:30%;"><input type="text" name="chngMsgIdName" readonly="readonly"/> <td><input type="text" name="chngMsgIdName" readonly="readonly" style="width: calc(100% - 60px);"/>
<!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="chngMsgIdName" class="layout_btn" /--> <!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="chngMsgIdName" class="layout_btn" /-->
<button type="button" class="cssbtn smallBtn2" id="chngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button> <button type="button" class="cssbtn smallBtn2" id="chngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
</td> </td>
@@ -1170,7 +1247,7 @@ html,hbody {
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngYn")%></th><td><input type="text" name="bascRspnsChngYn" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.bascRspnsChngYn")%></th><td><input type="text" name="bascRspnsChngYn" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngMsgIdName")%></th> <th><%=localeMessage.getString("trackingDetail.bascRspnsChngMsgIdName")%></th>
<td> <td>
<input type="text" name="bascRspnsChngMsgIdName" readonly="readonly"/> <input type="text" name="bascRspnsChngMsgIdName" readonly="readonly" style="width: calc(100% - 60px);"/>
<!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="bascRspnsChngMsgIdName" class="layout_btn" /--> <!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="bascRspnsChngMsgIdName" class="layout_btn" /-->
<button type="button" class="cssbtn smallBtn2" id="bascRspnsChngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button> <button type="button" class="cssbtn smallBtn2" id="bascRspnsChngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
</td> </td>
@@ -1180,10 +1257,10 @@ html,hbody {
<th><%=localeMessage.getString("trackingDetail.errRspnsChngYn")%></th><td><input type="text" name="errRspnsChngYn" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.errRspnsChngYn")%></th><td><input type="text" name="errRspnsChngYn" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.errRspnsChngMsgIdName")%></th> <th><%=localeMessage.getString("trackingDetail.errRspnsChngMsgIdName")%></th>
<td> <td>
<input type="text" name="errRspnsChngMsgIdName" readonly="readonly"/> <input type="text" name="errRspnsChngMsgIdName" readonly="readonly" style="width: calc(100% - 60px);"/>
<img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="errRspnsChngMsgIdName" class="layout_btn" /> </td> <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="errRspnsChngMsgIdName" class="layout_btn" /> </td>
</tr> </tr>
<tr> <%-- <tr>
<th><%=localeMessage.getString("trackingDetail.inptMsgIDName")%></th> <th><%=localeMessage.getString("trackingDetail.inptMsgIDName")%></th>
<td> <td>
<input type="text" name="inptMsgIDName" readonly="readonly" /> <input type="text" name="inptMsgIDName" readonly="readonly" />
@@ -1193,9 +1270,9 @@ html,hbody {
<tr> <tr>
<th><%=localeMessage.getString("trackingDetail.bascRspnsMsgCmprCtnt")%></th><td><input type="text" name="bascRspnsMsgCmprCtnt" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.bascRspnsMsgCmprCtnt")%></th><td><input type="text" name="bascRspnsMsgCmprCtnt" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.flovrYn")%></th><td><input type="text" name="flovrYn" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.flovrYn")%></th><td><input type="text" name="flovrYn" readonly="readonly"/> </td>
</tr> </tr> --%>
</table> </table>
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.etc")%></div> <%-- <div class="table_row_title"><%=localeMessage.getString("trackingDetail.etc")%></div>
<table class="table_row" cellspacing="0" > <table class="table_row" cellspacing="0" >
<tr> <tr>
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th> <th style="width:20%;"><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th>
@@ -1227,7 +1304,7 @@ html,hbody {
<th><%=localeMessage.getString("trackingDetail.trackAsisKey3Ctnt")%></th><td><input type="text" name="trackAsisKey3Ctnt" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.trackAsisKey3Ctnt")%></th><td><input type="text" name="trackAsisKey3Ctnt" readonly="readonly"/> </td>
<th><%=localeMessage.getString("trackingDetail.trackAsisKey4Ctnt")%></th><td><input type="text" name="trackAsisKey4Ctnt" readonly="readonly"/> </td> <th><%=localeMessage.getString("trackingDetail.trackAsisKey4Ctnt")%></th><td><input type="text" name="trackAsisKey4Ctnt" readonly="readonly"/> </td>
</tr> </tr>
</table> </table> --%>
</form> </form>
</div><!-- end.popup_box --> </div><!-- end.popup_box -->
<div id="bizLogObjectDialog" style="white-space: pre-wrap; overflow-y:auto" title="Biz Log Object Detail"> <div id="bizLogObjectDialog" style="white-space: pre-wrap; overflow-y:auto" title="Biz Log Object Detail">
@@ -1682,7 +1682,7 @@ $(document).ready(function () {
var args = new Object(); var args = new Object();
var url = '<c:url value="/onl/admin/rule/layoutMan.view" />'; var url = '<c:url value="/onl/admin/rule/layoutMan.view" />';
url += "?cmd=DETAILPOPUP"; url += "?cmd=DETAILPOPUP";
url += '&loutName='+layoutName; url += '&searchLoutName='+layoutName;
url += '&pop=true'; url += '&pop=true';
showModal2(url,args,890,940,'','scroll:on'); showModal2(url,args,890,940,'','scroll:on');
+343
View File
@@ -0,0 +1,343 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@
page import="org.quartz.*,
org.quartz.impl.matchers.GroupMatcher,
org.springframework.web.context.WebApplicationContext,
org.springframework.web.context.support.WebApplicationContextUtils,
java.util.*,
java.text.SimpleDateFormat"
%><%!
private String fmt(Date date) {
if (date == null) return "-";
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
}
private String relative(Date date) {
if (date == null) return "";
long diff = date.getTime() - System.currentTimeMillis();
String prefix = diff >= 0 ? "+" : "-";
long abs = Math.abs(diff) / 1000;
if (abs < 60) return prefix + abs + "s";
if (abs < 3600) return prefix + (abs / 60) + "m " + (abs % 60) + "s";
return prefix + (abs / 3600) + "h " + ((abs % 3600) / 60) + "m";
}
private String stateClass(Trigger.TriggerState state) {
if (state == Trigger.TriggerState.NORMAL) return "state-normal";
if (state == Trigger.TriggerState.PAUSED) return "state-paused";
if (state == Trigger.TriggerState.BLOCKED) return "state-blocked";
if (state == Trigger.TriggerState.ERROR) return "state-error";
return "";
}
private Map<String, Object> buildTriggerInfo(Scheduler scheduler, Trigger trigger) throws Exception {
Map<String, Object> info = new LinkedHashMap<>();
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
info.put("triggerName", trigger.getKey().getName());
info.put("triggerGroup", trigger.getKey().getGroup());
info.put("state", state.name());
info.put("stateClass", stateClass(state));
info.put("prevFire", fmt(trigger.getPreviousFireTime()));
info.put("prevFireRel", relative(trigger.getPreviousFireTime()));
info.put("nextFire", fmt(trigger.getNextFireTime()));
info.put("nextFireRel", relative(trigger.getNextFireTime()));
info.put("priority", trigger.getPriority());
if (trigger instanceof CronTrigger) {
info.put("type", "Cron");
info.put("cronExp", ((CronTrigger) trigger).getCronExpression());
} else if (trigger instanceof SimpleTrigger) {
SimpleTrigger st = (SimpleTrigger) trigger;
info.put("type", "Simple");
info.put("cronExp", "interval=" + st.getRepeatInterval() + "ms, repeat=" +
(st.getRepeatCount() == SimpleTrigger.REPEAT_INDEFINITELY ? "INF" : st.getRepeatCount()));
} else {
info.put("type", trigger.getClass().getSimpleName());
info.put("cronExp", "-");
}
if (trigger.getStartTime() != null) info.put("startTime", fmt(trigger.getStartTime()));
if (trigger.getEndTime() != null) info.put("endTime", fmt(trigger.getEndTime()));
return info;
}
private List<Map<String, Object>> getJobList(Scheduler scheduler) throws Exception {
List<Map<String, Object>> jobs = new ArrayList<>();
for (String group : scheduler.getJobGroupNames()) {
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group))) {
JobDetail detail = scheduler.getJobDetail(jobKey);
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
Map<String, Object> job = new LinkedHashMap<>();
job.put("name", jobKey.getName());
job.put("group", jobKey.getGroup());
job.put("jobClass", detail.getJobClass().getSimpleName());
job.put("disallowConcurrent", detail.isConcurrentExectionDisallowed());
StringBuilder dataStr = new StringBuilder();
for (String key : detail.getJobDataMap().getKeys()) {
if (dataStr.length() > 0) dataStr.append(", ");
dataStr.append(key).append("=").append(detail.getJobDataMap().getString(key));
}
job.put("jobData", dataStr.toString());
job.put("triggerCount", triggers.size());
List<Map<String, Object>> triggerList = new ArrayList<>();
for (Trigger trigger : triggers) {
triggerList.add(buildTriggerInfo(scheduler, trigger));
}
job.put("triggerList", triggerList);
if (!triggers.isEmpty()) {
Trigger trigger = triggers.get(0);
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
job.put("state", state.name());
job.put("stateClass", stateClass(state));
job.put("prevFire", fmt(trigger.getPreviousFireTime()));
job.put("prevFireRel", relative(trigger.getPreviousFireTime()));
job.put("nextFire", fmt(trigger.getNextFireTime()));
job.put("nextFireRel", relative(trigger.getNextFireTime()));
if (trigger instanceof CronTrigger) {
job.put("cronExp", ((CronTrigger) trigger).getCronExpression());
} else {
job.put("cronExp", "-");
}
} else {
job.put("state", "NO_TRIGGER");
job.put("stateClass", "state-error");
job.put("prevFire", "-");
job.put("prevFireRel", "");
job.put("nextFire", "-");
job.put("nextFireRel", "");
job.put("cronExp", "-");
}
jobs.add(job);
}
}
jobs.sort(Comparator.comparing(m -> (String) m.get("name")));
return jobs;
}
%><%
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application);
Scheduler scheduler = null;
Scheduler clusteredScheduler = null;
String error = null;
try {
scheduler = (Scheduler) ctx.getBean("scheduler");
} catch (Exception e) {
error = "scheduler bean not found: " + e.getMessage();
}
try {
clusteredScheduler = (Scheduler) ctx.getBean("clusteredScheduler");
} catch (Exception e) {
// clusteredScheduler is optional
}
// 자동 새로고침 간격 (초), 기본 10초
String refreshParam = request.getParameter("refresh");
int refreshSec = 10;
if (refreshParam != null) {
try { refreshSec = Integer.parseInt(refreshParam); } catch (NumberFormatException e) { /* ignore */ }
if (refreshSec < 1) refreshSec = 1;
}
boolean autoRefresh = refreshParam != null;
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<% if (autoRefresh) { %>
<meta http-equiv="refresh" content="<%= refreshSec %>;url=quartzStatus.jsp?refresh=<%= refreshSec %>">
<% } %>
<title>Quartz Scheduler Status</title>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; background: #f5f5f5; color: #333; }
h1 { font-size: 20px; margin-bottom: 5px; }
h2 { font-size: 16px; color: #555; margin: 20px 0 8px 0; border-bottom: 2px solid #ddd; padding-bottom: 4px; }
.info { font-size: 13px; color: #666; margin-bottom: 15px; }
table { width: 100%%; border-collapse: collapse; font-size: 13px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
th { background: #4a5568; color: #fff; padding: 8px 10px; text-align: left; font-weight: 500; }
td { padding: 7px 10px; border-bottom: 1px solid #eee; }
tr:hover td { background: #f7fafc; }
.state-normal { color: #38a169; font-weight: bold; }
.state-paused { color: #d69e2e; font-weight: bold; }
.state-blocked { color: #e53e3e; font-weight: bold; }
.state-error { color: #e53e3e; font-weight: bold; }
.mono { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
.rel { color: #718096; font-size: 11px; margin-left: 4px; }
.badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
.badge-yes { background: #c6f6d5; color: #276749; }
.badge-no { background: #fed7d7; color: #9b2c2c; }
.scheduler-meta { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 10px; }
.scheduler-meta span { font-size: 13px; }
.scheduler-meta strong { color: #4a5568; }
.error { color: #e53e3e; padding: 10px; background: #fff5f5; border: 1px solid #fc8181; border-radius: 4px; }
.toolbar { font-size: 12px; color: #718096; }
.toolbar a { color: #4299e1; text-decoration: none; margin: 0 4px; }
.toolbar a:hover { text-decoration: underline; }
.toolbar .active { font-weight: bold; color: #2b6cb0; background: #ebf4ff; padding: 2px 6px; border-radius: 3px; }
.auto-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; background: #bee3f8; color: #2a4365; margin-left: 6px; }
.trigger-toggle { cursor: pointer; user-select: none; }
.trigger-toggle:hover td { background: #edf2f7; }
.trigger-row { display: none; }
.trigger-row.open { display: table-row; }
.trigger-row td { background: #f7fafc; padding: 5px 10px 5px 30px; border-bottom: 1px solid #e2e8f0; font-size: 12px; }
.trigger-detail-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 0; }
.trigger-detail-table th { background: #e2e8f0; color: #4a5568; padding: 5px 8px; font-weight: 500; font-size: 11px; }
.trigger-detail-table td { padding: 4px 8px; border-bottom: 1px solid #e2e8f0; }
.trigger-detail-table tr:last-child td { border-bottom: none; }
.toggle-icon { display: inline-block; width: 16px; font-size: 11px; color: #718096; transition: transform 0.15s; }
.toggle-icon.open { transform: rotate(90deg); }
.trigger-count { font-size: 11px; color: #718096; font-weight: normal; margin-left: 4px; }
</style>
</head>
<body>
<h1>Quartz Scheduler Status
<% if (autoRefresh) { %><span class="auto-badge">Auto <%= refreshSec %>s</span><% } %>
</h1>
<p class="info">
Instance: <strong><%= System.getProperty("inst.Name", "N/A") %></strong>
| Server Time: <strong><%= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) %></strong>
</p>
<p class="toolbar">
Auto Refresh:
<% if (!autoRefresh) { %><span class="active">OFF</span><% } else { %><a href="quartzStatus.jsp">OFF</a><% } %>
|
<% int[] intervals = {5, 10, 30, 60}; %>
<% for (int iv : intervals) { %>
<% if (autoRefresh && refreshSec == iv) { %><span class="active"><%= iv %>s</span><% } else { %><a href="quartzStatus.jsp?refresh=<%= iv %>"><%= iv %>s</a><% } %>
<% } %>
</p>
<% if (error != null) { %>
<div class="error"><%= error %></div>
<% } %>
<% if (scheduler != null) { %>
<h2>Scheduler: <%= scheduler.getSchedulerName() %> (RAM)</h2>
<div class="scheduler-meta">
<span><strong>Status:</strong> <span class="<%= scheduler.isStarted() ? "state-normal" : "state-error" %>"><%= scheduler.isStarted() ? "STARTED" : "STOPPED" %></span></span>
<span><strong>Instance ID:</strong> <%= scheduler.getSchedulerInstanceId() %></span>
<span><strong>Job Count:</strong> <%= getJobList(scheduler).size() %></span>
</div>
<table>
<tr>
<th>Job Name</th>
<th>Job Class</th>
<th>Cron</th>
<th>State</th>
<th>Concurrent</th>
<th>Prev Fire</th>
<th>Next Fire</th>
<th>Job Data</th>
</tr>
<% int jobIdx = 0; for (Map<String, Object> job : getJobList(scheduler)) { String rowId = "s_" + jobIdx; List<Map<String, Object>> tList = (List<Map<String, Object>>) job.get("triggerList"); %>
<tr class="trigger-toggle" onclick="toggleTriggers('<%= rowId %>')">
<td><span class="toggle-icon" id="icon_<%= rowId %>">&#9654;</span><strong><%= job.get("name") %></strong><span class="trigger-count">(<%= job.get("triggerCount") %> triggers)</span></td>
<td class="mono"><%= job.get("jobClass") %></td>
<td class="mono"><%= job.get("cronExp") %></td>
<td class="<%= job.get("stateClass") %>"><%= job.get("state") %></td>
<td><span class="badge <%= (Boolean)job.get("disallowConcurrent") ? "badge-yes" : "badge-no" %>">
<%= (Boolean)job.get("disallowConcurrent") ? "NO (protected)" : "YES (allowed)" %></span></td>
<td class="mono"><%= job.get("prevFire") %><span class="rel"><%= job.get("prevFireRel") %></span></td>
<td class="mono"><%= job.get("nextFire") %><span class="rel"><%= job.get("nextFireRel") %></span></td>
<td class="mono"><%= job.get("jobData") %></td>
</tr>
<tr class="trigger-row" id="triggers_<%= rowId %>">
<td colspan="8" style="padding: 6px 10px 6px 30px;">
<% if (tList.isEmpty()) { %>
<em style="color:#a0aec0;">No triggers</em>
<% } else { %>
<table class="trigger-detail-table">
<tr><th>Trigger Name</th><th>Group</th><th>Type</th><th>Expression / Interval</th><th>State</th><th>Priority</th><th>Prev Fire</th><th>Next Fire</th></tr>
<% for (Map<String, Object> t : tList) { %>
<tr>
<td><%= t.get("triggerName") %></td>
<td><%= t.get("triggerGroup") %></td>
<td><%= t.get("type") %></td>
<td class="mono"><%= t.get("cronExp") %></td>
<td class="<%= t.get("stateClass") %>"><%= t.get("state") %></td>
<td><%= t.get("priority") %></td>
<td class="mono"><%= t.get("prevFire") %><span class="rel"><%= t.get("prevFireRel") %></span></td>
<td class="mono"><%= t.get("nextFire") %><span class="rel"><%= t.get("nextFireRel") %></span></td>
</tr>
<% } %>
</table>
<% } %>
</td>
</tr>
<% jobIdx++; } %>
</table>
<% } %>
<% if (clusteredScheduler != null) { %>
<h2>Clustered Scheduler: <%= clusteredScheduler.getSchedulerName() %> (DB)</h2>
<div class="scheduler-meta">
<span><strong>Status:</strong> <span class="<%= clusteredScheduler.isStarted() ? "state-normal" : "state-error" %>"><%= clusteredScheduler.isStarted() ? "STARTED" : "STOPPED" %></span></span>
<span><strong>Instance ID:</strong> <%= clusteredScheduler.getSchedulerInstanceId() %></span>
<span><strong>Job Count:</strong> <%= getJobList(clusteredScheduler).size() %></span>
</div>
<table>
<tr>
<th>Job Name</th>
<th>Job Class</th>
<th>Cron</th>
<th>State</th>
<th>Concurrent</th>
<th>Prev Fire</th>
<th>Next Fire</th>
<th>Job Data</th>
</tr>
<% int cJobIdx = 0; for (Map<String, Object> job : getJobList(clusteredScheduler)) { String rowId = "c_" + cJobIdx; List<Map<String, Object>> tList = (List<Map<String, Object>>) job.get("triggerList"); %>
<tr class="trigger-toggle" onclick="toggleTriggers('<%= rowId %>')">
<td><span class="toggle-icon" id="icon_<%= rowId %>">&#9654;</span><strong><%= job.get("name") %></strong><span class="trigger-count">(<%= job.get("triggerCount") %> triggers)</span></td>
<td class="mono"><%= job.get("jobClass") %></td>
<td class="mono"><%= job.get("cronExp") %></td>
<td class="<%= job.get("stateClass") %>"><%= job.get("state") %></td>
<td><span class="badge <%= (Boolean)job.get("disallowConcurrent") ? "badge-yes" : "badge-no" %>">
<%= (Boolean)job.get("disallowConcurrent") ? "NO (protected)" : "YES (allowed)" %></span></td>
<td class="mono"><%= job.get("prevFire") %><span class="rel"><%= job.get("prevFireRel") %></span></td>
<td class="mono"><%= job.get("nextFire") %><span class="rel"><%= job.get("nextFireRel") %></span></td>
<td class="mono"><%= job.get("jobData") %></td>
</tr>
<tr class="trigger-row" id="triggers_<%= rowId %>">
<td colspan="8" style="padding: 6px 10px 6px 30px;">
<% if (tList.isEmpty()) { %>
<em style="color:#a0aec0;">No triggers</em>
<% } else { %>
<table class="trigger-detail-table">
<tr><th>Trigger Name</th><th>Group</th><th>Type</th><th>Expression / Interval</th><th>State</th><th>Priority</th><th>Prev Fire</th><th>Next Fire</th></tr>
<% for (Map<String, Object> t : tList) { %>
<tr>
<td><%= t.get("triggerName") %></td>
<td><%= t.get("triggerGroup") %></td>
<td><%= t.get("type") %></td>
<td class="mono"><%= t.get("cronExp") %></td>
<td class="<%= t.get("stateClass") %>"><%= t.get("state") %></td>
<td><%= t.get("priority") %></td>
<td class="mono"><%= t.get("prevFire") %><span class="rel"><%= t.get("prevFireRel") %></span></td>
<td class="mono"><%= t.get("nextFire") %><span class="rel"><%= t.get("nextFireRel") %></span></td>
</tr>
<% } %>
</table>
<% } %>
</td>
</tr>
<% cJobIdx++; } %>
</table>
<% } %>
<script>
function toggleTriggers(rowId) {
var row = document.getElementById('triggers_' + rowId);
var icon = document.getElementById('icon_' + rowId);
if (row.classList.contains('open')) {
row.classList.remove('open');
icon.classList.remove('open');
} else {
row.classList.add('open');
icon.classList.add('open');
}
}
</script>
</body>
</html>
+2 -3
View File
@@ -87,9 +87,8 @@ dependencies {
//implementation project(':kjb-safedb') //implementation project(':kjb-safedb')
//implementation project(":eapim-admin-djb") //implementation project(":eapim-admin-djb")
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */ /* Custom Libs */
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
compileOnly files('libs/damo-manager.jar')
//implementation "org.dom4j:dom4j:2.1.3" //implementation "org.dom4j:dom4j:2.1.3"
//implementation "com.rabbitmq:amqp-client:3.6.6" //implementation "com.rabbitmq:amqp-client:3.6.6"
Binary file not shown.
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import com.eactive.eai.rms.common.acl.menu.ui.LeftMenuUI; import com.eactive.eai.rms.common.acl.menu.ui.LeftMenuUI;
import com.eactive.eai.rms.common.acl.role.RoleManService; import com.eactive.eai.rms.common.acl.role.RoleManService;
import com.eactive.eai.rms.common.base.BaseAnnotationController; import com.eactive.eai.rms.common.base.BaseAnnotationController;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.login.SessionManager; import com.eactive.eai.rms.common.login.SessionManager;
import com.eactive.eai.rms.data.entity.man.menu.MenuService; import com.eactive.eai.rms.data.entity.man.menu.MenuService;
@@ -31,6 +32,9 @@ public class MenuRenderController extends BaseAnnotationController {
@Autowired @Autowired
private MenuRenderService menuRenderService; private MenuRenderService menuRenderService;
@Autowired
private MonitoringContext monitoringContext;
@GetMapping("/left_top.do") @GetMapping("/left_top.do")
public String left_top(HttpServletRequest request, ModelMap modelMap) { public String left_top(HttpServletRequest request, ModelMap modelMap) {
return "/common/screen/left_top"; return "/common/screen/left_top";
@@ -87,6 +91,9 @@ public class MenuRenderController extends BaseAnnotationController {
List<DataSourceType> roleServiceTypeList = roleManService.selectRoleServiceTypeList(roleIds); List<DataSourceType> roleServiceTypeList = roleManService.selectRoleServiceTypeList(roleIds);
modelMap.addAttribute("roleServiceTypeList", roleServiceTypeList); modelMap.addAttribute("roleServiceTypeList", roleServiceTypeList);
modelMap.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 30));
if (StringUtils.isNotBlank(mainPage)) { if (StringUtils.isNotBlank(mainPage)) {
for (Map<String, Object> map : dataList) { for (Map<String, Object> map : dataList) {
String url = (String) map.get("LEV4_URL"); String url = (String) map.get("LEV4_URL");
@@ -259,6 +259,7 @@ public interface MonitoringContext {
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout"; public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
// 웹훅 // 웹훅
public static final String RMS_WEBHOOK_ENABLED = "rms.webhook.enabled";
public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url"; public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url";
public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count"; public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count";
public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time"; public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time";
@@ -269,6 +270,15 @@ public interface MonitoringContext {
// 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용) // 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용)
public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count"; public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count";
// 비밀번호 변경 시 재사용을 금지할 최근 이력 개수
public static final String RMS_PASSWORD_HISTORY_COUNT = "rms.password.history.count";
// 비밀번호 변경 시 SMS 2차 인증 사용 여부 (djb.sms_auth.enabled 와 함께 true 여야 동작)
public static final String RMS_PASSWORD_SMS_AUTH_ENABLED = "rms.password.sms_auth.enabled";
// API 상태변화 스윙챗 발송여부
public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled";
@@ -78,9 +78,16 @@ public class CrossScriptingFilter implements Filter {
} }
// Web Cache Deception // Web Cache Deception
// js/css 정적 리소스는 캐시를 허용 (화면 오픈 시마다 재다운로드되는 문제 방지)
String servletPath = request.getServletPath();
boolean isStaticAsset = servletPath != null
&& (servletPath.endsWith(".js") || servletPath.endsWith(".css"));
if (!isStaticAsset) {
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache"); response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0); response.setDateHeader("Expires", 0);
}
chain.doFilter(new RequestWrapper(request), response); chain.doFilter(new RequestWrapper(request), response);
// //
@@ -0,0 +1,53 @@
package com.eactive.eai.rms.common.login;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChangePasswordResponseDto {
/**
* 비밀번호 변경 성공 여부
*/
private boolean success;
/**
* 사용자에게 alert로 표시할 메시지
*/
private String message;
/**
* 성공 시 이동할 URL
*/
private String redirectUrl;
/**
* SMS 2차 인증 필요 여부 (true면 아직 비밀번호가 반영되지 않고 인증 대기 중)
*/
private boolean smsAuthRequired;
/**
* 마스킹된 전화번호 (예: 010****1234)
*/
private String maskedPhone;
/**
* 인증번호 만료 시각 (timestamp, 밀리초)
*/
private long expiresAt;
/**
* 재발급 가능 시각 (timestamp, 밀리초)
*/
private long resendableAt;
/**
* djb.sms_auth.mode=fixed 일 때만 채워지는 테스트용 인증번호 안내 문구 (예: "[테스트] 인증번호: 000000")
*/
private String smsTestHint;
}
@@ -50,4 +50,9 @@ public class LoginResponseDto {
* 초기 비밀번호 변경 필요 여부 * 초기 비밀번호 변경 필요 여부
*/ */
private boolean changePassword; private boolean changePassword;
/**
* djb.sms_auth.mode=fixed 일 때만 채워지는 테스트용 인증번호 안내 문구 (예: "[테스트] 인증번호: 000000")
*/
private String smsTestHint;
} }
@@ -47,6 +47,7 @@ import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService; import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistory; import com.eactive.eai.rms.data.entity.man.user.UserLoginHistory;
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService; import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService;
import com.eactive.eai.rms.data.entity.man.user.AdminUserPasswordHistoryService;
import com.eactive.eai.rms.data.entity.man.user.UserRole; import com.eactive.eai.rms.data.entity.man.user.UserRole;
import com.eactive.eai.rms.data.entity.man.user.UserRoleService; import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService; import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
@@ -62,10 +63,10 @@ public class MainController implements InterceptorSkipController {
private static final String USER_NOT_FOUND_KEY_2 = "login.useridnotfound2"; private static final String USER_NOT_FOUND_KEY_2 = "login.useridnotfound2";
private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp"; private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp";
private static final String REDIRECT_CHANGE_PASSWORD_URL = "redirect:/emergency.jsp";
private static final String USER_STATUS_LOCKED = "2"; private static final String USER_STATUS_LOCKED = "2";
private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5; private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5;
private static final int DEFAULT_PASSWORD_HISTORY_COUNT = 5;
private final LocaleMessage localeMessage; private final LocaleMessage localeMessage;
private final MonitoringContext monitoringContext; private final MonitoringContext monitoringContext;
@@ -76,6 +77,7 @@ public class MainController implements InterceptorSkipController {
private final LoginService loginService; private final LoginService loginService;
private final MonitoringPropertyService monitoringPropertyService; private final MonitoringPropertyService monitoringPropertyService;
private final UserLoginHistoryService userLoginHistoryService; private final UserLoginHistoryService userLoginHistoryService;
private final AdminUserPasswordHistoryService userPasswordHistoryService;
private final SmsAuthService smsAuthService; private final SmsAuthService smsAuthService;
@Autowired @Autowired
@@ -87,6 +89,7 @@ public class MainController implements InterceptorSkipController {
UserServiceTypeService userServiceTypeService, UserServiceTypeService userServiceTypeService,
MonitoringPropertyService monitoringPropertyService, MonitoringPropertyService monitoringPropertyService,
UserLoginHistoryService userLoginHistoryService, UserLoginHistoryService userLoginHistoryService,
AdminUserPasswordHistoryService userPasswordHistoryService,
SmsAuthService smsAuthService) { SmsAuthService smsAuthService) {
this.localeMessage = localeMessage; this.localeMessage = localeMessage;
this.monitoringContext = monitoringContext; this.monitoringContext = monitoringContext;
@@ -96,6 +99,7 @@ public class MainController implements InterceptorSkipController {
this.userServiceTypeService = userServiceTypeService; this.userServiceTypeService = userServiceTypeService;
this.monitoringPropertyService = monitoringPropertyService; this.monitoringPropertyService = monitoringPropertyService;
this.userLoginHistoryService = userLoginHistoryService; this.userLoginHistoryService = userLoginHistoryService;
this.userPasswordHistoryService = userPasswordHistoryService;
this.smsAuthService = smsAuthService; this.smsAuthService = smsAuthService;
} }
@@ -246,6 +250,7 @@ public class MainController implements InterceptorSkipController {
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno())) .maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
.expiresAt(smsAuthService.getExpiresAt(session)) .expiresAt(smsAuthService.getExpiresAt(session))
.resendableAt(smsAuthService.getResendableAt(session)) .resendableAt(smsAuthService.getResendableAt(session))
.smsTestHint(smsAuthService.getFixedModeHintText())
.build(); .build();
} }
@@ -453,6 +458,17 @@ public class MainController implements InterceptorSkipController {
MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT); MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT);
} }
// 비밀번호 재사용 금지 대상 최근 이력 개수 조회 (rms.password.history.count)
private int getPasswordHistoryCount() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_HISTORY_COUNT, DEFAULT_PASSWORD_HISTORY_COUNT);
}
// 비밀번호 변경 시 SMS 2차 인증 사용 여부 (djb.sms_auth.enabled 와 rms.password.sms_auth.enabled 가 모두 true 여야 동작)
private boolean isPasswordChangeSmsAuthEnabled() {
return monitoringContext.getBooleanProperty(MonitoringContext.RMS_PASSWORD_SMS_AUTH_ENABLED, false);
}
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금 // 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
private int increaseLoginFailCount(UserInfo userInfo) { private int increaseLoginFailCount(UserInfo userInfo) {
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1; int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
@@ -684,7 +700,7 @@ public class MainController implements InterceptorSkipController {
model.addAttribute("mainPage", mainPage); model.addAttribute("mainPage", mainPage);
model.addAttribute("menuId", menuId); model.addAttribute("menuId", menuId);
model.addAttribute("autoLogoutTimeout", model.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 60)); monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 30));
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("CURRENT SERVICE TYPE : " + service); logger.debug("CURRENT SERVICE TYPE : " + service);
@@ -824,7 +840,8 @@ public class MainController implements InterceptorSkipController {
} }
@RequestMapping("/changePassword.do") @RequestMapping("/changePassword.do")
public String changePassword(HttpServletRequest request, @ResponseBody
public ChangePasswordResponseDto changePassword(HttpServletRequest request,
String changePassword, String confirmPassword, String changePassword, String confirmPassword,
String resetUserId, String resetPassword) { String resetUserId, String resetPassword) {
HttpSession session = request.getSession(); HttpSession session = request.getSession();
@@ -842,9 +859,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString(USER_NOT_FOUND_KEY_1)); .getString(USER_NOT_FOUND_KEY_1));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString(USER_NOT_FOUND_KEY_1)); .success(false)
return REDIRECT_LOGIN_URL; .message(toAlertMessage(localeMessage.getString(USER_NOT_FOUND_KEY_1)))
.build();
} }
UserInfo userInfo = userInfoOptional.get(); UserInfo userInfo = userInfoOptional.get();
@@ -854,9 +872,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString("login.pwdmismatch1")); .getString("login.pwdmismatch1"));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString("login.pwdmismatch2")); .success(false)
return REDIRECT_CHANGE_PASSWORD_URL; .message(toAlertMessage(localeMessage.getString("login.pwdmismatch2")))
.build();
} }
// 2. 비밀번호 Validation // 2. 비밀번호 Validation
@@ -864,9 +883,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString("login.pwdmismatch3")); .getString("login.pwdmismatch3"));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString("login.pwdmismatch4")); .success(false)
return REDIRECT_CHANGE_PASSWORD_URL; .message(toAlertMessage(localeMessage.getString("login.pwdmismatch4")))
.build();
} }
// 3. 비밀번호 체크 // 3. 비밀번호 체크
@@ -875,37 +895,135 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage localeMessage
.getString("login.pwdmismatch5")); .getString("login.pwdmismatch5"));
session.setAttribute(RESULT_MSG, localeMessage return ChangePasswordResponseDto.builder()
.getString("login.pwdmismatch6")); .success(false)
return REDIRECT_CHANGE_PASSWORD_URL; .message(toAlertMessage(localeMessage.getString("login.pwdmismatch6")))
.build();
} }
// 4. 비밀번호 변경 // 4. 최근 사용한 비밀번호 재사용 금지
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,changePassword)); String newPasswordHash = DamoManager.getInstance().hash(DamoManager.SHA256, changePassword);
userInfoService.save(userInfo); if (userPasswordHistoryService.isPasswordReused(resetUserId, newPasswordHash, getPasswordHistoryCount())) {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString("login.pwdreused1"));
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdreused2")))
.build();
}
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage // 5. SMS 2차 인증 필요 여부 체크 (비상모드는 로그인과 동일하게 우회)
.getString("login.pwdchanged")); // djb.sms_auth.enabled 와 rms.password.sms_auth.enabled 가 모두 true 일 때만 SMS 인증 수행
session.setAttribute(RESULT_MSG, localeMessage Boolean emergencyMode = (Boolean) session.getAttribute("emergencyMode");
.getString("login.pwdchanged")); if (!Boolean.TRUE.equals(emergencyMode) && smsAuthService.isEnabled() && isPasswordChangeSmsAuthEnabled()) {
logger.debug("\n\n\n\n===================" + localeMessage if (!smsAuthService.hasValidPhoneNumber(userInfo)) {
.getString("login.pwdchanged")); UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", "휴대폰번호 미등록으로 비밀번호 변경 불가");
return REDIRECT_LOGIN_URL; return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage("휴대폰번호가 등록되어 있지 않아 비밀번호를 변경할 수 없습니다. 관리자에게 문의하세요."))
.build();
}
String authCode = smsAuthService.generateAuthCode();
boolean sent = smsAuthService.sendAuthCode(userInfo, authCode);
if (!sent) {
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요."))
.build();
}
// 실제 저장은 SMS 인증 성공 후(commitPendingPasswordChange)에 수행한다.
smsAuthService.setupPasswordChangeAuthSession(session, userInfo, newPasswordHash, authCode);
return ChangePasswordResponseDto.builder()
.success(true)
.smsAuthRequired(true)
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
.expiresAt(smsAuthService.getExpiresAt(session))
.resendableAt(smsAuthService.getResendableAt(session))
.smsTestHint(smsAuthService.getFixedModeHintText())
.build();
}
// SMS 인증 비활성화 또는 비상모드 - 기존처럼 즉시 반영
return applyPasswordChange(request, userInfo, newPasswordHash);
} catch (Exception e) { } catch (Exception e) {
logger.error("changePassword Error ", e); logger.error("changePassword Error ", e);
return "redirect:/common/errors/errorLogon.jsp"; return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdchangefail")))
.build();
} }
} }
/**
* 비밀번호 변경 SMS 인증 성공 후 호출. 세션에 대기 중인 새 비밀번호를 실제로 반영한다.
* ({@code SmsAuthController}에서 인증번호 검증 성공 시 호출)
*/
public ChangePasswordResponseDto commitPendingPasswordChange(HttpServletRequest request, HttpSession session) {
UserInfo userInfo = smsAuthService.getUserInfoFromSession(session);
String pendingPasswordHash = smsAuthService.getPendingPasswordHashFromSession(session);
if (userInfo == null || pendingPasswordHash == null) {
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage("인증 세션이 만료되었습니다. 다시 시도해주세요."))
.build();
}
try {
ChangePasswordResponseDto result = applyPasswordChange(request, userInfo, pendingPasswordHash);
smsAuthService.clearAuthSession(session);
return result;
} catch (Exception e) {
logger.error("commitPendingPasswordChange Error ", e);
smsAuthService.clearAuthSession(session);
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdchangefail")))
.build();
}
}
// 비밀번호 저장 + 변경 이력 기록 (SMS 인증 완료 여부와 무관하게 최종 반영 단계에서 공통 호출)
private ChangePasswordResponseDto applyPasswordChange(HttpServletRequest request, UserInfo userInfo, String newPasswordHash) {
userInfo.setPassword(newPasswordHash);
userInfoService.save(userInfo);
userPasswordHistoryService.recordPasswordChange(userInfo.getUserid(), newPasswordHash);
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage
.getString("login.pwdchanged"));
return ChangePasswordResponseDto.builder()
.success(true)
.message(toAlertMessage(localeMessage.getString("login.pwdchanged")))
.redirectUrl(request.getContextPath() + "/emergency.jsp")
.build();
}
private static final int PASSWORD_PATTERN_CHECK_LENGTH = 3;
private static final String[] KEYBOARD_SEQUENTIAL_ROWS = {
"1234567890",
"!@#$%^&*()",
"qwertyuiop",
"asdfghjkl",
"zxcvbnm"
};
private boolean isPasswordValid(String password) { private boolean isPasswordValid(String password) {
return password != null && password.getBytes().length == password return password != null && password.getBytes().length == password
.length() && password.getBytes().length >= 7 .length() && password.getBytes().length >= 7
&& isCombination(password) && isCombination(password)
&& !isRepeatPassword(password, 5) && !isRepeatPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
&& !isSerialPassword(password, 5); && !isSerialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
&& !isKeyboardSequentialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH);
} }
private boolean isRepeatPassword(String password, int count) { private boolean isRepeatPassword(String password, int count) {
@@ -930,20 +1048,40 @@ public class MainController implements InterceptorSkipController {
} }
private boolean isSerialPassword(String password, int count) { private boolean isSerialPassword(String password, int count) {
int preData = 0; int ascCount = 1;
int serialcount = 1; int descCount = 1;
for (int i = 0; i < password.length(); i++) { for (int i = 1; i < password.length(); i++) {
char prev = password.charAt(i - 1);
char curr = password.charAt(i);
ascCount = (curr == prev + 1) ? ascCount + 1 : 1;
descCount = (curr == prev - 1) ? descCount + 1 : 1;
if (ascCount >= count || descCount >= count) { return true; }
if (preData + 1 == password.charAt(i)) {
serialcount++;
} else {
serialcount = 1;
} }
preData = password.charAt(i); return false;
}
if (serialcount == count) { return true; } // 키보드 자판 배열상 연속된 문자열(qwer, asdf 등, 역순 포함) 사용 여부 체크
private boolean isKeyboardSequentialPassword(String password, int count) {
String lowerPassword = password.toLowerCase();
for (String row : KEYBOARD_SEQUENTIAL_ROWS) {
for (int i = 0; i <= row.length() - count; i++) {
String forward = row.substring(i, i + count);
String backward = new StringBuilder(forward).reverse().toString();
if (lowerPassword.contains(forward) || lowerPassword.contains(backward)) {
return true;
}
}
} }
@@ -20,22 +20,6 @@ public class ClusteredSchedulerConfig {
@Autowired @Qualifier("MONITORING") @Autowired @Qualifier("MONITORING")
private DataSourceType monitoringDatasource; private DataSourceType monitoringDatasource;
/*
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory {
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object job = super.createJobInstance(bundle);
applicationContext.getAutowireCapableBeanFactory().autowireBean(job);
return job;
}
}
@Bean
public SpringBeanJobFactory springBeanJobFactory() {
return new AutowiringSpringBeanJobFactory();
}
*/
@Bean(name = "clusteredScheduler") @Bean(name = "clusteredScheduler")
public SchedulerFactoryBean clusteredScheduler() { public SchedulerFactoryBean clusteredScheduler() {
@@ -55,7 +39,7 @@ public class ClusteredSchedulerConfig {
quartzProperties.put("org.quartz.threadPool.threadCount", "5"); quartzProperties.put("org.quartz.threadPool.threadCount", "5");
quartzProperties.put("org.quartz.threadPool.threadPriority", "5"); quartzProperties.put("org.quartz.threadPool.threadPriority", "5");
quartzProperties.put("org.quartz.jobStore.misfireThreshold", "60000"); quartzProperties.put("org.quartz.jobStore.misfireThreshold", "10000");
quartzProperties.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); quartzProperties.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate"); quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
quartzProperties.put("org.quartz.jobStore.useProperties", "false"); quartzProperties.put("org.quartz.jobStore.useProperties", "false");
@@ -81,7 +65,11 @@ public class ClusteredSchedulerConfig {
factory.setQuartzProperties(quartzProperties); factory.setQuartzProperties(quartzProperties);
factory.setAutoStartup(true); factory.setAutoStartup(true);
factory.setSchedulerName("EMS_ClusteredScheduler"); factory.setSchedulerName("EMS_ClusteredScheduler");
// factory.setJobFactory(springBeanJobFactory());
AutoWiringSpringBeanJobFactory jobFactory =
new AutoWiringSpringBeanJobFactory(applicationContext.getAutowireCapableBeanFactory());
jobFactory.setApplicationContext(applicationContext);
factory.setJobFactory(jobFactory);
factory.setApplicationContextSchedulerContextKey("applicationContext"); factory.setApplicationContextSchedulerContextKey("applicationContext");
factory.setApplicationContext(applicationContext); factory.setApplicationContext(applicationContext);
@@ -1,6 +1,7 @@
package com.eactive.eai.rms.common.vo; package com.eactive.eai.rms.common.vo;
import java.util.List; import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -10,6 +11,8 @@ public class GridResponse<T> {
private Integer page; private Integer page;
private Long records; private Long records;
private List<T> rows; private List<T> rows;
/** jqGrid footerrow + userDataOnFooter 용 하단 합계행 데이터 (colModel name -> value) */
private Map<String, Object> userdata;
public GridResponse() { public GridResponse() {
} }
@@ -54,6 +57,14 @@ public class GridResponse<T> {
this.rows = rows; this.rows = rows;
} }
public Map<String, Object> getUserdata() {
return userdata;
}
public void setUserdata(Map<String, Object> userdata) {
this.userdata = userdata;
}
public GridResponse(List<T> rows) { public GridResponse(List<T> rows) {
this.rows = rows; this.rows = rows;
this.total = 1; this.total = 1;
@@ -0,0 +1,77 @@
package com.eactive.eai.rms.data.entity.man.user;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.GenericGenerator;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
/**
* 관리콘솔(TSEAIRM02) 로그인 사용자의 비밀번호 변경 이력 (TSEAIRM08).
* 포털 API 소비자 비밀번호 이력({@code com.eactive.apim.portal.portaluser.entity.UserPasswordHistory})과는 별개 테이블/도메인이다.
*/
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "TSEAIRM08")
@org.hibernate.annotations.Table(appliesTo = "TSEAIRM08", comment = "관리콘솔 사용자 비밀번호 변경 이력")
public class AdminUserPasswordHistory extends AbstractEntity<Integer> implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GenericGenerator(
name = "TSEAIRM08_GEN",
strategy = "com.eactive.eai.rms.data.jpa.SchemaPrefixedSequenceGenerator",
parameters = {
@org.hibernate.annotations.Parameter(name = "sequence_name", value = "SEQ_TSEAIRM08"),
@org.hibernate.annotations.Parameter(name = "initial_value", value = "1"),
@org.hibernate.annotations.Parameter(name = "increment_size", value = "1")
}
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "TSEAIRM08_GEN"
)
@Column(name = "ID", unique = true, nullable = false)
@Comment("비밀번호 변경 이력 식별 ID")
private int pwdHistId;
@Column(name = "USERID", length = 255, nullable = false)
@Comment("사용자 식별자(사번)")
private String userId;
@Column(name = "PASSWORD_HASH", length = 255, nullable = false)
@Comment("변경된 비밀번호 해시값")
private String passwordHash;
@Column(name = "CHANGE_DATE", length = 14)
@Comment("비밀번호 변경 일시 (yyyyMMddHHmmss)")
private String changeDate;
@Column(name = "CREATED_BY", length = 200)
@Comment("등록자 식별자")
private String createdBy;
@Column(name = "CREATED_DATE", length = 14)
@Comment("레코드 생성 일시 (yyyyMMddHHmmss)")
private String createdDate;
@Override
public @NonNull Integer getId() {
return pwdHistId;
}
}
@@ -0,0 +1,9 @@
package com.eactive.eai.rms.data.entity.man.user;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.data.jpa.BaseRepository;
@EMSDataSource
public interface AdminUserPasswordHistoryRepository extends BaseRepository<AdminUserPasswordHistory, Integer> {
}
@@ -0,0 +1,53 @@
package com.eactive.eai.rms.data.entity.man.user;
import java.time.LocalDateTime;
import java.util.List;
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;
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class AdminUserPasswordHistoryService
extends AbstractEMSDataSerivce<AdminUserPasswordHistory, Integer, AdminUserPasswordHistoryRepository> {
/** 사용자의 최근 비밀번호 이력을 최신순으로 historyCount건 조회 */
public List<AdminUserPasswordHistory> findRecent(String userId, int historyCount) {
BooleanExpression predicate = QAdminUserPasswordHistory.adminUserPasswordHistory.userId.eq(userId);
Pageable pageable = PageRequest.of(0, historyCount, Sort.by(Sort.Direction.DESC, "pwdHistId"));
return repository.findAll(predicate, pageable).getContent();
}
/** 최근 historyCount건 이내에 동일한 비밀번호(해시값)가 있었는지 확인 */
public boolean isPasswordReused(String userId, String newPasswordHash, int historyCount) {
if (historyCount <= 0) {
return false;
}
return findRecent(userId, historyCount).stream()
.anyMatch(history -> history.getPasswordHash().equals(newPasswordHash));
}
/** 비밀번호 변경 이력 저장 */
public void recordPasswordChange(String userId, String passwordHash) {
String now = LocalDateTime.now().format(LocalDateTimeFormatters.FORMATTER_YYYYMMDDHHMMSS_14);
AdminUserPasswordHistory history = new AdminUserPasswordHistory();
history.setUserId(userId);
history.setPasswordHash(passwordHash);
history.setChangeDate(now);
history.setCreatedBy(userId);
history.setCreatedDate(now);
save(history);
}
}
@@ -30,4 +30,12 @@ public class WebhookReq {
@Column(name = "SECRET", length = 200) @Column(name = "SECRET", length = 200)
@Comment("HMAC 서명용 Secret Key") @Comment("HMAC 서명용 Secret Key")
private String secret; private String secret;
@Column(name = "USER_SECRET", length = 200)
@Comment("사용자 Secret Key")
private String userSecret;
@Column(name = "CREATED_DATE", length = 14)
@Comment("신청일시 (yyyyMMddHHmmss)")
private String createdDate;
} }
@@ -20,6 +20,6 @@ public class WebhookReqEventId implements Serializable {
private String webhookReqId; private String webhookReqId;
@Column(name = "EVENT_TYPE", nullable = false, length = 50) @Column(name = "EVENT_TYPE", nullable = false, length = 50)
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)") @Comment("이벤트 유형 (CHECK_START, ERROR_START 등)")
private String eventType; private String eventType;
} }
@@ -72,6 +72,9 @@ public class WebhookSendLog {
@Column(name = "SEND_BY", length = 100) @Column(name = "SEND_BY", length = 100)
private String sendBy; private String sendBy;
@Column(name = "USER_SECRET")
private String userSecret;
/* Boolean 편의 메서드 */ /* Boolean 편의 메서드 */
public void setSuccess(Boolean success) { public void setSuccess(Boolean success) {
this.success = (success != null && success) ? "Y" : "N"; this.success = (success != null && success) ? "Y" : "N";
@@ -13,7 +13,10 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
import com.eactive.eai.data.entity.onl.adapter.QAdapter; import com.eactive.eai.data.entity.onl.adapter.QAdapter;
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
import com.eactive.eai.data.entity.onl.authserver.QScopeEntity; import com.eactive.eai.data.entity.onl.authserver.QScopeEntity;
import com.eactive.eai.data.entity.onl.logger.QStatLog; import com.eactive.eai.data.entity.onl.logger.QStatLog;
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity; import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
@@ -31,6 +34,9 @@ import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections; import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
@Service @Service
@@ -38,8 +44,14 @@ import com.querydsl.jpa.impl.JPAQuery;
@RequiredArgsConstructor @RequiredArgsConstructor
public class EAIMessageService extends AbstractDataService<EAIMessageEntity, String, EAIMessageRepository> { public class EAIMessageService extends AbstractDataService<EAIMessageEntity, String, EAIMessageRepository> {
/** Oracle IN 절 표현식 개수 상한. 넘기면 ORA-01795 라 나눠 OR 로 잇는다 */
private static final int ORACLE_IN_LIMIT = 1000;
private final UserBusinessService userBusinessService; private final UserBusinessService userBusinessService;
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */
private final ApiSpecInfoRepository apiSpecInfoRepository;
public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) { public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) {
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity; QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
@@ -115,6 +127,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
return getJPAQueryFactory() return getJPAQueryFactory()
.select(qeaiMessageEntity .select(qeaiMessageEntity
, qServiceMessageEntity.psvintfacdsticname , qServiceMessageEntity.psvintfacdsticname
, qServiceMessageEntity.psvsysadptrbzwkgroupname
, qRouting.nonmotivrouturiname , qRouting.nonmotivrouturiname
, qStdInfo.apifullpath , qStdInfo.apifullpath
, qStdInfo.bzwksvckeyname , qStdInfo.bzwksvckeyname
@@ -136,8 +149,8 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity; QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity; QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity;
QRouting qRouting = QRouting.routing;
QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo; QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo;
QApiStatus qApiStatus = QApiStatus.apiStatus;
BooleanBuilder predicate = new BooleanBuilder(); BooleanBuilder predicate = new BooleanBuilder();
@@ -154,11 +167,10 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchEaiSvcDesc())) { if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchEaiSvcDesc())) {
predicate.and(qeaiMessageEntity.eaisvcdesc.containsIgnoreCase(eaiMessageUISearch.getSearchEaiSvcDesc())); predicate.and(qeaiMessageEntity.eaisvcdesc.containsIgnoreCase(eaiMessageUISearch.getSearchEaiSvcDesc()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchSvcMotivUsedStcd())) { if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchGstatSysAdptrBzwkGroupName())) {
predicate.and(qeaiMessageEntity.svcmotivusedstcd.eq(eaiMessageUISearch.getSearchSvcMotivUsedStcd())); predicate
} .and(qeaiMessageEntity.gstatsysadptrbzwkgroupname
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchFlowControl())) { .containsIgnoreCase(eaiMessageUISearch.getSearchGstatSysAdptrBzwkGroupName()));
predicate.and(qeaiMessageEntity.flowctrlroutname.eq(eaiMessageUISearch.getSearchFlowControl()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchTranType())) { if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchTranType())) {
predicate.and(qeaiMessageEntity.trantype.eq(eaiMessageUISearch.getSearchTranType())); predicate.and(qeaiMessageEntity.trantype.eq(eaiMessageUISearch.getSearchTranType()));
@@ -178,18 +190,11 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
predicate.and(qeaiMessageEntity.refkey.containsIgnoreCase(eaiMessageUISearch.getSearchRefKey())); predicate.and(qeaiMessageEntity.refkey.containsIgnoreCase(eaiMessageUISearch.getSearchRefKey()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchQueueName())) { if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchPsvSysAdptrBzwkGroupName())) {
predicate
.and(qeaiMessageEntity.flowctrlroutname.eq(qRouting.routname))
.and(qRouting.nonmotivrouturiname.containsIgnoreCase(eaiMessageUISearch.getSearchQueueName()));
}
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchPsvintfacdsticName())) {
predicate predicate
.and(qServiceMessageEntity.id.svcprcssno.eq(1)) .and(qServiceMessageEntity.id.svcprcssno.eq(1))
.and(qServiceMessageEntity.psvintfacdsticname .and(qServiceMessageEntity.psvsysadptrbzwkgroupname
.containsIgnoreCase(eaiMessageUISearch.getSearchPsvintfacdsticName())); .containsIgnoreCase(eaiMessageUISearch.getSearchPsvSysAdptrBzwkGroupName()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchApiFullPath())) { if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchApiFullPath())) {
@@ -197,9 +202,61 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
.and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath())); .and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchStatusCode())) {
String statusCode = eaiMessageUISearch.getSearchStatusCode();
if ("N".equals(statusCode)) {
// 정상 : API_STATUS 에 코드가 없거나(join 안 됨) E/D/C 가 아닌 경우
predicate.and(qApiStatus.statusCode.isNull().or(qApiStatus.statusCode.notIn("E", "D", "C")));
} else {
predicate.and(qApiStatus.statusCode.eq(statusCode));
}
}
if ("Y".equals(eaiMessageUISearch.getSearchApiSpecYn())) {
predicate.and(apiSpecRegisteredExpression(qeaiMessageEntity.eaisvcname));
}
if ("Y".equals(eaiMessageUISearch.getSearchPortalDisplayYn())) {
predicate.and(qeaiMessageEntity.eaisvcname.in(displayedGroupApiIdSubQuery()));
}
return predicate; return predicate;
} }
/**
* OpenAPI Spec 등록 여부 조건.
*
* <p>PTL_API_SPEC_INFO 는 EMSAPP 스키마라 TSEAIHE01 쿼리에 조인하면 ORA-00942 다. 스펙이 있는
* API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p>
*/
private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) {
List<String> specApiIds = apiSpecInfoRepository.findAllApiIds();
if (specApiIds.isEmpty()) {
// in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다
return Expressions.ONE.eq(Expressions.TWO);
}
BooleanExpression expression = null;
for (int from = 0; from < specApiIds.size(); from += ORACLE_IN_LIMIT) {
int to = Math.min(from + ORACLE_IN_LIMIT, specApiIds.size());
BooleanExpression chunk = eaiSvcNamePath.in(specApiIds.subList(from, to));
expression = (expression == null) ? chunk : expression.or(chunk);
}
return expression;
}
/** 노출 중인(display_yn='1') API 그룹에 편성된 API ID. API_GROUP 은 EAI 메시지와 같은 스키마다 */
private JPQLQuery<String> displayedGroupApiIdSubQuery() {
QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi;
QApiGroup qApiGroup = QApiGroup.apiGroup;
return JPAExpressions
.select(qApiGroupApi.id.apiId)
.from(qApiGroupApi)
.join(qApiGroupApi.apiGroup, qApiGroup)
.where(qApiGroup.displayYn.eq("1"));
}
public Page<RoutingEaiMsgDto> findAll(Pageable pageable, RoutingEaiMsgSearch routingEaiMsgSearch) { public Page<RoutingEaiMsgDto> findAll(Pageable pageable, RoutingEaiMsgSearch routingEaiMsgSearch) {
List<RoutingEaiMsgDto> result = routingEaiMsgDtoList(pageable, routingEaiMsgSearch); List<RoutingEaiMsgDto> result = routingEaiMsgDtoList(pageable, routingEaiMsgSearch);
@@ -8,13 +8,11 @@ public class EAIMessageUISearch {
private String searchEaiSvcName; private String searchEaiSvcName;
private String searchEaiBzwkDstcd; private String searchEaiBzwkDstcd;
private String searchEaiSvcDesc; private String searchEaiSvcDesc;
private String searchSvcMotivUsedStcd; private String searchGstatSysAdptrBzwkGroupName;
private String searchFlowControl;
private String searchTranType; private String searchTranType;
private String searchQueueName;
private String rmsMenuAuth; private String rmsMenuAuth;
private String searchPsvintfacdsticName; private String searchPsvSysAdptrBzwkGroupName;
private String searchExtBizCd; private String searchExtBizCd;
private String tranChangeSim; private String tranChangeSim;
@@ -22,4 +20,13 @@ public class EAIMessageUISearch {
private String searchApiFullPath; private String searchApiFullPath;
/** API_STATUS.STATUS_CODE (E:장애, D:지연, C:점검). "N" 이면 정상(코드 없음)만 조회 */
private String searchStatusCode;
/** "Y" 면 PTL_API_SPEC_INFO 에 OpenAPI Spec 이 등록된 API 만 조회 */
private String searchApiSpecYn;
/** "Y" 면 노출 중인(display_yn='1') API 그룹에 편성된 API 만 조회 */
private String searchPortalDisplayYn;
} }
@@ -0,0 +1,76 @@
package com.eactive.eai.rms.data.entity.onl.layoutsync.history;
import java.io.Serializable;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import org.hibernate.annotations.Comment;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter16;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
@Entity
@Table(name = "tseaitr10")
@org.hibernate.annotations.Table(appliesTo = "tseaitr10", comment = "레이아웃 연동 이력")
public class LayoutSyncHistoryEntity extends AbstractEntity<Long> implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@EqualsAndHashCode.Include
@Column(nullable = false)
@Comment("일련번호")
private Long seqno;
@Column(length = 36)
@Comment("연동로그처리일련번호")
private String logprcssseqno;
@Column(length = 100)
@Comment("서비스명")
private String servicename;
@Column(length = 20)
@Comment("커맨드")
private String command;
@Column(length = 50)
@Comment("레이아웃 명")
private String loutname;
@Lob
@Comment("수신 데이터")
private String recvdata;
@Column(length = 16)
@Comment("연동 시각")
@Convert(converter = LocalDateTimeToStringConverter16.class)
private LocalDateTime recvamndhms;
@Column(length = 1)
@Comment("처리 결과")
private String prcssrslt;
@Column(length = 4000)
@Comment("처리 결과 메세지")
private String prcssrsltcmnt;
@Override
public @NonNull Long getId() {
return seqno;
}
}
@@ -0,0 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.layoutsync.history;
import com.eactive.eai.data.jpa.BaseRepository;
interface LayoutSyncHistoryRepository extends BaseRepository<LayoutSyncHistoryEntity, Long> {
}
@@ -0,0 +1,86 @@
package com.eactive.eai.rms.data.entity.onl.layoutsync.history;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.eai.rms.onl.manage.rule.layoutsync.history.ui.LayoutSyncHistoryUI;
import com.eactive.eai.rms.onl.manage.rule.layoutsync.history.ui.LayoutSyncHistoryUISearch;
import com.querydsl.jpa.impl.JPAQuery;
@Service
@Transactional
public class LayoutSyncHistoryService extends AbstractDataService<LayoutSyncHistoryEntity, Long, LayoutSyncHistoryRepository> {
private static final DateTimeFormatter FMT_YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter FMT_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public Page<LayoutSyncHistoryUI> selectList(Pageable pageable, LayoutSyncHistoryUISearch search) {
QLayoutSyncHistoryEntity q = QLayoutSyncHistoryEntity.layoutSyncHistoryEntity;
JPAQuery<?> query = getJPAQueryFactory().from(q);
if (StringUtils.isNotBlank(search.getSearchLoutName())) {
query.where(q.loutname.containsIgnoreCase(search.getSearchLoutName()));
}
if (StringUtils.isNotBlank(search.getSearchServiceName())) {
query.where(q.servicename.containsIgnoreCase(search.getSearchServiceName()));
}
if (StringUtils.isNotBlank(search.getSearchCommand())) {
query.where(q.command.containsIgnoreCase(search.getSearchCommand()));
}
if (StringUtils.isNotBlank(search.getSearchStartDate())) {
query.where(q.recvamndhms.goe(parseYYYYMMDD(search.getSearchStartDate()).atStartOfDay()));
}
if (StringUtils.isNotBlank(search.getSearchEndDate())) {
query.where(q.recvamndhms.loe(parseYYYYMMDD(search.getSearchEndDate()).atTime(23, 59, 59)));
}
if (StringUtils.isNotBlank(search.getSearchPrcssRslt())) {
query.where(q.prcssrslt.eq(search.getSearchPrcssRslt()));
}
long totalCount = query.select(q.count()).fetchOne();
if (pageable.isPaged()) {
query.limit(pageable.getPageSize()).offset(pageable.getOffset());
}
List<LayoutSyncHistoryEntity> entities = query.select(q)
.orderBy(q.recvamndhms.desc(), q.seqno.desc())
.fetch();
List<LayoutSyncHistoryUI> rows = entities.stream().map(this::convertToUI).collect(Collectors.toList());
return new PageImpl<>(rows, pageable, totalCount);
}
public LayoutSyncHistoryUI selectDetail(Long seqno) {
return convertToUI(getById(seqno));
}
private static LocalDate parseYYYYMMDD(String yyyyMMdd) {
return LocalDate.parse(yyyyMMdd, FMT_YYYYMMDD);
}
private LayoutSyncHistoryUI convertToUI(LayoutSyncHistoryEntity e) {
LayoutSyncHistoryUI ui = new LayoutSyncHistoryUI();
ui.setSeqno(e.getSeqno());
ui.setServiceName(e.getServicename());
ui.setCommand(e.getCommand());
ui.setLoutName(e.getLoutname());
ui.setRecvAmndHms(e.getRecvamndhms() == null ? "" : e.getRecvamndhms().format(FMT_DATETIME));
ui.setPrcssRslt(e.getPrcssrslt());
ui.setPrcssRsltCmnt(e.getPrcssrsltcmnt());
ui.setRecvData(e.getRecvdata());
return ui;
}
}
@@ -19,7 +19,8 @@ public class EAILogDto {
public EAILogDto(EAILogId eaiLogId, String trackasiskey1ctnt, String trackasiskey2ctnt, String keymgtmsgctnt, public EAILogDto(EAILogId eaiLogId, String trackasiskey1ctnt, String trackasiskey2ctnt, String keymgtmsgctnt,
String eaisvcname, String extbizcd, String refkey, String clientname, String eaisvcname, String extbizcd, String refkey, String clientname,
String orgname, String msgprcssyms, String companycode, String eaierrctnt, String eaiSvcDesc) { String orgname, String msgprcssyms, String companycode, String eaierrctnt,
String eaierrcd, String eaiSvcDesc) {
this.eaiLog = new EAILog(); this.eaiLog = new EAILog();
this.eaiLog.setId(eaiLogId); this.eaiLog.setId(eaiLogId);
this.eaiLog.setTrackasiskey1ctnt(trackasiskey1ctnt); this.eaiLog.setTrackasiskey1ctnt(trackasiskey1ctnt);
@@ -33,6 +34,7 @@ public class EAILogDto {
this.eaiLog.setMsgprcssyms(msgprcssyms); this.eaiLog.setMsgprcssyms(msgprcssyms);
this.eaiLog.setCompanycode(companycode); this.eaiLog.setCompanycode(companycode);
this.eaiLog.setEaierrctnt(eaierrctnt); this.eaiLog.setEaierrctnt(eaierrctnt);
this.eaiLog.setEaierrcd(eaierrcd);
this.eaiSvcDesc = eaiSvcDesc; this.eaiSvcDesc = eaiSvcDesc;
} }
} }
@@ -5,6 +5,7 @@ import com.eactive.eai.data.entity.onl.logger.EAILogId;
import com.eactive.eai.data.entity.onl.logger.QEAILog; import com.eactive.eai.data.entity.onl.logger.QEAILog;
import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity; import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.QUserBusiness; import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.QUserBusiness;
import com.eactive.eai.rms.data.entity.onl.code.CodeService;
import com.querydsl.core.Tuple; import com.querydsl.core.Tuple;
import com.querydsl.core.types.Projections; import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanExpression;
@@ -12,6 +13,7 @@ import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -27,8 +29,15 @@ import java.util.stream.Collectors;
@Repository @Repository
public class EAILogRepositoryImpl implements EAILogRepository { public class EAILogRepositoryImpl implements EAILogRepository {
// 업무(BIZ) 오류코드 그룹 - 해당 코드는 시스템 에러가 아닌 정상적인 업무 거부 응답이므로
// "에러로그" 검색 조건에서는 제외한다.
private static final String ERRCODE_BIZ_GROUP = "ERRCODE_BIZ";
private EntityManager em; private EntityManager em;
@Autowired
private CodeService codeService;
@PersistenceContext @PersistenceContext
public void setEm(EntityManager em) { public void setEm(EntityManager em) {
this.em = em; this.em = em;
@@ -57,7 +66,7 @@ public class EAILogRepositoryImpl implements EAILogRepository {
qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt, qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt,
qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname, qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname,
qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt, qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt,
qeaiMessageEntity.eaisvcdesc)) qeaiLog.eaierrcd, qeaiMessageEntity.eaisvcdesc))
.from(entityPathBase) .from(entityPathBase)
.leftJoin(qeaiMessageEntity) .leftJoin(qeaiMessageEntity)
.on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname)) .on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname))
@@ -102,9 +111,15 @@ public class EAILogRepositoryImpl implements EAILogRepository {
} }
if (StringUtils.isNotBlank(eaiLogSearch.getSearchGuid())) { if (StringUtils.isNotBlank(eaiLogSearch.getSearchGuid())) {
query query.where(qeaiLog.pguid.eq(eaiLogSearch.getSearchGuid()));
.where(qeaiLog.pguid.eq(eaiLogSearch.getSearchGuid()))
.where(qeaiLog.id.logprcssserno.eq(eaiLogSearch.getSearchLogPrcssSerno())); // 처리단계(logprcssserno)를 명시적으로 선택한 경우에만 추가로 좁힌다.
// 하나의 GUID는 처리단계(100/200/300/400)별로 여러 로그 행에 걸쳐 존재하므로,
// 선택하지 않았으면 해당 GUID의 모든 단계를 조회한다.
if (StringUtils.isNotBlank(eaiLogSearch.getSearchLogPrcssSerno())) {
query.where(qeaiLog.id.logprcssserno.eq(eaiLogSearch.getSearchLogPrcssSerno()));
}
return; return;
} }
@@ -189,8 +204,18 @@ public class EAILogRepositoryImpl implements EAILogRepository {
.between(eaiLogSearch.getSearchStartTime(), eaiLogSearch.getSearchEndTime())) .between(eaiLogSearch.getSearchStartTime(), eaiLogSearch.getSearchEndTime()))
.where( .where(
qeaiLog.eaierrcd.trim().isNotEmpty() qeaiLog.eaierrcd.trim().isNotEmpty()
) );
.distinct();
// ERRCODE_BIZ(업무 오류) 코드는 시스템 에러가 아니므로 "에러로그" 검색 대상에서 제외
List<String> bizErrCodes = codeService.findByIdCodegroup(ERRCODE_BIZ_GROUP).stream()
.map(code -> code.getId().getCode())
.collect(Collectors.toList());
if (!bizErrCodes.isEmpty()) {
errorInstancesSubQuery.where(qeaiLog.eaierrcd.notIn(bizErrCodes));
}
errorInstancesSubQuery.distinct();
// 찾은 eaisevrinstncname에 해당하는 모든 로그 조회 // 찾은 eaisevrinstncname에 해당하는 모든 로그 조회
query.where(qeaiLog.id.eaisvcserno.in(errorInstancesSubQuery)); query.where(qeaiLog.id.eaisvcserno.in(errorInstancesSubQuery));
@@ -13,6 +13,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.common.util.UUIDGenerator;
import com.eactive.eai.data.RollingTable; import com.eactive.eai.data.RollingTable;
import com.eactive.eai.data.entity.onl.logger.EAILog; import com.eactive.eai.data.entity.onl.logger.EAILog;
import com.eactive.eai.data.entity.onl.logger.EAILogId; import com.eactive.eai.data.entity.onl.logger.EAILogId;
@@ -38,19 +39,36 @@ public class EAILogServiceImpl implements EAILogService {
@Override @Override
public Page<EAILogDto> findAll(Pageable pageable, EAILogSearch eaiLogSearch) { public Page<EAILogDto> findAll(Pageable pageable, EAILogSearch eaiLogSearch) {
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis(); long time = CommonUtil.getCalendar(resolveTableDate(eaiLogSearch)).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
Page<EAILogDto> eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch); Page<EAILogDto> eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch);
return eaiLogDtos; return eaiLogDtos;
} }
public Map<String, Long> getTransactionCounts(EAILogSearch eaiLogSearch) { public Map<String, Long> getTransactionCounts(EAILogSearch eaiLogSearch) {
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis(); long time = CommonUtil.getCalendar(resolveTableDate(eaiLogSearch)).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
Map<String, Long> detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch); Map<String, Long> detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch);
return detailedCounts; return detailedCounts;
} }
// GUID로 조회하는 경우, 대상 롤링 테이블을 검색시간이 아닌 GUID 자체에 포함된 시각으로 특정한다.
// GUID 포맷/오프셋은 UUIDGenerator.GUID_DATE_POSITION 기준.
private String resolveTableDate(EAILogSearch eaiLogSearch) {
String guid = eaiLogSearch.getSearchGuid();
if (StringUtils.isNotBlank(guid)) {
int start = UUIDGenerator.GUID_DATE_POSITION;
int end = start + 14; // yyyyMMddHHmmss
if (guid.length() >= end) {
return guid.substring(start, end);
}
}
return eaiLogSearch.getSearchStartTime();
}
@Override @Override
public EAILogDto getById(EAILogId eaiLogId) { public EAILogDto getById(EAILogId eaiLogId) {
long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis(); long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis();
@@ -22,6 +22,7 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
import com.eactive.apim.portal.common.entity.Auditable; import com.eactive.apim.portal.common.entity.Auditable;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident; 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.DjbApistatusIncidentApi;
@@ -48,6 +49,12 @@ import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
* 검수·게시해야 개발자포탈에 노출되며, {@code PUBLISH}면 탐지 즉시 노출된다. * 검수·게시해야 개발자포탈에 노출되며, {@code PUBLISH}면 탐지 즉시 노출된다.
* {@code OFF} 여도 이미 열려있는 장애의 복구 처리(ERROR_END/DELAY_END)는 계속 동작한다.</p> * {@code OFF} 여도 이미 열려있는 장애의 복구 처리(ERROR_END/DELAY_END)는 계속 동작한다.</p>
* *
* <p>개발자포탈 미게시 인터페이스 포함 여부는 {@code djb.apistatus.auto-incident-include-unpublished}
* (기본 {@code false}) 로 정한다. 기본값이면 게시 API 만 이슈에 담고,
* 게시 API 가 하나도 없는 탐지는 기록하지 않는다. {@code true}면 미게시 인터페이스도
* 포함해 건수로 집계된다. 복구 처리는 이 설정과 무관하게
* 이미 이슈에 담긴 API 를 대상으로 동작한다.</p>
*
* <p>점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다 * <p>점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다
* ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정). * ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정).
* 따라서 CONTROL 이벤트를 다시 기록하면 순환이 되므로 기록하지 않는다.</p> * 따라서 CONTROL 이벤트를 다시 기록하면 순환이 되므로 기록하지 않는다.</p>
@@ -58,8 +65,8 @@ public class ApiStatusDetectionService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class); private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
public static final String EVENT_CONTROL_START = "CONTROL_START"; public static final String EVENT_CHECK_START = "CHECK_START";
public static final String EVENT_CONTROL_END = "CONTROL_END"; public static final String EVENT_CHECK_END = "CHECK_END";
public static final String EVENT_ERROR_START = "ERROR_START"; public static final String EVENT_ERROR_START = "ERROR_START";
public static final String EVENT_ERROR_END = "ERROR_END"; public static final String EVENT_ERROR_END = "ERROR_END";
public static final String EVENT_DELAY_START = "DELAY_START"; public static final String EVENT_DELAY_START = "DELAY_START";
@@ -95,6 +102,12 @@ public class ApiStatusDetectionService {
/** 오탐이 그대로 고객에게 노출되지 않도록 기본은 검수(DRAFT) */ /** 오탐이 그대로 고객에게 노출되지 않도록 기본은 검수(DRAFT) */
private static final String DEFAULT_AUTO_INCIDENT_MODE = MODE_DRAFT; private static final String DEFAULT_AUTO_INCIDENT_MODE = MODE_DRAFT;
/** 자동 탐지 이슈에 개발자포탈 미게시 인터페이스도 포함할지 여부 - true / false */
public static final String KEY_INCLUDE_UNPUBLISHED = "djb.apistatus.auto-incident-include-unpublished";
/** 포탈 게시 API 장애만 고객 공지 대상이 되도록 기본은 제외(false) */
private static final String DEFAULT_INCLUDE_UNPUBLISHED = "false";
@Autowired @Autowired
private DjbApistatusIncidentRepository incidentRepository; private DjbApistatusIncidentRepository incidentRepository;
@@ -110,17 +123,25 @@ public class ApiStatusDetectionService {
@Autowired @Autowired
private ApiStatusRepository apiStatusRepository; private ApiStatusRepository apiStatusRepository;
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. AGWAPP 커넥션으로 도는 apiStatusRepository 로는 못 읽는다 */
@Autowired
private ApiSpecInfoRepository apiSpecInfoRepository;
@Autowired @Autowired
private PortalPropertyService portalPropertyService; private PortalPropertyService portalPropertyService;
/** 자동 초안 문구 양식 (classpath:apistatus-draft.yml) */ /** 공지 문구 양식 (PTL_PROPERTY > classpath:apistatus-draft.yml) */
@Autowired @Autowired
private ApiStatusDraftTemplate draftTemplate; private ApiStatusDraftTemplate draftTemplate;
/** 제목·본문 조립. 관리자 수동 등록 화면과 같은 양식을 쓰기 위해 분리했다 */
@Autowired
private ApiStatusNoticeComposer noticeComposer;
/** /**
* 이벤트 감지 * 이벤트 감지
* @param event "CONTROL_START" : 점검시작 * @param event "CHECK_START" : 점검시작
"CONTROL_END" : 점검종료 "CHECK_END" : 점검종료
"ERROR_START" : 장애시작 "ERROR_START" : 장애시작
"ERROR_END" : 장애종료 "ERROR_END" : 장애종료
"DELAY_START" : 지연시작 "DELAY_START" : 지연시작
@@ -154,8 +175,8 @@ public class ApiStatusDetectionService {
case EVENT_DELAY_END: case EVENT_DELAY_END:
markRecovered(event, targets); markRecovered(event, targets);
break; break;
case EVENT_CONTROL_START: case EVENT_CHECK_START:
case EVENT_CONTROL_END: case EVENT_CHECK_END:
// 점검은 관리자 등록 공지가 원천 - 재기록하지 않음 // 점검은 관리자 등록 공지가 원천 - 재기록하지 않음
log.debug("점검 이벤트는 장애 데이터로 기록하지 않음: {} {}", event, targets); log.debug("점검 이벤트는 장애 데이터로 기록하지 않음: {} {}", event, targets);
break; break;
@@ -203,6 +224,19 @@ public class ApiStatusDetectionService {
} }
boolean autoPublish = MODE_PUBLISH.equals(mode); boolean autoPublish = MODE_PUBLISH.equals(mode);
// 미게시 인터페이스 제외 설정(false)이면 게시 API 만 남기고, 하나도 없으면 기록하지 않는다
if (!resolveIncludeUnpublished()) {
List<String> published = filterPublished(apiIds);
if (published.isEmpty()) {
log.info("게시 API 없음 - 미게시 인터페이스 제외 설정으로 기록하지 않음: {} {}", event, apiIds);
return;
}
if (published.size() < apiIds.size()) {
log.info("미게시 인터페이스 제외: {} → {}", apiIds, published);
}
apiIds = published;
}
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
String interfaceId = makeInterfaceId(event, apiIds, now); String interfaceId = makeInterfaceId(event, apiIds, now);
@@ -309,6 +343,27 @@ public class ApiStatusDetectionService {
} }
} }
/**
* 자동 탐지 이슈에 개발자포탈 미게시 인터페이스도 포함할지 여부.
*
* <p>PTL_PROPERTY({@code Portal} / {@code djb.apistatus.auto-incident-include-unpublished}) 를 읽는다.
* 최초 접근 시 기본값으로 row 가 자동 생성된다. 기본 {@code false} - 게시 API 만 이슈에 담는다.
* 명시적으로 {@code true}/{@code Y} 를 설정한 경우에만 미게시 인터페이스를 포함하며,
* 알 수 없는 값·조회 실패는 기본값 쪽(false)으로 떨어뜨린다.</p>
*/
private boolean resolveIncludeUnpublished() {
try {
String value = portalPropertyService.getOrCreateProperty(
PROPERTY_GROUP, KEY_INCLUDE_UNPUBLISHED, DEFAULT_INCLUDE_UNPUBLISHED,
"API 상태 자동 탐지 이슈에 포탈 미게시 인터페이스 포함 여부(true/false)");
String normalized = StringUtils.trimToEmpty(value);
return "true".equalsIgnoreCase(normalized) || "Y".equalsIgnoreCase(normalized);
} catch (Exception e) {
log.warn("미게시 인터페이스 포함 여부 조회 실패 - 기본값 {} 적용", DEFAULT_INCLUDE_UNPUBLISHED, e);
return false;
}
}
private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event, IncidentKind kind, private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event, IncidentKind kind,
List<String> apiIds, LocalDateTime now, boolean autoPublish) { List<String> apiIds, LocalDateTime now, boolean autoPublish) {
Long incidentId = incident.getIncidentId(); Long incidentId = incident.getIncidentId();
@@ -534,14 +589,47 @@ public class ApiStatusDetectionService {
} }
} }
/** 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다. */ /**
* 게시 인터페이스 ID 집합 조회. 실패 처리는 호출부 정책에 맡긴다.
*
* <p>판정 조건 두 개가 서로 다른 스키마에 있어 조회를 나눈다 - API 그룹 편성은 AGWAPP,
* 스펙 존재는 EMSAPP(PTL_API_SPEC_INFO). 한 쿼리로 조인하면 tenant=APIGW 커넥션이
* EMSAPP 를 못 봐서 ORA-00942 가 난다.</p>
*/
private Set<String> lookupPublishedApiIds(List<String> apiIds) {
List<String> groupedIds = apiStatusRepository.findDisplayedGroupApiIds(apiIds);
return groupedIds.isEmpty()
? Collections.emptySet()
: new HashSet<>(apiSpecInfoRepository.findApiIdsIn(groupedIds));
}
/**
* 게시 API 만 남긴다 (미게시 인터페이스 제외 설정용). 판정에 실패하면 전체를 유지한다 -
* 제외 판단이 안 되는 상황에서 진짜 장애 기록까지 버리는 쪽이 더 위험하고,
* 명칭 노출은 표기 단계({@link #splitPublished})가 따로 막는다.
*/
private List<String> filterPublished(List<String> apiIds) {
try {
Set<String> publishedIds = lookupPublishedApiIds(apiIds);
return apiIds.stream()
.filter(publishedIds::contains)
.collect(Collectors.toList());
} catch (Exception e) {
log.warn("게시 API 판정 실패 - 미게시 인터페이스 제외 없이 전체 포함", e);
return apiIds;
}
}
/**
* 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다.
*/
private ApiSplit splitPublished(List<String> apiIds) { private ApiSplit splitPublished(List<String> apiIds) {
if (apiIds == null || apiIds.isEmpty()) { if (apiIds == null || apiIds.isEmpty()) {
return new ApiSplit(Collections.emptyList(), 0); return new ApiSplit(Collections.emptyList(), 0);
} }
Set<String> publishedIds; Set<String> publishedIds;
try { try {
publishedIds = new HashSet<>(apiStatusRepository.findPublishedApiIds(apiIds)); publishedIds = lookupPublishedApiIds(apiIds);
} catch (Exception e) { } catch (Exception e) {
// 실패 시 노출하는 쪽으로 기울면 내부 인터페이스가 고객 화면에 새어 나간다 // 실패 시 노출하는 쪽으로 기울면 내부 인터페이스가 고객 화면에 새어 나간다
log.warn("게시 API 판정 실패 - 전부 GW 인터페이스로 묶어 표기", e); log.warn("게시 API 판정 실패 - 전부 GW 인터페이스로 묶어 표기", e);
@@ -558,7 +646,7 @@ public class ApiStatusDetectionService {
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */ /** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
private String hiddenLabel(int hiddenCount) { private String hiddenLabel(int hiddenCount) {
return draftTemplate.text(ApiStatusDraftTemplate.GW_LABEL, "count", String.valueOf(hiddenCount)); return noticeComposer.hiddenLabel(hiddenCount);
} }
/** /**
@@ -583,16 +671,17 @@ public class ApiStatusDetectionService {
*/ */
private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) { private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) {
ApiSplit split = splitPublished(apiIds); ApiSplit split = splitPublished(apiIds);
if (split.published.isEmpty()) { return noticeComposer.buildTitle(titleKeyword, labelsOf(split.published, apiNames),
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_HIDDEN_ONLY, split.hiddenCount, apiIds.size());
"gw", hiddenLabel(apiIds.size()), "keyword", titleKeyword);
} }
String first = apiLabel(split.published.get(0), apiNames);
if (apiIds.size() > 1) { /** 인터페이스 ID 목록을 표시명 목록으로 바꾼다 (명칭을 못 찾으면 ID 그대로) */
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MULTI, private List<String> labelsOf(List<String> apiIds, Map<String, String> apiNames) {
"first", first, "rest", String.valueOf(apiIds.size() - 1), "keyword", titleKeyword); List<String> labels = new ArrayList<>();
for (String apiId : apiIds) {
labels.add(apiLabel(apiId, apiNames));
} }
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_SINGLE, "first", first, "keyword", titleKeyword); return labels;
} }
/** /**
@@ -606,39 +695,19 @@ public class ApiStatusDetectionService {
*/ */
private PortalNotice createNotice(String title, String summary, List<String> apiIds, private PortalNotice createNotice(String title, String summary, List<String> apiIds,
Map<String, String> apiNames, LocalDateTime now, boolean publish) { Map<String, String> apiNames, LocalDateTime now, boolean publish) {
StringBuilder detail = new StringBuilder(); String lead = draftTemplate.text(publish
detail.append(draftTemplate.text(publish
? ApiStatusDraftTemplate.BODY_LEAD_PUBLISH ? ApiStatusDraftTemplate.BODY_LEAD_PUBLISH
: ApiStatusDraftTemplate.BODY_LEAD_DRAFT, : ApiStatusDraftTemplate.BODY_LEAD_DRAFT,
"summary", summary)); "summary", summary);
// 게시된 API 만 이름으로 적고(인터페이스 ID 는 넣지 않는다) 나머지는 건수로 묶는다.
// 공지 본문은 저장 시점에 굳는 HTML 이라 나중에 사용자별로 가릴 수 없다.
ApiSplit split = splitPublished(apiIds); ApiSplit split = splitPublished(apiIds);
detail.append("<p><b>") String detail = noticeComposer.buildBody(
.append(draftTemplate.text(ApiStatusDraftTemplate.BODY_AFFECTED_HEADING)) lead, labelsOf(split.published, apiNames), split.hiddenCount, !publish);
.append("</b></p><ul>");
for (String apiId : split.published) {
detail.append("<li>").append(apiLabel(apiId, apiNames)).append("</li>");
}
if (split.hiddenCount > 0) {
detail.append("<li>").append(hiddenLabel(split.hiddenCount)).append("</li>");
}
detail.append("</ul>");
for (ApiStatusDraftTemplate.Section section : draftTemplate.getSections()) {
detail.append("<p><b>").append(section.getTitle()).append("</b></p>");
// 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
detail.append("<p>").append(publish
? draftTemplate.text(ApiStatusDraftTemplate.BODY_FILLED_TEXT)
: draftTemplate.text(ApiStatusDraftTemplate.BODY_DRAFT_TEXT, "hint", section.getHint()))
.append("</p>");
}
PortalNotice notice = new PortalNotice(); PortalNotice notice = new PortalNotice();
notice.setId(null); notice.setId(null);
notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH)); notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH));
notice.setNoticeDetail(detail.toString()); notice.setNoticeDetail(detail);
notice.setNoticeType(NOTICE_TYPE_INCIDENT); notice.setNoticeType(NOTICE_TYPE_INCIDENT);
notice.setUseYn(publish ? "Y" : "N"); notice.setUseYn(publish ? "Y" : "N");
notice.setFixYn("N"); notice.setFixYn("N");
@@ -11,21 +11,32 @@ import javax.annotation.PostConstruct;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
/** /**
* API Status 자동 탐지 초안 양식({@code classpath:apistatus-draft.yml}). * API Status 공지 양식. 자동 탐지 초안과 관리자 수동 등록이 같은 문구를 쓴다.
* *
* <p>자동 생성 공지의 제목·본문·타임라인 문구를 코드가 아닌 설정으로 관리한다. * <p>문구 출처는 우선순위 3단이다.</p>
* admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없으므로 SnakeYAML 로 직접 읽는다 * <ol>
* ({@code <context:property-placeholder>} 는 properties 전용이고, 검수 섹션이 리스트라 * <li>{@code PTL_PROPERTY} (그룹 {@link #PROPERTY_GROUP}, 키 {@link #PROPERTY_PREFIX} + 문구키)
* properties 로는 인덱스 키로 흩어진다).</p> * - 운영 중 포탈 프로퍼티 관리 화면에서 재배포 없이 고칠 수 있다.</li>
* <li>{@code classpath:apistatus-draft.yml} - 배포본에 남기는 문구 원본.</li>
* <li>코드 {@link #DEFAULTS} - 위 둘이 모두 없을 때.</li>
* </ol>
* *
* <p>파일이 없거나 파싱에 실패하면 {@link #DEFAULTS} 로 동작한다 - 문구 설정 문제로 * <p>yml 을 SnakeYAML 로 직접 읽는 이유: admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없고
* 장애 탐지 자체가 멈추면 안 되기 때문이다. 개별 키가 비어도 같은 이유로 기본값으로 떨어진다.</p> * ({@code <context:property-placeholder>} 는 properties 전용), 검수 섹션이 리스트라 properties 로는
* 인덱스 키로 흩어진다.</p>
*
* <p>DB 조회는 60초 스냅샷으로 캐시한다 - 공지 한 건을 만드는 데 {@link #text} 가 수십 번 불리므로
* 매번 조회하면 안 된다. 조회에 실패하거나 개별 키가 비어 있으면 그 아래 단계 문구로 떨어진다.
* 문구 설정 문제로 장애 탐지 자체가 멈추면 안 되기 때문이다.</p>
*/ */
@Component @Component
public class ApiStatusDraftTemplate { public class ApiStatusDraftTemplate {
@@ -34,6 +45,15 @@ public class ApiStatusDraftTemplate {
private static final String RESOURCE = "apistatus-draft.yml"; private static final String RESOURCE = "apistatus-draft.yml";
/** 문구 프로퍼티 그룹 (개발자포탈과 동일 그룹을 쓴다) */
public static final String PROPERTY_GROUP = "Portal";
/** 문구 프로퍼티 키 접두어. 접두어 뒤는 아래 문구 키와 같다 */
public static final String PROPERTY_PREFIX = "djb.apistatus.draft.";
/** DB 스냅샷 유효 시간 (ms) */
private static final long REFRESH_INTERVAL_MS = 60_000L;
// ---- 키 ---- // ---- 키 ----
public static final String GW_LABEL = "gw-label"; public static final String GW_LABEL = "gw-label";
@@ -41,12 +61,21 @@ public class ApiStatusDraftTemplate {
public static final String TITLE_MULTI = "title.multi"; public static final String TITLE_MULTI = "title.multi";
public static final String TITLE_HIDDEN_ONLY = "title.hidden-only"; public static final String TITLE_HIDDEN_ONLY = "title.hidden-only";
/** 관리자 수동 등록 제목 - 자동 탐지 표기([자동감지]·GW 인터페이스)를 쓰지 않는다 */
public static final String TITLE_MANUAL_SINGLE = "title.manual-single";
public static final String TITLE_MANUAL_MULTI = "title.manual-multi";
public static final String TITLE_MANUAL_EMPTY = "title.manual-empty";
public static final String BODY_LEAD_PUBLISH = "body.lead-publish"; public static final String BODY_LEAD_PUBLISH = "body.lead-publish";
public static final String BODY_LEAD_DRAFT = "body.lead-draft"; public static final String BODY_LEAD_DRAFT = "body.lead-draft";
public static final String BODY_LEAD_MANUAL = "body.lead-manual";
public static final String BODY_AFFECTED_HEADING = "body.affected-heading"; public static final String BODY_AFFECTED_HEADING = "body.affected-heading";
public static final String BODY_FILLED_TEXT = "body.filled-text"; public static final String BODY_FILLED_TEXT = "body.filled-text";
public static final String BODY_DRAFT_TEXT = "body.draft-text"; public static final String BODY_DRAFT_TEXT = "body.draft-text";
/** 검수 항목. 프로퍼티에서는 {@code 제목|힌트} 를 {@code ;;} 로 이어 붙인 문자열 한 건이다 */
public static final String BODY_SECTIONS = "body.sections";
public static final String TL_DETECTED = "timeline.detected"; public static final String TL_DETECTED = "timeline.detected";
public static final String TL_API_ADDED = "timeline.api-added"; public static final String TL_API_ADDED = "timeline.api-added";
public static final String TL_REDETECTED = "timeline.redetected"; public static final String TL_REDETECTED = "timeline.redetected";
@@ -54,17 +83,30 @@ public class ApiStatusDraftTemplate {
public static final String TL_RECOVERED_ALL = "timeline.recovered-all"; public static final String TL_RECOVERED_ALL = "timeline.recovered-all";
public static final String TL_RECOVERED_PART = "timeline.recovered-part"; public static final String TL_RECOVERED_PART = "timeline.recovered-part";
/** 검수 항목 프로퍼티 구분자 - 항목 사이 */
private static final String SECTION_DELIMITER = ";;";
/** 검수 항목 프로퍼티 구분자 - 제목과 힌트 사이 */
private static final String SECTION_FIELD_DELIMITER = "|";
/** yml 을 못 읽었을 때 쓰는 기본 문구 (기존 하드코딩과 동일) */ /** yml 을 못 읽었을 때 쓰는 기본 문구 (기존 하드코딩과 동일) */
private static final Map<String, String> DEFAULTS; private static final Map<String, String> DEFAULTS;
/** 프로퍼티 최초 등록 시 함께 적는 설명 (관리 화면에서 치환자 의미를 보여준다) */
private static final Map<String, String> DESCRIPTIONS;
static { static {
Map<String, String> defaults = new LinkedHashMap<>(); Map<String, String> defaults = new LinkedHashMap<>();
defaults.put(GW_LABEL, "GW 인터페이스 {count}건"); defaults.put(GW_LABEL, "GW 인터페이스 {count}건");
defaults.put(TITLE_SINGLE, "[자동감지] {first} API {keyword}"); defaults.put(TITLE_SINGLE, "[자동감지] {first} API {keyword}");
defaults.put(TITLE_MULTI, "[자동감지] {first} 외 {rest}종 API {keyword}"); defaults.put(TITLE_MULTI, "[자동감지] {first} 외 {rest}종 API {keyword}");
defaults.put(TITLE_HIDDEN_ONLY, "[자동감지] {gw} {keyword}"); defaults.put(TITLE_HIDDEN_ONLY, "[자동감지] {gw} {keyword}");
defaults.put(TITLE_MANUAL_SINGLE, "{first} API {keyword}");
defaults.put(TITLE_MANUAL_MULTI, "{first} 외 {rest}종 API {keyword}");
defaults.put(TITLE_MANUAL_EMPTY, "API {keyword}");
defaults.put(BODY_LEAD_PUBLISH, "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>"); defaults.put(BODY_LEAD_PUBLISH, "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>");
defaults.put(BODY_LEAD_DRAFT, "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>"); defaults.put(BODY_LEAD_DRAFT, "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>");
defaults.put(BODY_LEAD_MANUAL, "<p>[작성 필요] 장애 개요를 입력해 주십시오.</p>");
defaults.put(BODY_AFFECTED_HEADING, "영향 API"); defaults.put(BODY_AFFECTED_HEADING, "영향 API");
defaults.put(BODY_FILLED_TEXT, "확인 중입니다."); defaults.put(BODY_FILLED_TEXT, "확인 중입니다.");
defaults.put(BODY_DRAFT_TEXT, "[작성 필요] {hint}"); defaults.put(BODY_DRAFT_TEXT, "[작성 필요] {hint}");
@@ -75,6 +117,29 @@ public class ApiStatusDraftTemplate {
defaults.put(TL_RECOVERED_ALL, "전체 복구 확인 ({event})\n{affected}"); defaults.put(TL_RECOVERED_ALL, "전체 복구 확인 ({event})\n{affected}");
defaults.put(TL_RECOVERED_PART, "일부 복구 확인 ({event})\n{affected}"); defaults.put(TL_RECOVERED_PART, "일부 복구 확인 ({event})\n{affected}");
DEFAULTS = Collections.unmodifiableMap(defaults); DEFAULTS = Collections.unmodifiableMap(defaults);
Map<String, String> descriptions = new LinkedHashMap<>();
descriptions.put(GW_LABEL, "개발자포탈 미게시 인터페이스 묶음 라벨. {count}=건수");
descriptions.put(TITLE_SINGLE, "공지 제목(영향 API 1건). {first}=API명, {keyword}=장애|응답지연");
descriptions.put(TITLE_MULTI, "공지 제목(영향 API 2건 이상). {first}=첫 API명, {rest}=나머지 건수, {keyword}=장애|응답지연");
descriptions.put(TITLE_HIDDEN_ONLY, "게시 API 가 하나도 없을 때 공지 제목. {gw}=gw-label 적용 결과, {keyword}=장애|응답지연");
descriptions.put(TITLE_MANUAL_SINGLE, "수동 등록 공지 제목(영향 API 1건). {first}=API명, {keyword}=장애");
descriptions.put(TITLE_MANUAL_MULTI, "수동 등록 공지 제목(영향 API 2건 이상). {first}=첫 API명, {rest}=나머지 건수, {keyword}=장애");
descriptions.put(TITLE_MANUAL_EMPTY, "수동 등록 공지 제목(영향 API 미선택). {keyword}=장애");
descriptions.put(BODY_LEAD_PUBLISH, "본문 머리말(자동 탐지 즉시 게시). {summary}=탐지 사유");
descriptions.put(BODY_LEAD_DRAFT, "본문 머리말(자동 탐지 초안). {summary}=탐지 사유");
descriptions.put(BODY_LEAD_MANUAL, "본문 머리말(관리자 수동 등록). 치환 변수 없음");
descriptions.put(BODY_AFFECTED_HEADING, "영향 API 목록 소제목. 치환 변수 없음");
descriptions.put(BODY_FILLED_TEXT, "게시 상태로 나가는 검수 항목 기본 문구. 치환 변수 없음");
descriptions.put(BODY_DRAFT_TEXT, "초안 상태 검수 항목 문구. {hint}=body.sections 의 힌트");
descriptions.put(BODY_SECTIONS, "관리자가 채울 검수 항목. '제목|힌트' 를 ';;' 로 구분. 전체 1000바이트(한글 약 330자) 이내");
descriptions.put(TL_DETECTED, "최초 탐지 타임라인. {summary}=탐지 사유, {event}=ERROR_START 등, {affected}=영향 API 요약");
descriptions.put(TL_API_ADDED, "영향 API 추가 타임라인. {event}=이벤트, {affected}=영향 API 요약");
descriptions.put(TL_REDETECTED, "재탐지 타임라인. {event}=이벤트");
descriptions.put(TL_ESCALATED, "지연에서 장애로 격상 타임라인. {event}=이벤트");
descriptions.put(TL_RECOVERED_ALL, "전체 복구 타임라인. {event}=이벤트, {affected}=영향 API 요약");
descriptions.put(TL_RECOVERED_PART, "일부 복구 타임라인. {event}=이벤트, {affected}=영향 API 요약");
DESCRIPTIONS = Collections.unmodifiableMap(descriptions);
} }
/** 관리자가 검수 시 채워 넣을 항목 */ /** 관리자가 검수 시 채워 넣을 항목 */
@@ -106,10 +171,24 @@ public class ApiStatusDraftTemplate {
} }
}); });
/** 점(.) 으로 평탄화한 문구 맵 */ @Autowired(required = false)
private Map<String, String> texts = DEFAULTS; private PortalPropertyService portalPropertyService;
private List<Section> sections = DEFAULT_SECTIONS;
/** yml/코드 기본값 - DB 프로퍼티가 없을 때 쓰는 바탕 문구 */
private Map<String, String> baselineTexts = DEFAULTS;
private List<Section> baselineSections = DEFAULT_SECTIONS;
/** 바탕 문구 위에 DB 프로퍼티를 덮은 결과 (실제 사용분) */
private volatile Map<String, String> texts = DEFAULTS;
private volatile List<Section> sections = DEFAULT_SECTIONS;
/** 다음 DB 스냅샷 갱신 시각 (ms). 0 이면 아직 한 번도 안 읽음 */
private volatile long nextRefreshAt = 0L;
/**
* yml 바탕 문구 로딩. DB 는 여기서 읽지 않는다 - 기동 시점에는 데이터소스·테넌트가
* 준비되지 않았을 수 있고, 프로퍼티 조회 실패로 컨텍스트 기동이 막히면 안 된다.
*/
@PostConstruct @PostConstruct
public void load() { public void load() {
try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) { try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
@@ -127,14 +206,17 @@ public class ApiStatusDraftTemplate {
Map<String, String> loaded = new LinkedHashMap<>(DEFAULTS); Map<String, String> loaded = new LinkedHashMap<>(DEFAULTS);
flatten("", draft, loaded); flatten("", draft, loaded);
this.texts = loaded; this.baselineTexts = loaded;
this.sections = readSections(draft); this.baselineSections = readSections(draft);
log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, sections.size()); log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, baselineSections.size());
} catch (Exception e) { } catch (Exception e) {
// 문구 설정 문제로 장애 탐지가 멈추면 안 된다 // 문구 설정 문제로 장애 탐지가 멈추면 안 된다
log.warn("{} 로딩 실패 - 기본 초안 양식 사용", RESOURCE, e); log.warn("{} 로딩 실패 - 기본 초안 양식 사용", RESOURCE, e);
this.texts = DEFAULTS; this.baselineTexts = DEFAULTS;
this.sections = DEFAULT_SECTIONS; this.baselineSections = DEFAULT_SECTIONS;
} finally {
this.texts = this.baselineTexts;
this.sections = this.baselineSections;
} }
} }
@@ -143,7 +225,11 @@ public class ApiStatusDraftTemplate {
* 정의되지 않은 치환자는 그대로 남겨 어떤 키가 빠졌는지 화면에서 드러나게 한다. * 정의되지 않은 치환자는 그대로 남겨 어떤 키가 빠졌는지 화면에서 드러나게 한다.
*/ */
public String text(String key, String... args) { public String text(String key, String... args) {
ensureFresh();
String template = texts.get(key); String template = texts.get(key);
if (template == null) {
template = baselineTexts.get(key);
}
if (template == null) { if (template == null) {
template = DEFAULTS.get(key); template = DEFAULTS.get(key);
} }
@@ -159,9 +245,124 @@ public class ApiStatusDraftTemplate {
} }
public List<Section> getSections() { public List<Section> getSections() {
ensureFresh();
return sections; return sections;
} }
// ────────────── PTL_PROPERTY 스냅샷 ──────────────
/**
* DB 스냅샷이 만료됐으면 그룹 전체를 한 번에 다시 읽어 바탕 문구 위에 덮는다.
* 조회에 실패해도 다음 주기까지는 재시도하지 않는다 - DB 가 아플 때 공지 한 건마다 매달리면 안 된다.
*/
private void ensureFresh() {
if (portalPropertyService == null) {
return;
}
if (System.currentTimeMillis() < nextRefreshAt) {
return;
}
synchronized (this) {
if (System.currentTimeMillis() < nextRefreshAt) {
return;
}
try {
refreshFromProperties();
} catch (Exception e) {
log.warn("공지 양식 프로퍼티 조회 실패 - 파일/기본 문구 사용", e);
} finally {
nextRefreshAt = System.currentTimeMillis() + REFRESH_INTERVAL_MS;
}
}
}
private void refreshFromProperties() {
Map<String, String> group = portalPropertyService.getPortalPropertiesAsMap(PROPERTY_GROUP);
Map<String, String> merged = new LinkedHashMap<>(baselineTexts);
String sectionsValue = null;
for (String key : DESCRIPTIONS.keySet()) {
String value = group.get(PROPERTY_PREFIX + key);
if (isBlank(value)) {
// 프로퍼티가 없으면 지금 값으로 만들어 둔다 (Oracle 은 빈 문자열도 NULL 이라 같은 취급).
// 관리자가 값을 지운 경우엔 이미 행이 있으므로 새로 만들지 않고 바탕 문구로 떨어진다.
value = seedProperty(key);
}
if (isBlank(value)) {
continue;
}
if (BODY_SECTIONS.equals(key)) {
sectionsValue = value;
continue;
}
merged.put(key, value);
}
List<Section> parsedSections = parseSections(sectionsValue);
this.texts = merged;
this.sections = parsedSections == null ? baselineSections : parsedSections;
}
/** 프로퍼티가 없을 때 현재 바탕 문구를 기본값으로 등록하고 그 값을 돌려준다 */
private String seedProperty(String key) {
String defaultValue = BODY_SECTIONS.equals(key)
? formatSections(baselineSections)
: baselineTexts.get(key);
if (isBlank(defaultValue)) {
return null;
}
try {
return portalPropertyService.getOrCreateProperty(
PROPERTY_GROUP, PROPERTY_PREFIX + key, defaultValue, DESCRIPTIONS.get(key));
} catch (Exception e) {
log.warn("공지 양식 프로퍼티 등록 실패: {}{}", PROPERTY_PREFIX, key, e);
return null;
}
}
/** {@code 제목|힌트;;제목|힌트} 파싱. 쓸 수 있는 항목이 없으면 null (호출부가 바탕 섹션을 쓴다) */
private List<Section> parseSections(String value) {
if (isBlank(value)) {
return null;
}
List<Section> result = new ArrayList<>();
for (String item : value.split(SECTION_DELIMITER)) {
String entry = item.trim();
if (entry.isEmpty()) {
continue;
}
int idx = entry.indexOf(SECTION_FIELD_DELIMITER);
String title = idx < 0 ? entry : entry.substring(0, idx).trim();
String hint = idx < 0 ? "" : entry.substring(idx + 1).trim();
if (title.isEmpty()) {
continue;
}
result.add(new Section(title, hint));
}
if (result.isEmpty()) {
log.warn("검수 항목 프로퍼티를 해석하지 못함 - 파일/기본 항목 사용: {}", value);
return null;
}
return Collections.unmodifiableList(result);
}
/** 검수 항목을 프로퍼티 저장 형식으로 되돌린다 (최초 등록용) */
private String formatSections(List<Section> source) {
StringBuilder sb = new StringBuilder();
for (Section section : source) {
if (sb.length() > 0) {
sb.append(SECTION_DELIMITER);
}
sb.append(section.getTitle()).append(SECTION_FIELD_DELIMITER).append(section.getHint());
}
return sb.toString();
}
private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
// ────────────── 내부 헬퍼 ────────────── // ────────────── 내부 헬퍼 ──────────────
/** 중첩 맵을 {@code body.lead-draft} 형태의 평탄 키로 편다. 리스트(sections)는 별도로 읽는다. */ /** 중첩 맵을 {@code body.lead-draft} 형태의 평탄 키로 편다. 리스트(sections)는 별도로 읽는다. */
@@ -0,0 +1,107 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* API Status 공지의 제목·본문 조립. 자동 탐지({@link ApiStatusDetectionService})와
* 관리자 수동 등록(공지사항 관리 화면)이 같은 양식을 쓰도록 문구 조립만 떼어 둔 것이다.
*
* <p>게시 여부 판정·API 명 조회는 하지 않는다 - 자동 탐지는 APIGW 스키마 조회가 필요하고
* 수동 등록은 영향 API 팝업이 이미 게시 API 만 고르게 되어 있어, 판정 책임을 호출부에 남긴다.
* 여기는 "게시 API 표시명 목록 + 미게시 건수" 만 받는다.</p>
*/
@Component
public class ApiStatusNoticeComposer {
private final ApiStatusDraftTemplate draftTemplate;
@Autowired
public ApiStatusNoticeComposer(ApiStatusDraftTemplate draftTemplate) {
this.draftTemplate = draftTemplate;
}
/**
* 공지 제목. 게시 API 가 하나도 없으면 인터페이스 명을 쓰지 않고 건수로만 적는다.
*
* @param keyword 제목 키워드 (장애 / 응답지연)
* @param publishedLabels 개발자포탈 게시 API 표시명 (노출 가능한 것만)
* @param hiddenCount 미게시 인터페이스 건수
* @param totalCount 영향 인터페이스 전체 건수
*/
public String buildTitle(String keyword, List<String> publishedLabels, int hiddenCount, int totalCount) {
if (publishedLabels == null || publishedLabels.isEmpty()) {
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_HIDDEN_ONLY,
"gw", hiddenLabel(totalCount), "keyword", keyword);
}
String first = publishedLabels.get(0);
if (totalCount > 1) {
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MULTI,
"first", first, "rest", String.valueOf(totalCount - 1), "keyword", keyword);
}
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_SINGLE, "first", first, "keyword", keyword);
}
/**
* 관리자 수동 등록 공지 제목. 자동 탐지 표기("[자동감지]", "GW 인터페이스 N건")를 쓰지 않는다 -
* 사람이 직접 쓰는 공지이고, 영향 API 선택 팝업이 게시 API 만 보여주므로 미게시 묶음 표기도 필요 없다.
*
* @param keyword 제목 키워드 (장애)
* @param publishedLabels 화면에서 고른 영향 API 표시명. 비어 있으면 API 명 없는 제목을 만든다
*/
public String buildManualTitle(String keyword, List<String> publishedLabels) {
if (publishedLabels == null || publishedLabels.isEmpty()) {
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MANUAL_EMPTY, "keyword", keyword);
}
String first = publishedLabels.get(0);
if (publishedLabels.size() > 1) {
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MANUAL_MULTI,
"first", first, "rest", String.valueOf(publishedLabels.size() - 1), "keyword", keyword);
}
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MANUAL_SINGLE, "first", first, "keyword", keyword);
}
/**
* 공지 본문(HTML). 머리말 + 영향 API 목록 + 검수 항목 순이다.
*
* <p>게시된 API 만 이름으로 적고(인터페이스 ID 는 넣지 않는다) 나머지는 건수로 묶는다.
* 공지 본문은 저장 시점에 굳는 HTML 이라 나중에 사용자별로 가릴 수 없다.</p>
*
* @param leadText 머리말 (이미 치환이 끝난 문구)
* @param draftHints true 면 검수 항목에 작성 힌트를, false 면 게시용 기본 문구를 채운다
*/
public String buildBody(String leadText, List<String> publishedLabels, int hiddenCount, boolean draftHints) {
StringBuilder detail = new StringBuilder();
detail.append(leadText);
detail.append("<p><b>")
.append(draftTemplate.text(ApiStatusDraftTemplate.BODY_AFFECTED_HEADING))
.append("</b></p><ul>");
if (publishedLabels != null) {
for (String label : publishedLabels) {
detail.append("<li>").append(label).append("</li>");
}
}
if (hiddenCount > 0) {
detail.append("<li>").append(hiddenLabel(hiddenCount)).append("</li>");
}
detail.append("</ul>");
for (ApiStatusDraftTemplate.Section section : draftTemplate.getSections()) {
detail.append("<p><b>").append(section.getTitle()).append("</b></p>");
// 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
detail.append("<p>").append(draftHints
? draftTemplate.text(ApiStatusDraftTemplate.BODY_DRAFT_TEXT, "hint", section.getHint())
: draftTemplate.text(ApiStatusDraftTemplate.BODY_FILLED_TEXT))
.append("</p>");
}
return detail.toString();
}
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
public String hiddenLabel(int hiddenCount) {
return draftTemplate.text(ApiStatusDraftTemplate.GW_LABEL, "count", String.valueOf(hiddenCount));
}
}
@@ -1,6 +1,8 @@
package com.eactive.eai.rms.ext.djb.apistatus; package com.eactive.eai.rms.ext.djb.apistatus;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@@ -17,8 +19,8 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
/** /**
* API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E) * API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E)
* *
* 1. 정상/지연/장애 -> 점검중 : CONTROL_START * 1. 정상/지연/장애 -> 점검중 : CHECK_START
* 2. 점검 -> 점검X : CONTROL_END * 2. 점검 -> 점검X : CHECK_END
* 3. 정상/지연 -> 장애 : ERROR_START * 3. 정상/지연 -> 장애 : ERROR_START
* 4. 장애 -> 장애X : ERROR_END * 4. 장애 -> 장애X : ERROR_END
* 5. 정상 -> 지연 : DELAY_START * 5. 정상 -> 지연 : DELAY_START
@@ -34,8 +36,8 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " FROM (" + " FROM ("
+ " SELECT EAISVCNAME" + " SELECT EAISVCNAME"
+ " , STATUS_CODE AS PREV_STATUS_CODE " + " , STATUS_CODE AS PREV_STATUS_CODE "
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'" + " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CHECK_START'"
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'" + " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CHECK_END'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'" + " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
+ " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'" + " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'"
+ " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'" + " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'"
@@ -45,7 +47,7 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " SELECT A.EAISVCNAME" + " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE" + " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
// 점검(KIND='MAINTENANCE')만 CTRL_YN 대상이다. KIND 를 안 걸면 관리자가 시작~종료 시각을 // 점검(KIND='MAINTENANCE')만 CTRL_YN 대상이다. KIND 를 안 걸면 관리자가 시작~종료 시각을
// 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CONTROL_START 가 발생한다. // 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CHECK_START 가 발생한다.
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X" + " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID " + " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ " AND Y.KIND = 'MAINTENANCE'" + " AND Y.KIND = 'MAINTENANCE'"
@@ -82,6 +84,36 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@Param("delayAvgRespTime") int delayAvgRespTime @Param("delayAvgRespTime") int delayAvgRespTime
); );
/**
* 현재 저장된 STATUS_CODE가 prevStatusCode와 일치할 때만 newStatusCode로 갱신한다 (CAS).
*
* <p>{@code ApiStatusMonitorJob}이 둘 이상의 인스턴스에서 겹쳐 실행될 수 있는 경우를 대비한
* 방어다. 두 실행이 동시에 같은 API의 상태 전이(event)를 감지해도, 먼저 커밋한 쪽만
* 실제로 갱신에 성공(영향 1행)하고 나머지는 0행이 되어 중복 알림 발송을 막을 수 있다.</p>
*
* @return 갱신된 행 수(0 또는 1). 0이면 이미 다른 실행이 선점했거나 최초 기록(행 없음)이다.
*/
@Modifying
@Query("UPDATE ApiStatus a SET a.statusCode = :newStatusCode, a.modifiedBy = 'SCHEDULER', a.modifiedDate = CURRENT_TIMESTAMP " +
"WHERE a.eaisvcname = :id AND a.statusCode = :prevStatusCode")
int updateStatusCodeIfMatches(@Param("id") String id,
@Param("prevStatusCode") String prevStatusCode,
@Param("newStatusCode") String newStatusCode);
/**
* API_STATUS 최초 기록용 INSERT. {@link #updateStatusCodeIfMatches}가 0행을 갱신했을 때
* (해당 API의 상태 row가 아직 없을 때) 시도한다. 그 사이 다른 실행이 먼저 INSERT했다면
* PK(EAISVCNAME) 제약 위반으로 실패하며, 이는 선점 실패로 처리한다.
*/
@Modifying
@Query(nativeQuery = true, value =
"INSERT INTO API_STATUS (EAISVCNAME, STATUS_CODE, MODIFIED_BY, MODIFIED_DATE) " +
"VALUES (:id, :statusCode, :modifiedBy, :modifiedDate)")
void insertIfAbsent(@Param("id") String id,
@Param("statusCode") String statusCode,
@Param("modifiedBy") String modifiedBy,
@Param("modifiedDate") LocalDateTime modifiedDate);
/** /**
* 판정 구간에 트래픽이 있는 API 의 오류율·지연 판정값. 개발 모드(-Deai.systemmode=D) 진단 로그 전용. * 판정 구간에 트래픽이 있는 API 의 오류율·지연 판정값. 개발 모드(-Deai.systemmode=D) 진단 로그 전용.
* *
@@ -147,22 +179,25 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
List<ApiNameRow> findApiNames(@Param("apiIds") Collection<String> apiIds); List<ApiNameRow> findApiNames(@Param("apiIds") Collection<String> apiIds);
/** /**
* 개발자포탈에 게시되는 인터페이스(= API) 만 추린다. * 노출 중인 API 그룹에 편성된 인터페이스 ID.
* *
* <p>GW 인터페이스는 전부 탐지 대상이지만 개발자포탈에 노출되는 것은 그 일부다. * <p>GW 인터페이스는 전부 탐지 대상이지만 개발자포탈에 노출되는 것은 그 일부다.
* 자동 초안(제목·공지 본문·타임라인)에 게시되지 않은 인터페이스 ID 가 그대로 박히면 * 자동 초안(제목·공지 본문·타임라인)에 게시되지 않은 인터페이스 ID 가 그대로 박히면
* 고객 화면에 내부 인터페이스가 드러나므로, 여기서 걸러 나머지는 건수로만 표기한다.</p> * 고객 화면에 내부 인터페이스가 드러나므로, 여기서 걸러 나머지는 건수로만 표기한다.</p>
* *
* <p>판정 기준은 포털 "오픈 API" 목록과 같은 두 조건이다 - 노출 중인 API 그룹에 편성돼 있고 * <p>게시 판정은 포털 "오픈 API" 목록과 같은 두 조건이다 - 노출 중인 API 그룹에 편성돼 있고
* PTL_API_SPEC_INFO 에 스펙이 있을 것. 역할·소속에 따른 사용자별 공개 범위는 admin 에 * PTL_API_SPEC_INFO 에 스펙이 있을 것. 이 쿼리는 앞 조건(AGWAPP)만 본다. 뒤 조건은
* 사용자 컨텍스트가 없어 적용하지 않는다(= 포털 화면이 한 번 더 좁힌다).</p> * EMSAPP 스키마라 {@code ApiSpecInfoRepository.findApiIdsIn} 으로 따로 조회해 교집합을 잡는다
* - 이 리포지토리는 tenant=APIGW(AGWAPP) 커넥션으로 도는 탓에 한 쿼리로 조인하면 ORA-00942 다.</p>
*
* <p>역할·소속에 따른 사용자별 공개 범위는 admin 에 사용자 컨텍스트가 없어 적용하지 않는다
* (= 포털 화면이 한 번 더 좁힌다).</p>
*/ */
@Query(nativeQuery = true, value = @Query(nativeQuery = true, value =
" SELECT DISTINCT GA.API_ID" " SELECT DISTINCT GA.API_ID"
+ " FROM API_GROUP_API GA" + " FROM API_GROUP_API GA"
+ " JOIN API_GROUP G ON G.ID = GA.API_GROUP_ID" + " JOIN API_GROUP G ON G.ID = GA.API_GROUP_ID"
+ " WHERE GA.API_ID IN (:apiIds)" + " WHERE GA.API_ID IN (:apiIds)"
+ " AND G.DISPLAY_YN = '1'" + " AND G.DISPLAY_YN = '1'")
+ " AND EXISTS (SELECT 1 FROM EMSAPP.PTL_API_SPEC_INFO S WHERE S.API_ID = GA.API_ID)") List<String> findDisplayedGroupApiIds(@Param("apiIds") Collection<String> apiIds);
List<String> findPublishedApiIds(@Param("apiIds") Collection<String> apiIds);
} }
@@ -2,21 +2,29 @@ package com.eactive.eai.rms.ext.djb.apistatus;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.PostConstruct;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.data.entity.man.role.Role; import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.ums.UmsManager; import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@@ -36,6 +44,22 @@ public class ApiStatusService {
@Autowired @Autowired
private ApiStatusDetectionService apiStatusDectionService; private ApiStatusDetectionService apiStatusDectionService;
@Autowired
private MonitoringContext monitoringContext;
@Autowired
@Qualifier("transactionManagerForEMS")
private PlatformTransactionManager transactionManager;
/** {@link #claimStatusChange} 의 최초 INSERT 시도를 바깥 트랜잭션과 분리하기 위한 템플릿 (REQUIRES_NEW) */
private TransactionTemplate insertTransactionTemplate;
@PostConstruct
private void initTransactionTemplate() {
this.insertTransactionTemplate = new TransactionTemplate(transactionManager);
this.insertTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
public void updateApiStatus(HashMap<String, String> param) { public void updateApiStatus(HashMap<String, String> param) {
@@ -60,12 +84,12 @@ public class ApiStatusService {
String newStatusCode = resolveStatusCode(event.getEvent()); String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue; if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus()); // 인스턴스가 둘 이상 동시에 이 job을 돌릴 수 있으므로, CAS로 선점(claim)에 성공한 경우만 처리한다.
// (그렇지 않으면 두 인스턴스가 같은 상태 전이를 각자 감지해 알림을 중복 발송하게 된다)
apiStatus.setEaisvcname(event.getEaisvcname()); if (!claimStatusChange(event.getEaisvcname(), event.getPrevStatusCode(), newStatusCode)) {
apiStatus.setStatusCode(newStatusCode); log.debug("API 상태 변경 선점 실패(다른 인스턴스가 이미 처리) - {}", event.getEaisvcname());
continue;
apiStatusRepository.saveAndFlush(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT (즉시 flush) }
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode); log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
@@ -86,17 +110,48 @@ public class ApiStatusService {
String event = entry.getKey(); String event = entry.getKey();
List<String> apiIds = entry.getValue(); List<String> apiIds = entry.getValue();
//내부직원 스윙챗 발송
if (monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
HashMap<String, Object> params = getSwingChatMessage(event, apiIds); HashMap<String, Object> params = getSwingChatMessage(event, apiIds);
ums.send("api-monitor", MessageCode.API_STATUS_CHANGED, params); ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, params);
}
//제휴사 웹훅 발송 //제휴사 웹훅 발송
if (monitoringContext.getBooleanProperty(MonitoringContext.RMS_WEBHOOK_ENABLED, true)) {
ums.sendWebhook(event, apiIds); ums.sendWebhook(event, apiIds);
}
//개발자포탈 API상태모니터링 화면을 위한 처리 //개발자포탈 API상태모니터링 화면을 위한 처리
apiStatusDectionService.detect(event, apiIds); apiStatusDectionService.detect(event, apiIds);
} }
} }
/**
* 현재 저장된 상태가 prevStatusCode와 일치할 때만 newStatusCode로 갱신한다 (CAS).
*
* <p>갱신 대상 행이 없으면(해당 API 최초 기록) INSERT를 시도한다. 그 사이 다른 인스턴스가
* 먼저 INSERT했다면 PK 제약 위반이 발생하는데, 이를 선점 실패로 처리한다. 이 INSERT 시도는
* 실패해도 {@code updateApiStatus}의 바깥 트랜잭션 전체가 rollback-only가 되지 않도록
* 별도 트랜잭션(REQUIRES_NEW)으로 격리한다.</p>
*
* @return 이번 실행이 상태 변경을 선점했으면 true
*/
private boolean claimStatusChange(String eaisvcname, String prevStatusCode, String newStatusCode) {
int updated = apiStatusRepository.updateStatusCodeIfMatches(eaisvcname, prevStatusCode, newStatusCode);
if (updated == 1) {
return true;
}
try {
insertTransactionTemplate.execute(status -> {
apiStatusRepository.insertIfAbsent(eaisvcname, newStatusCode, "SCHEDULER", LocalDateTime.now());
return null;
});
return true;
} catch (DataIntegrityViolationException e) {
return false;
}
}
/** /**
* 판정 구간에 거래가 있는 API 의 오류율·지연 판정값을 debug 로그로 남긴다. * 판정 구간에 거래가 있는 API 의 오류율·지연 판정값을 debug 로그로 남긴다.
@@ -179,8 +234,8 @@ public class ApiStatusService {
private String resolveStatusCode(String event) { private String resolveStatusCode(String event) {
switch (event) { switch (event) {
case "CONTROL_START": return "C"; //점검 case "CHECK_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상 case "CHECK_END": return "N"; //정상
case "ERROR_START": return "E"; //장애 case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상 case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연 case "DELAY_START": return "D"; //지연
@@ -195,10 +250,10 @@ public class ApiStatusService {
private HashMap<String, Object> getSwingChatMessage(String event, List<String> apiIds) { private HashMap<String, Object> getSwingChatMessage(String event, List<String> apiIds) {
String message = ""; String message = "";
switch (event) { switch (event) {
case "CONTROL_START": case "CHECK_START":
message = "API 서비스 점검이 시작되었습니다"; //점검 message = "API 서비스 점검이 시작되었습니다"; //점검
break; break;
case "CONTROL_END": case "CHECK_END":
message = "API 서비스 점검이 완료되었습니다"; //정상 message = "API 서비스 점검이 완료되었습니다"; //정상
break; break;
case "ERROR_START": case "ERROR_START":
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode; import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient; import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.ums.UmsManager; import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@@ -23,18 +24,24 @@ public class InflowTokenService {
@Autowired @Autowired
private InflowTokenRepository inflowTokenRepository; private InflowTokenRepository inflowTokenRepository;
@Autowired
private MonitoringContext monitoringContext;
@Autowired @Autowired
private UmsManager ums; private UmsManager ums;
public void checkRecentFails(long rangeMinute) { public void checkRecentFails(long rangeMinute) {
if (monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute); List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) { for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt()); log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
String message = "유량제어 토큰 획득 실패가 발생하였습니다 \n" + info.getEaisvcname(); String message = "유량제어 토큰 획득 실패가 발생하였습니다 \n" + info.getEaisvcname();
HashMap<String,Object> params = new HashMap<String,Object>(); HashMap<String,Object> params = new HashMap<String,Object>();
params.put("message", message); params.put("message", message);
ums.send("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params); ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params);
}
} }
} }
} }
@@ -126,9 +126,9 @@ public class ApiStatsHourlyAggregationJob implements Job {
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + " , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
" , COUNT(EAISVCSERNO)" + " , COUNT(EAISVCSERNO)" +
" , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" + " , SUM(CASE WHEN A.ERROR_CODE IS NULL OR C.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" + " , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL THEN 1 ELSE 0 END)" + " , SUM(CASE WHEN A.ERROR_CODE IS NOT NULL AND B.CODE IS NULL AND C.CODE IS NULL THEN 1 ELSE 0 END)" +
" , 0, 0, 0, 0" + " , 0, 0, 0, 0" +
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" + " , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
" FROM (" + " FROM (" +
@@ -143,12 +143,12 @@ public class ApiStatsHourlyAggregationJob implements Job {
" + TO_NUMBER(SUBSTR(MAX(MSGPRCSSYMS),15,3))" + " + TO_NUMBER(SUBSTR(MAX(MSGPRCSSYMS),15,3))" +
" - TO_NUMBER(SUBSTR(MIN(MSGPRCSSYMS),15,3))" + " - TO_NUMBER(SUBSTR(MIN(MSGPRCSSYMS),15,3))" +
" ) AS RESP_TIME" + " ) AS RESP_TIME" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" + " , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN EAIERRCD ELSE '' END) AS ERROR_CODE" +
" , MAX(EAIERRCD) AS ERROR_CODE" + " FROM " + logTableName + " A " +
" FROM " + logTableName +
" WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" + " WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" +
" GROUP BY EAISVCSERNO" + " GROUP BY EAISVCSERNO" +
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " + " ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
" LEFT OUTER JOIN TSEAICM20 C ON A.ERROR_CODE = C.CODE AND C.CODEGROUP = 'ERRCODE_BIZ' AND C.USEYN = 'Y' " +
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + " GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER"; " , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
@@ -6,7 +6,6 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job; import org.quartz.Job;
import org.quartz.JobDataMap; import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionContext;
@@ -44,8 +43,12 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
* </ul> * </ul>
* *
* </pre> * </pre>
*
* <p>동시 실행 허용: DB 커넥션풀 경합/락 대기로 특정 실행이 멈추더라도(트랜잭션 매니저가
* 다른 여러 퍼시스턴스 경로가 얽혀 있어 발생 가능) 다음 fire까지 막히지 않도록
* {@code @DisallowConcurrentExecution}을 두지 않는다. 상태 판정/알림 로직은
* interfaceId 시간창 기반 중복 제거가 있어 겹쳐 실행돼도 안전하다.</p>
*/ */
@DisallowConcurrentExecution
public class ApiStatusMonitorJob implements Job { public class ApiStatusMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class); private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class);
@@ -1,9 +1,7 @@
package com.eactive.eai.rms.ext.djb.job; package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -13,7 +11,6 @@ import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException; import org.quartz.JobExecutionException;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@@ -37,7 +34,7 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
* <p>Job 파라미터 (JobDataMap):</p> * <p>Job 파라미터 (JobDataMap):</p>
* <ul> * <ul>
* <li> * <li>
* NONE * <b>api.inflow.fail.range_minute</b>: 유량제어 토큰 획득 실패 조회 구간 (단위: 분, 기본값: 5)
* </li> * </li>
* </ul> * </ul>
* *
@@ -48,6 +45,12 @@ public class InflowTokenMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class); private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
/** Job 파라미터 키: 유량제어 실패 조회 구간(분) */
public static final String KEY_API_INFLOW_FAIL_RANGE_MINUTE = "api.inflow.fail.range_minute";
/** 유량제어 실패 조회 구간 기본값(분) */
public static final String DEFAULT_RANGE_MINUTE = "5";
private transient MonitoringContext monitoringContext; private transient MonitoringContext monitoringContext;
@Override @Override
@@ -69,18 +72,19 @@ public class InflowTokenMonitorJob implements Job {
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm")); log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
long execMinute = 1; //default 배치 실행주기(분) // Job 프로퍼티에서 유량제어 실패 조회 구간(분)을 읽는다. 미지정 시 기본값 사용
long execMinute;
Trigger trigger = context.getTrigger(); String rangeMinute = jobDataMap.getString(KEY_API_INFLOW_FAIL_RANGE_MINUTE);
Date prevFireTime = trigger.getPreviousFireTime(); if (StringUtils.isEmpty(rangeMinute)) {
Date nextFireTime = trigger.getNextFireTime(); rangeMinute = DEFAULT_RANGE_MINUTE;
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
if (prevFireTime != null && nextFireTime != null) {
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
log.info("실행 주기: {}분", execMinute);
} }
try {
execMinute = Long.parseLong(rangeMinute.trim());
} catch (NumberFormatException e) {
log.warn("잘못된 {} 값: {} - 기본값 {} 사용", KEY_API_INFLOW_FAIL_RANGE_MINUTE, rangeMinute, DEFAULT_RANGE_MINUTE);
execMinute = Long.parseLong(DEFAULT_RANGE_MINUTE);
}
log.info("유량제어 실패 조회 구간: {}분", execMinute);
ApplicationContext appContext; ApplicationContext appContext;
try { try {
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.ext.djb.smsauth;
import com.eactive.apim.portal.user.entity.UserInfo; import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController; import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.common.login.ChangePasswordResponseDto;
import com.eactive.eai.rms.common.login.LoginVo; import com.eactive.eai.rms.common.login.LoginVo;
import com.eactive.eai.rms.common.login.MainController; import com.eactive.eai.rms.common.login.MainController;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -70,6 +71,7 @@ public class SmsAuthController implements InterceptorSkipController {
.maskedPhone(getSmsAuthService().maskPhoneNumber(userInfo.getCphnno())) .maskedPhone(getSmsAuthService().maskPhoneNumber(userInfo.getCphnno()))
.expiresAt(getSmsAuthService().getExpiresAt(session)) .expiresAt(getSmsAuthService().getExpiresAt(session))
.resendableAt(getSmsAuthService().getResendableAt(session)) .resendableAt(getSmsAuthService().getResendableAt(session))
.smsTestHint(getSmsAuthService().getFixedModeHintText())
.build(); .build();
} else { } else {
return SmsAuthDto.builder() return SmsAuthDto.builder()
@@ -119,6 +121,7 @@ public class SmsAuthController implements InterceptorSkipController {
.maskedPhone(getSmsAuthService().maskPhoneNumber(userInfo.getCphnno())) .maskedPhone(getSmsAuthService().maskPhoneNumber(userInfo.getCphnno()))
.expiresAt(getSmsAuthService().getExpiresAt(session)) .expiresAt(getSmsAuthService().getExpiresAt(session))
.resendableAt(getSmsAuthService().getResendableAt(session)) .resendableAt(getSmsAuthService().getResendableAt(session))
.smsTestHint(getSmsAuthService().getFixedModeHintText())
.build(); .build();
} else { } else {
return SmsAuthDto.builder() return SmsAuthDto.builder()
@@ -208,6 +211,45 @@ public class SmsAuthController implements InterceptorSkipController {
} }
} }
/**
* SMS 인증번호 검증 및 비밀번호 변경 완료
*/
@RequestMapping(value = "/verify-password-change.json", method = RequestMethod.POST)
@ResponseBody
public ChangePasswordResponseDto verifyPasswordChangeAuthCode(HttpServletRequest request,
@RequestParam("authCode") String authCode) {
HttpSession session = request.getSession();
UserInfo userInfo = getSmsAuthService().getUserInfoFromSession(session);
String pendingPasswordHash = getSmsAuthService().getPendingPasswordHashFromSession(session);
if (userInfo == null || pendingPasswordHash == null) {
log.warn("SMS 인증 세션 정보 없음 - verify-password-change 요청");
return ChangePasswordResponseDto.builder()
.success(false)
.message("인증 세션이 만료되었습니다. 다시 시도해주세요.")
.build();
}
if (getSmsAuthService().isExpired(session)) {
log.warn("SMS 인증번호 만료됨(비밀번호 변경) - userId: {}", userInfo.getUserid());
getSmsAuthService().clearAuthSession(session);
return ChangePasswordResponseDto.builder()
.success(false)
.message("인증번호가 만료되었습니다. 다시 시도해주세요.")
.build();
}
if (!getSmsAuthService().validateAuthCode(session, authCode)) {
log.warn("SMS 인증번호 불일치(비밀번호 변경) - userId: {}", userInfo.getUserid());
return ChangePasswordResponseDto.builder()
.success(false)
.message("인증번호가 일치하지 않습니다.")
.build();
}
return mainController.commitPendingPasswordChange(request, session);
}
/** /**
* SMS 인증 상태 조회 * SMS 인증 상태 조회
*/ */
@@ -50,4 +50,9 @@ public class SmsAuthDto {
* 에러 메시지 * 에러 메시지
*/ */
private String errorMessage; private String errorMessage;
/**
* djb.sms_auth.mode=fixed 일 때만 채워지는 테스트용 인증번호 안내 문구 (예: "[테스트] 인증번호: 000000")
*/
private String smsTestHint;
} }
@@ -30,6 +30,7 @@ public class SmsAuthService {
public static final String SESSION_KEY_ISSUED_AT = "SMS_AUTH_ISSUED_AT"; public static final String SESSION_KEY_ISSUED_AT = "SMS_AUTH_ISSUED_AT";
public static final String SESSION_KEY_USER_INFO = "SMS_AUTH_USER_INFO"; public static final String SESSION_KEY_USER_INFO = "SMS_AUTH_USER_INFO";
public static final String SESSION_KEY_LOGIN_VO = "SMS_AUTH_LOGIN_VO"; public static final String SESSION_KEY_LOGIN_VO = "SMS_AUTH_LOGIN_VO";
public static final String SESSION_KEY_PENDING_PASSWORD_HASH = "SMS_AUTH_PENDING_PASSWORD_HASH";
// 시간 설정 (밀리초) // 시간 설정 (밀리초)
public static final long EXPIRY_MS = 60_000; // 1분 public static final long EXPIRY_MS = 60_000; // 1분
@@ -130,6 +131,27 @@ public class SmsAuthService {
log.debug("SMS 인증 세션 설정 완료 - userId: {}", userInfo.getUserid()); log.debug("SMS 인증 세션 설정 완료 - userId: {}", userInfo.getUserid());
} }
/**
* 비밀번호 변경용 SMS 인증 세션 설정.
* 로그인 플로우의 {@link #setupAuthSession}과 세션 키(AUTH_CODE/ISSUED_AT/USER_INFO)를 공유하되,
* LOGIN_VO 대신 검증 성공 시 실제로 반영할 새 비밀번호 해시를 저장한다.
*/
public void setupPasswordChangeAuthSession(HttpSession session, UserInfo userInfo, String pendingPasswordHash, String authCode) {
long now = System.currentTimeMillis();
session.setAttribute(SESSION_KEY_AUTH_CODE, authCode);
session.setAttribute(SESSION_KEY_ISSUED_AT, now);
session.setAttribute(SESSION_KEY_USER_INFO, userInfo);
session.setAttribute(SESSION_KEY_PENDING_PASSWORD_HASH, pendingPasswordHash);
log.debug("SMS 인증 세션(비밀번호 변경용) 설정 완료 - userId: {}", userInfo.getUserid());
}
/**
* 세션에서 대기 중인 새 비밀번호 해시 조회 (비밀번호 변경 SMS 인증용)
*/
public String getPendingPasswordHashFromSession(HttpSession session) {
return (String) session.getAttribute(SESSION_KEY_PENDING_PASSWORD_HASH);
}
/** /**
* 인증번호 검증 * 인증번호 검증
*/ */
@@ -224,6 +246,7 @@ public class SmsAuthService {
session.removeAttribute(SESSION_KEY_ISSUED_AT); session.removeAttribute(SESSION_KEY_ISSUED_AT);
session.removeAttribute(SESSION_KEY_USER_INFO); session.removeAttribute(SESSION_KEY_USER_INFO);
session.removeAttribute(SESSION_KEY_LOGIN_VO); session.removeAttribute(SESSION_KEY_LOGIN_VO);
session.removeAttribute(SESSION_KEY_PENDING_PASSWORD_HASH);
log.debug("SMS 인증 세션 정보 클리어 완료"); log.debug("SMS 인증 세션 정보 클리어 완료");
} }
@@ -266,4 +289,17 @@ public class SmsAuthService {
public boolean isEnabled() { public boolean isEnabled() {
return getProperty().isSmsAuthEnabled(); return getProperty().isSmsAuthEnabled();
} }
/**
* fixed 모드(djb.sms_auth.mode=fixed)일 때 화면에 표시할 테스트용 안내 문구.
* 실제 SMS 발송 없이도 개발/테스트 환경에서 인증번호를 알 수 있도록 노출한다.
* fixed 모드가 아니면 null (화면에서는 힌트를 표시하지 않음).
*/
public String getFixedModeHintText() {
DjbProperty prop = getProperty();
if (!MODE_FIXED.equalsIgnoreCase(prop.getSmsAuthMode())) {
return null;
}
return "[테스트] 인증번호: " + prop.getSmsAuthFixedValue();
}
} }
@@ -3,12 +3,12 @@ package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -35,9 +35,30 @@ public class ApiUseStatsController {
} }
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST") @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) { public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search) {
Page<ApiStatsUI> page = service.selectList(search, pageable); List<ApiStatsUI> list = service.selectList(search);
return ResponseEntity.ok(new GridResponse<>(page));
GridResponse<ApiStatsUI> response = new GridResponse<>(list);
ApiStatsUI total = service.selectTotal(search);
if (total != null) {
response.setUserdata(toFooterData(total));
}
return ResponseEntity.ok(response);
}
private Map<String, Object> toFooterData(ApiStatsUI total) {
Map<String, Object> footer = new LinkedHashMap<>();
footer.put("orgName", "합계");
footer.put("totalCnt", total.getTotalCnt());
footer.put("successCnt", total.getSuccessCnt());
footer.put("successRate", total.getSuccessRate());
footer.put("failRate", total.getFailRate());
footer.put("timeoutCnt", total.getTimeoutCnt());
footer.put("systemErrCnt", total.getSystemErrCnt());
footer.put("avgRespTime", total.getAvgRespTime());
footer.put("minRespTime", total.getMinRespTime());
footer.put("maxRespTime", total.getMaxRespTime());
return footer;
} }
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT") @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT")
@@ -57,6 +78,12 @@ public class ApiUseStatsController {
return; return;
} }
ApiStatsUI total = service.selectTotal(search);
if (total != null) {
total.setOrgName("합계");
uiList.add(total);
}
String fileName = generateFileName(search); String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName); log.info("Generating Excel file: {}", fileName);
@@ -10,9 +10,6 @@ import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContext;
import javax.persistence.Query; import javax.persistence.Query;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -33,23 +30,6 @@ public class ApiUseStatsService
@PersistenceContext @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
@SuppressWarnings("unchecked")
public Page<ApiStatsUI> selectList(ApiStatsSearch search, Pageable pageable) {
Query dataQuery = getDataQuery(search);
dataQuery.setFirstResult((int) pageable.getOffset());
dataQuery.setMaxResults(pageable.getPageSize());
List<Object[]> rows = dataQuery.getResultList();
List<ApiStatsUI> results = rows.stream()
.map(this::toVO)
.collect(Collectors.toList());
long records = rows.size() > 0 ? StringUtils.toLong(rows.get(0)[0]) : 0;
return new PageImpl<>(results, pageable, records);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<ApiStatsUI> selectList(ApiStatsSearch search) { public List<ApiStatsUI> selectList(ApiStatsSearch search) {
Query dataQuery = getDataQuery(search); Query dataQuery = getDataQuery(search);
@@ -60,14 +40,64 @@ public class ApiUseStatsService
} }
/**
* 하단 합계(Total)행 조회 (grid footer 용, 그룹핑 없이 전체 집계)
*/
@SuppressWarnings("unchecked")
public ApiStatsUI selectTotal(ApiStatsSearch search) {
Query totalQuery = getTotalQuery(search);
List<Object[]> rows = totalQuery.getResultList();
return rows.isEmpty() ? null : toVO(rows.get(0));
}
private Query getDataQuery(ApiStatsSearch search) { private Query getDataQuery(ApiStatsSearch search) {
StringBuilder where = buildNativeWhere(search); StringBuilder where = buildNativeWhere(search);
String dataSql = ""; String dataSql = "";
if ("ORG".equals(search.getSearchType())) { if ("ORG".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS " dataSql = "SELECT NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류') AS ORGNAME, '' AS APIDESC";
+ ", NVL(B.ORGNAME, D.ORG_NAME) AS ORGNAME" dataSql += getQueryBody(search);
+ ", SUM(A.TOTAL_CNT)" dataSql += " GROUP BY NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류')";
dataSql += " ORDER BY ORGNAME";
}
else if ("API".equals(search.getSearchType())) {
dataSql = "SELECT A.API_NAME, A.EAISVCDESC ";
dataSql += getQueryBody(search);
dataSql += " GROUP BY A.API_NAME, A.EAISVCDESC ";
dataSql += " ORDER BY API_NAME";
}
else if ("DATE".equals(search.getSearchType())) {
dataSql = "SELECT TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME, '' AS APIDESC ";
dataSql += getQueryBody(search);
dataSql += " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')";
dataSql += " ORDER BY STAT_TIME";
}
Query dataQuery = entityManager.createNativeQuery(dataSql);
bindParams(dataQuery, search);
return dataQuery;
}
private Query getTotalQuery(ApiStatsSearch search) {
StringBuilder where = buildNativeWhere(search);
String totalSql;
totalSql = "SELECT '합계' AS ORGNAME, '' AS APIDESC ";
totalSql += getQueryBody(search);
Query totalQuery = entityManager.createNativeQuery(totalSql);
bindParams(totalQuery, search);
return totalQuery;
}
private String getQueryBody(ApiStatsSearch search) {
String dataSql = ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)" + ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
@@ -79,6 +109,7 @@ public class ApiUseStatsService
+ " FROM (" + " FROM ("
+ " SELECT A.STAT_TIME " + " SELECT A.STAT_TIME "
+ " , A.API_NAME" + " , A.API_NAME"
+ " , C.EAISVCDESC"
+ " , A.CLIENT_ID " + " , A.CLIENT_ID "
+ " , A.TOTAL_CNT" + " , A.TOTAL_CNT"
+ " , A.SUCCESS_CNT" + " , A.SUCCESS_CNT"
@@ -87,14 +118,14 @@ public class ApiUseStatsService
+ " , A.SYSTEM_ERR_CNT" + " , A.SYSTEM_ERR_CNT"
+ " , A.MIN_RESP_TIME" + " , A.MIN_RESP_TIME"
+ " , A.MAX_RESP_TIME" + " , A.MAX_RESP_TIME"
+ " , CASE WHEN API_NAME LIKE '%1' THEN SUBSTR(INBOUND_ADAPTER,2,3) ELSE SUBSTR(OUTBOUND_ADAPTER,2,3) END AS ORG_CODE" + " , CASE WHEN API_NAME LIKE '%1' THEN SUBSTR(OUTBOUND_ADAPTER,2,3) ELSE SUBSTR(INBOUND_ADAPTER,2,3) END AS ORG_CODE"
+ " FROM API_STATS_DAY A " + " FROM API_STATS_DAY A LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ " WHERE A.STAT_TIME >= :searchStartDate " + " WHERE A.STAT_TIME >= :searchStartDate "
+ " AND A.STAT_TIME <= :searchEndDate "; + " AND A.STAT_TIME <= :searchEndDate ";
if (StringUtils.isNotBlank(search.getSearchApiName())) { if (StringUtils.isNotBlank(search.getSearchApiName())) {
dataSql += " AND A.API_NAME LIKE :searchApiName "; dataSql += " AND UPPER(A.API_NAME) LIKE :searchApiName ";
} }
dataSql += " ) A" dataSql += " ) A"
@@ -103,64 +134,24 @@ public class ApiUseStatsService
+ " WHERE 1=1 "; + " WHERE 1=1 ";
if (StringUtils.isNotBlank(search.getSearchOrgName())) { if (StringUtils.isNotBlank(search.getSearchOrgName())) {
dataSql += " AND UPPER(NVL(B.ORGNAME, D.ORG_NAME)) LIKE :searchOrgName "; dataSql += " AND UPPER(NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류')) LIKE :searchOrgName ";
} }
dataSql += " GROUP BY NVL(B.ORGNAME, D.ORG_NAME)" return dataSql;
+ " ORDER BY ORGNAME";
}
else if ("API".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", A.API_NAME"
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A "
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ where
+ " GROUP BY A.API_NAME"
+ " ORDER BY API_NAME";
}
else if ("DATE".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A "
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ where
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
+ " ORDER BY STAT_TIME";
} }
Query dataQuery = entityManager.createNativeQuery(dataSql);
dataQuery.setParameter("searchStartDate", search.getParsedStartDate()); private void bindParams(Query query, ApiStatsSearch search) {
dataQuery.setParameter("searchEndDate", search.getParsedEndDate()); query.setParameter("searchStartDate", search.getParsedStartDate());
query.setParameter("searchEndDate", search.getParsedEndDate());
if (StringUtils.isNotBlank(search.getSearchApiName())) { if (StringUtils.isNotBlank(search.getSearchApiName())) {
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%"); query.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
} }
if (StringUtils.isNotBlank(search.getSearchOrgName())) { if (StringUtils.isNotBlank(search.getSearchOrgName())) {
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%"); query.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
} }
return dataQuery;
} }
@@ -169,7 +160,8 @@ public class ApiUseStatsService
*/ */
private ApiStatsUI toVO(Object[] row) { private ApiStatsUI toVO(Object[] row) {
ApiStatsUI vo = new ApiStatsUI(); ApiStatsUI vo = new ApiStatsUI();
vo.setOrgName(StringUtils.toString(row[1])); vo.setOrgName(StringUtils.toString(row[0]));
vo.setApiDesc(StringUtils.toString(row[1]));
vo.setTotalCnt(StringUtils.toLong(row[2])); vo.setTotalCnt(StringUtils.toLong(row[2]));
vo.setSuccessCnt(StringUtils.toLong(row[3])); vo.setSuccessCnt(StringUtils.toLong(row[3]));
vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP)); vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
@@ -3,6 +3,7 @@ package com.eactive.eai.rms.ext.djb.ums;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@@ -43,7 +44,10 @@ public class UmsManager {
} }
for (UserInfo user : users) { for (UserInfo user : users) {
String cleanedPhone = user.getCphnno().replaceAll("[^0-9]", "").trim(); String cleanedPhone = "";
if (StringUtils.isNotEmpty(user.getCphnno())) {
cleanedPhone = user.getCphnno().replaceAll("[^0-9]", "").trim();
}
MessageRecipient recipient = MessageRecipient.builder() MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid()) .userId(user.getUserid())
.username(user.getUsername()) .username(user.getUsername())
@@ -52,9 +56,48 @@ public class UmsManager {
.messengerId(user.getUserid()) .messengerId(user.getUserid())
.build(); .build();
messageHandlerService.publishEvent(messageCode, recipient, params); messageHandlerService.publishEvent(messageCode, recipient, params);
} }
} }
/**
* roleId에 해당하는 역할을 가진 직원에게 messenger 발송 (제주은행은 sms, email은 1인 발송이나, messenger는 다수 발송)
* (기존 ums 로직을 멀티 receiver로 변경할려면 많은 부분을 수정해야하기 때문에, messenger 전용만 추가)
* @param role
* @param messageCode
* @param params
* @return
*/
public void sendMessenger(String roleId, MessageCode messageCode, HashMap<String, Object> params) {
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
if (users.isEmpty()) {
log.info("sendMessenger: no users found for role '{}'", roleId);
return;
}
String messengerIds = "";
String userNames = "";
for (UserInfo user : users) {
if (StringUtils.isEmpty(messengerIds)) {
messengerIds += user.getUserid();
userNames += user.getUsername();
} else {
messengerIds += "," + user.getUserid();
userNames += "," + user.getUsername();
}
}
MessageRecipient recipient = MessageRecipient.builder()
.userId("")
.username(userNames)
.email("")
.phone("")
.messengerId(messengerIds)
.build();
messageHandlerService.publishEvent(messageCode, recipient, params);
}
/** /**
* 직원 또는 회원에게 UMS(sms,email,messenger) 발송 * 직원 또는 회원에게 UMS(sms,email,messenger) 발송
* @param user * @param user
@@ -166,29 +166,28 @@ public class UmsService {
if ("MESSENGER".equals(messageRequest.getMessageType()) ) { if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
stdHeadVO.setChnl_tycd("FEP"); // 채널유형코드 stdHeadVO.setChnl_tycd("FEP"); // 채널유형코드
stdHeadVO.setHmab_dvcd("2"); // 내외구분코드 1:내부 2:외부(타발)
stdHeadVO.setTx_brcd("132");
stdHeadVO.setTlrno("ET0001");
stdHeadVO.setInsl_brcd("132");
stdHeadVO.setBlng_brcd("132");
stdHeadVO.setMgmt_brcd("132");
// 스윙챗 알림 공용 // 스윙챗 알림 공용
SwingCommonVO swiComVO = new SwingCommonVO(); SwingCommonVO swiComVO = new SwingCommonVO();
swiComVO.setClientld(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_id", "")); swiComVO.setClientId(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_id", ""));
swiComVO.setClientSecret(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_secret", "")); swiComVO.setClientSecret(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_secret", ""));
swiComVO.setCompanyCode("CB"); swiComVO.setCompanyCode("CB");
swiComVO.setRequestUniqueKey(this.genrateRandomStringNumber26()); swiComVO.setRequestUniqueKey(this.genrateRandomStringNumber26());
// 스윙챗 알림 데이터 // 스윙챗 알림 데이터
SwingDataVO swiDataVO = new SwingDataVO(); SwingDataVO swiDataVO = new SwingDataVO();
swiDataVO.setNotiCode("CNO_FEP_APIM_0002"); //0001:긴급, 0002:중요 swiDataVO.setNotiCode("CNO_FEP_API_0002"); //0001:긴급, 0002:중요
swiDataVO.setNotiTitle("알림"); swiDataVO.setNotiTitle("알림");
swiDataVO.setNotiContent(messageRequest.getMessage()); swiDataVO.setNotiContent(messageRequest.getMessage());
// 스윙챗 알림 데이터 // 스윙챗 알림 데이터
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>(); swiDataVO.setReceiverInfo(getSwingReceiver(messageRequest));
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
receiver.setType("PSN");
receiver.setCompanyCode("CB");
receiver.setCode(messageRequest.getMessengerId());
swiReDataVOList.add(receiver);
swiDataVO.setReceiverInfo(swiReDataVOList);
String header = gson.toJson(stdHeadVO); String header = gson.toJson(stdHeadVO);
String common = gson.toJson(swiComVO); String common = gson.toJson(swiComVO);
@@ -253,12 +252,9 @@ public class UmsService {
dataVO.setUms_evnt_id(propertyService.getPropertyValue("Monitoring", "djb.ums.email.ums_evnt_id", "")); // UMS이벤트ID(필수) dataVO.setUms_evnt_id(propertyService.getPropertyValue("Monitoring", "djb.ums.email.ums_evnt_id", "")); // UMS이벤트ID(필수)
dataVO.setSnd_dman_dt(jsonData[0]); dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType())); dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
dataVO.setSnd_dman_sno("1"); dataVO.setDman_seqno(1); // 발송요청순번
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목 dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
dataVO.setCstno(propertyService.getPropertyValue("Monitoring", "djb.ums.email.cstno", "")); // 고객번호(필수) dataVO.setCstno(propertyService.getPropertyValue("Monitoring", "djb.ums.email.cstno", "")); // 고객번호(필수)
dataVO.setSendr_emaddr(propertyService.getPropertyValue("Monitoring", "djb.ums.email.sendr_emaddr", "")); // 발신자 이메일
dataVO.setSendr_nm(propertyService.getPropertyValue("Monitoring", "djb.ums.email.sendr_nm", "")); // 발신자 이름
dataVO.setSndbk_emaddr(propertyService.getPropertyValue("Monitoring", "djb.ums.email.sndbk_emaddr", "")); // 리턴 이메일
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수) dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수) dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보 dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
@@ -291,6 +287,25 @@ public class UmsService {
} }
private List<SwingReceiverDataVO> getSwingReceiver(MessageRequest messageRequest) {
String[] messengerIds = messageRequest.getMessengerId().split(",");
String[] userNames = messageRequest.getUsername().split(",");
// 스윙챗 알림 데이터
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
for (int i = 0; i < messengerIds.length; i++) {
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
receiver.setType("PSN");
receiver.setCompanyCode("CB");
receiver.setCode(messengerIds[i]);
receiver.setName(userNames[i]);
swiReDataVOList.add(receiver);
}
return swiReDataVOList;
}
private String[] makeStdGuidInfo() { private String[] makeStdGuidInfo() {
// 리턴값 // 리턴값
String rtnVal[] = new String[3]; String rtnVal[] = new String[3];
@@ -15,7 +15,7 @@ public class EmailDataVO {
private String snd_dman_id; private String snd_dman_id;
/** 발송요청순번 */ /** 발송요청순번 */
private String snd_dman_sno; private int dman_seqno;
/** 메일제목 */ /** 메일제목 */
private String email_titl; private String email_titl;
@@ -6,7 +6,7 @@ import lombok.Data;
public class SwingCommonVO { public class SwingCommonVO {
/** 클라이업트 D */ /** 클라이업트 D */
private String clientld; private String clientId;
/** 클라이언트 시크릿 */ /** 클라이언트 시크릿 */
private String clientSecret; private String clientSecret;
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.webhook.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.base.BaseAnnotationController;
import com.eactive.eai.rms.common.combo.ComboService;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqDetailUI;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUI;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUISearch;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReqManService;
import lombok.RequiredArgsConstructor;
/**
* 웹훅 신청내역(PTL_WEBHOOK_REQ) 조회 화면 컨트롤러.
*
* <p>조회 전용 (등록/수정/삭제 없음)</p>
*/
@Controller
@RequiredArgsConstructor
public class WebhookReqManController extends BaseAnnotationController {
private final WebhookReqManService webhookReqManService;
private final ComboService comboService;
@GetMapping(value = "/onl/apim/webhook/webhookReqMan.view")
public void view() {
// 목록 view
}
@GetMapping(value = "/onl/apim/webhook/webhookReqMan.view", params = "cmd=DETAIL")
public String detailView() {
return "/onl/apim/webhook/webhookReqManDetail";
}
@PostMapping(value = "/onl/apim/webhook/webhookReqMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<WebhookReqUI>> selectList(
@SortDefault(sort = "createdDate", direction = Sort.Direction.DESC) Pageable pageable,
WebhookReqUISearch search) {
Page<WebhookReqUI> page = webhookReqManService.selectList(pageable, search);
return ResponseEntity.ok(new GridResponse<>(page));
}
@PostMapping(value = "/onl/apim/webhook/webhookReqMan.json", params = "cmd=LIST_INIT_COMBO")
public ResponseEntity<Map<String, Object>> initCombo() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("eventTypeList", comboService.getMonitoringCodeSortedBySeq(
WebhookReqManService.CODE_GROUP_EVENT_TYPE));
return ResponseEntity.ok(resultMap);
}
@PostMapping(value = "/onl/apim/webhook/webhookReqMan.json", params = "cmd=DETAIL")
public ResponseEntity<WebhookReqDetailUI> selectDetail(String id) {
return ResponseEntity.ok(webhookReqManService.selectDetail(id));
}
}
@@ -60,12 +60,13 @@ public class WebhookSendController {
* X-Webhook-Signature : sha256={hmac값} * X-Webhook-Signature : sha256={hmac값}
* X-Webhook-Event : ORDER_CREATED * X-Webhook-Event : ORDER_CREATED
* X-Webhook-Timestamp : 1712345678901 * X-Webhook-Timestamp : 1712345678901
*/
@PostMapping("/receive") @PostMapping("/receive")
public ResponseEntity<Map<String, Object>> receiveWebhook( public ResponseEntity<Map<String, Object>> receiveWebhook(
@RequestHeader(value = "X-Webhook-Signature", required = false) String signature, @RequestHeader(value = "x-webhook-signature", required = false) String signature,
@RequestHeader(value = "X-Webhook-Event", required = false) String eventType, @RequestHeader(value = "x-webhook-event", required = false) String eventType,
@RequestHeader(value = "X-Webhook-Timestamp", required = false) String timestamp, @RequestHeader(value = "x-webhook-secret", required = false) String secret,
@RequestHeader(value = "x-webhook-timestamp", required = false) String timestamp,
@RequestBody String rawPayload) { @RequestBody String rawPayload) {
log.info("[Webhook] 수신 - eventType: {}, timestamp: {}", eventType, timestamp); log.info("[Webhook] 수신 - eventType: {}, timestamp: {}", eventType, timestamp);
@@ -79,13 +80,15 @@ public class WebhookSendController {
} }
// 2. 서명 검증 // 2. 서명 검증
boolean isValid = webhookReceiveService.verifySignature(rawPayload, signature); Boolean isValid = webhookReceiveService.verifySignature(rawPayload, signature, "");
if (isValid != null) {
if (!isValid) { if (!isValid) {
log.warn("[Webhook] 수신 거부 - 서명 불일치 / eventType: {}", eventType); log.warn("[Webhook] 수신 거부 - 서명 불일치 / eventType: {}", eventType);
return ResponseEntity return ResponseEntity
.status(HttpStatus.UNAUTHORIZED) .status(HttpStatus.UNAUTHORIZED)
.body(errorResponse("서명 검증에 실패하였습니다.")); .body(errorResponse("서명 검증에 실패하였습니다."));
} }
}
// 3. 타임스탬프 유효성 검증 (5분 이내 요청만 허용) // 3. 타임스탬프 유효성 검증 (5분 이내 요청만 허용)
boolean isTimestampValid = webhookReceiveService.verifyTimestamp(timestamp); boolean isTimestampValid = webhookReceiveService.verifyTimestamp(timestamp);
@@ -113,6 +116,7 @@ public class WebhookSendController {
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
*/
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* 공통 */ /* 공통 */
@@ -17,13 +17,15 @@ public class WebhookPayload {
private String eventType; private String eventType;
private String eventId; private String eventId;
private String message;
private long timestamp; private long timestamp;
private Object data; private Object data;
public static WebhookPayload of(String eventType, Object data) { public static WebhookPayload of(String eventType, String message, Object data) {
return WebhookPayload.builder() return WebhookPayload.builder()
.eventType(eventType) .eventType(eventType)
.eventId(UUID.randomUUID().toString()) .eventId(UUID.randomUUID().toString())
.message(message)
.timestamp(System.currentTimeMillis()) .timestamp(System.currentTimeMillis())
.data(data) .data(data)
.build(); .build();
@@ -0,0 +1,34 @@
package com.eactive.eai.rms.ext.djb.webhook.dto;
import java.util.List;
import lombok.Data;
/**
* 웹훅 신청내역(PTL_WEBHOOK_REQ) 상세 화면 응답 DTO.
*
* <p>신청 상세 정보에 더해, 연관된 API 목록(PTL_WEBHOOK_REQ_API)과
* 신청 이벤트 목록(PTL_WEBHOOK_REQ_EVENT)을 함께 담는다.</p>
*/
@Data
public class WebhookReqDetailUI {
private String id;
private String orgId;
private String orgName;
private String reverseProxyPath;
private String targetUrl;
private String secret;
private String userSecret;
/* 신청일시 — yyyy-MM-dd HH:mm:ss */
private String createdDate;
/** PTL_WEBHOOK_REQ_API 목록 (API ID) */
private List<String> apiIds;
/** PTL_WEBHOOK_REQ_EVENT 목록 (이벤트유형 코드). 코드명은 화면에서 콤보로 변환한다. */
private List<String> eventTypes;
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.ext.djb.webhook.dto;
import lombok.Data;
/**
* 웹훅 신청내역(PTL_WEBHOOK_REQ) 목록 화면 응답 DTO.
*/
@Data
public class WebhookReqUI {
private String id;
private String orgId;
private String orgName;
private String reverseProxyPath;
private String targetUrl;
/* 신청일시 — yyyy-MM-dd HH:mm:ss */
private String createdDate;
}
@@ -0,0 +1,16 @@
package com.eactive.eai.rms.ext.djb.webhook.dto;
import lombok.Data;
@Data
public class WebhookReqUISearch {
/* 기관명 (ptl_org.org_name, LIKE 검색) */
private String searchOrgName;
/* 리버스 프록시 경로 (ptl_org.reverse_proxy_path, LIKE 검색) */
private String searchReverseProxyPath;
/* 수신 URL (LIKE 검색) */
private String searchTargetUrl;
}
@@ -24,6 +24,7 @@ public class WebhookSendLogUI {
private String payload; private String payload;
private String signature; private String signature;
private String userSecret;
private Integer statusCode; private Integer statusCode;
private String responseBody; private String responseBody;
@@ -12,5 +12,6 @@ public class WebhookSendRequest {
private String eventType; private String eventType;
private String secret; private String secret;
private String reverseProxyPath; private String reverseProxyPath;
private String userSecret;
private Object data; private Object data;
} }
@@ -1,11 +1,11 @@
package com.eactive.eai.rms.ext.djb.webhook.repository; package com.eactive.eai.rms.ext.djb.webhook.repository;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq; import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
@EMSDataSource @EMSDataSource
public interface WebhookReqRepository extends JpaRepository<WebhookReq, String> { public interface WebhookReqRepository extends BaseRepository<WebhookReq, String> {
} }
@@ -20,7 +20,6 @@ public class WebhookReceiveService {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
private static final String HMAC_ALGORITHM = "HmacSHA256"; private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final long TIMESTAMP_LIMIT = 5 * 60 * 1000L; // 5분 (ms) private static final long TIMESTAMP_LIMIT = 5 * 60 * 1000L; // 5분 (ms)
@@ -33,14 +32,14 @@ public class WebhookReceiveService {
* 수신된 서명과 payload 로 재계산한 서명을 비교 * 수신된 서명과 payload 로 재계산한 서명을 비교
* 헤더값 형식 : "sha256={hex값}" * 헤더값 형식 : "sha256={hex값}"
*/ */
public boolean verifySignature(String rawPayload, String receivedSignature) { public boolean verifySignature(String rawPayload, String receivedSignature, String secretKey) {
try { try {
// "sha256=" 접두어 제거 // "sha256=" 접두어 제거
String receivedHex = receivedSignature.startsWith("sha256=") String receivedHex = receivedSignature.startsWith("sha256=")
? receivedSignature.substring(7) ? receivedSignature.substring(7)
: receivedSignature; : receivedSignature;
String expectedHex = generateSignature(rawPayload); String expectedHex = generateSignature(rawPayload, secretKey);
// 타이밍 공격 방지 : MessageDigest.isEqual 사용 // 타이밍 공격 방지 : MessageDigest.isEqual 사용
return MessageDigest.isEqual( return MessageDigest.isEqual(
@@ -85,10 +84,10 @@ public class WebhookReceiveService {
log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType); log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType);
switch (eventType) { switch (eventType) {
case "CONTROL_START": case "CHECK_START":
handleControlStart(data); handleControlStart(data);
break; break;
case "CONTROL_END": case "CHECK_END":
handleControlEnd(data); handleControlEnd(data);
break; break;
case "ERROR_START": case "ERROR_START":
@@ -107,11 +106,11 @@ public class WebhookReceiveService {
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
private void handleControlStart(JsonNode data) { private void handleControlStart(JsonNode data) {
log.info("[Webhook] CONTROL_START 처리 - data: {}", data); log.info("[Webhook] CHECK_START 처리 - data: {}", data);
// TODO: 점검시작 처리 로직 // TODO: 점검시작 처리 로직
} }
private void handleControlEnd(JsonNode data) { private void handleControlEnd(JsonNode data) {
log.info("[Webhook] CONTROL_END 처리 - data: {}", data); log.info("[Webhook] CHECK_END 처리 - data: {}", data);
// TODO: 점검종료 처리 로직 // TODO: 점검종료 처리 로직
} }
private void handleErrorStart(JsonNode data) { private void handleErrorStart(JsonNode data) {
@@ -124,7 +123,7 @@ public class WebhookReceiveService {
/* 공통 유틸 */ /* 공통 유틸 */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
private String generateSignature(String payload) throws Exception { private String generateSignature(String payload, String secretKey) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM); Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec( SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM); secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
@@ -0,0 +1,202 @@
package com.eactive.eai.rms.ext.djb.webhook.service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.util.StringUtils;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqDetailUI;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUI;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUISearch;
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookReqRepository;
import com.eactive.eai.rms.onl.common.exception.BizException;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
/**
* 웹훅 신청내역(PTL_WEBHOOK_REQ) 조회 화면 서비스.
*
* <p>조회 전용. orgId 는 엔티티 연관관계가 아닌 단순 컬럼이므로,
* 기관명은 QueryDSL 로 ptl_org 를 별도 조회하여 매핑한다.</p>
*/
@Service
@RequiredArgsConstructor
@Transactional(transactionManager = "transactionManagerForEMS")
public class WebhookReqManService extends BaseService {
/** 모니터링 공통코드(tseairm28) 상의 웹훅 이벤트유형 코드 그룹. */
public static final String CODE_GROUP_EVENT_TYPE = "EVENT_TYPE";
private static final DateTimeFormatter FMT_DATETIME_OUT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter FMT_DATETIME_IN = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private final WebhookReqRepository webhookReqRepository;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private EntityManager entityManager;
public Page<WebhookReqUI> selectList(Pageable pageable, WebhookReqUISearch search) {
QWebhookReq req = QWebhookReq.webhookReq;
BooleanBuilder predicate = new BooleanBuilder();
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
List<String> orgIds = findOrgIdsByName(search.getSearchOrgName());
if (orgIds.isEmpty()) {
// 일치하는 기관이 없으면 쿼리를 실행할 필요 없이 빈 결과를 바로 반환한다.
return Page.empty(pageable);
}
predicate.and(req.orgId.in(orgIds));
}
if (StringUtils.isNotBlank(search.getSearchReverseProxyPath())) {
List<String> orgIds = findOrgIdsByReverseProxyPath(search.getSearchReverseProxyPath());
if (orgIds.isEmpty()) {
return Page.empty(pageable);
}
predicate.and(req.orgId.in(orgIds));
}
if (StringUtils.isNotBlank(search.getSearchTargetUrl())) {
predicate.and(req.targetUrl.containsIgnoreCase(search.getSearchTargetUrl()));
}
Page<WebhookReq> page = webhookReqRepository.findAll(predicate, pageable);
Map<String, OrgInfo> orgInfoMap = findOrgInfo(page.getContent());
return page.map(e -> convertToListUI(e, orgInfoMap.get(e.getOrgId())));
}
public WebhookReqDetailUI selectDetail(String id) {
WebhookReq entity = webhookReqRepository.findById(id)
.orElseThrow(() -> new BizException("존재하지 않는 웹훅 신청내역입니다."));
Map<String, OrgInfo> orgInfoMap = findOrgInfo(Collections.singletonList(entity));
OrgInfo orgInfo = orgInfoMap.get(entity.getOrgId());
WebhookReqDetailUI ui = new WebhookReqDetailUI();
ui.setId(entity.getId());
ui.setOrgId(entity.getOrgId());
ui.setOrgName(orgInfo != null ? orgInfo.orgName : null);
ui.setReverseProxyPath(orgInfo != null ? orgInfo.reverseProxyPath : null);
ui.setTargetUrl(entity.getTargetUrl());
ui.setSecret(entity.getSecret());
ui.setUserSecret(entity.getUserSecret());
ui.setCreatedDate(fmtCreatedDate(entity.getCreatedDate()));
ui.setApiIds(findApiIds(id));
ui.setEventTypes(findEventTypes(id));
return ui;
}
/** 기관명 LIKE 검색으로 매칭되는 orgId 목록. */
private List<String> findOrgIdsByName(String orgName) {
QPortalOrg org = QPortalOrg.portalOrg;
return new JPAQueryFactory(entityManager)
.select(org.id)
.from(org)
.where(org.orgName.containsIgnoreCase(orgName))
.fetch();
}
/** 리버스 프록시 경로 LIKE 검색으로 매칭되는 orgId 목록. */
private List<String> findOrgIdsByReverseProxyPath(String reverseProxyPath) {
QPortalOrg org = QPortalOrg.portalOrg;
return new JPAQueryFactory(entityManager)
.select(org.id)
.from(org)
.where(org.reverseProxyPath.containsIgnoreCase(reverseProxyPath))
.fetch();
}
/** 목록/상세에 필요한 orgId → (기관명, 리버스 프록시 경로) 매핑 (일괄 조회로 N+1 방지). */
private Map<String, OrgInfo> findOrgInfo(List<WebhookReq> reqs) {
Set<String> orgIds = reqs.stream()
.map(WebhookReq::getOrgId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
if (orgIds.isEmpty()) {
return Collections.emptyMap();
}
QPortalOrg org = QPortalOrg.portalOrg;
return new JPAQueryFactory(entityManager)
.select(org.id, org.orgName, org.reverseProxyPath)
.from(org)
.where(org.id.in(orgIds))
.fetch()
.stream()
.collect(Collectors.toMap(t -> t.get(org.id),
t -> new OrgInfo(t.get(org.orgName), t.get(org.reverseProxyPath))));
}
/** 신청 건에 등록된 API ID 목록 (PTL_WEBHOOK_REQ_API). */
private List<String> findApiIds(String webhookReqId) {
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
return new JPAQueryFactory(entityManager)
.select(api.id.apiId)
.from(api)
.where(api.id.webhookReqId.eq(webhookReqId))
.fetch();
}
/** 신청 건에 등록된 이벤트유형 코드 목록 (PTL_WEBHOOK_REQ_EVENT). */
private List<String> findEventTypes(String webhookReqId) {
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
return new JPAQueryFactory(entityManager)
.select(evt.id.eventType)
.from(evt)
.where(evt.id.webhookReqId.eq(webhookReqId))
.fetch();
}
private WebhookReqUI convertToListUI(WebhookReq e, OrgInfo orgInfo) {
WebhookReqUI ui = new WebhookReqUI();
ui.setId(e.getId());
ui.setOrgId(e.getOrgId());
ui.setOrgName(orgInfo != null ? orgInfo.orgName : null);
ui.setReverseProxyPath(orgInfo != null ? orgInfo.reverseProxyPath : null);
ui.setTargetUrl(e.getTargetUrl());
ui.setCreatedDate(fmtCreatedDate(e.getCreatedDate()));
return ui;
}
/** ptl_org 조회 결과(기관명 + 리버스 프록시 경로) 보관용. */
private static final class OrgInfo {
private final String orgName;
private final String reverseProxyPath;
private OrgInfo(String orgName, String reverseProxyPath) {
this.orgName = orgName;
this.reverseProxyPath = reverseProxyPath;
}
}
/** CREATED_DATE(yyyyMMddHHmmss 문자열) → 화면 표시용(yyyy-MM-dd HH:mm:ss). */
private static String fmtCreatedDate(String raw) {
if (StringUtils.isBlank(raw)) {
return "";
}
try {
return LocalDateTime.parse(raw, FMT_DATETIME_IN).format(FMT_DATETIME_OUT);
} catch (Exception e) {
return raw;
}
}
}
@@ -157,6 +157,7 @@ public class WebhookSendLogManService extends BaseService {
ui.setProxyUrl(e.getProxyUrl()); ui.setProxyUrl(e.getProxyUrl());
ui.setPayload(e.getPayload()); ui.setPayload(e.getPayload());
ui.setSignature(e.getSignature()); ui.setSignature(e.getSignature());
ui.setUserSecret(e.getUserSecret());
ui.setStatusCode(e.getStatusCode()); ui.setStatusCode(e.getStatusCode());
ui.setResponseBody(e.getResponseBody()); ui.setResponseBody(e.getResponseBody());
ui.setSuccess(e.getSuccess()); ui.setSuccess(e.getSuccess());

Some files were not shown because too many files have changed in this diff Show More