126 Commits

Author SHA1 Message Date
Rinjae(gf63) 4bf7f661e3 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Waiting to run
2026-09-11 18:38:59 +09:00
Rinjae(gf63) 124df00f51 - Gradle 실행 설정 추가 (clean, compileJava)
- API 편집기 닫기 전 변경사항 알림/확인 로직 추가
- 저장 성공/초기 로드 시점 기준 스냅샷 로직 및 UI 처리를 반영
2026-09-11 18:38:50 +09:00
eastargh e4de8ebc8b 조회시간 최대 59분이 되도록 변경
eapim-admin CI / build (push) Waiting to run
2026-09-11 17:35:47 +09:00
eastargh 52e8c3be39 조회시간 최대 59분이 되도록 변경 2026-09-11 17:35:23 +09:00
eastargh 136626e7c4 사용자 권한변경시 거래현황-업무구분 기본 선택되도록 변경
eapim-admin CI / build (push) Waiting to run
2026-09-11 14:03:16 +09:00
eastargh 0848cd3469 UMS 카톡 템플릿 적용
eapim-admin CI / build (push) Waiting to run
2026-09-10 13:19:00 +09:00
eastargh 0b6f273132 UMS 응답 저장 및 상세조회
eapim-admin CI / build (push) Has been cancelled
2026-09-10 09:56:50 +09:00
eastargh 266644798f 이메일발송 설정 상수로 원복
eapim-admin CI / build (push) Has been cancelled
2026-09-09 15:28:29 +09:00
eastargh 2e8b45c69e email 발송 변수설정
eapim-admin CI / build (push) Has been cancelled
2026-09-09 14:07:54 +09:00
Rinjae ea3d23424c Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-09-09 11:05:51 +09:00
Rinjae 2a9dfaa093 API Spec 상태 조회 및 관리 기능 추가
- ApiSpecStatusRepository 신규 추가
- 스펙 상태 검색 조건/그룹 조회 및 UI 반영
2026-09-09 11:05:46 +09:00
eastargh 3b74f9ce38 sms발송 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-09-09 10:22:13 +09:00
Rinjae 145ad37499 리치 텍스트 파라미터 처리 조건 추가
eapim-admin CI / build (push) Has been cancelled
- 메시지 템플릿 등록/수정 시 XSS 필터만 적용하도록 수정
- 문자 변환 건너뛰는 조건에 특정 경로 및 cmd 추가
2026-09-09 09:52:47 +09:00
eastargh a235b0656c 로그 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-09-08 17:08:35 +09:00
eastargh da397b21b0 로그인 에러 메세지 표출
eapim-admin CI / build (push) Has been cancelled
2026-09-07 19:04:18 +09:00
eastargh e084436c90 UMS 발송시 was ip, mac 주소 동적으로 설정
eapim-admin CI / build (push) Has been cancelled
2026-09-07 18:44:14 +09:00
eastargh 15652e158d 오픈소스 취약점 조치
eapim-admin CI / build (push) Has been cancelled
2026-09-07 15:35:29 +09:00
Rinjae 8e4c2efe88 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-09-04 13:22:15 +09:00
Rinjae cad51df3cb API Spec 자동 생성 기능 및 개발자포탈 반영 로직 추가
- DjbApiSpecAutoGenService 신규 추가 및 관련 규칙 구현
- PortalCredentialSyncService 도입으로 포탈 인증정보 동기화 로직 개선
- ClientManService 및 UI 업데이트: Spec 자동 생성 상태 반영
2026-09-04 13:22:07 +09:00
eastargh d19a9eb593 제주은행용 갱신
eapim-admin CI / build (push) Has been cancelled
2026-09-04 11:31:15 +09:00
eastargh a05870fd1b 유량제어토큰감시 스케쥴러 메세지 1건만 발송 2026-09-04 11:30:45 +09:00
eastargh 47259da0f6 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git 2026-09-04 11:29:47 +09:00
eastargh 0bdcc440b7 페이징 버그 수정 2026-09-04 11:29:31 +09:00
Rinjae 778197c1dd Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-09-03 15:46:50 +09:00
Rinjae 91631ea750 "X-API-KEY 방식 제거 및 내부 API 토큰 검증으로 변경
- ApiKeyInterceptor 삭제, InternalApiTokenInterceptor 도입
- 관련 config 및 테스트 코드 X-Internal-Token 방식으로 수정
- PortalPropertyService 연관 코드 정리 및 불필요 메서드 제거"
2026-09-03 15:46:43 +09:00
eastargh a1d7f8f10e UI 변경
eapim-admin CI / build (push) Has been cancelled
2026-09-03 15:10:48 +09:00
eastargh 1ba140ddea 게시판 첨부파일 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-09-03 10:09:29 +09:00
eastargh 7117420f38 비빌번호 변경 규칙 프라퍼티로 변경
eapim-admin CI / build (push) Has been cancelled
2026-09-02 14:16:46 +09:00
eastargh 536151a737 차트 색상 조정 2026-09-02 14:16:17 +09:00
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
179 changed files with 9475 additions and 3617 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE
AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE
ApiInterfaceController_API관리_APIGW_INSERT,UPDATE,DELETE,CLONE ApiInterfaceController_API관리_APIGW_INSERT,UPDATE,DELETE,CLONE
ClientController_Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE ClientController_Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE,SYNC_PORTAL
RoleController_Role (역할)관리_APIGW_INSERT,UPDATE,DELETE RoleController_Role (역할)관리_APIGW_INSERT,UPDATE,DELETE
MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE
PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE
@@ -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
@@ -110,7 +110,7 @@
</mvc:interceptor> </mvc:interceptor>
<mvc:interceptor> <mvc:interceptor>
<mvc:mapping path="/_onl/**"/> <mvc:mapping path="/_onl/**"/>
<bean class="com.eactive.eai.rms.common.interceptor.ApiKeyInterceptor" /> <bean class="com.eactive.eai.rms.common.interceptor.InternalApiTokenInterceptor" />
</mvc:interceptor> </mvc:interceptor>
<!-- BaseRestController 엔드포인트 IP 화이트리스트 인증 --> <!-- BaseRestController 엔드포인트 IP 화이트리스트 인증 -->
<mvc:interceptor> <mvc:interceptor>
+1 -1
View File
@@ -175,6 +175,6 @@
</jsp-config> </jsp-config>
<session-config> <session-config>
<session-timeout>60</session-timeout> <session-timeout>60</session-timeout>
</session-config> </session-config>
</web-app> </web-app>
+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>
+56 -34
View File
@@ -29,6 +29,14 @@
// (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가) // (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가)
var djbAuthType = ''; var djbAuthType = '';
// 마지막 저장(또는 최초 로드) 시점의 state.data 스냅샷 — 닫기 전 미저장 변경 여부 판별용.
var savedSnapshot = null;
function isDirty() { return savedSnapshot !== null && JSON.stringify(state.data) !== savedSnapshot; }
function closeEditor() {
if (isDirty() && !window.confirm('저장하지 않은 변경사항이 있습니다. 저장하지 않고 닫으시겠습니까?')) return;
window.close();
}
// ============================================================ // ============================================================
// 2. 유틸 // 2. 유틸
// ============================================================ // ============================================================
@@ -191,25 +199,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 +232,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 +978,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 +1476,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 +1491,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 }
@@ -1707,7 +1711,7 @@
var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false)); var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false));
var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate); var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate);
var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', () => window.close()); var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', closeEditor);
// Cmd/Ctrl + S → 저장 (브라우저 기본 저장 대화상자 차단) // Cmd/Ctrl + S → 저장 (브라우저 기본 저장 대화상자 차단)
document.addEventListener('keydown', function (e) { document.addEventListener('keydown', function (e) {
@@ -1741,6 +1745,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 +1795,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 +1840,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 +1879,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 +1925,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));
} }
}); });
} }
@@ -2054,6 +2074,7 @@
.then(function (res) { .then(function (res) {
if (res && res.status === 'success') { if (res && res.status === 'success') {
toast(temp ? '임시저장되었습니다' : '저장되었습니다'); toast(temp ? '임시저장되었습니다' : '저장되었습니다');
savedSnapshot = JSON.stringify(state.data); // 저장 성공 시점을 새 저장 기준선으로 갱신
var badge = $('#hdr-source'); if (badge) { badge.textContent = '저장본'; badge.className = 'ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200'; badge.classList.remove('hidden'); } var badge = $('#hdr-source'); if (badge) { badge.textContent = '저장본'; badge.className = 'ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200'; badge.classList.remove('hidden'); }
} else toast('저장 실패: ' + (res && res.errorMsg || ''), 'error'); } else toast('저장 실패: ' + (res && res.errorMsg || ''), 'error');
}) })
@@ -2090,6 +2111,7 @@
// ============================================================ // ============================================================
function init() { function init() {
loadInitialData().then(function () { loadInitialData().then(function () {
savedSnapshot = JSON.stringify(state.data); // 초기 로드(자동생성 포함) 완료 시점을 저장 기준선으로 설정
renderStepper(); renderStepper();
renderForm(); renderForm();
bindGlobalEvents(); bindGlobalEvents();
+18 -14
View File
@@ -44,10 +44,14 @@ $(document).ready(function() {
$(this).val(getEndTime()); $(this).val(getEndTime());
} }
}); });
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 //검색조건 달력
gridPostData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val() + $("input[name=searchStartHHMM]").val(); $("#startDatepicker").datepicker();
gridPostData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val() + $("input[name=searchEndHHMM]").val(); $("#endDatepicker").datepicker();
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
gridPostData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchStartHHMM]").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',
@@ -106,9 +110,9 @@ $(document).ready(function() {
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#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>
@@ -28,9 +28,11 @@ function init(){
success:function(json){ success:function(json){
new makeOptions("CODE","NAME").setObj($("select[name=searchJobUseYn]")).setNoValueInclude(true).setNoValue("", new makeOptions("CODE","NAME").setObj($("select[name=searchJobUseYn]")).setNoValueInclude(true).setNoValue("",
"<%= localeMessage.getString("combo.all")%>").setData(json.useYnTypeRows).setFormat(codeName3OptionFormat).rendering(); "<%= localeMessage.getString("combo.all")%>").setData(json.useYnTypeRows).setFormat(codeName3OptionFormat).rendering();
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();
+109 -12
View File
@@ -7,6 +7,19 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%> <%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %> <%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
// 비밀번호 규칙 안내문구용 모니터링 프로퍼티 조회
com.eactive.eai.rms.common.context.MonitoringContext monitoringContext =
(com.eactive.eai.rms.common.context.MonitoringContext) WebApplicationContextUtils
.getRequiredWebApplicationContext(pageContext.getServletContext())
.getBean("monitoringContext");
int pwdLengthCheck = monitoringContext.getIntProperty(
com.eactive.eai.rms.common.context.MonitoringContext.RMS_PASSWORD_LENGTH_CHECK, 7);
int pwdCombiCheck = monitoringContext.getIntProperty(
com.eactive.eai.rms.common.context.MonitoringContext.RMS_PASSWORD_COMBI_CHECK, 2);
int pwdRepeatCheck = monitoringContext.getIntProperty(
com.eactive.eai.rms.common.context.MonitoringContext.RMS_PASSWORD_REPEAT_CHECK, 3);
%>
<c:set var="themeColor" value="<%=System.getProperty(\"theme.color\")%>" scope="session" /> <c:set var="themeColor" value="<%=System.getProperty(\"theme.color\")%>" scope="session" />
<% <%
@@ -65,6 +78,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 ){
@@ -91,18 +105,26 @@
data: $('#loginForm').serialize(), data: $('#loginForm').serialize(),
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
console.log('res', response)
if (response.changePassword) { if (response.changePassword) {
showChangeInitPassword(response); showChangeInitPassword(response);
} }
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 {
// 로그인 실패 if (response.errorMessage == 'NO_PHONE_NUMBER') {
alert(response.errorMessage || '로그인에 실패했습니다.'); alert("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요")
} else if (response.errorMessage == 'FAIL_SEND_AUTH') {
alert("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
} else {
// 로그인 실패
alert('로그인에 실패했습니다');
}
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
@@ -123,11 +145,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 +213,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 +227,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 +236,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() {
@@ -262,7 +303,47 @@
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 +357,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 +393,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 +483,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 +501,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종류 이상 <%= pwdLengthCheck %>글자 이상 & 영문/숫자/특수문자 <%= pwdCombiCheck %>종류 이상<br/>
※ 연속된 숫자/문자(예: 123, abc, qwer) 사용 불가<%= pwdRepeatCheck >= 2 ? ", 동일 문자 " + pwdRepeatCheck + "자 이상 반복 불가" : "" %>
</span> </span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -427,9 +524,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 () {
+32 -3
View File
@@ -555,7 +555,35 @@
<% session.setAttribute("dualLogin", "N"); %> <% session.setAttribute("dualLogin", "N"); %>
} }
}); });
// 세션(자동로그아웃) 타임아웃 카운트다운 - 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>
@@ -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;">
@@ -179,8 +179,8 @@
autowidth: true, autowidth: true,
viewrecords : true, viewrecords : true,
gridview : true, gridview : true,
ondblClickRow : function(rowId) { ondblClickRow : function(rowId) {
console.log("rowId", rowId); /* console.log("rowId", rowId);
var rowData = $(this) var rowData = $(this)
.getRowData(rowId); .getRowData(rowId);
var key = rowData['EaiSevrInstncName']; var key = rowData['EaiSevrInstncName'];
@@ -193,7 +193,7 @@
url2 += "&searchAdptrBzwkGroupName=" url2 += "&searchAdptrBzwkGroupName="
+ key2; + key2;
url2 += '&menuId='+'${param.menuId}'; url2 += '&menuId='+'${param.menuId}';
goNav(url2); goNav(url2); */
}, },
loadComplete : function(d) { loadComplete : function(d) {
$("button[name*=img]").unbind("click"); $("button[name*=img]").unbind("click");
@@ -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);
@@ -306,7 +308,8 @@
dataType: "json", dataType: "json",
data: { data: {
cmd: 'SYNC_PORTAL', cmd: 'SYNC_PORTAL',
clientId: clientId clientId: clientId,
serviceType: 'APIGW'
}, },
success: function(response) { success: function(response) {
showSyncResultModal(response); showSyncResultModal(response);
@@ -451,8 +454,8 @@
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<!-- <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영 <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
</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 type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
</button> </button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
@@ -506,18 +509,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 +540,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 +557,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>
@@ -740,7 +742,7 @@ function showSyncResultModal(response) {
if (response.success) { if (response.success) {
statusClass = "status-success"; statusClass = "status-success";
var createdCount = response.createdApiSpecCount || 0; var createdCount = response.createdApiSpecCount || 0;
var createdMsg = createdCount > 0 ? "<br/><span style='color:#007bff;'>* " + createdCount + "개의 API Spec 자동 생성되었습니다.</span>" : ""; var createdMsg = createdCount > 0 ? "<br/><span style='color:#007bff;'>* " + createdCount + "개의 API Spec을 비공개(N)로 자동 생성습니다. API Spec 관리에서 확인 후 공개하세요.</span>" : "";
if (response.action === 'INSERTED') { if (response.action === 'INSERTED') {
statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg; statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg;
@@ -770,6 +772,7 @@ function showSyncResultModal(response) {
var syncedApis = response.syncedApis || []; var syncedApis = response.syncedApis || [];
var invalidApis = response.invalidApis || []; var invalidApis = response.invalidApis || [];
var createdApiSpecs = response.createdApiSpecs || []; var createdApiSpecs = response.createdApiSpecs || [];
var specGenFailed = response.specGenFailed || [];
$targetCount.text(targetApis.length); $targetCount.text(targetApis.length);
@@ -783,8 +786,11 @@ function showSyncResultModal(response) {
if (invalidApis.includes(apiId)) { if (invalidApis.includes(apiId)) {
status = "미존재 (APIGW에 없음)"; status = "미존재 (APIGW에 없음)";
statusClass = "api-invalid"; statusClass = "api-invalid";
} else if (specGenFailed.includes(apiId)) {
status = "반영됨 (Spec 자동생성 실패·빈 항목)";
statusClass = "api-created";
} else if (createdApiSpecs.includes(apiId)) { } else if (createdApiSpecs.includes(apiId)) {
status = "반영됨 (Spec 신규생성)"; status = "반영됨 (Spec 자동생성·비공개)";
statusClass = "api-created"; statusClass = "api-created";
} else if (syncedApis.includes(apiId)) { } else if (syncedApis.includes(apiId)) {
status = "반영됨"; status = "반영됨";
@@ -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>
@@ -44,6 +44,9 @@
.div-hover { .div-hover {
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"/>
@@ -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
@@ -985,6 +988,49 @@
jsPlumbInstance.deleteConnectionsForElement(document.getElementById(targetEndpointElementId)); jsPlumbInstance.deleteConnectionsForElement(document.getElementById(targetEndpointElementId));
} }
}); });
$("#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" );
@@ -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">
+111 -40
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,62 +24,48 @@
<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 }
], ],
jsonReader: { jsonReader: {
@@ -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)
@@ -429,6 +435,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>
@@ -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>
@@ -19,7 +19,8 @@
<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 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);
}); });
}); });
@@ -144,7 +146,7 @@ $(document).ready(function() {
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_regist" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.register") %></button> <button type="button" class="cssbtn" id="btn_regist" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.register") %></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>
<%-- <img src="<c:url value="/img/btn_regist.png"/>" alt="" id="btn_regist" level="W" status="DETAIL,NEW" /> --%> <%-- <img src="<c:url value="/img/btn_regist.png"/>" alt="" id="btn_regist" level="W" status="DETAIL,NEW" /> --%>
@@ -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>
+14 -26
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,15 +106,13 @@ $(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},
{ name : 'VERINFO', align:'center', width:30 } { name : 'VERINFO', align:'center', width:30 }
], ],
jsonReader: { jsonReader: {
repeatitems:false repeatitems:false
@@ -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>
</tr> <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>
<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>
@@ -47,6 +47,8 @@
$('#sentDate').text(data.sentDateFull || ''); $('#sentDate').text(data.sentDateFull || '');
// 서버에서 마스킹+escape 된 안전 HTML // 서버에서 마스킹+escape 된 안전 HTML
$('#messageHtml').html(data.messageHtml || ''); $('#messageHtml').html(data.messageHtml || '');
// 응답내용(RESPONSE_DATA) — 원문이므로 text 로만 출력
$('#responseData').text(data.responseData || '');
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당) // PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
if (data.requestStatus !== 'PENDING') { if (data.requestStatus !== 'PENDING') {
@@ -142,6 +144,10 @@
<th>메세지 내용</th> <th>메세지 내용</th>
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td> <td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
</tr> </tr>
<tr>
<th>응답내용</th>
<td colspan="3"><div id="responseData" class="msg-detail"></div></td>
</tr>
</table> </table>
</div> </div>
</div> </div>
@@ -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);
@@ -490,6 +530,10 @@
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return; if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
var formData = new FormData($("#ajaxForm")[0]); var formData = new FormData($("#ajaxForm")[0]);
// Summernote 그림 다이얼로그의 내부 파일 input(name="files")이 #ajaxForm 안에 있어
// 선택한 이미지가 지워지지 않고 남아 있으면 여기서 함께 딸려 온다.
// 본 화면은 첨부파일 기능을 쓰지 않으므로 항상 제거한다.
formData.delete("files");
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N"); formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N"); formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
formData.set("noticeDetail", $('#contents').summernote('code')); formData.set("noticeDetail", $('#contents').summernote('code'));
@@ -587,7 +631,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,11 +99,12 @@
{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}
], ],
jsonReader: {repeatitems: false}, jsonReader: {repeatitems: false},
pager: $('#pager'), pager: $('#pager'),
page: '${param.page}', page: '${param.page}',
rowNum: '${rmsDefaultRowNum}', rowNum: '${rmsDefaultRowNum}',
autoheight: true, autoheight: true,
@@ -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>
@@ -58,6 +70,7 @@
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -99,7 +112,7 @@
show: true show: true
}, },
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } } { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
] ]
@@ -286,6 +299,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 +451,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 +494,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 +541,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 +582,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 +644,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>
@@ -639,11 +687,11 @@
</div> </div>
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<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>
@@ -40,6 +52,8 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var url_left_menu = '<c:url value="/leftMenu.do"/>';
var DASHBOARD_MENU_ID = '0305001'; // apiStatsMinuteMan(대시보드) 메뉴 ID
var totalDonutChart, callChart; var totalDonutChart, callChart;
@@ -59,6 +73,7 @@
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -100,7 +115,7 @@
show: true show: true
}, },
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } } { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
] ]
@@ -149,23 +164,20 @@
var hour = parseInt(statTime.substring(8, 10)); var hour = parseInt(statTime.substring(8, 10));
var startDate = year + '-' + month + '-' + day; var startDate = year + '-' + month + '-' + day;
var startTime = (hour < 10 ? '0' + hour : hour) + ':00'; var hourStr = (hour < 10 ? '0' + hour : hour);
var startTime = hourStr + ':00';
// 1시간 후 계산 (23시인 경우 다음날 00시로 자동 계산) // 해당 시간대 끝(정시~59분)으로 이동 (예: 10:00~10:59)
var d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), hour, 0, 0); var endTime = hourStr + ':59';
d.setHours(d.getHours() + 1);
var endDate = d.getFullYear() + '-' +
(d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1) + '-' +
(d.getDate() < 10 ? '0' : '') + d.getDate();
var endTime = (d.getHours() < 10 ? '0' : '') + d.getHours() + ':00';
var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime
+ '&searchEndDate=' + endDate + '&searchEndTime=' + endTime; + '&searchEndDate=' + startDate + '&searchEndTime=' + endTime;
params += '&menuId='+'${param.menuId}'; params += '&menuId='+DASHBOARD_MENU_ID;
params += '&cmd='+'LIST'; params += '&cmd='+'LIST';
params += '&serviceType='+'APIGW'; params += '&serviceType='+'APIGW';
// 왼쪽 메뉴도 대시보드(분단위 통계) 메뉴로 전환
parent.leftFrame.location.href = url_left_menu + '?menuId=' + DASHBOARD_MENU_ID
+ '&serviceType=' + sessionStorage["serviceType"];
location.href = url_minute_view + params; location.href = url_minute_view + params;
} }
@@ -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 },
@@ -459,8 +491,8 @@
datatype: "json", datatype: "json",
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>
@@ -51,6 +63,9 @@
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
// updateCharts()에서 series[].data를 itemStyle 없이 재설정하면 조각별 itemStyle.color가
// 병합 과정에서 유실되므로, 옵션 레벨 color 팔레트(성공/Timeout/시스템오류 순)로 색을 고정한다.
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -92,7 +107,7 @@
show: true show: true
}, },
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } } { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
] ]
@@ -203,7 +218,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 +233,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시간치(정시~59분)로 조정
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000)); endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
} else { } else {
endDateTime = endDate + endTime; endDateTime = formatDateTime(end);
} }
} else { } else {
// 종료시간 미입력 시 시작 + 1시간으로 설정 // 종료시간 미입력 시 시작시각 기준 1시간치(정시~59분)로 설정
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000)); endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
} }
return { start: startDateTime, end: endDateTime }; return { start: startDateTime, end: endDateTime };
@@ -302,6 +324,45 @@
fetchChartData(range); fetchChartData(range);
} }
// 조회기간을 정시 단위 1시간 구간으로 이동 (direction: -1 이전, 1 이후)
// ex) 시작 08:35, 이전 클릭 -> 08:00~08:59, 다시 -> 07:00~07:59
// ex) 시작 08:35, 이후 클릭 -> 09:00~09:59, 다시 -> 10:00~10:59
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);
}
// 종료시각은 같은 시간대의 59분으로 설정 (예: 10:00~10:59)
var end = new Date(start.getTime() + 59 * 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 +378,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 +489,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 +532,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,12 +576,14 @@
$(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 });
// 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분)) $("input[name=searchStartDate]").datepicker();
// 기본값 설정 (현재 시간(분) 기준 59분 전 ~ 현재 시간(분))
var now = new Date(); var now = new Date();
var oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); var oneHourAgo = new Date(now.getTime() - 59 * 60 * 1000);
function formatDate(d) { function formatDate(d) {
return d.getFullYear() + '-' + return d.getFullYear() + '-' +
@@ -537,8 +598,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 +614,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 +636,8 @@
} }
}); });
// 종료일/시간 변경 시 1시간 초과 검증 // 종료시각 변경 시 1시간 초과 검증
$("input[name=searchEndDate], input[name=searchEndTime]").on('change blur', function() { $("input[name=searchEndTime]").on('change blur', function() {
validateAndAdjustDateRange(); validateAndAdjustDateRange();
}); });
@@ -601,16 +669,21 @@
<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>
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
@@ -58,6 +58,7 @@
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -99,7 +100,7 @@
show: true show: true
}, },
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } } { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
] ]
@@ -421,12 +422,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 +465,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 +512,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 +605,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>
@@ -632,10 +644,10 @@
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<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>
@@ -58,6 +58,7 @@
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -99,7 +100,7 @@
show: true show: true
}, },
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } } { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
] ]
@@ -397,12 +398,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 +441,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 +565,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 +606,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,10 +274,23 @@
$("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()) {
$("input[name=searchStartDate]").val(startDate); $("input[name=searchStartDate]").val(startDate);
@@ -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,17 +374,16 @@
<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>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<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" %>
<% <%
@@ -37,6 +38,13 @@
success: function (json) { success: function (json) {
new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering(); new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
var apiGroupSelect = $('select[name=searchApiGroupId]').empty();
apiGroupSelect.append($('<option>').val('').text('전체'));
apiGroupSelect.append($('<option>').val('UNREGISTERED').text('미등록'));
(json.apiGroupList || []).forEach(function (group) {
apiGroupSelect.append($('<option>').val(group.id).text(group.groupName));
});
setSearchable(selectName); // 콤보에 searchable 설정 setSearchable(selectName); // 콤보에 searchable 설정
putSelectFromParam(); putSelectFromParam();
@@ -85,6 +93,17 @@
} }
} }
function specStatusFormatter(cellvalue, options, rowObject) {
switch (cellvalue) {
case 'PUBLISHED':
var groupNames = rowObject.publishedGroupNames || [];
var tooltip = groupNames.length ? groupNames.join('\n') : '게시된 그룹 없음';
return $('<span>').text('게시(' + groupNames.length + ')').attr('title', tooltip).css('cursor', 'help').prop('outerHTML');
case 'REGISTERED': return '등록';
default: return '';
}
}
function list() { function list() {
detail() detail()
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
@@ -100,6 +119,7 @@
'요청', '요청',
'응답', '응답',
'상태', '상태',
'스펙 상태',
'작성자', '작성자',
'가상응답여부', '가상응답여부',
'변경일시(*)', '변경일시(*)',
@@ -112,6 +132,7 @@
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false}, {name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false}, {name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false}, {name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
{name: 'specStatus', align: 'center', width: '40', formatter: specStatusFormatter, sortable: false, title: false},
{name: 'author', align: 'center', width: '40', sortable: false}, {name: 'author', align: 'center', width: '40', sortable: false},
{name : 'simYn', align : 'center' , width:'40', hidden: true }, {name : 'simYn', align : 'center' , width:'40', hidden: true },
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true}, {name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
@@ -246,6 +267,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 파일 가져오기 할 때 중복 검사 기능추가.
@@ -380,7 +406,8 @@
</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">
<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 +423,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 +436,49 @@
<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="searchInOutType" style="width: 100%">
<option value="">전체</option>
<option value="1">당발</option>
<option value="2">타발</option>
</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>
</tr>
<tr>
<th style="width:180px;">API 스펙 상태</th>
<td>
<select name="searchSpecStatus" style="width: 100%">
<option value="">전체</option>
<option value="UNREGISTERED">미등록</option>
<option value="REGISTERED">등록</option>
<option value="PUBLISHED">게시</option>
</select>
</td>
<th style="width:180px;">API 그룹</th>
<td>
<select name="searchApiGroupId" style="width: 100%">
<option value="">전체</option>
<option value="UNREGISTERED">미등록</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>
@@ -449,4 +513,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
@@ -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>
+15 -6
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"
@@ -114,9 +113,9 @@ dependencies {
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api // https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1' compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.2.2'
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1' //implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2'
// apistatus-draft.yml (자동 탐지 초안 양식) 로딩용. // apistatus-draft.yml (자동 탐지 초안 양식) 로딩용.
// admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없어 직접 파싱한다. // admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없어 직접 파싱한다.
@@ -141,13 +140,18 @@ dependencies {
implementation "org.apache.ibatis:ibatis-sqlmap:2.3.4.726" implementation "org.apache.ibatis:ibatis-sqlmap:2.3.4.726"
implementation "org.jdom:jdom:1.1" implementation "org.jdom:jdom:1.1"
implementation "com.google.code.gson:gson:2.3.1" implementation "com.google.code.gson:gson:2.8.9"
implementation "javax.annotation:javax.annotation-api:1.2" implementation "javax.annotation:javax.annotation-api:1.2"
implementation "net.sf.json-lib:json-lib-ext-spring:1.0.2" implementation "net.sf.json-lib:json-lib-ext-spring:1.0.2"
implementation "taglibs:standard:1.1.2" implementation "taglibs:standard:1.1.2"
implementation "javax.servlet:jstl:1.1.2" implementation "javax.servlet:jstl:1.1.2"
implementation group: 'com.jcraft', name: 'jsch', version: '0.1.51' implementation group: 'com.jcraft', name: 'jsch', version: '0.1.51'
// springapp-servlet.xml 의 CommonsMultipartResolver 가 사용.
// 기존엔 elink-online-common(api 'commons-fileupload:1.5')에서 전이적으로 받아왔으나,
// eapim-online 쪽에서 해당 의존성을 제거할 예정이라 직접 선언으로 전환.
implementation "commons-fileupload:commons-fileupload:1.6.0"
implementation "org.quartz-scheduler:quartz:${quartzVersion}" implementation "org.quartz-scheduler:quartz:${quartzVersion}"
// https://mvnrepository.com/artifact/org.apache.poi/poi // https://mvnrepository.com/artifact/org.apache.poi/poi
@@ -235,6 +239,11 @@ configurations.all {
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아 // 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일. // xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
force 'xml-apis:xml-apis:1.4.01' force 'xml-apis:xml-apis:1.4.01'
// logback 1.2.10 은 SLF4J 1.7 바인딩(StaticLoggerBinder)만 제공한다.
// transitive (Java-WebSocket:1.5.7 등) 가 slf4j-api 2.x 를 끌어올리면
// SLF4J 가 provider 를 못 찾아 NOP 로거로 폴백 → 콘솔/파일 로그가 전부 사라진다.
force 'org.slf4j:slf4j-api:1.7.36'
} }
} }
+2 -2
View File
@@ -2,7 +2,7 @@
POST http://localhost:7090/monitoring/_onl/admin/authserver/clientMan.json?serviceType=APIGW POST http://localhost:7090/monitoring/_onl/admin/authserver/clientMan.json?serviceType=APIGW
Content-Type: application/json Content-Type: application/json
User-Agent: insomnia/10.1.1 User-Agent: insomnia/10.1.1
X-API-KEY: EAPIM_KEY X-Internal-Token: {{internalApiToken}}
action: insert action: insert
target_host: stg target_host: stg
@@ -118,4 +118,4 @@ target_host: stg
"errTransformYn": "N" "errTransformYn": "N"
} }
] ]
} }
Binary file not shown.
+275 -377
View File
@@ -1,470 +1,368 @@
# EAPIM Admin # eapim-admin
주은행 엔터프라이즈 API 관리 시스템 - 관리 콘솔 주은행(DJB) eLink EMS 관리 콘솔
## 프로젝트 개요 ## 개요
**EAPIM Admin**은 광주은행의 eLink EMS (eLink Management System) 관리 콘솔로, API 게이트웨이 구성, 모니터링, 통계, 트랜잭션 로그 조회 기능을 제공하는 웹 기반 플랫폼입니다. **eapim-admin** eLink EMS(eLink Management System)의 웹 기반 관리 콘솔입니다.
API 게이트웨이 구성, 실시간 모니터링/대시보드, 통계, 트랜잭션 로그 조회, 인터페이스·레이아웃·어댑터·라우팅 관리 기능을 제공합니다.
eLink EMS 는 세 가지 애플리케이션으로 구성됩니다.
| 애플리케이션 | 설명 |
|---|---|
| **eapim-admin** | 관리 콘솔 (이 저장소) |
| eapim-online | 실시간 트랜잭션 처리 API 게이트웨이 |
| eapim-portal | API 소비자용 개발자 포털 (Spring Boot) |
> 패키지·클래스에 남아 있는 `kjb` / `KJB` 명칭은 초기 광주은행 커스터마이징에서 유래한 것으로,
> 현재 배포 대상은 제주은행(DJB)입니다. 제주은행 전용 코드는 `com.eactive.eai.rms.ext.djb` 하위에 있습니다.
## 기술 스택 ## 기술 스택
- Spring Framework 5.3.27 | 구분 | 버전 / 내용 |
- Java 17 |---|---|
- Gradle 8.7 | Java | **8** (Gradle toolchain 으로 강제, `JavaLanguageVersion.of(8)`) |
- Oracle 19c | 빌드 | Gradle **8.7** (wrapper), `war` 패키징 |
- Spring Data JPA 2.5.2 | Spring Framework | 5.3.27 (MVC / ORM / JDBC) |
- Hibernate 5.4.33/5.6.15 | Spring Data JPA | 2.5.2 |
- QueryDSL 5.0.0 | Hibernate | 5.6.15.Final |
- iBATIS 2.3.4 (레거시 SQL 매핑) | QueryDSL | 5.0.0 (빌드 시 Q-class 생성) |
- Quartz 2.2.1 | iBATIS | 2.3.4 (레거시 SQL 매핑) |
- Logback 1.2.10 | Quartz | 2.2.1 (스케줄링) |
| 로깅 | SLF4J + Logback 1.2.10 (모든 `log4j` 의존성 제외) |
| 기타 | Apache POI 3.17(Excel), Jackson 2.13.1, AWS SDK 2.x(S3), EhCache, Lombok, MapStruct |
## 초기 세팅 가이드 애플리케이션 프레임워크는 Spring Boot 가 아닌 전통적 Spring 5.3 WAR 이며 Tomcat / WebLogic 등에 배포됩니다.
(Spring Boot 는 테스트 유틸리티 `spring-boot-starter-test:2.6.15` 에만 사용)
### 사전 요구사항 ## 사전 요구사항
- JDK 17 이상 - **JDK 8** - `.envrc`(direnv) 또는 셸 rc 에서 `JAVA_HOME` 을 JDK 8 위치로 export. Gradle 8 toolchain auto-detect 가 `JAVA_HOME` 도 후보로 인식합니다. `gradle.properties` 에 JDK 경로를 넣지 않습니다.
- Gradle 8.7
- Git - Git
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB) - Oracle 접근 (개발/테스트 환경에 따라). 테스트는 H2 인메모리 DB 사용.
- **형제 디렉토리 체크아웃 필요** - 멀티 모듈 프로젝트로 아래 저장소가 같은 부모 디렉토리에 있어야 합니다.
```
<workspace>/
├── eapim-admin/ # 이 저장소
├── eapim-online/ # elink-online-* 모듈
└── elink-portal-common/ # 포털 공통 JPA 엔티티
```
### 환경별 설정 파일 ## 멀티 모듈 구조
프로젝트 클론 후 필요한 환경 설정을 수정하세요:
- `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경 `settings.gradle` 에서 형제 디렉토리의 소스 프로젝트를 참조합니다.
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
| 모듈 | 위치 | 설명 |
|---|---|---|
| elink-online-core | `../eapim-online/elink-online-core` | 핵심 인터페이스 / 도메인 모델 |
| elink-online-core-jpa | `../eapim-online/elink-online-core-jpa` | JPA 엔티티 / 리포지토리 |
| elink-online-transformer | `../eapim-online/elink-online-transformer` | 메시지 변환 로직 |
| elink-online-common | `../eapim-online/elink-online-common` | 공통 유틸리티 |
| elink-online-emsclient | `../eapim-online/elink-online-emsclient` | EMS 통신 클라이언트 |
| elink-portal-common | `../elink-portal-common` | 포털 공통 컴포넌트 (JPA 엔티티) |
### 주의사항 > `kjb-safedb`, `eapim-admin-djb` 는 `settings.gradle` 에 주석 처리되어 있으며,
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치 > 필요 시 주석을 해제하고 해당 디렉토리를 체크아웃합니다.
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
## 빌드 및 실행 ## 빌드 및 실행
### Gradle 명령어
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다. 프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
### 빌드 및 실행 명령어
```bash ```bash
# 표준 빌드 # 표준 빌드
./gradlew build ./gradlew build
# Weblogic 배포용 빌드 (테스트 제외)
./gradlew build -x test -Pprofile=weblogic
# WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log)
./gradlew war
# 클린 빌드 # 클린 빌드
./gradlew clean build ./gradlew clean build
# 테스트 실행 # WAR 빌드 (build/libs/eapim-admin.war)
./gradlew test ./gradlew war
# 패키징 없이 클래스만 컴파일 # WebLogic 배포용 빌드 (테스트 제외, weblogic-web.xml 사용)
./gradlew build -x test -Pprofile=weblogic
# 패키징 없이 컴파일만
./gradlew classes ./gradlew classes
# QueryDSL Q-classes 및 기타 애노테이션 생성 # QueryDSL Q-class 등 생성 코드만 생성
./gradlew compileJava ./gradlew compileJava
# 의존성 트리 보기 # 전체 테스트
./gradlew dependencies ./gradlew test
# 사용 가능한 모든 태스크 목록 # 특정 테스트
./gradlew test --tests "com.eactive.eai.rms.*"
# 의존성 트리 / 태스크 목록
./gradlew dependencies
./gradlew tasks --all ./gradlew tasks --all
``` ```
- WAR 파일명: `eapim-admin.war`, 컨텍스트 경로: `/monitoring`
- 프로필: `-Pprofile=weblogic` 지정 시 `web.xml` 대신 `weblogic-web.xml` 사용 (DefaultServlet 문제 회피)
## 로컬 개발 빠른 시작
1. **생성 코드 생성** - 최초 1회 및 pull 이후
```bash
./gradlew compileJava # Q-class IDE 오류 시: ./gradlew clean compileJava
```
2. **빌드/테스트**
```bash
./gradlew build
```
3. **IDE 열기** - 아래 "IDE 설정" 참조
4. **WAS(Tomcat 등) 실행 구성** 에 `eapim-admin` 배포 + 아래 VM 옵션 추가 후 기동
### 로컬 Tomcat VM 옵션 예시
전체 목록과 설명은 `CLAUDE.md` 의 "필수 환경 변수 (Tomcat)" 를 참조하세요.
```
-Deai.datasource.type=DEV
-Dinst.Name=emsSvr11
-Deai.tableowner=EMSADM
-Deai.systemmode=D
-Dfile.encoding=utf-8
-Dlogin.mode=db
-DLOGBACK_LOG_LEVEL=DEBUG
-Ddamo-manager.enabled=true
-Dlogging.log-path=/logs/prod/eapim/emsSvr11
-Dlogback.configurationFile=classpath:logback-dev.xml
-Dhibernate.dialect=org.hibernate.dialect.Oracle12cDialect
```
- `inst.Name` 은 클러스터 내에서 인스턴스마다 고유해야 합니다.
- `eai.tableowner` 는 Oracle 스키마 소유자.
## 프로젝트 구조 ## 프로젝트 구조
### 전체 프로젝트 구조
```
kjb-eapim/
├── eapim-admin/ # Admin 관리 콘솔 (현재 프로젝트)
├── eapim-online/ # Online 게이트웨이 코어 모듈
│ ├── elink-online-core/
│ ├── elink-online-core-jpa/
│ ├── elink-online-transformer/
│ ├── elink-online-common/
│ ├── elink-online-emsclient/
│ └── elink-online-adapter/
├── elink-portal-common/ # 포털 공통 컴포넌트 (JPA 엔티티)
├── kjb-safedb/ # SafeDB 암호화 라이브러리
└── eapim-portal/ # Portal 개발자 포털 (선택사항)
```
### eapim-admin 내부 구조
``` ```
src/main/java/com/eactive/eai/ src/main/java/com/eactive/eai/
├── agent/command - 커맨드 패턴 구현 ├── agent/command - 커맨드 패턴 구현
├── common/ - 공통 유틸리티, iBatis, JSON 직렬화 ├── common - 공통 유틸리티, iBatis, JSON 직렬화
├── custom/ - 고객사별 커스터마이제이션 ├── custom - 고객사별 커스터마이제이션
├── rms/ - 메인 애플리케이션 코드 ├── rms - 메인 애플리케이션 코드
│ ├── bap/ - 배치 처리 (BAP) │ ├── bap - 배치 처리(BAP): adaptor / manage / tansaction
│ ├── bat/ - 추가 배치 기능 │ ├── bat - 추가 배치 기능
│ ├── common/ - RMS 공통 (필터, 시작, 서비스) │ ├── common - RMS 공통 (필터, 시작, 서비스)
│ ├── data/ - 데이터 엔티티, 리포지토리 │ ├── data - 데이터 엔티티, 리포지토리
│ ├── env/ - 환경 설정 │ ├── env - 환경 설정
│ ├── kakao/ - 카카오 연동 │ ├── kakao - 카카오 연동
│ ├── onl/ - 온라인 트랜잭션 관리 │ ├── onl - 온라인 트랜잭션 관리 (apim / manage / transaction)
── service/ - 비즈니스 서비스 ── service - 비즈니스 서비스
└── ext.kjb/ - 광주은행(KJB) 확장 └── ext
``` │ ├── djb - 제주은행(DJB) 확장 (webhook, UMS 연동, 통계 등)
│ └── kjb - 광주은행(KJB) 유래 확장
└── (com.eactive.ext.kjb) - 통계 화면 등 일부 확장 코드
### 웹 리소스 구조
```
WebContent/ WebContent/
├── WEB-INF/ ├── WEB-INF/
│ ├── applicationContext.xml - Root Spring 설정 │ ├── applicationContext.xml - Root Spring 설정
│ ├── springapp-servlet.xml - Servlet Spring 설정 │ ├── springapp-servlet.xml - Servlet Spring 설정
│ ├── web.xml - Tomcat용 배포 디스크립터 │ ├── web.xml / weblogic-web.xml
── weblogic-web.xml - WebLogic용 배포 디스크립터 ── properties/ - env.D / env.T / env.P / env.L .properties
│ └── properties/ - 환경별 프로퍼티 ├── jsp/ - 화면
│ ├── env.D.properties - 개발 └── guide/ - 사용자 가이드(docsify)
│ ├── env.T.properties - 테스트
│ └── env.P.properties - 운영
└── guide/ - 사용자 가이드
``` ```
## 주요 기능 ## 데이터 액세스 (하이브리드 ORM)
다음과 같은 관리 기능을 수행하며, 모든 관리 대상은 실시간으로 DB, 메모리에 있는 정보가 수정, 반영됩니다. JPA 와 iBATIS 를 함께 사용합니다.
| 기능 | 설명 | - **신규 기능**: JPA + Spring Data 리포지토리 (`com.eactive.eai.rms.data.entity`, `elink-portal-common`)
| -------------------- | ------------------------------------------------------------ | - **레거시**: iBATIS SQL 매퍼 `src/main/resources/com/eactive/eai/**/*.xml` (Spring XML 설정, 점진적 마이그레이션 대상)
| 사용자 관리 | 사용자 및 권한을 관리하는 기능 | - 벤더별 매핑: Oracle `*-oracle.xml`, MariaDB `*-mariadb.xml`, PostgreSQL `*-postgresql.xml`
| 로그 조회 | 거래 수행 내역을 조회 하는 기능(온라인/배치/일괄) |
| 통계 조회 | 거래 통계 조회 기능 |
| 대시보드 조회 | 거래 내역 및 장애상황을 모니터링 할 수 있는 대시보드 조회 기능 |
| 인터페이스 관리 | 인터페이스 등록/관리 기능 |
| 레이아웃 관리 | 레이아웃 등록/관리 기능 |
| 변환 관리 | 변환 등록/관리 기능 |
| 어댑터 관리 | 통신 어댑터 등록/관리 기능 |
| 라우팅룰 관리 | 라우팅 룰 등록/관리 기능 |
| 배치 어댑터 관리 | 배치 어댑터 등록/관리 기능 |
| 배치 인터페이스 관리 | 배치 인터페이스 등록/관리 기능 |
| 일괄 관리 | 일괄 등록/관리 기능 |
## 외부 모듈 ### Multi-tenancy (런타임 스키마 전환)
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조): Hibernate Multi-tenancy 로 요청 시점에 스키마를 전환합니다.
`DataSourceContextHolder`(ThreadLocal) → `TenantIdentifierResolver` → `ConfigurableMultiTenantConnectionProvider.setSchema()`.
| 이름 | 버전 | 설명 | 요청 파라미터 `serviceType`(예: `APIGW`) 를 `DataSourceTypeInterceptor` 가 읽어 자동 설정하거나, 컨트롤러에서 명시 설정합니다.
| ------------------------ | ------------ | ---------------------------------------------- | 자세한 내용은 `CLAUDE.md` 의 "Multi-tenancy" 절 참조.
| elink-online-core | 4.5-SNAPSHOT | 코어 모듈로 인터페이스 클래스 정의 |
| elink-online-core-jpa | 4.5-SNAPSHOT | JPA 엔티티 및 리포지토리 |
| elink-online-transformer | 4.5-SNAPSHOT | 변환 모듈로 메시지 및 변환 관련 클래스 정의 |
| elink-online-adapter | 4.5-SNAPSHOT | 어댑터 모듈로 통신에 필요한 어댑터 클래스 정의 |
| elink-online-common | 4.5-SNAPSHOT | 나머지 공통 프로그램이 정의되어있다 |
| elink-online-emsclient | 4.5-SNAPSHOT | ems와 통신하기 위한 모듈 |
| elink-portal-common | - | 포털 공통 컴포넌트 (JPA 엔티티) |
| kjb-safedb | - | 커스텀 암호화 라이브러리 |
| eapim-admin-kjb | - | KJB 은행 특화 커스터마이제이션 |
## 하이브리드 ORM 방식
이 애플리케이션은 **JPA와 iBATIS 모두를 사용**합니다:
- **JPA/Hibernate**: `elink-portal-common`을 통한 현대적인 데이터 액세스 (Spring Data repositories)
- **iBATIS**: 기존 코드를 위한 레거시 SQL 매핑 (점진적으로 마이그레이션 예정)
데이터 액세스 작업 시:
- 새로운 기능은 JPA와 Spring Data repositories를 사용해야 합니다
- 레거시 iBATIS 매퍼는 `src/main/resources/com/eactive/eai/**/*.xml`에 있으며 Spring XML을 통해 설정됩니다
## 코드 생성 ## 코드 생성
빌드 프로세스에서 생성되는 것들: 빌드 시 생성됩니다.
- **QueryDSL Q-classes**: 타입 안전 쿼리 클래스
- **Lombok**: 애노테이션 프로세싱을 통한 Getters/setters/builders
- **MapStruct**: DTO와 엔티티 간의 객체 매퍼
### 이중 Q-Class 생성 (Gradle + Eclipse)
이 프로젝트는 **Gradle과 Eclipse 애노테이션 프로세서 모두를 사용**하므로, Q-classes가 **두 위치에 생성**됩니다:
1. **`build/generated/java/`** - `gradle compileJava` 또는 `gradle build` 실행 시 Gradle이 생성
2. **`WebContent/generated/`** - Eclipse IDE에서 빌드 시 Eclipse APT가 생성
**이는 예상된 동작이며 의도적입니다.** 두 디렉토리 모두 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다.
## AI 코딩 어시스턴트 지침 파일
이 프로젝트는 여러 AI 코딩 어시스턴트를 지원합니다. 각 도구는 다음 파일을 참조합니다:
### 지침 파일 위치
| AI 도구 | 지침 파일 경로 | 용도 |
|---------|---------------|------|
| **Claude Code** | `CLAUDE.md` | 마스터 지침 파일 (한글) |
| **GitHub Copilot** | `.github/copilot-instructions.md` | Copilot용 지침 |
| **Cursor** | `.cursorrules` | Cursor용 간결한 규칙 |
### 지침 파일 관리 방법
1. **마스터 파일**: `CLAUDE.md`
- 가장 상세한 프로젝트 지침을 포함
- 한글로 작성되어 한국 개발팀이 이해하기 쉬움
- 수정 시 이 파일을 먼저 업데이트
2. **동기화**:
```bash
# CLAUDE.md 수정 후 다른 파일들 동기화
cp CLAUDE.md .github/copilot-instructions.md
# .cursorrules는 간결한 버전이므로 필요시 수동 업데이트
```
3. **새로운 AI 도구 지원 추가**:
- Windsurf: `.windsurfrules` 파일 생성
- 기타 도구: 해당 도구의 규칙 파일명 확인 후 생성
- **QueryDSL Q-class** - `build/generated/java/` 에 생성, `.gitignore` 대상 (커밋 금지)
- **Lombok** - getter/setter/builder 등
- **MapStruct** - DTO ↔ 엔티티 매퍼
pull 이후 또는 Q-class IDE 오류 시 `./gradlew compileJava` (또는 `clean compileJava`) 로 재생성합니다.
## 환경 설정 ## 환경 설정
### 시스템 모드 ### 시스템 모드
- **D (DEV)**: 개발 환경 `-Deai.systemmode` 로 지정하며 해당 프로퍼티 파일을 로드합니다.
- **T (TEST)**: 테스트/스테이징 환경
- **P (PROD)**: 운영 환경
시스템 모드는 `-Deai.systemmode` JVM 옵션으로 설정하며, 해당 환경의 프로퍼티 파일(`env.D.properties` 등)을 로드합니다. | 값 | 환경 | 파일 |
|---|---|---|
| D | 개발 | `env.D.properties` |
| T | 테스트 / 스테이징 | `env.T.properties` |
| P | 운영 | `env.P.properties` |
| L | 로컬 | `env.L.properties` |
### TOMCAT 기동 옵션 참조 ### 데이터베이스
#### 은행 개발 서버용 - **Oracle**: 운영 주 DB (`hibernate.dialect=org.hibernate.dialect.Oracle12cDialect`)
``` - **MariaDB / PostgreSQL**: 벤더별 SQL 매핑으로 지원 (JDBC 의존성은 필요 시 활성화)
-Deai.datasource.type=DEV - **H2**: 테스트용 인메모리 DB (`src/test/resources`)
-Dinst.Name=emsSvr11 - 연결 타입은 `-Deai.datasource.type`(DEV/STG/PROD) 로 선택
-Deai.tableowner=EMSAPP
-Deai.systemmode=D
-Dfile.encoding=utf-8
-DLOGBACK_LOG_LEVEL=info
```
#### 로컬 개발용 ### DB 암호화
```
-Deai.datasource.type=DEV `damo-manager.jar` 모듈은 환경에 따라 다른 암호화로직을 제공한다.
-Dinst.Name=emsSvr99
-Deai.tableowner=EMSAPP | 환경 | 설명 |
-Deai.systemmode=D |---|---|
-Dfile.encoding=utf-8 | 운영 | 실제 D'amo 네이티브 라이브러리 |
-Dlogin.mode=db | 개발 | SHA-256 |
-DLOGBACK_LOG_LEVEL=DEBUG
-Dlogback.configurationFile=classpath:logback-dev.xml 로컬 개발 시 `-Ddamo-manager.enabled=false`
-Dhibernate.dialect=org.hibernate.dialect.Oracle12cDialect
-Dkjb_safedb.mode=fake ## DJB (제주은행) 전용 설정
DJB 전용 프로퍼티는 DB 테이블 `TSEAIRM24` 에 그룹명 `'Monitoring'` 과
DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다.
> Monitoring 프라퍼티 (환경정보 > EMS관리 > 모니터링 프라퍼티)
**계정관리**
- `djb.sms_auth.enabled` - 로그인시 2-factor 인증 적용여부
- `djb.sms_auth.fixed_value` - sms_auth.mode=fixed일 경우 고정인증값
- `djb.sms_auth.mode` - 2-factor 인증 모드 (real:실제, fixed:고정값-개발환경에서 사용)
- `rms.DUAL_LOGIN_ENABLED` - 중복 로그인 허용 여부
- `rms.auto.logout.timeout` - 자동 로그아웃 시간(분)
- `rms.password.combi.check` - 비밀번호 영문/숫자/특수문자 조합 종류수
- `rms.password.fail.count` - 비밀번호 실패 허용 횟수(이 값 이상 실패시 계정 잠김)
- `rms.password.history.count` - 이전 비밀번호 변경 불가 횟수
- `rms.password.init.subfix` - 관리자가 비밀번호 초기화시 추가 문자 ex) 행번@!
- `rms.password.length.check` - 비밀번호 최소 자리수
- `rms.password.repeat.check` - 비밀번호 동일문자,연속문자 반복 불가 횟수
- `rms.password.sms_auth.enabled` - 비밀번호 변경시 2-factor 인증 여부
**인터페이스 배포**
- `iomap.download.path` - 인터페이스.json 파일 다운로드 서버 경로
- `iomap.upload.path` - 인터페이스.json 파일 업로드 서버 경로
**UMS(공통) 연동**
- `djb.ums.was_ip_address` - 관리자포탈 WAS IP 주소 (UMS 연동시 필요)
- `djb.ums.was_mac_address` - 관리자포탈 WAS MAC 주소 (UMS 연동 시 필요)
**UMS(이메일) 연동**
- `djb.ums.email.url` - 이메일 API 엔드포인트 (EAI)
- `djb.ums.email.cstno` - 고객번호
- `djb.ums.email.ums_evnt_id` - UMS이벤트 ID
**UMS(사내메신저) 연동**
- `djb.ums.messenger.url` - 사내메신저 API 앤드포인트 (FEP)
- `djb.ums.messenger.client_id` - 사내메신저 클라이언트 아이디
- `djb.ums.messenger.client_secret` - 사내메신저 클라이언트 시크릿
- `djb.ums.messenger.api-monitor.enabled` - API 상태변화시 직원에게 사내메신저 발송 여부
**UMS(SMS) 연동**
- `djb.ums.sms.url` - SMS API 엔드포인트 (EAI)
- `djb.ums.sms.almtk_snd_prtl_key.intra` - 카톡 알림톡 채널 ID (제주은행 임직원)
- `djb.ums.sms.almtk_snd_prtl_key.public` - 카톡 알림톡 채널 ID (제주은행)
**Webhook 발송**
- `rms.webhook.enabled` - 웹훅 발송 여부
- `rms.webhook.retry_count` - 웹훅 재발송 횟수
- `rms.webhook.retry_time` - 웹훅 재발송 시간간격
- `rms.webhook.reverse_proxy.url` - 리버스 프록시 경로
-Dlogback.configurationFile="C:\eactive\workspaces\kjb-eapim\eapim-admin\src\main\resources\logback-rinjae.xml" > Portal 프라퍼티 (파트너포탈 > 포탈관리 > Property관리)
```
### 데이터베이스 지원 **UMS 연동**
- `djb.ums.email.if_id` - 이메일 연동 인터페이스 ID
- `djb.ums.email.tx_id` - 이메일 거래 ID
- `djb.ums.messenger.if_id` - 사내메신저 인터페이스 ID
- `djb.ums.messenger.tx_id` - 사내메신저 거래 ID
- `djb.ums.sms.if_id` - SMS 인터페이스 ID
- `djb.ums.sms.tx_id` - SMS 거래 ID
- **Oracle**: 운영 데이터베이스 (주)
- **PostgreSQL/MariaDB**: 대체 운영 DB
- **H2**: 테스트용 인메모리 데이터베이스
데이터베이스 벤더는 프로퍼티 파일의 `db.vendor`와 `hibernate.dialect`를 통해 설정됩니다. ## 스케쥴러
### SafeDB 암호화 라이브러리 **API 상태 모니터링**
- `ApiStatusMonitorJob11, ApiStatusMonitorJob21` - API 상태 변화를 모니터링한다. API 상태 변화 발생시 직원에게 메신저 및 제휴사에게 웹훅을 발송한다.
- 실행간격(1분)이 짧아서 CLUSTERED는 적용을 못하고, 서버수만큼 등록하여 번갈아 가면서 실행하도록 설정
- `api.status.error.range_minute` - API 장애 판단을 위한 로그 구간(분)
- `api.status.error.rate` - API 장애판단을 위한 오류율 (ex : 10분동안 90% 이상 오류 발생시 장애로 판단)
- `api.status.delay.range_minute` - API 지연 상태 판단을 위한 로그 구간(분)
- `api.status.delay.avg_resp_time` - API 지연 상태 판단을 위한 평균 응답속도 (ex: 10분동안 평균 응답속도가 10초이상이면 지연으로 판단)
`kjb-safedb` 모듈은 세 가지 동작 모드를 제공합니다: **유량제어토큰실패감시**
- **REAL**: 실제 SafeDB 네이티브 라이브러리 사용 - `InflowTokenMonitorJob` - 유량제어 토큰 획득 실패(TSEAIFR11) 발생시 직원에게 메신저를 발송한다
- **FAKE**: SHA-256/AES-256을 사용하는 개발 모드 (SafeDB 설치 불필요) - `api.inflow.fail.range_minute` - 최근 몇분동안 감시하는지 설정
- **NONE**: 암호화 우회 (테스트 전용)
개발 시 SafeDB가 로컬에 설치되지 않은 경우 `-Dkjb_safedb.mode=fake`를 설정합니다. **API 통계**
- `ApiStatsHourlyAggregationJob` - 당일 API 로그 정보를 시간별로 집계 (TSEAILG00 > API_STATS_HOUR)
- `APISTATSDAILYJOB` - API 사용통계 일집계 (API_STATS_HOUR > API_STATS_DAY)
- `APISTATSMONTHLYJOB` - API 사용통계 월집계 (API_STATS_DAY > API_STATS_MONTH)
- `APISTATSYEARLYJOB` - API 사용통계 년집계 (API_STATS_MONTH > API_STATS_YEAR)
**UMS발송**
- `UMSDISPATCHJOB11, UMSDISPATCHJOB21` - UMS를 발송한다.
- 실행간격(5초)이 짧아서 CLUSTERED는 적용을 못하고, 서버수만큼 등록하여 번갈아 가면서 실행하도록 설정
**게시글종료**
- `PortalInquiryClosingJob` - 답변완료 이후 일정기간 댓글이 없는 경우 게시글을 종료한다
- `inquiry.comment.closing_day` - 게시글 종료 대기일수
**메모리초기화**
- `MEMORYTRINIT` - 관리자포탈 서버 메모리 초기화(G/W에서 받은 거래현황)
**토큰발급이력삭제**
- `TOKENISSUANCELOGCLEANUPJOB` - 7일이 지난 토큰 발급내역을 삭제한다
## 배포 ## 배포
### 지원 서버 | 대상 | 비고 |
- **Tomcat** (기본, 개발) |---|---|
- **WebLogic 14.1.2** (운영, `-Pprofile=weblogic` 필요) | Tomcat | 기본, 개발 |
- **JEUS** (지원) | WebLogic 14.1.2 | 운영, `-Pprofile=weblogic` 필요 |
- **JBoss/WildFly** (descriptors와 함께 지원) | JEUS | 지원 |
| JBoss / WildFly | descriptor 포함 지원 |
### WebLogic 전용 빌드 - 모든 관리 변경사항은 DB 와 메모리에 즉시 반영됩니다.
- 인코딩은 UTF-8. `CharacterEncodingFilter` 가 `*.excel` 을 제외한 모든 요청에 적용.
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다: - XSS 보호: `com.eactive.eai.rms.common.filter.CrossScriptingFilter` 전역 적용.
```bash
gradle build -x test -Pprofile=weblogic
```
### WAR 배포
WAR 파일은 context path `/monitoring`으로 `eapim-admin.war`로 빌드됩니다.
## IDE 설정 ## IDE 설정
### Eclipse `build.gradle` 은 `eclipse` / `eclipse-wtp` / `idea` 플러그인을 모두 포함합니다.
IntelliJ 전용 변형은 `build.gradle.intellij` 로 별도 관리합니다 (필요 시 `build.gradle` 로 교체).
```bash ```bash
gradle eclipse # Eclipse
./gradlew eclipse # .project / .classpath, WTP(context path: /monitoring), APT 설정 생성
# IntelliJ IDEA
./gradlew idea # 또는 IDE 에서 "Import Gradle Project"
``` ```
다음을 생성합니다:
- `.project` 및 `.classpath` 파일
- Eclipse WTP 설정 (context path: `/monitoring`)
- UTF-8 인코딩 설정
- QueryDSL/Lombok을 위한 애노테이션 프로세서 설정
### IntelliJ IDEA
```bash
gradle idea
```
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
## KJB (광주은행) 전용 설정
### 프로퍼티 관리
KJB 전용 프로퍼티는 데이터베이스 테이블 `TSEAIRM24`에 그룹명 `'Monitoring'`으로 저장됩니다.
### 주요 프로퍼티 설정
**이메일 알림 (EAI Batch를 통해)**
- `kjb.eai_batch.url` - 이메일 발송을 위한 EAI 배치 서버 URL
- 개발: http://172.31.32.111:10230/BATAppWeb/EaiBatCall
- QA: http://172.31.33.111:10430/BATAppWeb/EaiBatCall
- 운영: http://172.21.1.40:10860/BATAppWeb/EaiBatCall
- 입력하지 않을 경우 EAI 배치 호출을 하지 않고 내부적으로 통신하지 않으며 warning 로그만 남김
- `kjb.ums.eai_batch.send_dir` - 이메일 파일 발송 디렉토리 (기본값: `/Data/eapim/portal/sendmail/snd`)
- `kjb.ums.eai_batch.backup_dir` - 이메일 백업 디렉토리 (기본값: `/Data/eapim/portal/sendmail/bak`)
- `kjb.ums.eai_batch.interface_id` - 고객 이메일용 인터페이스 ID (예: `UAGFF00001UIE`)
**UMS (SMS/LMS/카카오톡) 연동**
- `kjb.ums.host_url` - UMS API 엔드포인트
- `kjb.ums.api_key` - UMS API 인증 키
- `kjb.ums.connection_timeout` - 연결 타임아웃 (초 단위)
- `kjb.ums.response_timeout` - 응답 타임아웃 (초 단위)
### 보안
- 커스텀 패스워드 인코더: SafeDB 연동을 위한 `KjbSafedbPasswordEncoder`
## 테스트 ## 테스트
테스트에 사용되는 것들: - **JUnit 5 (Jupiter)** - 테스트 프레임워크
- **JUnit 5 (Jupiter)**: 테스트 프레임워크 - **Spring Boot Test 2.6.15** - 통합 테스트 유틸리티
- **Spring Boot Test 2.6.15**: 통합 테스트 유틸리티 - **H2** - 인메모리 DB (`src/test/resources`)
- **H2 Database**: 테스트용 인메모리 데이터베이스 - **Mockito** - 모킹 (Spring Boot Test 경유)
- **Mockito**: 모킹 프레임워크 (Spring Boot Test를 통해)
```bash ```bash
# 모든 테스트 실행 ./gradlew test
gradle test ./gradlew test --tests "com.eactive.eai.rms.*"
# 특정 테스트 클래스 실행
gradle test --tests "com.example.ClassName"
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
gradle test --tests "com.eactive.eai.rms.*"
``` ```
## 관련 프로젝트
프로젝트 구조 (kjb-eapim 디렉토리 기준):
- `eapim-admin/` - Admin 관리 콘솔 (현재 프로젝트)
- `eapim-online/` - Online 게이트웨이 (코어 모듈 포함)
- `eapim-portal/` - Portal 개발자 포털
- `elink-portal-common/` - 공통 유틸리티 라이브러리
- `kjb-safedb/` - SafeDB 암호화 라이브러리
- `kjb-eapim-sql/` - SQL 스크립트 (선택사항)
## 문서 ## 문서
- **프로젝트 상세 지침**: `CLAUDE.md` (한글) - **프로젝트 상세 지침**: `CLAUDE.md` (AI 어시스턴트 및 개발자용 마스터 문서, 한글)
- **사용자 가이드**: `WebContent/guide/` (한글) - **사용자 가이드**: `WebContent/guide/` (docsify)
## 참고: KJB 데이터베이스 파라미터 설정
### 추가 파라미터 SQL
```sql
-- 로컬 개발용 (외부)
-- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리
--insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
-- insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
--insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit;
-- 은행 개발 서버용
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.31.35.144:9021');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit;
-- 은행 운영 서버용
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.21.1.40:10860/BATAppWeb/EaiBatCall');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.24.128.66:9021');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit;
```
## 라이선스 ## 라이선스
© 광주은행 (Kwangju Bank) 사내 전용(proprietary). 제주은행 eLink EMS.
@@ -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");
@@ -22,6 +22,9 @@ import com.eactive.eai.rms.common.util.CommonConstants;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusiness; import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusiness;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessId; import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessId;
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService; import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilter;
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilterId;
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilterService;
import com.eactive.eai.rms.data.entity.onl.unifbwk.UnifBwkTpService; import com.eactive.eai.rms.data.entity.onl.unifbwk.UnifBwkTpService;
@Service @Service
@@ -40,6 +43,9 @@ public class BizManService extends BaseService {
@Autowired @Autowired
private UserBusinessService userBusinessService; private UserBusinessService userBusinessService;
@Autowired
private TransactionTrackingFilterService transactionTrackingFilterService;
@Autowired @Autowired
private UserBizUIMapper userBizUIMapper; private UserBizUIMapper userBizUIMapper;
@@ -101,7 +107,14 @@ public class BizManService extends BaseService {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
userBusinessService.deleteOrSave(userId, userBusinesses); userBusinessService.deleteOrSave(userId, userBusinesses);
transactionTrackingFilterService.deleteAllByUserId(userId);
List<TransactionTrackingFilter> filters = userBusinesses.stream()
.map(ub -> new TransactionTrackingFilter(
new TransactionTrackingFilterId(userId, ub.getId().getEaiBzwkDstcd())))
.collect(Collectors.toList());
transactionTrackingFilterService.saveAll(filters);
String userBusinessServiceLists = userBusinesses.stream() String userBusinessServiceLists = userBusinesses.stream()
.map(e -> e.getId().getEaiBzwkDstcd()) .map(e -> e.getId().getEaiBzwkDstcd())
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
@@ -258,7 +258,8 @@ public interface MonitoringContext {
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 60) // 자동 로그아웃 타임아웃 (단위: 분, 기본값: 60)
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";
@@ -268,6 +269,24 @@ 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";
// 비밀번호 최소 길이 (기본 7)
public static final String RMS_PASSWORD_LENGTH_CHECK = "rms.password.length.check";
// 비밀번호 문자 조합 종류 수 (영문/숫자/특수문자 중 N종류 이상, 기본 2)
public static final String RMS_PASSWORD_COMBI_CHECK = "rms.password.combi.check";
// 동일 문자 연속 반복 제한 개수 (기본 3, 2 미만이면 미검사)
public static final String RMS_PASSWORD_REPEAT_CHECK = "rms.password.repeat.check";
// 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
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); // js/css 정적 리소스는 캐시를 허용 (화면 오픈 시마다 재다운로드되는 문제 방지)
response.setHeader("Pragma", "no-cache"); String servletPath = request.getServletPath();
response.setDateHeader("Expires", 0); boolean isStaticAsset = servletPath != null
&& (servletPath.endsWith(".js") || servletPath.endsWith(".css"));
if (!isStaticAsset) {
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
}
chain.doFilter(new RequestWrapper(request), response); chain.doFilter(new RequestWrapper(request), response);
// //
@@ -158,7 +158,7 @@ class RequestWrapper extends HttpServletRequestWrapper {
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과 // src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
value = filterSrcAttribute(value); value = filterSrcAttribute(value);
// 리치 텍스트 파라미터는 문자 변환을 건너뛰고 XSS 패턴 필터링만 적용 // 리치 텍스트 파라미터와 메시지 템플릿 저장 요청은 문자 변환 없이 XSS 패턴 필터링만 적용
if (!shouldSkipCharConvert()) { if (!shouldSkipCharConvert()) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
value = convertChars(value, sb); value = convertChars(value, sb);
@@ -179,6 +179,15 @@ class RequestWrapper extends HttpServletRequestWrapper {
if (param == null) { if (param == null) {
return false; return false;
} }
// 상세 화면은 .view로 열리지만 등록/수정 데이터는 .json으로 POST됨.
// 문자 발송에 사용되는 입력값이 HTML 엔티티로 저장되지 않도록 함.
if ("POST".equalsIgnoreCase(getMethod())
&& "/onl/apim/template/messageTemplateMan.json".equals(getServletPath())) {
String cmd = super.getParameter("cmd");
if ("INSERT".equals(cmd) || "UPDATE".equals(cmd)) {
return true;
}
}
for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) { for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) {
if (StringUtils.equalsIgnoreCase(param, skipParam)) { if (StringUtils.equalsIgnoreCase(param, skipParam)) {
return true; return true;
@@ -1,64 +0,0 @@
package com.eactive.eai.rms.common.interceptor;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@Slf4j
public class ApiKeyInterceptor implements HandlerInterceptor {
private static final String API_KEY_HEADER = "X-API-KEY";
private static final String PORTAL_PROPERTY_GROUP = "Portal";
@Autowired
private PortalPropertyService portalPropertyService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
String requestApiKey = request.getHeader(API_KEY_HEADER);
if (StringUtils.isEmpty(requestApiKey)) {
log.warn("Request received without API key. URI: {}", request.getRequestURI());
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "API key is missing");
return false;
}
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
String validApiKey = properties.get("apiKey");
if (StringUtils.isEmpty(validApiKey)) {
log.error("API key not configured in properties");
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "API key is not configured");
return false;
}
if (!validApiKey.equals(requestApiKey)) {
log.warn("Invalid API key received. URI: {}", request.getRequestURI());
sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, "Invalid API key");
return false;
}
return true;
} catch (Exception e) {
log.error("Error processing API key validation", e);
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error processing request");
return false;
}
}
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType("application/json");
response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
}
}
@@ -0,0 +1,44 @@
package com.eactive.eai.rms.common.interceptor;
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* {@code /_onl/**} 내부 연동 요청을 공유 내부 토큰으로 검증한다.
*
* <p>토큰과 헤더명은 PTL_PROPERTY {@code Portal / internal.api.header-name},
* {@code Portal / internal.api.token}을 사용한다. admin은 토큰을 생성하지 않고 읽기 전용으로
* 검증하므로, portal이 먼저 기동되어 토큰을 생성해야 한다.</p>
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class InternalApiTokenInterceptor implements HandlerInterceptor {
private final InternalApiTokenService internalApiTokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
String headerName = internalApiTokenService.findHeaderName();
String presentedToken = request.getHeader(headerName);
if (!internalApiTokenService.matchesReadOnly(presentedToken)) {
log.warn("Invalid internal API token. URI: {}, header: {}", request.getRequestURI(), headerName);
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Internal API token is invalid");
return false;
}
return true;
}
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
}
}
@@ -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,13 @@ 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 static final int DEFAULT_PASSWORD_LENGTH_CHECK = 7;
private static final int DEFAULT_PASSWORD_COMBI_CHECK = 2;
private static final int DEFAULT_PASSWORD_REPEAT_CHECK = 3;
private final LocaleMessage localeMessage; private final LocaleMessage localeMessage;
private final MonitoringContext monitoringContext; private final MonitoringContext monitoringContext;
@@ -76,6 +80,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 +92,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 +102,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;
} }
@@ -216,7 +223,7 @@ public class MainController implements InterceptorSkipController {
// 휴대폰번호 없는 사용자 // 휴대폰번호 없는 사용자
return LoginResponseDto.builder() return LoginResponseDto.builder()
.success(false) .success(false)
.errorMessage("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요.") .errorMessage("NO_PHONE_NUMBER")
.build(); .build();
} }
@@ -227,7 +234,7 @@ public class MainController implements InterceptorSkipController {
if (!sent) { if (!sent) {
return LoginResponseDto.builder() return LoginResponseDto.builder()
.success(false) .success(false)
.errorMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.") .errorMessage("FAIL_SEND_AUTH")
.build(); .build();
} }
@@ -246,6 +253,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 +461,35 @@ 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);
}
// 비밀번호 최소 길이 조회 (rms.password.length.check)
private int getPasswordLengthCheck() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_LENGTH_CHECK, DEFAULT_PASSWORD_LENGTH_CHECK);
}
// 비밀번호 문자 조합 종류 수 조회 (rms.password.combi.check, 영문/숫자/특수문자 중 N종류 이상)
private int getPasswordCombiCheck() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_COMBI_CHECK, DEFAULT_PASSWORD_COMBI_CHECK);
}
// 동일 문자 연속 반복 제한 개수 조회 (rms.password.repeat.check, 2 미만이면 미검사)
private int getPasswordRepeatCheck() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_REPEAT_CHECK, DEFAULT_PASSWORD_REPEAT_CHECK);
}
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금 // 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
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 +721,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 +861,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 +880,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 +893,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,48 +904,154 @@ 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. 비밀번호 체크
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256,resetPassword)) if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256,resetPassword))
|| !userInfo.getUserid().equals(resetUserId)) { || !userInfo.getUserid().equals(resetUserId)) {
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 if (password == null || password.getBytes().length != password.length()) {
.length() && password.getBytes().length >= 7 return false;
&& isCombination(password) }
&& !isRepeatPassword(password, 5)
&& !isSerialPassword(password, 5); int lengthCheck = getPasswordLengthCheck();
int combiCheck = getPasswordCombiCheck();
int repeatCheck = getPasswordRepeatCheck();
return password.getBytes().length >= lengthCheck
&& isCombination(password, combiCheck)
&& (repeatCheck < 2 || !isRepeatPassword(password, repeatCheck))
&& !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,27 +1076,47 @@ 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++) {
if (preData + 1 == password.charAt(i)) { char prev = password.charAt(i - 1);
serialcount++; char curr = password.charAt(i);
} else {
serialcount = 1;
}
preData = password.charAt(i); ascCount = (curr == prev + 1) ? ascCount + 1 : 1;
descCount = (curr == prev - 1) ? descCount + 1 : 1;
if (serialcount == count) { return true; } if (ascCount >= count || descCount >= count) { return true; }
} }
return false; return false;
} }
private boolean isCombination(String password) { // 키보드 자판 배열상 연속된 문자열(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;
}
}
}
return false;
}
private boolean isCombination(String password, int minTypes) {
int combicount = 0; int combicount = 0;
// 숫자포함여부 // 숫자포함여부
String pattern = "[0-9]+"; String pattern = "[0-9]+";
@@ -979,8 +1145,8 @@ public class MainController implements InterceptorSkipController {
combicount++; combicount++;
} }
// combicount가 2 이상이면 true, 그렇지 않으면 false 반환 // combicount가 minTypes 이상이면 true, 그렇지 않으면 false 반환
return combicount >= 2; return combicount >= minTypes;
} }
@@ -19,23 +19,7 @@ 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,11 +65,15 @@ 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);
return factory; return factory;
} }
@@ -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,7 +11,9 @@ 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() {
} }
@@ -53,7 +56,15 @@ public class GridResponse<T> {
public void setRows(List<T> rows) { public void setRows(List<T> rows) {
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);
}
}
@@ -8,9 +8,12 @@ import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
import com.eactive.eai.data.jpa.AbstractDataService; import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch; import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.Tuple;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -47,6 +50,24 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
}); });
} }
/** 검색 콤보용 그룹 ID/명칭. 게시 여부와 관계없이 모든 그룹을 제공한다. */
public List<Map<String, String>> findGroupOptions() {
QApiGroup group = QApiGroup.apiGroup;
List<Tuple> rows = getJPAQueryFactory()
.select(group.id, group.groupName)
.from(group)
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
.fetch();
List<Map<String, String>> options = new ArrayList<>();
for (Tuple row : rows) {
Map<String, String> option = new HashMap<>();
option.put("id", row.get(group.id));
option.put("groupName", StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
options.add(option);
}
return options;
}
public void deleteApiGroupApiByApiId(String apiId) { public void deleteApiGroupApiByApiId(String apiId) {
List<ApiGroup> apiGroups = repository.findAll(); List<ApiGroup> apiGroups = repository.findAll();
for (ApiGroup apiGroup : apiGroups) { for (ApiGroup apiGroup : apiGroups) {
@@ -77,6 +98,28 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
.fetch(); .fetch();
} }
/** 목록 툴팁용 게시 그룹명. Oracle IN 절 제한에 맞춰 페이지의 API들을 일괄 조회한다. */
public Map<String, List<String>> findPublishedGroupNamesByApiIds(List<String> apiIds) {
Map<String, List<String>> groupNames = new HashMap<>();
QApiGroup group = QApiGroup.apiGroup;
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
for (int from = 0; from < apiIds.size(); from += 1000) {
List<String> chunk = apiIds.subList(from, Math.min(from + 1000, apiIds.size()));
List<Tuple> rows = getJPAQueryFactory()
.select(membership.id.apiId, group.groupName)
.from(membership)
.join(membership.apiGroup, group)
.where(membership.id.apiId.in(chunk), group.displayYn.eq("1"))
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
.fetch();
for (Tuple row : rows) {
groupNames.computeIfAbsent(row.get(membership.id.apiId), key -> new ArrayList<>())
.add(StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
}
}
return groupNames;
}
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */ /** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
public void setApiGroupsForApi(String apiId, List<String> groupIds) { public void setApiGroupsForApi(String apiId, List<String> groupIds) {
deleteApiGroupApiByApiId(apiId); deleteApiGroupApiByApiId(apiId);
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
import java.util.Collection;
import java.util.List;
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
/** EAI 목록과 스키마가 다른 EMS 스펙의 ID/게시 여부만 조회한다. */
@EMSDataSource
public interface ApiSpecStatusRepository extends Repository<ApiSpecInfo, String> {
@Query("SELECT a.apiId FROM ApiSpecInfo a WHERE "
+ "(:status = 'PUBLISHED' AND a.displayYn = 'Y') OR "
+ "(:status = 'REGISTERED' AND (a.displayYn IS NULL OR a.displayYn <> 'Y'))")
List<String> findApiIdsByStatus(@Param("status") String status);
@Query("SELECT a.apiId AS apiId, a.displayYn AS displayYn FROM ApiSpecInfo a WHERE a.apiId IN :apiIds")
List<SpecStatus> findStatusesByApiIds(@Param("apiIds") Collection<String> apiIds);
interface SpecStatus {
String getApiId();
String getDisplayYn();
}
}
@@ -1,7 +0,0 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
String getEaisvcdesc();
Long getCnt();
}
@@ -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;
} }
@@ -71,6 +71,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) {
@@ -1,6 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.eaimsg; package com.eactive.eai.rms.data.entity.onl.eaimsg;
import java.util.List; import java.util.List;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -13,7 +14,11 @@ 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.rms.data.entity.onl.apim.apispec.ApiSpecStatusRepository;
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 +36,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 +46,15 @@ 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;
private final ApiSpecStatusRepository apiSpecStatusRepository;
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 +130,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 +152,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 +170,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,28 +193,114 @@ 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())) {
predicate predicate
.and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath())); .and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchInOutType())) {
predicate.and(qeaiMessageEntity.eaisvcname.endsWith(eaiMessageUISearch.getSearchInOutType()));
}
String specStatus = eaiMessageUISearch.getSearchSpecStatus();
if ("UNREGISTERED".equals(specStatus)) {
predicate.and(apiSpecRegisteredExpression(qeaiMessageEntity.eaisvcname).not());
} else if ("REGISTERED".equals(specStatus) || "PUBLISHED".equals(specStatus)) {
predicate.and(apiIdInExpression(qeaiMessageEntity.eaisvcname,
apiSpecStatusRepository.findApiIdsByStatus(specStatus)));
}
String apiGroupId = eaiMessageUISearch.getSearchApiGroupId();
if (StringUtils.isNotBlank(apiGroupId)) {
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
QApiGroup group = QApiGroup.apiGroup;
JPQLQuery<Integer> groupQuery = JPAExpressions.selectOne()
.from(membership)
.join(membership.apiGroup, group)
.where(membership.id.apiId.eq(qeaiMessageEntity.eaisvcname));
if ("UNREGISTERED".equals(apiGroupId)) {
predicate.and(groupQuery.notExists());
} else {
predicate.and(groupQuery.where(group.id.eq(apiGroupId)).exists());
}
}
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) {
return apiIdInExpression(eaiSvcNamePath, apiSpecInfoRepository.findAllApiIds());
}
private BooleanExpression apiIdInExpression(StringPath eaiSvcNamePath, List<String> specApiIds) {
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;
}
/** 현재 페이지의 스펙 상태를 일괄 조회한다. Oracle IN 절은 1,000개씩 나눈다. */
public Map<String, String> findApiSpecStatuses(List<String> apiIds) {
Map<String, String> statuses = new HashMap<>();
for (int from = 0; from < apiIds.size(); from += ORACLE_IN_LIMIT) {
int to = Math.min(from + ORACLE_IN_LIMIT, apiIds.size());
for (ApiSpecStatusRepository.SpecStatus spec : apiSpecStatusRepository.findStatusesByApiIds(apiIds.subList(from, to))) {
statuses.put(spec.getApiId(), "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED");
}
}
return statuses;
}
/** 노출 중인(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,18 +8,31 @@ 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;
private String searchRefKey; private String searchRefKey;
private String searchApiFullPath; private String searchApiFullPath;
/** API ID 마지막 자리 (1: 당발, 2: 타발) */
private String searchInOutType;
/** UNREGISTERED: 미등록, REGISTERED: 등록(비게시), PUBLISHED: 게시 */
private String searchSpecStatus;
/** 그룹 ID. UNREGISTERED이면 소속 그룹이 없는 API */
private String searchApiGroupId;
/** 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;
} }
@@ -62,6 +62,8 @@ public class InboundErrorInfoService extends AbstractDataService<InboundErrorInf
List<InboundErrorInfo> list = query List<InboundErrorInfo> list = query
.select(qInboundErrorInfo) .select(qInboundErrorInfo)
.orderBy(qInboundErrorInfo.erroccurhms.desc()) .orderBy(qInboundErrorInfo.erroccurhms.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch(); .fetch();
return new PageImpl<>(list, pageable, totalCount); return new PageImpl<>(list, pageable, totalCount);
@@ -336,12 +336,12 @@ public class ApiStatsMinuteService
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endDateTime = LocalDateTime.parse(search.getSearchEndDateTime(), DATE_TIME_FORMATTER); endDateTime = LocalDateTime.parse(search.getSearchEndDateTime(), DATE_TIME_FORMATTER);
// 1시간(60분) 초과 시 시작시간 기준 1시간로 강제 설정 // 1시간(60분) 초과 시 시작시간 기준 1시간치(정시~59분, statTime 조건이 양끝 포함이므로 -1분)로 강제 설정
if (java.time.Duration.between(startDateTime, endDateTime).toMinutes() > MAX_MINUTES) { if (java.time.Duration.between(startDateTime, endDateTime).toMinutes() > MAX_MINUTES) {
endDateTime = startDateTime.plusMinutes(MAX_MINUTES); endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
} }
} else { } else {
endDateTime = startDateTime.plusMinutes(MAX_MINUTES); endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
} }
search.setParsedStartDateTime(startDateTime); search.setParsedStartDateTime(startDateTime);
@@ -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();
@@ -20,4 +20,12 @@ public class TransactionTrackingFilterService extends
.fetch(); .fetch();
} }
public void deleteAllByUserId(String userId) {
repository.deleteAll(findAllByUserId(userId));
}
public void saveAll(List<TransactionTrackingFilter> filters) {
repository.saveAll(filters);
}
} }
@@ -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); }
/** 인터페이스 ID 목록을 표시명 목록으로 바꾼다 (명칭을 못 찾으면 ID 그대로) */
private List<String> labelsOf(List<String> apiIds, Map<String, String> apiNames) {
List<String> labels = new ArrayList<>();
for (String apiId : apiIds) {
labels.add(apiLabel(apiId, apiNames));
} }
String first = apiLabel(split.published.get(0), apiNames); return labels;
if (apiIds.size() > 1) {
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MULTI,
"first", first, "rest", String.valueOf(apiIds.size() - 1), "keyword", titleKeyword);
}
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_SINGLE, "first", first, "keyword", titleKeyword);
} }
/** /**
@@ -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;
@@ -35,8 +43,24 @@ 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) {
int errorRate = Integer.parseInt(param.get("errorRate")); int errorRate = Integer.parseInt(param.get("errorRate"));
@@ -60,16 +84,16 @@ 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)에 성공한 경우만 처리한다.
// (그렇지 않으면 두 인스턴스가 같은 상태 전이를 각자 감지해 알림을 중복 발송하게 된다)
if (!claimStatusChange(event.getEaisvcname(), event.getPrevStatusCode(), newStatusCode)) {
log.debug("API 상태 변경 선점 실패(다른 인스턴스가 이미 처리) - {}", event.getEaisvcname());
continue;
}
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
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);
//이벤트별로 api분리 저장 //이벤트별로 api분리 저장
List<String> apis = (List<String>)eventMap.get(event.getEvent()); List<String> apis = (List<String>)eventMap.get(event.getEvent());
if (apis == null) { if (apis == null) {
@@ -86,18 +110,49 @@ public class ApiStatusService {
String event = entry.getKey(); String event = entry.getKey();
List<String> apiIds = entry.getValue(); List<String> apiIds = entry.getValue();
HashMap<String, Object> params = getSwingChatMessage(event, apiIds); //내부직원 스윙챗 발송
ums.send("api-monitor", MessageCode.API_STATUS_CHANGED, params); if (monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
HashMap<String, Object> params = getSwingChatMessage(event, apiIds);
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, params);
}
//제휴사 웹훅 발송 //제휴사 웹훅 발송
ums.sendWebhook(event, apiIds); if (monitoringContext.getBooleanProperty(MonitoringContext.RMS_WEBHOOK_ENABLED, true)) {
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 로그로 남긴다.
* 개발 모드(-Deai.systemmode=D) 에서만 동작한다. * 개발 모드(-Deai.systemmode=D) 에서만 동작한다.
@@ -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":
@@ -1,21 +1,16 @@
package com.eactive.eai.rms.ext.djb.inflow; package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
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;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog; import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId; import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> { public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@Query(nativeQuery = true, value = @Query(nativeQuery = true, value =
" SELECT EAISVCNAME" + " SELECT COUNT(*)" +
" , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" + " FROM TSEAIFR11" +
" , COUNT(*) AS CNT" + " WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')")
" FROM TSEAIFR11 A" + long countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" + }
" GROUP BY EAISVCNAME")
List<InflowTokenInsufficient> countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
}
@@ -1,7 +1,6 @@
package com.eactive.eai.rms.ext.djb.inflow; package com.eactive.eai.rms.ext.djb.inflow;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -10,7 +9,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.data.entity.onl.djb.inflow.InflowTokenInsufficient; import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.ext.djb.ums.UmsManager; import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@@ -22,19 +21,29 @@ public class InflowTokenService {
@Autowired @Autowired
private InflowTokenRepository inflowTokenRepository; private InflowTokenRepository inflowTokenRepository;
@Autowired @Autowired
private UmsManager ums; private MonitoringContext monitoringContext;
@Autowired
private UmsManager ums;
public void checkRecentFails(long rangeMinute) { public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute); if (!monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
return;
for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
String message = "유량제어 토큰 획득 실패가 발생하였습니다 \n" + info.getEaisvcname();
HashMap<String,Object> params = new HashMap<String,Object>();
params.put("message", message);
ums.send("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params);
} }
long failCount = inflowTokenRepository.countTokenInsufficient(rangeMinute);
if (failCount <= 0) {
return;
}
log.debug("유량제어 토큰 획득 실패: 최근 {}분 동안 {}건", rangeMinute, failCount);
String message = "유량제어 토큰 획득 실패가 발생하였습니다\n최근 " + rangeMinute + "분 동안 " + failCount + "";
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("message", message);
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회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다 try {
if (prevFireTime != null && nextFireTime != null) { execMinute = Long.parseLong(rangeMinute.trim());
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime(); } catch (NumberFormatException e) {
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis); log.warn("잘못된 {} 값: {} - 기본값 {} 사용", KEY_API_INFLOW_FAIL_RANGE_MINUTE, rangeMinute, DEFAULT_RANGE_MINUTE);
log.info("실행 주기: {}분", execMinute); 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();
}
} }

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