359 Commits

Author SHA1 Message Date
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
Rinjae a92fc85730 포탈 메뉴 관리 기능 추가
- PortalMenuCacheClient 클래스 작성
- JSP 화면 portalMenuMan.jsp 추가 및 렌더링 로직 구현
- PortalMenuManController 클래스 작성
2026-08-05 14:19:04 +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
Rinjae 317634b5b7 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-08-05 10:44:38 +09:00
Rinjae 27fa79ddd9 자동 장애 탐지 초안 양식 추가
- apistatus-draft.yml 파일 및 관련 로직 추가
- SnakeYAML 의존성 추가 및 안전한 yml 파싱 구현
- 자동 탐지 공지 제목·본문·타임라인 문구 분리 및 설정 관리
2026-08-05 10:44:31 +09:00
eastargh ccc55fd015 장애,지연 판단기준 >에서 >= 으로 변경
eapim-admin CI / build (push) Has been cancelled
2026-08-05 10:04:05 +09:00
Rinjae 3bf8370d0c API 상태 판정 로직 개선
eapim-admin CI / build (push) Has been cancelled
- 오류율 및 지연 판정 쿼리 조건 수정
- 상태 판정 진단 로그 메시지 포맷 및 로직 업데이트
2026-08-04 19:18:43 +09:00
eastargh 46bd79bc36 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-04 17:59:08 +09:00
eastargh cdc1a6f342 IIM delete 연동 수정 2026-08-04 17:58:24 +09:00
Rinjae 42d7fa977e 오류율 계산 및 로깅 로직 추가
eapim-admin CI / build (push) Has been cancelled
- ApiErrorRateRow 인터페이스 및 쿼리 메서드 추가
- 오류율 진단 로그 로직 구현 및 개발 모드 조건 처리
- 로그 설정(DEBUG) 변경 및 ApiStatusRepository 쿼리 개선
2026-08-04 16:19:54 +09:00
Rinjae cc0ef13d5d - IncidentTimelineUI VO 추가 - 장애 타임라인 화면 구현 지원
eapim-admin CI / build (push) Has been cancelled
- PortalNoticeManService 장애 상태 타임라인 로직 추가
- 메인 CSS에 타임라인 스타일 추가
2026-08-04 13:10:31 +09:00
Rinjae 6604f7c9e8 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-08-03 17:03:41 +09:00
Rinjae cd8f826ba5 자동 장애 탐지 및 공지 처리 개선
- 점검 판정 로직에 KIND='MAINTENANCE' 조건 추가
- 자동 등록모드 OFF/DRAFT/PUBLISH 지원 및 기본값 적용
- 장애/지연 병합 및 자동 공지 후 게시 상태 동기화
2026-08-03 17:03:35 +09:00
eastargh 031796dac4 IIM 삭제 연동시 결과메세지 수정
eapim-admin CI / build (push) Has been cancelled
2026-08-03 14:32:39 +09:00
eastargh f091b9ab19 그리드 칼럼 사이즈 조정
eapim-admin CI / build (push) Has been cancelled
2026-08-03 11:39:28 +09:00
eastargh f47480674d UMS 설정
eapim-admin CI / build (push) Has been cancelled
2026-08-03 10:43:17 +09:00
Rinjae cd20aeee2a Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-31 17:31:55 +09:00
Rinjae 0365f4342e "SBOM 생성 및 변환 추가 - CycloneDX -> Excel(xlsx) 지원 - Jenkins 단계에 SBOM 빌드 및 아티팩트 보관 연결" 2026-07-31 17:31:46 +09:00
eastargh 0576fd4c6e UMS 발송 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-31 17:12:26 +09:00
eastargh 8215b596fe 레이아웃 저장시 numeric칼럼 숫자만 입력하도록 validation & focus 이동
eapim-admin CI / build (push) Has been cancelled
2026-07-31 13:58:32 +09:00
eastargh fc6b96d7cd 사이트맵 권한 있는 메뉴만 나오도록
eapim-admin CI / build (push) Has been cancelled
2026-07-31 13:27:14 +09:00
eastargh 5fe247df65 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-07-31 10:24:41 +09:00
eastargh d11f7d9f38 client id 없는 거래 제휴사명 표기 2026-07-31 10:24:33 +09:00
Rinjae 889fd1f9f5 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-30 19:41:14 +09:00
Rinjae c0844e8d36 Swing 알림 체계 도입 - 기존 Q&A 관리자 알림 제거 - Swing Notifier 구현 2026-07-30 19:35:54 +09:00
Rinjae db38b8f6e9 Swing 알림 체계 도입 - 기존 Q&A 관리자 알림 제거 - Swing Notifier 구현 2026-07-30 19:33:11 +09:00
Rinjae 8abe0a8585 GW 클라이언트 해지 처리 로직 추가 - BLOCK/DELETE 처리방식 지원
- APP 승인 시 GW 처리방식 설정 기능 추가 및 상세 표시
- DELETE 요청 처리 중 클라이언트 상태 동기화 로직 개선
- JSP에 GW 처리방식 선택 UI 및 승인 결과 표시 추가
2026-07-30 16:23:09 +09:00
eastargh ca8d46c7b8 사용자관리 최종로그인일시 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-30 14:58:18 +09:00
eastargh 628ab01c13 그리드 정렬 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-30 13:53:47 +09:00
eastargh f10f5ce08e 형상관리 연동시 API 스펙추가
eapim-admin CI / build (push) Has been cancelled
2026-07-30 11:44:19 +09:00
Rinjae 90ba584e4b API 스펙 관리 문구 및 로직 개선
eapim-admin CI / build (push) Has been cancelled
- 게이트웨이 관련 문구 수정 및 그룹 설정 안내 추가
- 비 로그인 사용자 공개 관련 문구 수정
- description 필드 저장/복원 로직 추가
2026-07-30 11:23:38 +09:00
Rinjae e59f64653f Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-29 19:24:38 +09:00
Rinjae 2422c7f01e OpenAPI 스펙 내보내기(UI/DL) 기능 추가
- 내보내기 활성화 여부 설정 키 추가(EXPORT_ENABLED_KEY)
- JSP/JS: 내보내기 버튼 및 다운로드 로직 연동
- YAML/JSON 포맷 저장 기능 구현
2026-07-29 19:24:31 +09:00
eastargh 29025d5f47 ..
eapim-admin CI / build (push) Has been cancelled
2026-07-29 15:21:04 +09:00
eastargh e91b12f2d7 IIM연동시 반복참조필드에 .있으면 이후 값만 저장
eapim-admin CI / build (push) Has been cancelled
2026-07-29 15:04:54 +09:00
eastargh da90a3a002 AUDIT 로그 - 파라미터 검색조건 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-29 10:18:44 +09:00
eastargh a4614ca7fd 로그인시 비밀번호 n회 실패시 계정 잠금처리 2026-07-29 10:14:50 +09:00
Rinjae 2d414dc31b - 삭제된 사용자/법인 포함 승인 요청 삭제 기능 추가
eapim-admin CI / build (push) Has been cancelled
- 삭제 버튼 관련 JSP/JS 수정 및 승인 상태별 버튼 표시 개선
- 초기 패스워드 설정 접미사 적용 및 강제 변경 유도 로직 추가
2026-07-28 15:37:58 +09:00
Rinjae 775d69eb53 - 셀 클릭 시 포커스 이동을 위한 clickedCol 변수 추가
eapim-admin CI / build (push) Has been cancelled
- onCellSelect 이벤트에 클릭한 컬럼 인덱스 저장 로직 구현
- editRow 호출 시 focusField로 클릭한 컬럼 지정
2026-07-28 12:40:41 +09:00
Rinjae 8f4162569d - 초기 패스워드 접미사 설정 로직 추가 및 관련 JSP 수정
eapim-admin CI / build (push) Has been cancelled
- API 허용 IP 리스트 변수명 변경(dapiAllowIpList → apiAllowIpList)
- 초기 패스워드 강제 변경 유도 로직 추가(비밀번호 변경일 초기화)
2026-07-27 15:05:51 +09:00
Rinjae adc5371df8 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-24 17:22:16 +09:00
Rinjae e7297458f6 강제 반려 기능 추가
- 삭제된 승인자 있는 승인 요청 강제 반려 로직 추가
- JSP/JS: 강제 반려 버튼 추가 및 승인 상태별 표시 로직 구현
2026-07-24 17:22:10 +09:00
eastargh c10bfe8326 프라퍼티 수정시 서버 동기화 (자신 포함으로 변경)
eapim-admin CI / build (push) Has been cancelled
2026-07-24 12:57:00 +09:00
eastargh c224f80cdf 개발자포탈 정보 반영 주석처리
eapim-admin CI / build (push) Has been cancelled
2026-07-24 10:35:44 +09:00
eastargh 6c70d9611b 대시보드 상세통계 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-24 09:32:39 +09:00
Rinjae 26679cf649 - 테스트베드 프록시 설정 키 및 기능 추가(DjbApiSpecController)
eapim-admin CI / build (push) Has been cancelled
- 인증 스킴 라벨 판별 로직(schemeAuthLabel) 및 프록시 사용 여부 표시 로직 추가
- JSP/JS: 프록시 여부 관련 UI 및 데이터 연계 처리
2026-07-23 20:26:11 +09:00
Rinjae 406dd34e28 - Mock Server URL 처리 개선: 경로 분리(splitFullUrl) 로직 구현
eapim-admin CI / build (push) Has been cancelled
- 응답유형별 호출 주소 처리 로직 보완(GW/Mock 분리 대응)
- Mock 응답 시 전체 URL 유지 및 Swagger 스펙 반영 logic 수정
2026-07-23 19:30:39 +09:00
Rinjae 59dd28c23b - JSON 레이아웃: ARRAY 타입 스칼라/객체 배열 처리 로직 추가
eapim-admin CI / build (push) Has been cancelled
- JS: 배열 내부 객체(children) 및 타입(Render, Save) 동기화 로직 개선
- 스칼라 배열 예제 추가 및 Swagger 타입 매핑 로직 구현
2026-07-23 19:13:16 +09:00
Rinjae cd93e34840 weblogic.xml 수정
eapim-admin CI / build (push) Has been cancelled
- session-descriptor의 cookie-name 값 변경
2026-07-23 11:06:09 +09:00
eastargh 2fddffe29d IIM 연동시 로그 저장
eapim-admin CI / build (push) Has been cancelled
2026-07-23 10:41:51 +09:00
eastargh 7a73ef370f api통계 집계 쿼리 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-22 16:29:28 +09:00
eastargh c9be6069ae iim연동시 기본값 추가 & 서버연동 제거
eapim-admin CI / build (push) Has been cancelled
2026-07-22 15:52:36 +09:00
Rinjae a13c8f154f Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-22 14:39:05 +09:00
Rinjae 8c98297634 - JSP: OpenAPI Spec 버튼 상태 표시 및 설명 레이어 추가
- Controller: OpenAPI Spec 상태 조회 API(specStatus) 구현
- Service: API 그룹명 기본값 적용 메서드 추가 및 그룹 설명 UI 구성
2026-07-22 14:38:58 +09:00
eastargh f36bfcc642 이전 1시간 데이타만 집계하도록 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-22 11:15:29 +09:00
eastargh 2833944fe4 평균응답시간 오류 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-22 09:44:06 +09:00
eastargh 18110641d6 IIM 레이아웃 연동 djb 커스텀
eapim-admin CI / build (push) Has been cancelled
2026-07-22 09:17:27 +09:00
eastargh 316f787276 API상태변환 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-21 11:30:01 +09:00
eastargh c3c9f7a2da IIM 연동시 마스킹코드 적용
eapim-admin CI / build (push) Has been cancelled
2026-07-20 16:19:12 +09:00
eastargh 95e31befbb Djb 커스텀 원복
eapim-admin CI / build (push) Has been cancelled
2026-07-20 14:47:59 +09:00
eastargh f1f7576b9d 인터페이스 목록에 상태칼럼 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-20 11:46:07 +09:00
eastargh 545e09891b 어뎁터 없을때 상세조회 되도록 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-20 10:53:33 +09:00
eastargh 70c2ac8376 서버 등록시 중복체크
eapim-admin CI / build (push) Has been cancelled
2026-07-16 14:52:36 +09:00
eastargh 21c493cb0e 통계 화면 레이아웃 조정
eapim-admin CI / build (push) Has been cancelled
2026-07-16 11:11:01 +09:00
eastargh b9c6a0da06 파이차트 글자 겹쳐보이는 버그 수정 (legend 오른쪽으로 이동)
eapim-admin CI / build (push) Has been cancelled
2026-07-16 10:14:37 +09:00
eastargh a0725b57a6 sms->kakao로 통일
eapim-admin CI / build (push) Has been cancelled
2026-07-16 09:27:37 +09:00
eastargh 50bfcacf27 전문레이아웃 변환 매핑 - api선택 팝업 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-15 16:52:34 +09:00
eastargh f400f0322f 유량제어 이력 검색조건 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-14 15:05:57 +09:00
eastargh d0cd8118f3 유량제어관련 메뉴 - 사용여부 검색조건 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-14 11:30:55 +09:00
Rinjae 07e65c055b - JSP: OpenAPI Spec 버튼 상태 수정 - NEW → DETAIL로 변경
eapim-admin CI / build (push) Has been cancelled
- Service: BIGDECIMAL 매핑 추가 - 반환 유형 'number'
- Monitoring: Properties 초기화 및 null 체크 로직 추가
- JS: 샘플 응답 상태 시 안내 메시지 추가
2026-07-14 11:11:24 +09:00
eastargh af8a9cca51 프라퍼티 정리
eapim-admin CI / build (push) Has been cancelled
2026-07-13 17:47:35 +09:00
eastargh 39a09d0f5d 유량제어 url 특수문자 escape 2026-07-13 17:47:08 +09:00
Rinjae 5b940717c4 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-13 15:30:13 +09:00
Rinjae 8ccf52fd66 - JSP: OpenAPI Spec 모달 처리 개선 - 동적 크기 조절 및 스타일 추가
- Service: GET/DELETE 요청 바디 항목→쿼리 파라미터 매핑 로직 추가
- JS: 요청 정보 진입 시 탭 자동 전환 로직(firstReqTabWithData) 구현
2026-07-13 15:30:08 +09:00
eastargh dedc58a07f 유량제어 추가임계치가 null or 단위선택 않하면 0으로 설정
eapim-admin CI / build (push) Has been cancelled
2026-07-13 15:24:30 +09:00
Rinjae 4530af9dec Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
# Conflicts:
#	build.gradle
2026-07-13 14:28:49 +09:00
Rinjae cbf3066a56 API 그룹 팝업 JSP 파일 추가 - 예제 코드 및 UI 구조 작성
Summernote 에디터 스타일 CSS 추가 - 텍스트 및 컴포넌트 스타일 정의
OpenAPI POC JS 파일 추가 - API 스펙 빌더 및 스텝별 렌더 구현
2026-07-13 14:25:09 +09:00
eastargh 160e28444a eapim-admin-djb 제거
eapim-admin CI / build (push) Has been cancelled
2026-07-13 13:20:22 +09:00
Rinjae d7dcf9a81c Q&A 문의 공개범위 기능 추가
eapim-admin CI / build (push) Has been cancelled
- 공개범위 설정 기능 개발(ALL/ORG/PRIVATE) - 게시물/댓글 정교화
- 요청 및 DB 처리 확장 - UI, DTO, Service 계층 업데이트
- 감사 로그 및 조회수 처리 로직 추가
2026-07-13 11:32:34 +09:00
Rinjae acfe6b2d03 weblogic.xml 업데이트
eapim-admin CI / build (push) Has been cancelled
- XML 태그 단순화 및 불필요한 속성 제거
- session-descriptor에 cookie-name 추가
2026-07-10 17:48:34 +09:00
eastargh 481e82fec7 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-07-10 11:37:43 +09:00
eastargh 1b382f5859 ums 설정 추가 2026-07-10 11:37:33 +09:00
Rinjae 608cb4188e "Jenkinsfile 개선:
eapim-admin CI / build (push) Has been cancelled
- Tomcat 배포 구조 → WebLogic 기반으로 변경
- stage별 agent 명시 및 불필요한 환경변수 제거
- Webhook 알림 로직 추가"
2026-07-09 16:20:54 +09:00
eastargh 534947637d qna 상세 회원이름 표시
eapim-admin CI / build (push) Has been cancelled
2026-07-09 14:55:07 +09:00
eastargh 153c68c6ab 사이트맵 위치 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-09 11:47:23 +09:00
eastargh 80ef41f4fc QnA 문의검토중 상태 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-09 09:26:24 +09:00
Rinjae 8aebabe005 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-08 18:31:45 +09:00
Rinjae 6d33b69bed 인증 서버 클라이언트 차단 API 추가:
- `ClientBlockApiController` 신규 생성 및 차단 로직 구현
- `ClientManService`에 클라이언트 차단 메서드 추가
- 승인 요청 삭제 로직 개발 환경 제한 문구 수정
2026-07-08 18:31:39 +09:00
eastargh 49ebdce4c3 전문레이아웃 그리드 항목명 순서 변경(김태호이사 요청)
eapim-admin CI / build (push) Has been cancelled
2026-07-08 11:13:43 +09:00
eastargh 979e43e382 - 포함된 인터페이스명 복제 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-08 10:37:03 +09:00
eastargh 53b075967c 스타일시트 정리
eapim-admin CI / build (push) Has been cancelled
2026-07-07 16:53:22 +09:00
eastargh 724117860b 탑메뉴 오버레이 높이 조정
eapim-admin CI / build (push) Has been cancelled
2026-07-07 14:47:02 +09:00
eastargh 0ee52392aa 조회기간 기본설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-07 14:09:58 +09:00
eastargh f0af06da88 사이트맵
eapim-admin CI / build (push) Has been cancelled
2026-07-07 13:38:44 +09:00
eastargh b16d03517a 사이트맵 신규 개발
eapim-admin CI / build (push) Has been cancelled
2026-07-07 13:19:33 +09:00
Rinjae edf024f0a3 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-07 10:35:26 +09:00
Rinjae a1a1f27498 승인 관리: 승인 상태별 행 배경색 강조 로직 추가 2026-07-07 10:34:41 +09:00
eastargh c62f395983 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-07-06 16:34:55 +09:00
eastargh c3955cefd8 Scope관리 Api Reload 버그 수정 2026-07-06 16:26:40 +09:00
Rinjae 5c378eac0d Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-06 15:33:39 +09:00
Rinjae e51cdbc42a 감사로그 및 삭제 프로세스 개선:
- 감사 사유 입력 기능 추가 및 UI 수정
- 사용/법인 삭제 시 '완전삭제' 여부 확인 및 로직 적용
- 감사로그에 사유(message) 저장 필드 추가
2026-07-06 15:33:32 +09:00
eastargh 142d402bad 레이아웃관리 - Array 아이템유형 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-06 11:25:16 +09:00
eastargh f23cb02b1c 웹훅 발송 내역 2026-07-03 17:15:57 +09:00
eastargh eca120812c 관리자포탈 비밀번호 규칙 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-02 16:34:45 +09:00
eastargh 71b0fc7009 오류변환만 있는 경우, 전문변환 ON으로 설정 2026-07-02 16:34:20 +09:00
eastargh 7b12ef2fb5 웹훅 발송 내역 신규
eapim-admin CI / build (push) Has been cancelled
2026-07-02 14:46:02 +09:00
eastargh 292e27e66e 에러메세지 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-02 13:28:31 +09:00
eastargh 61d710dd8c 초기비밀번호로 로그인시 비밀번호변경 모달 오픈
eapim-admin CI / build (push) Has been cancelled
2026-07-01 14:23:55 +09:00
eastargh 8494758fcd 신규 비밀번호 및 초기화시 사번+@! 로 설정
eapim-admin CI / build (push) Has been cancelled
2026-07-01 10:27:18 +09:00
eastargh f2b25d2775 API명 검색조건 sql에 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-01 10:06:56 +09:00
Rinjae 18489e2440 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-30 19:40:30 +09:00
Rinjae 681e4bd014 법인 정보 삭제 시 민감 데이터 초기화 메서드 적용:
- `clearSensitiveDataOnRemoval` 메서드 추가/적용
2026-06-30 19:40:23 +09:00
eastargh 27abe51b7b 로그인 2-factor 인증
eapim-admin CI / build (push) Has been cancelled
2026-06-30 15:49:39 +09:00
eastargh 55ef71c5e7 화면 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-30 14:19:01 +09:00
Rinjae 235f4b67b3 Jenkinsfile 및 Gradle 제거/최적화:
eapim-admin CI / build (push) Has been cancelled
- EXT_LIB_DIR 환경변수 및 관련 빌드 파라미터 제거
- build.gradle: 불필요 코드/설정 제거 및 의존성 최적화
2026-06-30 11:29:02 +09:00
Rinjae 89c4b89a71 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-30 09:28:03 +09:00
Rinjae 7193070ebd Jenkinsfile 수정:
- EXT_LIB_DIR 환경변수 추가
- Gradle 빌드에 extLibDir 파라미터 적용
2026-06-30 09:27:58 +09:00
rinjae 49616cdd84 build.gradle 업데이트
eapim-admin CI / build (push) Has been cancelled
2026-06-30 09:27:38 +09:00
Rinjae d1683cc319 전화번호 정규화 적용: 숫자 추출 유틸(PhoneNumberUtil) 활용
eapim-admin CI / build (push) Has been cancelled
2026-06-30 09:05:44 +09:00
Rinjae b5ba2c2bee Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-29 18:39:25 +09:00
Rinjae baa528a628 전화번호 정규화 유틸 추가 및 적용:
- PhoneNumberUtil 도입: 한국번호 정규화/숫자 추출 지원
- 유틸 적용: 모든 저장 및 조회에 정규형 강제
- 초대 매칭 오류 수정: 휴대폰 형식 차이 대응
2026-06-29 18:26:15 +09:00
eastargh f49814b2a1 개발서버 로그설정
eapim-admin CI / build (push) Has been cancelled
2026-06-29 16:09:58 +09:00
eastargh 0ea73aa66f 공용톰켓으로 이동 2026-06-29 16:01:14 +09:00
eastargh ed44183de3 배치잡 Reload시 data도 reload되도록 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-29 13:32:08 +09:00
eastargh aee02cc2a6 검색조건 답변종료 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-29 13:12:06 +09:00
eastargh 99b4470ffe CLUSTERED에서도 실행되도록 변경
eapim-admin CI / build (push) Has been cancelled
2026-06-29 10:57:26 +09:00
eastargh d9201eb603 담당자연락처 암복호화
eapim-admin CI / build (push) Has been cancelled
2026-06-26 14:08:22 +09:00
eastargh 185a23d748 관리자 로그인 비밀번호 damo 암호화 적용
eapim-admin CI / build (push) Has been cancelled
2026-06-26 11:24:12 +09:00
eastargh 99a440094d 관리자 로그인 비밀번호 암호화 Damo 단방향 적용
eapim-admin CI / build (push) Has been cancelled
2026-06-26 11:12:21 +09:00
eastargh 37380f2975 유형 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-26 10:35:05 +09:00
eastargh 908b2ebcc8 IIM연동 DJB 커스텀
eapim-admin CI / build (push) Has been cancelled
2026-06-25 11:42:09 +09:00
Rinjae 25a8e13752 앱/사용자 승인 및 거절 시 SMS 통지 로직 추가
eapim-admin CI / build (push) Has been cancelled
- 승인/거절 템플릿 변수 매핑 (APP_NAME, REASON 등)
- 법인 관리자 승인/거절 통지 로직 개선
2026-06-24 19:43:30 +09:00
eastargh 7928a0c27b 기관관리 - 리버스 프록시 경로 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-24 16:38:00 +09:00
Rinjae 65ba6c1115 OBP 파트너코드 조회 기능 삭제
eapim-admin CI / build (push) Has been cancelled
- ObpPartnerCodeService 및 관련 컨트롤러 메서드 제거
- JSP에서 OBP 조회 버튼 및 관련 스크립트 제거
2026-06-24 15:44:40 +09:00
Rinjae 2dcf524cd6 XSS 문자 변환 제외 파라미터 추가 - subjectTemplate, smsTemplate 등
eapim-admin CI / build (push) Has been cancelled
2026-06-24 14:46:44 +09:00
Rinjae bb991cea81 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-23 19:27:47 +09:00
Rinjae 702a9a899b - 약관 유형에 개인정보수집동의서(PRIVACY_COLLECT) 항목 추가
- 관리화면 약관 유형 표시 시 '(사용안함)' 라벨 추가 처리
- 약관 유형 라벨 변환 로직 PortalTermsManService로 이동 및 적용
2026-06-23 19:27:37 +09:00
eastargh 7520b96ed5 UMS 발송
eapim-admin CI / build (push) Has been cancelled
2026-06-23 17:46:19 +09:00
eastargh 7935e69871 inbound adapter 변경시 inboundHttpMethod값 누락 버그 수정 2026-06-23 17:41:37 +09:00
eastargh 4cf6dc4da0 UMS 발송
eapim-admin CI / build (push) Has been cancelled
2026-06-23 14:50:03 +09:00
Rinjae a5d55f2647 WAR 파일 해시 파일 확장자 변경 및 MD5SUM 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-23 13:58:02 +09:00
eastargh af8b925e65 서버 기동 인자에 따라 암복호화 실행
eapim-admin CI / build (push) Has been cancelled
2026-06-23 10:41:45 +09:00
eastargh 62ae2d93ff converter 직접 생성하여 사용 2026-06-23 10:35:39 +09:00
eastargh 124ecabccf 로그조회 업무데이타 복호화 처리 2026-06-23 10:21:10 +09:00
Rinjae c521835633 - 사용자 검색 시 암호화 필드 처리 로직 개선: 부분검색 제거 및 정확일치 검색 추가.
eapim-admin CI / build (push) Has been cancelled
- UI에 암호화 필드 검색 제한 안내 추가.
- 개인정보 마스킹 규칙 확대: 이름/ID/전화번호 모두 적용.
2026-06-22 19:21:08 +09:00
Rinjae fe201b88cd Merge Gitea master into z-eactive master
eapim-admin CI / build (push) Has been cancelled
2026-06-22 17:24:22 +09:00
Rinjae 9f81ad697d Merge remote-tracking branch 'origin/master' 2026-06-22 17:03:41 +09:00
Rinjae ca94f2e70a 휴대폰번호 중복 사용자 조회 기능 추가
- 중복 사용자 필터링 및 목록 조회 기능 구현
- 중복 사용자 알림 배너 및 상세 조회 UI 개선
- 중복 허용 여부 설정 동기화 및 관련 서비스 추가
2026-06-22 17:03:33 +09:00
eastargh 97937a8a35 UMS 발송 2026-06-22 16:56:20 +09:00
eastargh a8e124db5d Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-06-22 16:31:39 +09:00
eastargh a514b9d7a9 UMS 정리 2026-06-22 16:31:27 +09:00
Rinjae 2ac61636a0 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-22 16:29:58 +09:00
Rinjae 399bd9c58d Damo 적용 준비 코드
- KjbNotRnnoJpaAttributeConverter 삭제 및 PersonalDataEncryptConverter 추가
- damo-manager 지원을 위한 JSP 추가 및 레거시 암호화 마이그레이션 컨트롤러 구현
- 기존 엔티티 암호화 로직 변경 및 build.gradle에 damo-manager 의존성 추가
2026-06-22 16:29:45 +09:00
eastargh b8beeb7c3e Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-06-22 13:40:24 +09:00
eastargh 0bb00b4fb9 프라퍼티 암호화 기능 추가 2026-06-22 13:40:13 +09:00
Rinjae 655c35b1e1 - 메시지 발송 내역 화면 추가: 조회/상세/상태변경(PENDING→FAILED)
eapim-admin CI / build (push) Has been cancelled
- 본문·수신자 마스킹 유틸(MessagePatternMaskingUtils) 개발
- 안전 HTML 생성 마스킹/검증 테스트 및 JSP 바인딩
2026-06-19 19:40:15 +09:00
Rinjae efd1c748ed Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-19 17:17:50 +09:00
Rinjae 64a9c7408e - 사용자 조회 시 방어적 로직 추가: 삭제된 사용자 처리
- PortalFaq/PortalInquiry 컨벤션 동일 적용
2026-06-19 17:17:45 +09:00
eastargh 8f9411f990 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-06-19 13:43:52 +09:00
eastargh ed1f55c534 유량제어 서버버킷 조회 port 변경 2026-06-19 13:43:36 +09:00
Rinjae d1392821ef Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-19 11:23:13 +09:00
Rinjae 6542b0c0f8 - 메인 화면 serviceType 설정 수정: session/localStorage 분리
- emergency.jsp JSTL core 태그 추가
2026-06-19 11:23:07 +09:00
Rinjae-gf63 f7de1b6885 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-18 17:58:26 +09:00
Rinjae-gf63 3e32220061 포탈 공지사항 관리 - 콤보 로드 후 그리드 생성으로 경합 방지 2026-06-18 17:57:37 +09:00
Rinjae 10db8129eb 마스킹 표준화 적용 (eapim-admin/portal/JS)
eapim-admin CI / build (push) Has been cancelled
- admin: elink-online-core 표준 위임(e메일/이름/전화/IP)으로 규칙 일원화
- portal/JS: 표준 로직 직접 구현 + 기존 owner 비마스킹 유지
- 호출처 무수정/인라인 중복 제거/테스트 통과
2026-06-18 17:56:32 +09:00
Rinjae e34829437d 피드백/개선요청 삭제 로직 수정
eapim-admin CI / build (push) Has been cancelled
- 삭제 버튼 UI 개선 및 상세 페이지 삭제 기능 추가
- audit log에 DELETE 이벤트 추가
2026-06-18 16:45:28 +09:00
Rinjae-gf63 ef3f0e802a - 사용자 ID 검증 로직 추가 (PortalUserService)
eapim-admin CI / build (push) Has been cancelled
- 사업제휴 관리 > 삭제/다중 삭제 기능 추가 및 화면 수정
- 피드백/개선 요청으로 명칭 변경 (사업제휴 관리)
2026-06-18 16:31:22 +09:00
rinjae b741a7e563 Jenkinsfile 업데이트
eapim-admin CI / build (push) Has been cancelled
2026-06-18 12:17:49 +09:00
rinjae e8069ef371 Jenkinsfile 업데이트
eapim-admin CI / build (push) Has been cancelled
2026-06-18 12:10:31 +09:00
rinjae c0e218451e Jenkinsfile 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-18 11:42:30 +09:00
eastargh af63b1a256 배치잡 기본에서 클러스터로 수정시 next fire time =null인 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-18 11:05:26 +09:00
eastargh 02072df973 api사용현황 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-18 09:17:58 +09:00
eastargh bc478ad4ed 배치잡 수정시 clustered에 무조건 추가되는 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-17 10:58:00 +09:00
Rinjae 161a68a0a2 - Tomcat readiness probe 타임아웃 180s → 300s 연장
eapim-admin CI / build (push) Has been cancelled
- readiness probe URL /monitoring/login.do → /monitoring/loginForm.do 수정
- readiness 실패 시 로그 출력 내용 및 포트 점검 추가
2026-06-16 18:46:28 +09:00
Rinjae-gf63 3b59f7acd7 Merge branch 'master' of ssh://172.30.1.50:2222/djb-eapim/eapim-admin
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:44:16 +09:00
Rinjae-gf63 1e29d1d721 Merge remote-tracking branch 'origin/master' into master
Resolve conflict in PortalNoticeManService.java import block:
union both sides — keep Arrays (HEAD) and LocalDateTime/ArrayList/Collections (origin/master),
all of which are used in the merged body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:41:57 +09:00
Rinjae-gf63 9342b6484c 포탈 공지사항 삭제 기능 추가 / ADM -> APP 2026-06-16 16:37:56 +09:00
eastargh dc3842aa9b 배치잡 CLUSTERED 기능 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:33:55 +09:00
eastargh a440ac842d API상태 변화 감지 (점검 게시판 적용)
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:05:44 +09:00
Rinjae df5bbb162a ci 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-16 15:56:46 +09:00
Rinjae 3eec267769 Merge commit '58fe176715b2027f783a2dae98e1583beaf34c8c'
eapim-admin CI / build (push) Has been cancelled
2026-06-16 15:48:41 +09:00
Rinjae 6ab7e1fe16 ci 수정 2026-06-16 15:48:30 +09:00
eastargh 58fe176715 조회시 정렬순서 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-16 15:05:48 +09:00
Rinjae 6b6e2863cd 공지사항 관리 - 장애 점검 등록 UI 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-16 14:55:39 +09:00
eastargh cfafccc34d CA 인증타입 제거
eapim-admin CI / build (push) Has been cancelled
2026-06-16 14:32:35 +09:00
eastargh fe98ac45c4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-06-16 14:26:21 +09:00
eastargh 5f9de93d35 인증타입 CA 제거 2026-06-16 14:19:27 +09:00
Rinjae 48dfcf7714 Merge branch 'feats/ci-test'
eapim-admin CI / build (push) Has been cancelled
2026-06-16 13:22:38 +09:00
Rinjae 6e3906878a Merge commit 'fafe7e45cb35b7475cdc54f6b390b6c4845777d5' 2026-06-16 13:21:55 +09:00
eastargh fafe7e45cb 서버명 표시 2026-06-16 13:19:10 +09:00
eastargh eee7f46aa7 ums 관련 수정 2026-06-16 11:10:36 +09:00
eastargh b5eb63448a 레이아웃 기본 데이타타입 Boolean 추가 2026-06-15 17:52:34 +09:00
eastargh 54291e5cbb ums 관련 정리 2026-06-15 13:16:23 +09:00
Rinjae a0743465ab - generic package 업로드 단계 제거
eapim-admin CI / build (push) Has been cancelled
- 인증 정리 전까지 artifact만 업로드 유지
2026-06-12 20:51:36 +09:00
Rinjae d3e05256b7 - package upload 인증 헤더 방식 변경
- basic oauth2 user → Authorization token 헤더
2026-06-12 20:28:17 +09:00
Rinjae 4d5572f880 로그 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-06-12 19:43:33 +09:00
Rinjae e21014f992 - gitea actions 워크플로우 추가
- 7개 sibling clone 후 monitoring.war 빌드
- systemd user unit으로 tomcat 라이프사이클 분리
- /monitoring/login.do 로 readiness probe (120s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 19:39:01 +09:00
Rinjae 43fe63964f Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManController.java
2026-06-12 16:08:41 +09:00
eastargh ac0d081a9a 인터페이스 아웃바운드 path에 {} 포함시 variableUrlRequest로 설정 2026-06-12 10:18:39 +09:00
eastargh 164ab92919 BAP 서버 목록조회 쿼리에 rownum=1 제거 2026-06-12 09:27:08 +09:00
eastargh a6e7d1adb3 아웃바운드 URL restoption type 판단시 {}안에 .도 포함되도록 변경 2026-06-11 11:38:38 +09:00
Rinjae e51331986d Merge remote-tracking branch 'origin/master' 2026-06-09 19:08:38 +09:00
eastargh 1aa40f2c61 UMS관련 정리 2026-06-09 15:39:05 +09:00
eastargh c8caeeda8c 유량제어 모니터링 2026-06-09 13:04:32 +09:00
eastargh 06ad1230b2 유량제어 모니터링 2026-06-09 11:34:10 +09:00
eastargh 8109f91aa2 유량제어 현황 모니터링 기능 추가 2026-06-09 09:51:05 +09:00
eastargh 62ccebd773 Q&A 7일후 자동 종료 배치 2026-06-08 17:19:04 +09:00
eastargh ec8d6bfd7b Q&A 게시판 댓글 작성 기능 2026-06-05 17:41:37 +09:00
eastargh 480d7dac5a 앱승인시 예상완료일 항목 추가 & 승인요청 상세 조회시 "진행중"으로 상태변경 2026-06-05 15:10:06 +09:00
eastargh feb4f845a8 팝업 두번째 이후 오픈시 선택버그 수정 2026-06-05 10:11:21 +09:00
eastargh 7857f5b45c 자동 Log-off 기능 추가 2026-06-04 18:11:31 +09:00
eastargh 68c8e18e8d 스윙채 & webhook 발송 수정 2026-06-04 17:38:52 +09:00
eastargh 35e5564252 webhook 발송 2026-06-02 09:43:03 +09:00
eastargh 0d7d006f92 api 상태 변화 감지 & 웹훅 발송 2026-06-01 17:37:06 +09:00
eastargh 737b493f7c API사용현황 통계 2026-05-28 16:02:52 +09:00
eastargh af0a878a39 DB스키마변경 BAPADM->BAPAPP 2026-05-28 11:32:31 +09:00
eastargh b639e29039 DB스키마 변경 : AGWADM->AGWAPP, EMSADM->EMSAPP 2026-05-28 10:53:33 +09:00
eastargh 350297aaa2 클라이언트 유량제어 테이블 수정 2026-05-27 16:54:19 +09:00
Rinjae 23ba7993c3 Merge remote-tracking branch 'origin/master' 2026-05-21 15:16:41 +09:00
eastargh 7677e3b26e 거래로그 집계시 공통코드 테이블 변경 2026-05-21 15:10:24 +09:00
Rinjae 90c43ea7b0 - K8sUtil 파일 삭제
- 승인관리 완전삭제 기능 추가
- DJBank로 명칭 업데이트
2026-05-21 09:51:39 +09:00
eastargh 191857811f 공지사항 첨부파일 안씀 2026-05-20 11:50:40 +09:00
eastargh 88ffc1cc28 API 사용현황 신규개발 2026-05-20 11:45:00 +09:00
eastargh d3bd4acb62 요청/응답구분 선택 버그 수정 2026-05-19 10:57:48 +09:00
eastargh 1f7166502f API통계 업무오류 제외 2026-05-15 14:53:52 +09:00
eastargh 91f36605e8 스케쥴러 실행시간 format 변경 2026-05-14 11:22:47 +09:00
eastargh 0ebd31e256 API 통계 화면 리뉴얼 2026-05-14 11:21:14 +09:00
eastargh 45c6edbbeb 거래로그>시간별 수동집계 기능 추가 2026-05-14 11:20:33 +09:00
eastargh 335c7ec53a 거래로그 > 시간별 집계하는 배치잡 2026-05-14 11:19:55 +09:00
eastargh 0b341b85f1 평균응답속도 계산시 분모를 성공건수에서 총건수로 변경 2026-05-14 11:17:55 +09:00
eastargh e9ac62d3ed 인터페이스 상세 저장 버그 수정 2026-05-12 10:37:32 +09:00
eastargh 133c05f1d7 알림 발송 관련 배치 리네이밍 2026-05-08 17:24:17 +09:00
eastargh 4a1a4be54d 검색조건 유지 2026-05-08 10:37:57 +09:00
curry772 dbd36f28d4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-05-07 15:39:27 +09:00
curry772 4ee50be000 Merge branch 'feature/crypto-module' into master 2026-05-07 15:39:13 +09:00
eastargh 9d55db3281 게시판관리 - 게시유형, 고정여부 추가 2026-05-07 09:50:49 +09:00
eastargh 9a5d5e4815 클라이언트 유량제어 신규생성 2026-05-07 09:39:59 +09:00
409 changed files with 28738 additions and 6229 deletions
+140
View File
@@ -0,0 +1,140 @@
name: eapim-admin CI
on:
push:
branches:
- master
- develop
- stage
- feats/ci-test
jobs:
build:
runs-on: [self-hosted, linux, eapim-admin]
env:
JAVA_HOME: /apps/opts/jdk8
JAVA_HOME_TOMCAT: /apps/opts/jdk17
CATALINA_BASE: /prod/eapim/adminportal
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
CATALINA_PID: /prod/eapim/adminportal/temp/catalina.pid
DEPLOY_HTTP_PORT: "39120"
defaults:
run:
working-directory: eapim-admin
steps:
- name: Checkout eapim-admin (trigger SHA)
uses: http://172.30.1.50:3000/actions/checkout@v4
with:
path: eapim-admin
- name: Checkout sibling modules (master)
working-directory: ${{ github.workspace }}
run: |
set -e
mkdir -p eapim-online
# eapim-online/* — 5 modules referenced from settings.gradle
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
rm -rf "eapim-online/$REPO"
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
"eapim-online/$REPO"
done
# standalone siblings
for REPO in elink-portal-common eapim-admin-djb; do
rm -rf "$REPO"
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
"$REPO"
done
- name: Verify toolchain
run: |
java -version
gradle --version
- name: Gradle build (no tests)
run: gradle clean build -x test --no-daemon
- name: Upload WAR artifact
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
with:
name: eapim-admin-war-${{ github.sha }}
path: eapim-admin/build/libs/eapim-admin.war
if-no-files-found: error
- name: Stop tomcat (pre-clean)
working-directory: ${{ github.workspace }}
run: |
# 1) systemd 관리 인스턴스 정지 (이번 워크플로가 띄운 것)
systemctl --user stop eapim-admin 2>/dev/null || true
# 2) systemd 외부 detached 인스턴스 정리
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null; then
JAVA_HOME=$JAVA_HOME_TOMCAT "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
for i in $(seq 1 30); do
[ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null || break
sleep 1
done
fi
rm -f "$CATALINA_PID"
- name: Deploy war as /monitoring
working-directory: ${{ github.workspace }}
run: |
DEPLOY_DIR="$CATALINA_BASE/webapps"
rm -rf "$DEPLOY_DIR/monitoring" "$DEPLOY_DIR/monitoring.war"
cp eapim-admin/build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
ls -la "$DEPLOY_DIR"
- name: Start tomcat and readiness probe (timeout 300s)
working-directory: ${{ github.workspace }}
run: |
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
BEFORE=0
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
# systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음
systemctl --user start eapim-admin
# /loginForm.do 는 GET 매핑(MainController#loginForm) → tomcat·webapp 양쪽 살아있으면 200
# /login.do 는 POST 처리 — GET 으로 probe 시 405/잘못된 응답 가능성 있어 회피
PROBE_URL="http://localhost:$DEPLOY_HTTP_PORT/monitoring/loginForm.do"
DEADLINE=$(($(date +%s) + 300))
STATUS=000
while [ $(date +%s) -lt $DEADLINE ]; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
"$PROBE_URL" 2>/dev/null || echo "000")
case "$STATUS" in
200|302|401)
echo "Readiness OK ($PROBE_URL HTTP $STATUS)"
break
;;
esac
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
echo "::error::Startup error detected in log"
tail -c +$((BEFORE+1)) "$LOG" | tail -80
exit 1
fi
sleep 2
done
case "$STATUS" in
200|302|401) ;;
*)
echo "::error::Readiness probe failed within 300s (last HTTP status: $STATUS)"
echo "--- last 300 lines of catalina log ---"
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -300
echo "--- systemd unit status ---"
systemctl --user status eapim-admin --no-pager || true
echo "--- listening ports ---"
ss -tlnp 2>/dev/null | grep -E ":$DEPLOY_HTTP_PORT\b" || echo "(port $DEPLOY_HTTP_PORT 미점유)"
exit 1
;;
esac
echo "--- key boot log lines ---"
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
+3
View File
@@ -246,3 +246,6 @@ eapim-admin-kjb/
*.report.md *.report.md
*.claude.md *.claude.md
tmpclaude-*-cwd tmpclaude-*-cwd
logs
rinjae_
tomcat-base
+36 -28
View File
@@ -10,7 +10,7 @@ AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할
- **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다. - **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다.
- **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다. - **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다.
- **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다. - **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다.
- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다. - **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `./gradlew compileJava` 를 먼저 실행하여 코드를 생성해야 합니다.
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다. - **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
## 프로젝트 개요 ## 프로젝트 개요
@@ -39,50 +39,48 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
### 빌드 명령어 ### 빌드 명령어
**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다. 프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조).
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
```bash ```bash
# 표준 빌드 # 표준 빌드
kjb-gradle.sh build ./gradlew build
# Weblogic 배포용 빌드 (테스트 제외) # Weblogic 배포용 빌드 (테스트 제외)
kjb-gradle.sh build -x test -Pprofile=weblogic ./gradlew build -x test -Pprofile=weblogic
# WAR 파일 빌드 # WAR 파일 빌드
kjb-gradle.sh war ./gradlew war
# 클린 빌드 # 클린 빌드
kjb-gradle.sh clean build ./gradlew clean build
``` ```
### 테스트 실행 ### 테스트 실행
```bash ```bash
# 모든 테스트 실행 # 모든 테스트 실행
kjb-gradle.sh test ./gradlew test
# 특정 테스트 클래스 실행 # 특정 테스트 클래스 실행
kjb-gradle.sh test --tests "com.example.ClassName" ./gradlew test --tests "com.example.ClassName"
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용) # 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
kjb-gradle.sh test --tests "com.eactive.eai.rms.*" ./gradlew test --tests "com.eactive.eai.rms.*"
``` ```
### 개발 태스크 ### 개발 태스크
```bash ```bash
# 패키징 없이 클래스만 컴파일 # 패키징 없이 클래스만 컴파일
kjb-gradle.sh classes ./gradlew classes
# QueryDSL Q-classes 및 기타 애노테이션 생성 # QueryDSL Q-classes 및 기타 애노테이션 생성
kjb-gradle.sh compileJava ./gradlew compileJava
# 의존성 트리 보기 # 의존성 트리 보기
kjb-gradle.sh dependencies ./gradlew dependencies
# 사용 가능한 모든 태스크 목록 # 사용 가능한 모든 태스크 목록
kjb-gradle.sh tasks --all ./gradlew tasks --all
``` ```
## 로컬 개발 빠른 시작 ## 로컬 개발 빠른 시작
@@ -92,26 +90,26 @@ kjb-gradle.sh tasks --all
1. **IDE 설정 파일 생성 (최초 1회)** 1. **IDE 설정 파일 생성 (최초 1회)**
```bash ```bash
# Eclipse 사용 시 (기본) # Eclipse 사용 시 (기본)
kjb-gradle.sh eclipse ./gradlew eclipse
# IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요) # IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요)
# 1. build.gradle을 build.gradle.eclipse로 백업 # 1. build.gradle을 build.gradle.eclipse로 백업
# 2. build.gradle.intellij를 build.gradle로 복사 # 2. build.gradle.intellij를 build.gradle로 복사
# 3. kjb-gradle.sh idea 실행 # 3. ./gradlew idea 실행
``` ```
2. **QueryDSL 등 소스 코드 생성** 2. **QueryDSL 등 소스 코드 생성**
```bash ```bash
kjb-gradle.sh compileJava ./gradlew compileJava
``` ```
3. **전체 빌드 및 테스트 실행** 3. **전체 빌드 및 테스트 실행**
```bash ```bash
kjb-gradle.sh build ./gradlew build
``` ```
4. **IDE에서 프로젝트 열기** 4. **IDE에서 프로젝트 열기**
- Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다. - Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다. - IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
5. **실행 구성(Run Configuration) 설정** 5. **실행 구성(Run Configuration) 설정**
@@ -162,14 +160,14 @@ eapim-admin (root project)
#### Q-Class 생성 위치 #### Q-Class 생성 위치
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다. Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성 - `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다 - 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
#### 생성된 코드 작업하기 #### 생성된 코드 작업하기
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다 - **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다 - **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다
- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다 - **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다
#### IDE별 빌드 설정 파일 #### IDE별 빌드 설정 파일
@@ -255,12 +253,21 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다: WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
```bash ```bash
kjb-gradle.sh build -x test -Pprofile=weblogic ./gradlew build -x test -Pprofile=weblogic
``` ```
## 기술 스택 ## 기술 스택
- **Java 8**: Gradle toolchain을 통해 강제 - **Java 8**: Gradle toolchain을 통해 강제 (`JavaLanguageVersion.of(8)`). 프로젝트의 `gradle.properties` 에는 JDK 경로를 기입하지 않습니다 — 개인 환경별로 `.envrc`(direnv) 또는 셸 rc 파일에서 `JAVA_HOME` 을 JDK 8 위치로 export 하세요. Gradle 8 의 toolchain auto-detect 가 `JAVA_HOME` 도 후보 경로로 인식합니다.
```bash
# 예: 프로젝트 루트의 .envrc (Zulu 8 / macOS Apple Silicon)
JAVA_HOME=${HOME}/opts/jdks/zulu8.94.0.17-ca-jdk8.0.492-macosx_aarch64
GRADLE_USER_HOME=${HOME}/eactive/djb-eapim/gradle-home
export PATH=${JAVA_HOME}/bin:${PATH}
```
Gradle 표준 위치(`/Library/Java/JavaVirtualMachines/...`, Homebrew, SDKMAN 등)에 설치한 경우는 별도 설정 없이도 auto-detect 됩니다.
- **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA) - **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA)
- **Spring Data JPA 2.5.2**: 리포지토리 추상화 - **Spring Data JPA 2.5.2**: 리포지토리 추상화
- **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM) - **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM)
@@ -271,9 +278,10 @@ kjb-gradle.sh build -x test -Pprofile=weblogic
- **Jackson 2.13.1**: JSON 처리 - **Jackson 2.13.1**: JSON 처리
- **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께) - **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께)
- **AWS SDK 2.20.142**: S3 연동 - **AWS SDK 2.20.142**: S3 연동
- **Kubernetes Client 18.0.1**: K8s 관리
- **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑 - **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑
> Kubernetes Client 의존성은 JDK 8 호환 문제로 제거됨(`io.kubernetes:client-java:18.0.1` 은 JDK 11+ 필요). K8s 모드 기능이 필요하면 `≤13.0.2` 로 다운그레이드 후 재도입 검토.
## 프로젝트 규칙 ## 프로젝트 규칙
### 패키지 구조 ### 패키지 구조
@@ -376,7 +384,7 @@ com.eactive.eai
### IntelliJ IDEA (권장) ### IntelliJ IDEA (권장)
```bash ```bash
kjb-gradle.sh idea ./gradlew idea
``` ```
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다. 또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
@@ -395,7 +403,7 @@ cp build.gradle build.gradle.intellij
cp build.gradle.eclipse build.gradle cp build.gradle.eclipse build.gradle
# 3. Eclipse 프로젝트 생성 # 3. Eclipse 프로젝트 생성
kjb-gradle.sh eclipse ./gradlew eclipse
``` ```
Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다: Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다:
Vendored
+189
View File
@@ -0,0 +1,189 @@
pipeline {
agent none
options {
timestamps()
disableConcurrentBuilds()
skipDefaultCheckout() // stage별 agent의 자동 SCM checkout 방지 (Deploy 노드는 git 접근 불필요, unstash로만 WAR 수신)
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
}
environment {
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
WEBHOOK_URL = 'http://172.30.1.50:18000/api/hooks/01ba51b01d3c4c98ae8f3183a23a84ed713197857d024b5da4f303458195eb32'
}
stages {
stage('Build (djb-vm)') {
agent { label 'djb-vm' }
environment {
JAVA_HOME = '/apps/opts/jdk8'
GRADLE_HOME = '/apps/opts/gradle-8.7'
GRADLE_USER_HOME = '/apps/opts/gradle-home'
NODE_HOME = '/apps/opts/node-v24'
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Checkout dependencies') {
steps {
sh '''
set -eu
cd "$WORKSPACE/.."
mkdir -p eapim-online
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
TARGET="eapim-online/$REPO"
if [ ! -d "$TARGET/.git" ]; then
rm -rf "$TARGET"
git clone --depth=1 --branch master \
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
"$TARGET"
else
git -C "$TARGET" fetch --depth=1 origin master
git -C "$TARGET" reset --hard origin/master
git -C "$TARGET" clean -fdx
fi
done
for REPO in elink-portal-common eapim-admin-djb; do
if [ ! -d "$REPO/.git" ]; then
rm -rf "$REPO"
git clone --depth=1 --branch master \
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
"$REPO"
else
git -C "$REPO" fetch --depth=1 origin master
git -C "$REPO" reset --hard origin/master
git -C "$REPO" clean -fdx
fi
done
'''
}
}
stage('Verify toolchain') {
steps {
sh '''
set -eu
java -version
gradle --version
'''
}
}
stage('Build WAR') {
steps { sh 'gradle clean build -x test --no-daemon -Pprofile=weblogic' }
post {
success {
sh '''
set -eu
cd build/libs
sha256sum eapim-admin.war > eapim-admin.war.sha256
'''
archiveArtifacts artifacts: 'build/libs/eapim-admin.war,build/libs/eapim-admin.war.sha256', fingerprint: true
stash name: 'war', includes: 'build/libs/eapim-admin.war'
}
}
}
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-admin-sbom.xlsx 를 아티팩트로 보관.
// 실패해도 배포는 진행하도록 UNSTABLE 로만 표시한다.
stage('SBOM') {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh 'gradle sbomXlsx --no-daemon -Pprofile=weblogic'
}
}
post {
always {
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/sbom/*.xlsx', fingerprint: true
}
}
}
}
}
stage('Deploy (weblogic)') {
agent { label 'weblogic' }
environment {
JENKINS_NODE_COOKIE = 'dontKillMe' // background weblogic를 빌드 종료 시 죽이지 않게
WL_HOME = '/app/eapim/adminportal'
WL_DEPLOY_DIR = '/app/eapim/adminportal'
WL_WAR_NAME = 'eapim-admin.war'
WL_HTTP_PORT = '39120'
WL_NOHUP = '/logs/weblogic/domains/eapimDomain/nohup.emsSvr11.out'
}
stages {
stage('Stop WebLogic') {
steps {
sh '"$WL_HOME/stopEms11.sh"' // 동기: 완전 종료까지 블록, 미기동 시에도 안전
}
}
stage('Deploy WAR') {
steps {
unstash 'war'
sh '''
set -eu
cp build/libs/eapim-admin.war "$WL_DEPLOY_DIR/$WL_WAR_NAME"
ls -la "$WL_DEPLOY_DIR"
'''
}
}
stage('Start WebLogic and readiness') {
steps {
sh '''
set -eu
"$WL_HOME/startEms11.sh" # nohup & 로 백그라운드 기동, 즉시 리턴
DEADLINE=$(($(date +%s) + 300))
STATUS=000
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
"http://localhost:$WL_HTTP_PORT/monitoring/loginForm.do" 2>/dev/null || echo "000")
case "$STATUS" in
200|302|401) echo "Readiness OK (/monitoring/loginForm.do HTTP $STATUS)"; break ;;
esac
sleep 3
done
case "$STATUS" in
200|302|401) ;;
*)
echo "Readiness failed within 300s, last HTTP=$STATUS"
[ -f "$WL_NOHUP" ] && tail -120 "$WL_NOHUP"
exit 1
;;
esac
'''
}
}
}
}
}
post {
success {
node('weblogic') {
sh '''
set +e
payload=$(printf '{"text":"[%s #%s](%s) SUCCESS"}' "$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL")
curl -sS --fail --max-time 5 -H 'Content-Type: application/json' -X POST --data "$payload" "$WEBHOOK_URL" >/dev/null || echo "Webhook failed"
'''
}
}
failure {
node('weblogic') {
sh '''
set +e
payload=$(printf '{"text":"[%s #%s](%s) FAILURE"}' "$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL")
curl -sS --fail --max-time 5 -H 'Content-Type: application/json' -X POST --data "$payload" "$WEBHOOK_URL" >/dev/null || echo "Webhook failed"
'''
}
}
}
}
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
p:render="ONL,COM" p:render="ONL,COM"
/> />
<!-- BAP 일괄전송FTP --> <!-- BAP 일괄전송FTP -->
<!-- <bean <bean
id="BAP" id="BAP"
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
p:render="BAP,COM" p:render="BAP,COM"
/> --> />
<!-- RMS_DEFAULT --> <!-- RMS_DEFAULT -->
<bean <bean
id="MONITORING" id="MONITORING"
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
p:render="ONL,COM" p:render="ONL,COM"
/> />
<!-- BAP 일괄전송FTP --> <!-- BAP 일괄전송FTP -->
<!-- <bean--> <bean
<!-- id="BAP"--> id="BAP"
<!-- class="com.eactive.eai.rms.common.datasource.DataSourceType"--> class="com.eactive.eai.rms.common.datasource.DataSourceType"
<!-- p:name="BAP"--> p:name="BAP"
<!-- p:text="BAP"--> p:text="BAP"
<!-- p:schema="BAPADM"--> p:schema="BAPAPP"
<!-- p:dynamic="true"--> p:dynamic="true"
<!-- p:online="true"--> p:online="true"
<!-- p:jndiName="jdbc/dsOBP_BAP"--> p:jndiName="jdbc/dsOBP_BAP"
<!-- p:render="BAP,COM"--> p:render="BAP,COM"
<!-- />--> />
<!-- RMS_DEFAULT --> <!-- RMS_DEFAULT -->
<bean <bean
id="MONITORING" id="MONITORING"
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -30,9 +30,9 @@
<entry <entry
key="APIGW" key="APIGW"
value-ref="APIGW" /> value-ref="APIGW" />
<!-- <entry <entry
key="BAP" key="BAP"
value-ref="BAP" /> --> value-ref="BAP" />
</map> </map>
</property> </property>
<property <property
@@ -91,7 +91,7 @@
<property name="configLocations"> <property name="configLocations">
<list> <list>
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value>
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value> --> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value> <!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value>
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> --> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> -->
</list> </list>
@@ -30,9 +30,9 @@
<entry <entry
key="APIGW" key="APIGW"
value-ref="APIGW" /> value-ref="APIGW" />
<!-- <entry--> <entry
<!-- key="BAP"--> key="BAP"
<!-- value-ref="BAP" />--> value-ref="BAP" />
</map> </map>
</property> </property>
<property <property
@@ -91,7 +91,7 @@
<property name="configLocations"> <property name="configLocations">
<list> <list>
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value>
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>--> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value> <!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value>
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> --> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> -->
</list> </list>
@@ -69,6 +69,7 @@
<context:component-scan base-package="com.eactive.apim.portal" /> <context:component-scan base-package="com.eactive.apim.portal" />
<context:component-scan base-package="com.eactive.ext.kjb.statistics" /> <context:component-scan base-package="com.eactive.ext.kjb.statistics" />
<context:component-scan base-package="com.eactive.ext.djb" />
<bean id="cacheManager" <bean id="cacheManager"
@@ -115,7 +115,7 @@
</bean> </bean>
<bean id="ioAcceptor" class="com.eactive.eai.rms.onl.server.UdpServer" init-method="init" destroy-method="destory"> <bean id="ioAcceptor" class="com.eactive.eai.rms.onl.server.UdpServer" init-method="init" destroy-method="destory">
<property name="port" value="10800" /> <property name="port" value="39126" />
<property name="handler" ref="handler"/> <property name="handler" ref="handler"/>
</bean> </bean>
+6 -3
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
@@ -12,6 +12,9 @@ PortalTermsManController_약관관리_APIGW_INSERT,UPDATE,DELETE
ProdClientManController_운영Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE ProdClientManController_운영Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE
ApiSpecController_API스펙관리_APIGW_INSERT,DELETE ApiSpecController_API스펙관리_APIGW_INSERT,DELETE
MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE
PortalPartnershipManController_사업제휴신청관리_APIGW_UNMASK PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
PortalInquiryManController_Q&A문의관리_APIGW_VISIBILITY
PortalMenuManController_포탈메뉴관리_APIGW_INSERT,UPDATE,DELETE,TRANSACTION_PLACEMENT,INITIALIZE,TRANSACTION_RELOAD
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=D eai.systemmode=D
eai.drmode=N eai.drmode=N
@@ -10,9 +10,9 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr96 inst.Name=emsSvr96
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
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
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=P eai.systemmode=P
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=T eai.systemmode=T
eai.drmode=N eai.drmode=N
+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>
+13 -10
View File
@@ -9,15 +9,7 @@
<param-name>contextConfigLocation</param-name> <param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value> <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param> </context-param>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter> <filter>
<filter-name>encodingFilter</filter-name> <filter-name>encodingFilter</filter-name>
@@ -56,6 +48,17 @@
<filter-name>encodingFilter</filter-name> <filter-name>encodingFilter</filter-name>
<url-pattern>*.json</url-pattern> <url-pattern>*.json</url-pattern>
</filter-mapping> </filter-mapping>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- <!--
<filter> <filter>
@@ -172,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>
-43
View File
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://xmlns.oracle.com/weblogic/weblogic-web-app"
xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app"
elementFormDefault="qualified">
<xs:element name="weblogic-web-app">
<xs:complexType>
<xs:all>
<xs:element name="context-root" type="xs:string" minOccurs="0"/>
<xs:element name="session-descriptor" minOccurs="0">
<xs:complexType>
<xs:all>
<xs:element name="timeout-secs" type="xs:integer" minOccurs="0"/>
<xs:element name="persistent-store-type" type="xs:string" minOccurs="0"/>
</xs:all>
</xs:complexType>
</xs:element>
<xs:element name="container-descriptor" minOccurs="0">
<xs:complexType>
<xs:all>
<xs:element name="prefer-application-packages" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="package-name" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="prefer-application-resources" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="resource-name" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
</xs:schema>
+3 -5
View File
@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<weblogic-web-app <weblogic-web-app xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app">
xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.oracle.com/weblogic/weblogic-web-app weblogic-web-app.xsd">
<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>
<persistent-store-type>replicated_if_clustered</persistent-store-type> <persistent-store-type>replicated_if_clustered</persistent-store-type>
</session-descriptor> </session-descriptor>
+1
View File
@@ -1,6 +1,7 @@
@charset "utf-8"; @charset "utf-8";
.gnb .depth1 > li:hover > a{color:#008cc5;} /* theme */ .gnb .depth1 > li:hover > a{color:#008cc5;} /* theme */
.gnb .sitemap-link:hover{color:#008cc5;} /* theme */
.gnb .depth1 > li > .red_box{background:#008cc5;}/* theme */ .gnb .depth1 > li > .red_box{background:#008cc5;}/* theme */
.gnb .depth2 > li > a:hover{color:#008cc5;}/* theme */ .gnb .depth2 > li > a:hover{color:#008cc5;}/* theme */
+1
View File
@@ -1,6 +1,7 @@
@charset "utf-8"; @charset "utf-8";
.gnb .depth1 > li:hover > a{color:#16a085;} /* theme */ .gnb .depth1 > li:hover > a{color:#16a085;} /* theme */
.gnb .sitemap-link:hover{color:#16a085;} /* theme */
.gnb .depth1 > li > .red_box{background:#16a085;}/* theme */ .gnb .depth1 > li > .red_box{background:#16a085;}/* theme */
.gnb .depth2 > li > a:hover{color:#16a085;}/* theme */ .gnb .depth2 > li > a:hover{color:#16a085;}/* theme */
+1
View File
@@ -1,6 +1,7 @@
@charset "utf-8"; @charset "utf-8";
.gnb .depth1 > li:hover > a{color:#e74c3c;} /* theme */ .gnb .depth1 > li:hover > a{color:#e74c3c;} /* theme */
.gnb .sitemap-link:hover{color:#e74c3c;} /* theme */
.gnb .depth1 > li > .red_box{background:#e74c3c;}/* theme */ .gnb .depth1 > li > .red_box{background:#e74c3c;}/* theme */
.gnb .depth2 > li > a:hover{color:#e74c3c;}/* theme */ .gnb .depth2 > li > a:hover{color:#e74c3c;}/* theme */
+1
View File
@@ -1,6 +1,7 @@
@charset "utf-8"; @charset "utf-8";
.gnb .depth1 > li:hover > a{color:#ff80c0;} /* theme */ .gnb .depth1 > li:hover > a{color:#ff80c0;} /* theme */
.gnb .sitemap-link:hover{color:#ff80c0;} /* theme */
.gnb .depth1 > li > .red_box{background:#ff80c0;}/* theme */ .gnb .depth1 > li > .red_box{background:#ff80c0;}/* theme */
.gnb .depth2 > li > a:hover{color:#ff80c0;}/* theme */ .gnb .depth2 > li > a:hover{color:#ff80c0;}/* theme */
+1
View File
@@ -1,6 +1,7 @@
@charset "utf-8"; @charset "utf-8";
.gnb .depth1 > li:hover > a{color:#be8200;} /* theme */ .gnb .depth1 > li:hover > a{color:#be8200;} /* theme */
.gnb .sitemap-link:hover{color:#be8200;} /* theme */
.gnb .depth1 > li > .red_box{background:#be8200;}/* theme */ .gnb .depth1 > li > .red_box{background:#be8200;}/* theme */
.gnb .depth2 > li > a:hover{color:#be8200;}/* theme */ .gnb .depth2 > li > a:hover{color:#be8200;}/* theme */
+9 -9
View File
@@ -62,8 +62,8 @@ header.sub{position:relative; padding-top:80px;}
100% { color:rgba(255,96,78,0); } 100% { color:rgba(255,96,78,0); }
} */ } */
.gnb_bg{position:absolute; top:80px; left:0; display:none; width:100%; height:382px; box-sizing:border-box; background:white; box-shadow: 0px 5px 5px 0px rgba(0,0,0,0.2);} .gnb_bg{position:absolute; top:80px; left:0; display:none; width:100%; height:282px; box-sizing:border-box; background:white; box-shadow: 0px 5px 5px 0px rgba(0,0,0,0.2);}
.gnb{position:absolute; top:0px; left:230px; display:inline-block; height:80px; overflow-y:hidden;} .gnb{position:absolute; top:0px; left:180px; display:inline-block; height:80px; overflow-y:hidden;}
.gnb:hover{height:auto; overflow-y:visible;} .gnb:hover{height:auto; overflow-y:visible;}
.gnb:hover + .gnb_bg{display:block;} .gnb:hover + .gnb_bg{display:block;}
.gnb h1{float:left; display:block; width:auto; height:100%; line-height:80px;} .gnb h1{float:left; display:block; width:auto; height:100%; line-height:80px;}
@@ -71,15 +71,15 @@ header.sub{position:relative; padding-top:80px;}
.gnb .depth1 > li{position:relative;} .gnb .depth1 > li{position:relative;}
.gnb .depth1 > li > a{position:relative; display:block; box-sizing:border-box; color:#000; font-size:14px; width:160px; height:80px; text-align:center; line-height:80px; letter-spacing:-0.5px; opacity:0.7; transition:all 0.3s;} .gnb .depth1 > li > a{position:relative; display:block; box-sizing:border-box; color:#000; font-size:14px; width:160px; height:80px; text-align:center; line-height:80px; letter-spacing:-0.5px; opacity:0.7; transition:all 0.3s;}
.gnb .depth1 > li > a:after{content:''; position:absolute; top:34px; right:0; display:inline-block; width:1px; height:13px; background:#ddd;} .gnb .depth1 > li > a:after{content:''; position:absolute; top:34px; right:0; display:inline-block; width:1px; height:13px; }
.gnb .depth1 > li:first-child > a:before{content:''; position:absolute; top:34px; left:0; display:inline-block; width:1px; height:13px; background:#ddd;} .gnb .depth1 > li:first-child > a:before{content:''; position:absolute; top:34px; left:0; display:inline-block; width:1px; height:13px; }
/* /*
.gnb .depth1 > li:hover .red_box{transform:scaleX(1);} .gnb .depth1 > li:hover .red_box{transform:scaleX(1);}
.gnb .depth1 > li:hover > a{opacity:1;} .gnb .depth1 > li:hover > a{opacity:1;}
.gnb .depth1 > li > .red_box{display:block; position:absolute; width:100%; height:2px; top:78px; box-sizing:border-box; background:#ed1c24; transform-origin:center center; transform:scaleX(0); transition:all 0.3s;} .gnb .depth1 > li > .red_box{display:block; position:absolute; width:100%; height:2px; top:78px; box-sizing:border-box; background:#ed1c24; transform-origin:center center; transform:scaleX(0); transition:all 0.3s;}
*/ */
.gnb .depth2{position:absolute; left:0; top:80px; width:160px; height:410px; box-sizing:border-box; padding:20px 0; border-right:1px solid #eee; text-align:center; z-index:9;} .gnb .depth2{position:absolute; left:0; top:80px; width:160px; height:280px; box-sizing:border-box; padding:20px 0; border-right:1px solid #eee; text-align:center; z-index:9;}
.gnb .depth2.first{border-left:1px solid #eee;} .gnb .depth1 > li:first-child .depth2{border-left:1px solid #eee;}
.gnb .depth2 > li > a{display:block; color:#000; font-size:13px; line-height:30px;} .gnb .depth2 > li > a{display:block; color:#000; font-size:13px; line-height:30px;}
.gnb .depth2 > li > a:hover{color:#ff0000;} .gnb .depth2 > li > a:hover{color:#ff0000;}
@@ -94,9 +94,9 @@ header.sub{position:relative; padding-top:80px;}
.left_box .depth3 > li:hover > a, .left_box .depth3 > li.on > a{color:#ed1c24;} .left_box .depth3 > li:hover > a, .left_box .depth3 > li.on > a{color:#ed1c24;}
.content_top{position:fixed; top:0; left:0; padding:0 30px; width:100%; height:50px; line-height:50px; background:#eee; border-top:1px solid #ddd; border-left:1px solid #ddd; z-index:100;} .content_top{position:fixed; top:0; left:0; padding:0 30px; width:100%; height:50px; line-height:50px; background:#eee; border-top:1px solid #ddd; border-left:1px solid #ddd; z-index:100;}
.content_top .path{} .content_top .path{margin:0; padding:0;}
.content_top .path > li{display:inline-block;} .content_top .path > li{display:inline-block; margin:0; padding:0;}
.content_top .path > li a{display:block; height:50px; line-height:50px; padding-left:10px; font-size:12px; color:#999;} .content_top .path > li a{display:block; height:50px; line-height:50px; padding-left:10px; font-size:12px; color:#999; text-decoration:none;}
.content_top .path > li a:hover{color:#666;} .content_top .path > li a:hover{color:#666;}
.content_top .path > li a:before{content:'>'; margin-right:10px;} .content_top .path > li a:before{content:'>'; margin-right:10px;}
.content_top .path > li:first-child a:before{content:'';} .content_top .path > li:first-child a:before{content:'';}
+170
View File
@@ -0,0 +1,170 @@
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@ page import="com.eactive.eai.rms.common.login.SessionManager" %>
<%!
// 최소 HTML 이스케이프 (진단 페이지 XSS 방지)
private String esc(String s) {
if (s == null) return "";
StringBuilder b = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&': b.append("&amp;"); break;
case '<': b.append("&lt;"); break;
case '>': b.append("&gt;"); break;
case '"': b.append("&quot;"); break;
case '\'': b.append("&#39;"); break;
default: b.append(c);
}
}
return b.toString();
}
%>
<%
// 로그인 사용자만 접근 — real 모드에서 운영 키 암복호화 오라클이 되지 않도록 보호
if (StringUtils.isBlank(SessionManager.getUserId(request))
&& StringUtils.isBlank(SessionManager.getRoleIdString(request))) {
response.sendRedirect(request.getContextPath() + "/loginForm.do");
return;
}
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("utf-8");
String op = request.getParameter("op");
String input = request.getParameter("input");
boolean run = request.getParameter("run") != null;
if (op == null) op = "encrypt";
if (input == null) input = "";
String mode = "UNKNOWN";
String revision = "-";
String returnCode = "-";
String result = "";
String error = "";
boolean loaded = false;
try {
Class<?> cls = Class.forName("com.eactive.ext.djb.DamoManager");
Object damo = cls.getMethod("getInstance").invoke(null);
loaded = true;
boolean bypass = (Boolean) cls.getMethod("isBypassMode").invoke(damo);
boolean fake = (Boolean) cls.getMethod("isFakeMode").invoke(damo);
mode = bypass ? "BYPASS" : (fake ? "FAKE" : "REAL");
revision = String.valueOf(cls.getMethod("getRevision").invoke(damo));
returnCode = String.valueOf(cls.getMethod("getReturnCode").invoke(damo));
if (run) {
if ("encrypt".equals(op)) {
result = (String) cls.getMethod("encrypt", String.class).invoke(damo, input);
} else if ("decrypt".equals(op)) {
result = (String) cls.getMethod("decrypt", String.class).invoke(damo, input);
} else if ("sha256".equals(op)) {
int t = cls.getField("SHA256").getInt(null);
result = (String) cls.getMethod("hash", int.class, String.class).invoke(damo, t, input);
} else if ("sha512".equals(op)) {
int t = cls.getField("SHA512").getInt(null);
result = (String) cls.getMethod("hash", int.class, String.class).invoke(damo, t, input);
} else {
error = "알 수 없는 op: " + op;
}
}
} catch (ClassNotFoundException e) {
error = "com.eactive.ext.djb.DamoManager 클래스를 찾을 수 없습니다. "
+ "damo-manager.jar 를 WAS lib (운영) 또는 WEB-INF/lib 에 배포하세요.";
} catch (Throwable t) {
// require-real fail-fast(ExceptionInInitializerError) 또는 초기화 실패(NoClassDefFoundError) 포함
Throwable c = (t.getCause() != null) ? t.getCause() : t;
error = c.toString();
if (c instanceof NoClassDefFoundError
|| error.contains("require-real")
|| error.contains("Could not initialize")) {
error += " (※ -Ddamo-manager.require-real=true 인데 scpdb 가 없어 기동에 실패했을 수 있습니다. "
+ "scpdb 를 갖추거나 옵션을 내리세요.)";
}
}
String badgeColor = "REAL".equals(mode) ? "#1a7f37"
: "BYPASS".equals(mode) ? "#9a3412"
: "FAKE".equals(mode) ? "#9a6700" : "#6e7781";
%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>DamoManager 테스트</title>
<style>
body { font-family: -apple-system, "Malgun Gothic", sans-serif; margin: 24px; color: #24292f; }
h1 { font-size: 20px; margin: 0 0 4px; }
.sub { color: #6e7781; font-size: 13px; margin-bottom: 16px; }
.badge { display:inline-block; color:#fff; padding:2px 10px; border-radius:12px; font-size:12px; font-weight:600; background:<%= badgeColor %>; }
.meta { font-size:12px; color:#57606a; margin-left:8px; }
.card { border:1px solid #d0d7de; border-radius:8px; padding:16px; max-width:760px; }
label { display:block; font-size:13px; font-weight:600; margin:12px 0 4px; }
textarea, select { width:100%; box-sizing:border-box; font-size:14px; padding:8px; border:1px solid #d0d7de; border-radius:6px; font-family:ui-monospace, Menlo, monospace; }
textarea { height:90px; resize:vertical; }
.row { display:flex; gap:12px; }
.row > div { flex:1; }
button { margin-top:14px; background:#1f6feb; color:#fff; border:0; border-radius:6px; padding:9px 18px; font-size:14px; font-weight:600; cursor:pointer; }
.result { margin-top:16px; }
.out { background:#f6f8fa; border:1px solid #d0d7de; border-radius:6px; padding:12px; white-space:pre-wrap; word-break:break-all; font-family:ui-monospace, Menlo, monospace; font-size:13px; min-height:20px; }
.err { color:#cf222e; background:#fff5f5; border-color:#ffc1c1; }
.hint { font-size:12px; color:#6e7781; margin-top:18px; line-height:1.6; }
code { background:#eff1f3; padding:1px 5px; border-radius:4px; }
</style>
</head>
<body>
<h1>DamoManager 테스트</h1>
<div class="sub">웹에서 encrypt / decrypt / hash 동작을 즉시 확인 (eapim-admin)</div>
<div class="card">
<div>
<% if (loaded) { %>
<span class="badge"><%= mode %> MODE</span>
<span class="meta">revision=<%= esc(revision) %> · returnCode=<%= esc(returnCode) %></span>
<% } else { %>
<span class="badge" style="background:#cf222e;">NOT LOADED</span>
<span class="meta">damo-manager.jar 미배포</span>
<% } %>
</div>
<form method="post" action="<%= request.getContextPath() %>/damo.jsp">
<div class="row">
<div>
<label for="op">동작</label>
<select id="op" name="op">
<option value="encrypt" <%= "encrypt".equals(op) ? "selected" : "" %>>encrypt (암호화)</option>
<option value="decrypt" <%= "decrypt".equals(op) ? "selected" : "" %>>decrypt (복호화)</option>
<option value="sha256" <%= "sha256".equals(op) ? "selected" : "" %>>hash SHA-256</option>
<option value="sha512" <%= "sha512".equals(op) ? "selected" : "" %>>hash SHA-512</option>
</select>
</div>
</div>
<label for="input">입력</label>
<textarea id="input" name="input" placeholder="암호화/복호화/해시할 문자열"><%= esc(input) %></textarea>
<input type="hidden" name="run" value="1">
<button type="submit">실행</button>
</form>
<% if (run || error.length() > 0) { %>
<div class="result">
<label>결과<% if (run && error.length() == 0) { %> <span class="meta">(<%= esc(op) %>)</span><% } %></label>
<div class="out <%= error.length() > 0 ? "err" : "" %>"><%= error.length() > 0 ? esc(error) : esc(result) %></div>
</div>
<% } %>
<div class="hint">
모드 의미 — <b>BYPASS</b>: 무변환 원문 통과(<b>기본값</b>, 옵션 없음) · <b>FAKE</b>: Base64/JRE 해시(<code>-Ddamo-manager.enabled=true</code>, scpdb 없음) · <b>REAL</b>: penta scpdb 실 암복호화(<code>enabled=true</code> + scpdb).<br>
BYPASS 모드에서는 encrypt/decrypt/hash 모두 입력을 그대로 반환합니다.<br>
운영은 <code>-Ddamo-manager.require-real=true</code> 로 real 강제(아니면 기동 실패). CLI 로도 동일 확인: <code>java -jar damo-manager.jar enc &lt;text&gt;</code>
</div>
</div>
</body>
</html>
+1
View File
@@ -1,6 +1,7 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%> <%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@page import="org.apache.commons.lang3.StringUtils"%> <%@page import="org.apache.commons.lang3.StringUtils"%>
<%@page import="com.eactive.eai.rms.common.login.SessionManager"%> <%@page import="com.eactive.eai.rms.common.login.SessionManager"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,334 @@
/**
* Editor Content Styles (editor-content.css)
*
* Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일
* 관리자포탈/개발자포탈 양쪽에서 동일하게 사용
*
* 사용법:
* - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가
* - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가
*
* @version 1.0.0
* @date 2025-12-30
*/
/* ==========================================================================
CSS Reset + 기본 설정
========================================================================== */
.editor-content {
all: revert;
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
font-size: 16px !important;
font-weight: 400 !important;
line-height: 1.8 !important;
color: #1A1A2E !important;
word-wrap: break-word;
text-align: left !important;
}
/* ==========================================================================
헤딩 (Headings)
========================================================================== */
.editor-content h1 {
font-size: 20px !important;
font-weight: 700 !important;
margin: 32px 0 16px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
.editor-content h1:first-child {
margin-top: 0 !important;
}
.editor-content h2 {
font-size: 18px !important;
font-weight: 700 !important;
margin: 24px 0 8px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
.editor-content h3 {
font-size: 16px !important;
font-weight: 600 !important;
margin: 16px 0 8px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-size: 16px !important;
font-weight: 600 !important;
margin: 16px 0 8px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
/* ==========================================================================
단락 (Paragraphs)
========================================================================== */
.editor-content p {
margin-bottom: 16px !important;
line-height: 1.8 !important;
font-size: 16px !important;
}
.editor-content p:last-child {
margin-bottom: 0 !important;
}
/* ==========================================================================
리스트 (Lists) - 글로벌 reset 대응
========================================================================== */
.editor-content ul {
list-style-type: disc !important;
padding-left: 32px !important;
margin: 16px 0 !important;
}
.editor-content ol {
list-style-type: decimal !important;
padding-left: 32px !important;
margin: 16px 0 !important;
}
.editor-content li {
margin-bottom: 8px !important;
line-height: 1.8 !important;
font-size: 16px !important;
}
.editor-content li:last-child {
margin-bottom: 0 !important;
}
/* 중첩 리스트 */
.editor-content ul ul {
list-style-type: circle !important;
margin: 8px 0 !important;
}
.editor-content ul ul ul {
list-style-type: square !important;
}
.editor-content ol ol {
list-style-type: lower-alpha !important;
margin: 8px 0 !important;
}
.editor-content ol ol ol {
list-style-type: lower-roman !important;
}
/* ==========================================================================
테이블 (Tables)
========================================================================== */
.editor-content table {
width: 100% !important;
border-collapse: collapse !important;
margin: 24px 0 !important;
border: 1px solid #E2E8F0 !important;
border-radius: 8px !important;
overflow: hidden !important;
font-size: 14px !important;
}
.editor-content th,
.editor-content td {
border: 1px solid #E2E8F0 !important;
padding: 8px 16px !important;
text-align: left !important;
vertical-align: top !important;
}
.editor-content th {
background: #EFF6FF !important;
font-weight: 600 !important;
color: #1A1A2E !important;
}
.editor-content tr:hover {
background: #F8FAFC !important;
}
/* ==========================================================================
링크 (Links)
========================================================================== */
.editor-content a {
color: #0049b4 !important;
text-decoration: underline !important;
transition: color 0.3s ease !important;
}
.editor-content a:hover {
color: #003080 !important;
}
/* ==========================================================================
인용문 (Blockquote)
========================================================================== */
.editor-content blockquote {
border-left: 4px solid #0049b4 !important;
padding: 16px 24px !important;
margin: 24px 0 !important;
background: #F8FAFC !important;
font-style: italic !important;
color: #64748B !important;
border-radius: 0 8px 8px 0 !important;
}
.editor-content blockquote p {
margin-bottom: 0 !important;
}
/* ==========================================================================
이미지 (Images)
========================================================================== */
.editor-content img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px !important;
margin: 16px 0 !important;
display: block !important;
}
/* ==========================================================================
코드 (Code)
========================================================================== */
.editor-content code {
background: #F8FAFC !important;
padding: 2px 6px !important;
border-radius: 4px !important;
font-family: 'Fira Code', 'Courier New', monospace !important;
font-size: 0.9em !important;
color: #0049b4 !important;
}
.editor-content pre {
background: #F8FAFC !important;
border: 1px solid #E2E8F0 !important;
border-radius: 8px !important;
padding: 16px !important;
overflow-x: auto !important;
margin: 16px 0 !important;
}
.editor-content pre code {
background: transparent !important;
padding: 0 !important;
color: #1A1A2E !important;
}
/* ==========================================================================
구분선 (Horizontal Rule)
========================================================================== */
.editor-content hr {
border: none !important;
border-top: 1px solid #E2E8F0 !important;
margin: 32px 0 !important;
}
/* ==========================================================================
텍스트 강조 (Text Emphasis)
========================================================================== */
.editor-content strong,
.editor-content b {
font-weight: 700 !important;
}
.editor-content em,
.editor-content i {
font-style: italic !important;
}
.editor-content u {
text-decoration: underline !important;
}
.editor-content s,
.editor-content strike,
.editor-content del {
text-decoration: line-through !important;
}
.editor-content mark {
background-color: #FEF3C7 !important;
padding: 0 2px !important;
}
.editor-content sub {
vertical-align: sub !important;
font-size: 0.8em !important;
}
.editor-content sup {
vertical-align: super !important;
font-size: 0.8em !important;
}
/* ==========================================================================
반응형 (Responsive)
========================================================================== */
@media (max-width: 768px) {
.editor-content {
font-size: 14px !important;
}
.editor-content h1 {
font-size: 18px !important;
}
.editor-content h2 {
font-size: 16px !important;
}
.editor-content h3,
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-size: 14px !important;
}
.editor-content p,
.editor-content li {
font-size: 14px !important;
}
.editor-content table {
font-size: 12px !important;
}
.editor-content th,
.editor-content td {
padding: 4px 8px !important;
}
.editor-content ul,
.editor-content ol {
padding-left: 24px !important;
}
.editor-content blockquote {
padding: 8px 16px !important;
}
.editor-content pre {
padding: 8px !important;
}
}
+75
View File
@@ -0,0 +1,75 @@
// OpenAPI 에디터 초기 상태 골격(빈 껍데기).
// - specToData() 가 deepClone 하여 서버 spec 값으로 덮어쓰는 구조 기준값.
// - 실제 데모/샘플 값은 두지 않는다(팝업은 항상 서버 spec 로드 후 렌더).
// - docOptions 만 기능 기본값으로 유지(문서 미리보기 옵션).
// locked:true = 게이트웨이 자동 주입(회색+자물쇠) / locked:false = 사용자 편집.
window.SAMPLE_DATA = (function () {
function f() { return { value: '', locked: false }; }
return {
info: {
title: f(),
version: f(),
summary: f(),
description: f(),
termsOfService: f(),
contact: { name: f(), email: f(), url: f() },
license: { name: f(), url: f() }
},
tags: [],
externalDocs: { description: f(), url: f() },
servers: [],
serverVariables: [],
operation: {
method: f(),
path: f(),
operationId: f(),
summary: f(),
description: f(),
tags: { value: [], locked: false },
deprecated: { value: false, locked: false }
},
// 파라미터 (Path/Query/Header/Cookie)
parameters: [],
// Request Body 스키마
requestBody: {
mediaType: 'application/json',
required: { value: false, locked: false },
schema: []
},
// 응답 스키마 (상태코드 별)
responses: {
'200': { description: f(), schema: [] }
},
// 보안 스킴
securitySchemes: [],
globalSecurity: [],
// 예제
examples: {
request: {},
response: {}
},
// 문서 옵션
docOptions: {
theme: 'light',
lang: 'ko',
includeExamples: true,
tryItOut: true,
defaultExpandDepth: -1,
layout: 'BaseLayout',
tagFilter: [],
sideToc: true
}
};
})();
+103
View File
@@ -0,0 +1,103 @@
/* DJB OpenAPI Editor POC — Tailwind 보강 커스텀 */
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo",
"Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
}
/* === 잠금 행/카드 좌측 띠 === */
.locked-row td:first-child {
position: relative;
}
.locked-row td:first-child::before {
content: '';
position: absolute;
left: 0;
top: 6px;
bottom: 6px;
width: 3px;
background: #cbd5e1;
border-radius: 2px;
}
.locked-card {
position: relative;
background: linear-gradient(to right, #f8fafc 0%, #ffffff 4px) #ffffff;
}
.locked-card::before {
content: '';
position: absolute;
left: 0; top: 8px; bottom: 8px;
width: 3px;
background: #cbd5e1;
border-radius: 0 2px 2px 0;
}
/* === 스키마 테이블 — 가독성 보조 === */
.schema-table th { white-space: nowrap; }
.schema-table tbody tr:hover { background: #fafbfc; }
.schema-table input,
.schema-table select {
font-size: 12.5px;
}
.schema-table input[type="text"] {
padding-top: 4px;
padding-bottom: 4px;
}
/* === 스텝퍼 항목 hover === */
.step-item:hover { background: #f8fafc; }
/* === 미리보기 패널: Swagger UI 크기 미세조정 === */
#preview-swagger .swagger-ui {
font-size: 13px;
}
#preview-swagger .swagger-ui .info { margin: 18px 0; }
#preview-swagger .swagger-ui .info .title { font-size: 22px; }
#preview-swagger .swagger-ui .scheme-container { padding: 12px 0; box-shadow: none; }
#preview-swagger .swagger-ui .opblock { margin: 0 0 12px 0; }
/* === 다크 테마 (POC 시뮬레이션 — Swagger 자체 다크는 미지원이라 컨테이너만) === */
body.theme-dark #preview-swagger { background: #1e293b; }
/* === 토스트 애니메이션 === */
#toast { transition: opacity 0.2s; }
/* === Monaco 컨테이너가 hidden 일 때 layout 깨짐 방지 === */
#preview-monaco {
overflow: hidden;
}
/* === Stepper 6단 grid 셀 좌우 라인 (마지막 셀 제외) === */
#stepper li {
position: relative;
}
#stepper li:not(:last-child)::after {
content: '';
position: absolute;
top: 50%;
right: -8px;
width: 16px;
height: 1px;
background: #e2e8f0;
}
/* === 보안 스킴 카드 — 잠금 표시 === */
.locked-card {
background: #fafbfc;
}
/* === 칩(태그) 미세 조정 === */
.tag-chip { transition: background 0.15s; }
/* === Export menu fade === */
#export-menu { animation: fadeIn 0.12s ease-out; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* === 1600px 기준 폼 패널 내부 가로 여유 확보 === */
#form-content { max-width: 100%; }
/* === readonly input 위에서 텍스트 커서 안 보이게 === */
input[readonly] { user-select: text; }
+20 -15
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,12 +184,13 @@ 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>
<th style="width:180px;"></th> <th style="width:180px;"><%= localeMessage.getString("auditLogMan.parameters") %></th>
<td> <td>
<input type="text" name="searchParameters" value="${param.searchParameters}" style="width:100%">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -54,10 +54,18 @@ function detail(key){
$('#systemCode').val(data.systemCode); $('#systemCode').val(data.systemCode);
$('#logTypeText').val(data.logTypeText); $('#logTypeText').val(data.logTypeText);
$('#remoteAddress').val(data.remoteAddress); $('#remoteAddress').val(data.remoteAddress);
$('#logMsg').val(data.message);
$('#logSubMsg').val(data.command); $('#logSubMsg').val(data.command);
$('#parameters').val(data.parameters); $('#parameters').val(data.parameters);
// 사유(message)는 값이 있을 때만 노출
if (data.message) {
$('#logMsg').val(data.message);
$('#messageRow').show();
} else {
$('#logMsg').val('');
$('#messageRow').hide();
}
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
@@ -121,6 +129,9 @@ $(document).ready(function() {
<tr> <tr>
<th><%= localeMessage.getString("auditLogMan.command") %></th><td><input type="text" id="logSubMsg" name="logSubMsg" readonly="readonly"/></td> <th><%= localeMessage.getString("auditLogMan.command") %></th><td><input type="text" id="logSubMsg" name="logSubMsg" readonly="readonly"/></td>
</tr> </tr>
<tr id="messageRow" style="display:none;">
<th><%= localeMessage.getString("auditLogMan.message") %></th><td><textarea id="logMsg" name="logMsg" style="width:100%;height:80px" readonly="readonly"></textarea></td>
</tr>
<tr height="100px"> <tr height="100px">
<th><%= localeMessage.getString("auditLogMan.parameters") %></th><td><textarea id="parameters" name="parameters" style="width:100%;height:200px" readonly="readonly"></textarea></td> <th><%= localeMessage.getString("auditLogMan.parameters") %></th><td><textarea id="parameters" name="parameters" style="width:100%;height:200px" readonly="readonly"></textarea></td>
</tr> </tr>
@@ -0,0 +1,120 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.List"%>
<%@ page import="com.eactive.eai.rms.common.acl.sitemap.ui.SitemapNode"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%!
private void printNodeLabel(javax.servlet.jsp.JspWriter out, SitemapNode node) throws java.io.IOException {
String url = node.getMenuUrl();
boolean hasLink = url != null && !url.trim().isEmpty() && !"NAN".equalsIgnoreCase(url.trim());
if (hasLink) {
out.print("<a href=\"javascript:void(0);\" class=\"sitemap-name\" onclick=\"goPage('"
+ escapeJs(url) + "','" + escapeJs(node.getMenuId()) + "');return false;\">"
+ escapeHtml(node.getMenuName()) + "</a>");
} else {
out.print("<span class=\"sitemap-name\">" + escapeHtml(node.getMenuName()) + "</span>");
}
}
private void printSitemapNode(javax.servlet.jsp.JspWriter out, SitemapNode node) throws java.io.IOException {
out.print("<li>");
printNodeLabel(out, node);
List<SitemapNode> children = node.getChildren();
if (children != null && !children.isEmpty()) {
out.print("<ul>");
for (SitemapNode child : children) {
printSitemapNode(out, child);
}
out.print("</ul>");
}
out.print("</li>");
}
private String escapeHtml(String value) {
if (value == null) {
return "";
}
return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
private String escapeJs(String value) {
if (value == null) {
return "";
}
return value.replace("\\", "\\\\").replace("'", "\\'");
}
%>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
List<SitemapNode> sitemapTree = (List<SitemapNode>) request.getAttribute("sitemapTree");
%>
<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>
.sitemap-columns { display: flex; flex-wrap: wrap; gap: 40px; }
.sitemap-column { min-width: 180px; }
.sitemap-category { font-size: 1.1em; font-weight: bold; padding-bottom: 6px; margin-bottom: 8px; border-bottom: 2px solid #ccc; }
.sitemap-tree, .sitemap-tree ul { list-style-type: disc; margin: 0; padding-left: 18px; }
.sitemap-tree { padding-left: 0; list-style-type: none; }
.sitemap-tree li { line-height: 1.8; }
a.sitemap-name { text-decoration: none; cursor: pointer; }
a.sitemap-name:hover { text-decoration: underline; }
</style>
<script language="javascript">
function goPage(url, menuId) {
var serviceType = sessionStorage["serviceType"];
var pageUrl = url;
pageUrl += (pageUrl.indexOf("?") > -1 ? "&" : "?") + "menuId=" + menuId;
pageUrl += "&serviceType=" + serviceType;
parent.leftFrame.location.href = '<c:url value="/leftMenu.do"/>?menuId=' + menuId + '&serviceType=' + serviceType;
location.href = pageUrl;
}
</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">
<!-- <div class="title">사이트맵</div> -->
<div class="sitemap-columns">
<%
for (SitemapNode category : sitemapTree) {
%>
<div class="sitemap-column">
<div class="sitemap-category">
<%
printNodeLabel(out, category);
%>
</div>
<ul class="sitemap-tree">
<%
List<SitemapNode> children = category.getChildren();
if (children != null) {
for (SitemapNode child : children) {
printSitemapNode(out, child);
}
}
%>
</ul>
</div>
<%
}
%>
</div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
+19 -5
View File
@@ -18,7 +18,17 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/common/acl/user/userMan.json" />'; var url = '<c:url value="/common/acl/user/userMan.json" />';
var url_view = '<c:url value="/common/acl/user/userMan.view" />'; var url_view = '<c:url value="/common/acl/user/userMan.view" />';
$(document).ready(function() {
function userStatusFormat (cellvalue){
if ( cellvalue == '1' ){
return '<%= localeMessage.getString("code.normal") %>';
}else if ( cellvalue == '2' ) {
return '<span style="color:red"><%= localeMessage.getString("code.lock") %></span>';
}
return '';
}
$(document).ready(function() {
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
@@ -28,16 +38,20 @@ $(document).ready(function() {
colNames:['<%= localeMessage.getString("login.userId") %>', colNames:['<%= localeMessage.getString("login.userId") %>',
'<%= localeMessage.getString("user.name") %>', '<%= localeMessage.getString("user.name") %>',
'<%= localeMessage.getString("user.roleName") %>', '<%= localeMessage.getString("user.roleName") %>',
'<%= localeMessage.getString("user.status") %>',
'<%= localeMessage.getString("user.createOn") %>', '<%= localeMessage.getString("user.createOn") %>',
'<%= localeMessage.getString("user.lastLoginYms") %>',
'<%= localeMessage.getString("user.depart") %>', '<%= localeMessage.getString("user.depart") %>',
'<%= localeMessage.getString("user.teamName") %>', '<%= localeMessage.getString("user.teamName") %>',
'<%= localeMessage.getString("user.position") %>' '<%= localeMessage.getString("user.position") %>'
], ],
colModel:[ colModel:[
{ name : 'USERID' , align:'center' ,sortable:false }, { name : 'USERID' , align:'center', width: 100, sortable:false },
{ name : 'USERNAME' , align:'center' }, { name : 'USERNAME' , align:'center', width: 100 },
{ name : 'ROLEIDNFINAME' , align:'center' }, { name : 'ROLEIDNFINAME' , align:'center', width: 300 },
{ name : 'LASTAMNDYMS' , align:'center', formatter: timeStampFormat }, { name : 'STATUS' , align:'center', width: 80, formatter: userStatusFormat },
{ name : 'REGDYMS' , align:'center', width: 120, formatter: timeStampFormat },
{ name : 'LASTLOGINYMS' , align:'center', width: 120, formatter: timeStampFormat },
{ name : 'DVSNNAME' , align:'center', hidden: true }, { name : 'DVSNNAME' , align:'center', hidden: true },
{ name : 'TEAMNAME' , align:'center', hidden: true }, { name : 'TEAMNAME' , align:'center', hidden: true },
{ name : 'JOBTLNAME' , align:'center', hidden: true } { name : 'JOBTLNAME' , align:'center', hidden: true }
@@ -88,7 +88,8 @@ function detail(key){
$(tag+"[name="+name+"]").val(data[name.toUpperCase()] ? data[name.toUpperCase()].trim():""); $(tag+"[name="+name+"]").val(data[name.toUpperCase()] ? data[name.toUpperCase()].trim():"");
}); });
$("input[name=LASTAMNDYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true}); $("input[name=REGDYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
$("input[name=LASTLOGINYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
/* $("input[name=SECONDMENTSTDT]").inputmask("9999-99-99",{'autoUnmask':true}); /* $("input[name=SECONDMENTSTDT]").inputmask("9999-99-99",{'autoUnmask':true});
$("input[name=SECONDMENTENDT]").inputmask("9999-99-99",{'autoUnmask':true}); */ $("input[name=SECONDMENTENDT]").inputmask("9999-99-99",{'autoUnmask':true}); */
@@ -216,6 +217,7 @@ $(document).ready(function() {
dataType:"json", dataType:"json",
data:{cmd: 'UPDATE_PASSWORD', userId : $("input[name=userId]").val()}, data:{cmd: 'UPDATE_PASSWORD', userId : $("input[name=userId]").val()},
success:function(json){ success:function(json){
$('select[name="status"]').val("1")
alert(json.result); alert(json.result);
}, },
error:function(e){ error:function(e){
@@ -277,10 +279,10 @@ $(document).ready(function() {
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<input type="hidden" name="jobclcd"> <input type="hidden" name="jobclcd">
<tr> <tr>
<th style="width:180px;"><%= localeMessage.getString("login.userId") %></th><td ><input type="text" name="userId"/> </td> <th style="width:180px;"><%= localeMessage.getString("login.userId") %> <span class=\"required-mark\">*</span></th><td ><input type="text" name="userId"/> </td>
</tr> </tr>
<tr> <tr>
<th><%= localeMessage.getString("user.name") %> </th><td><input type="text" name="userName" /></td> <th><%= localeMessage.getString("user.name") %> <span class=\"required-mark\">*</span></th><td><input type="text" name="userName" /></td>
</tr> </tr>
<tr class="datailT"> <tr class="datailT">
<th><%= localeMessage.getString("user.auth") %> </th><td><input type="text" name="roleidnfiname" /></td> <th><%= localeMessage.getString("user.auth") %> </th><td><input type="text" name="roleidnfiname" /></td>
@@ -317,7 +319,6 @@ $(document).ready(function() {
<td> <td>
<div class="select-style"> <div class="select-style">
<select name="status"> <select name="status">
<option value=""><%= localeMessage.getString("code.normal") %></option>
<option value="1"><%= localeMessage.getString("code.normal") %></option> <option value="1"><%= localeMessage.getString("code.normal") %></option>
<option value="2"><%= localeMessage.getString("code.lock") %></option> <option value="2"><%= localeMessage.getString("code.lock") %></option>
</select> </select>
@@ -334,7 +335,10 @@ $(document).ready(function() {
<th><%= localeMessage.getString("user.fieldAgentName") %> </th><td><input type="text" name="spotprxyname" /></td> <th><%= localeMessage.getString("user.fieldAgentName") %> </th><td><input type="text" name="spotprxyname" /></td>
</tr> --%> </tr> --%>
<tr> <tr>
<th><%= localeMessage.getString("user.createOn") %> </th><td><input type="text" name="LASTAMNDYMS" readonly/></td> <th><%= localeMessage.getString("user.createOn") %> </th><td><input type="text" name="REGDYMS" readonly/></td>
</tr>
<tr>
<th><%= localeMessage.getString("user.lastLoginYms") %> </th><td><input type="text" name="LASTLOGINYMS" readonly/></td>
</tr> </tr>
<%-- <tr> <%-- <tr>
<th><%= localeMessage.getString("user.secondmentbrncd") %> </th><td><input type="text" name="SECONDMENTBRNCD" readonly/></td> <th><%= localeMessage.getString("user.secondmentbrncd") %> </th><td><input type="text" name="SECONDMENTBRNCD" readonly/></td>
+90
View File
@@ -30,6 +30,14 @@
<script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script>
<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">
/* 자동 로그아웃: 서버로 실제 요청이 발생한 경우에만 활동으로 기록 (main.jsp 타이머와 sessionStorage 공유) */
(function() {
var mark = function() { sessionStorage.setItem('lastActivity', String(Date.now())); };
mark(); // 화면(프레임) 로드 자체가 서버 요청이므로 활동으로 기록
$(document).ajaxSend(mark); // 이후 이 프레임에서 발생하는 모든 AJAX 요청을 활동으로 기록
})();
</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>
<%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%> <%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%>
@@ -527,6 +535,88 @@
} }
}, 200); }, 200);
} }
/**
* 사유 입력 모달 - 확인 시 입력한 사유 문자열을 onConfirm(reason)으로 전달
* @param {string} message - 안내 메시지
* @param {object} options - (title, confirmText, cancelText, placeholder, required, onConfirm, onCancel)
* required 기본 true (빈 사유 차단)
*/
function showReasonPrompt(message, options) {
options = options || {};
var title = options.title || '사유 입력';
var confirmText = options.confirmText || '확인';
var cancelText = options.cancelText || '취소';
var placeholder = options.placeholder || '사유를 입력하세요.';
var required = options.required !== false;
var onConfirm = options.onConfirm || function(){};
var onCancel = options.onCancel || function(){};
// 기존 모달 제거
$('#commonReasonModal').remove();
var iconHtml = getAlertIcon('warning');
var modalHtml =
'<div id="commonReasonModal" class="common-alert-overlay">' +
'<div class="common-alert-modal alert-warning">' +
'<div class="common-alert-header">' +
'<span class="common-alert-icon">' + iconHtml + '</span>' +
'<span class="common-alert-title">' + title + '</span>' +
'</div>' +
'<div class="common-alert-body">' +
'<p class="common-alert-message">' + message + '</p>' +
'<textarea id="commonReasonInput" maxlength="200" placeholder="' + placeholder + '" style="width:100%;height:80px;margin-top:10px;box-sizing:border-box;font-size:13px;padding:8px;border:1px solid #ddd;border-radius:4px;resize:vertical;"></textarea>' +
'<p id="commonReasonError" style="display:none;color:#f44336;font-size:12px;margin:6px 0 0;">사유를 입력해 주세요.</p>' +
'</div>' +
'<div class="common-alert-footer">' +
'<button type="button" class="common-confirm-cancel-btn" id="commonReasonCancelBtn">' + cancelText + '</button>' +
'<button type="button" class="common-alert-btn" id="commonReasonOkBtn">' + confirmText + '</button>' +
'</div>' +
'</div>' +
'</div>';
$('body').append(modalHtml);
setTimeout(function() {
$('#commonReasonModal').addClass('show');
$('#commonReasonInput').focus();
}, 10);
// 확인 버튼
$('#commonReasonOkBtn').on('click', function() {
var reason = $.trim($('#commonReasonInput').val());
if (required && reason === '') {
$('#commonReasonError').show();
$('#commonReasonInput').focus();
return;
}
closeCommonReason(function() { onConfirm(reason); });
});
// 취소 버튼
$('#commonReasonCancelBtn').on('click', function() {
closeCommonReason(onCancel);
});
// ESC 키로 취소
$(document).on('keydown.commonReason', function(e) {
if (e.keyCode === 27) {
closeCommonReason(onCancel);
}
});
}
function closeCommonReason(callback) {
$('#commonReasonModal').removeClass('show');
setTimeout(function() {
$('#commonReasonModal').remove();
$(document).off('keydown.commonReason');
if (typeof callback === 'function') {
callback();
}
}, 200);
}
</script> </script>
<!-- 공용 Alert 모달 스타일 --> <!-- 공용 Alert 모달 스타일 -->
@@ -28,22 +28,24 @@ 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();
if(typeof callback === 'function') { initGrid();
callback(url,key);
}
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
$(document).ready(function() {
init(); function initGrid() {
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
console.log(gridPostData);
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
@@ -125,9 +127,13 @@ $(document).ready(function() {
} }
}); });
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
init();
$("#btn_search").click(function(){ $("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); var postData = getSearchForJqgrid("cmd","LIST");
$("#grid").setGridParam({ postData: postData ,page:"1" }).trigger("reloadGrid"); $("#grid").setGridParam({ postData: postData ,page:"1" }).trigger("reloadGrid");
@@ -465,21 +465,21 @@ $(document).ready(function() {
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<tr> <tr>
<th style="width:180px;">Default Scheduler</th> <th style="width:180px;">Default Scheduler</th>
<td>On Memory: <span id="deft_onMemory"></span></td> <td style="width:150px">On Memory: <span id="deft_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="deft_previous"></span></td> <td >Previous Fired: <span id="deft_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="deft_next"></span></td> <td >Next Fire Time: <span id="deft_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td> <td style="width: 250px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="deft_persistJobData"></span></td> <td style="width: 120px;">PersistJobData: <span id="deft_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="deft_durable"></span></td> <td style="width: 120px;">Durable: <span id="deft_durable"></span></td>
</tr> </tr>
<tr> <tr>
<th style="width:180px;">Clustered Scheduler</th> <th style="width:180px;">Clustered Scheduler</th>
<td>On Memory: <span id="clus_onMemory"></span></td> <td style="width:150px">On Memory: <span id="clus_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="clus_previous"></span></td> <td>Previous Fired: <span id="clus_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="clus_next"></span></td> <td>Next Fire Time: <span id="clus_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td> <td style="width: 250px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="clus_persistJobData"></span></td> <td style="width: 120px;">PersistJobData: <span id="clus_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="clus_durable"></span></td> <td style="width: 120px;">Durable: <span id="clus_durable"></span></td>
</tr> </tr>
</table> </table>
+152 -39
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,15 +105,26 @@
data: $('#loginForm').serialize(), data: $('#loginForm').serialize(),
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
if (response.smsAuthRequired) { console.log('res', response)
if (response.changePassword) {
showChangeInitPassword(response);
}
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) {
@@ -108,16 +133,33 @@
} }
}); });
} }
function showChangeInitPassword(data) {
$('input[name="resetUserId"]').val($('input[name="userId"]').val());
$('input[name="resetPassword"]').val($('input[name="password"]').val());
$('#changeInitPassword').hide();
$('#pwdChgModal').modal('show');
}
// SMS 인증 모달 표시 // SMS 인증 모달 표시
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);
@@ -171,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('인증번호가 재발송되었습니다.');
@@ -184,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) {
@@ -193,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() {
@@ -218,46 +269,81 @@
clearInterval(resendTimer); clearInterval(resendTimer);
$('#smsAuthModal').modal('hide'); $('#smsAuthModal').modal('hide');
} }
function changePwd(){
function checkPwd(){
if ($("input[name=resetUserId]").val() == null || $("input[name=resetUserId]").val().length == 0 ){ if ($("input[name=resetUserId]").val() == null || $("input[name=resetUserId]").val().length == 0 ){
alert("<%= localeMessage.getString("login.checkid") %>"); alert("<%= localeMessage.getString("login.checkid") %>");
$("input[name=resetUserId]").trigger("focus"); $("input[name=resetUserId]").trigger("focus");
return; return false;
} }
if ($("input[name=resetPassword]").val() == null || $("input[name=resetPassword]").val().trim().length == 0 ){ if ($("input[name=resetPassword]").val() == null || $("input[name=resetPassword]").val().trim().length == 0 ){
alert("<%= localeMessage.getString("login.checkpwd1") %>"); alert("<%= localeMessage.getString("login.checkpwd1") %>");
$("input[name=resetPassword]").trigger("focus"); $("input[name=resetPassword]").trigger("focus");
return; return false;
} }
if ($("input[name=changePassword]").val() == null || $("input[name=changePassword]").val().trim().length == 0 ){ if ($("input[name=changePassword]").val() == null || $("input[name=changePassword]").val().trim().length == 0 ){
alert("<%= localeMessage.getString("login.checkpwd2") %>"); alert("<%= localeMessage.getString("login.checkpwd2") %>");
$("input[name=changePassword]").trigger("focus"); $("input[name=changePassword]").trigger("focus");
return ; return false;
} }
if ($("input[name=confirmPassword]").val() == null || $("input[name=confirmPassword]").val().trim().length == 0 ){ if ($("input[name=confirmPassword]").val() == null || $("input[name=confirmPassword]").val().trim().length == 0 ){
alert("<%= localeMessage.getString("login.checkpwd3") %>"); alert("<%= localeMessage.getString("login.checkpwd3") %>");
return ; return false;
} }
if ($("input[name=confirmPassword]").val() != $("input[name=confirmPassword]").val()){ if ($("input[name=changePassword]").val() != $("input[name=confirmPassword]").val()){
alert("<%= localeMessage.getString("login.checkpwd4") %>"); alert("<%= localeMessage.getString("login.checkpwd4") %>");
$("input[name=confirmPassword]").trigger("focus"); $("input[name=confirmPassword]").trigger("focus");
return ; return false;
} }
if ($("input[name=resetUserId]").val() == $("input[name=changePassword]").val()){ if ($("input[name=resetUserId]").val() == $("input[name=changePassword]").val()){
alert("<%= localeMessage.getString("login.checkpwd5") %>"); alert("<%= localeMessage.getString("login.checkpwd5") %>");
$("input[name=changePassword]").trigger("focus"); $("input[name=changePassword]").trigger("focus");
return ; return false;
} }
//if (idCheck.checked){ return true;
// setCookie("key",id);
//}
<%-- $("form").attr("action","<c:url value="/changePassword.do"/>");
//$("form").action = "<%=request.getContextPath()%>/changePassword.do";
--%>
$("form").submit();
} }
// 비밀번호 변경 요청 (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 ) {
@@ -271,14 +357,24 @@
}); });
$("input[name=changePassword]").keydown(function(event){ $("input[name=changePassword]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
changePwd(); event.preventDefault();
submitChangePassword();
} }
}); });
$("input[name=confirmPassword]").keydown(function(event){ $("input[name=confirmPassword]").keydown(function(event){
if ( event.which == 13 ) { if ( event.which == 13 ) {
changePwd(); event.preventDefault();
submitChangePassword();
} }
}); });
// Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음)
$('#pwdChgModal').on('show.bs.modal', function(event) {
if (event.relatedTarget) {
$('#modalLoginForm')[0].reset();
$('#changeInitPassword').show();
}
});
$("select[name=serviceType]").change(function(){ $("select[name=serviceType]").change(function(){
fncPreMain(); fncPreMain();
}); });
@@ -286,9 +382,7 @@
$("#btn_login").click(function(){ $("#btn_login").click(function(){
fncPreMain(); fncPreMain();
}); });
$("#changePwd").click(function(){
changePwd();
});
$("#btn_sso_login").click(function(){ $("#btn_sso_login").click(function(){
fncSsoLogin(); fncSsoLogin();
}); });
@@ -299,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() {
@@ -353,10 +460,10 @@
</div> </div>
<!-- SSO 로그인 버튼 (비상모드 시 숨김) --> <!-- SSO 로그인 버튼 (비상모드 시 숨김) -->
<c:if test="${not emergencyMode}"> <c:if test="${not emergencyMode}">
<div class="form-group"> <!-- <div class="form-group">
<input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3" <input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3"
value="SSO Login"> value="SSO Login">
</div> </div> -->
</c:if> </c:if>
<p style="color:red;"><%=resultMsg %></p> <p style="color:red;"><%=resultMsg %></p>
</form> </form>
@@ -366,7 +473,7 @@
</div> </div>
</div> </div>
<!-- 비밀번호 변경 모달 -->
<div class="modal fade" id="pwdChgModal" tabindex="-1" role="dialog" aria-labelledby="pwdChgModalLabel" aria-hidden="true"> <div class="modal fade" id="pwdChgModal" tabindex="-1" role="dialog" aria-labelledby="pwdChgModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
@@ -376,13 +483,15 @@
<span aria-hidden="true">X</span> <span aria-hidden="true">X</span>
</button> </button>
</div> </div>
<form name="form" id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post"> <form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return submitChangePassword()" novalidate>
<div class="modal-body"> <div class="modal-body">
<div class="form-group d-flex"> <div id="changeInitPassword">
<input type="text" name="resetUserId" class="form-control rounded-left" placeholder="User Id" required> <div class="form-group d-flex">
</div> <input type="text" name="resetUserId" class="form-control rounded-left" placeholder="User Id" required>
<div class="form-group d-flex"> </div>
<input type="password" name="resetPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderCurrentPassword") %>" autocomplete="off" required> <div class="form-group d-flex">
<input type="password" name="resetPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderCurrentPassword") %>" autocomplete="off" required>
</div>
</div> </div>
<p><%= localeMessage.getString("login.toChangePasswordMessage") %></p> <p><%= localeMessage.getString("login.toChangePasswordMessage") %></p>
<div class="form-group d-flex"> <div class="form-group d-flex">
@@ -391,9 +500,13 @@
<div class="form-group d-flex"> <div class="form-group d-flex">
<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">
※ <%= pwdLengthCheck %>글자 이상 & 영문/숫자/특수문자 <%= pwdCombiCheck %>종류 이상<br/>
※ 연속된 숫자/문자(예: 123, abc, qwer) 사용 불가<%= pwdRepeatCheck >= 2 ? ", 동일 문자 " + pwdRepeatCheck + "자 이상 반복 불가" : "" %>
</span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<input type="button" id ="changePwd" class="form-control btn btn-primary rounded submit px-3" value="변경"> <button type="submit" id="changePwd" class="form-control btn btn-primary rounded submit px-3">변경</button>
</div> </div>
</form> </form>
</div> </div>
@@ -411,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">
+103 -8
View File
@@ -52,6 +52,13 @@
var urlParam = window.document.location.search; var urlParam = window.document.location.search;
var mod = ""; var mod = "";
var strServiceType = "<%=request.getParameter("serviceType")%>"; var strServiceType = "<%=request.getParameter("serviceType")%>";
function getServiceType() {
var serviceType = sessionStorage.getItem("serviceType");
if (!serviceType || serviceType == "undefined" || serviceType == "null") {
serviceType = localStorage.getItem("serviceType") || "";
}
return serviceType;
}
if(urlParam != ""){ if(urlParam != ""){
if(urlParam.indexOf("mod=") > -1){ if(urlParam.indexOf("mod=") > -1){
mod = urlParam.substring((urlParam.indexOf("mod=") + 4), urlParam.lastIndexOf("&")); mod = urlParam.substring((urlParam.indexOf("mod=") + 4), urlParam.lastIndexOf("&"));
@@ -67,15 +74,18 @@
} }
} }
$(document).ready(function() { $(document).ready(function() {
var url = '<%=topPage%>'; var url = '<%=topPage%>';
if (url.indexOf("?")>=0){ var serviceType = getServiceType();
url = url + "&serviceType="+ sessionStorage["serviceType"]; if (serviceType) {
}else{ if (url.indexOf("?")>=0){
url = url + "?serviceType="+ sessionStorage["serviceType"]; url = url + "&serviceType="+ serviceType;
}else{
url = url + "?serviceType="+ serviceType;
}
} }
$("#topFrame").attr('src',url); $("#topFrame").attr('src',url);
$(".topMenu").load(function(){ // iframe이 모두 load된후 제어 $(".topMenu").load(function(){ // iframe이 모두 load된후 제어
$(".topMenu").contents().find('.gnb').on("mouseenter", function(e){ $(".topMenu").contents().find('.gnb').on("mouseenter", function(e){
@@ -99,6 +109,91 @@
<iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe> <iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe>
</body>
<%-- 자동 로그아웃 경고 모달 --%>
<div id="autoLogoutModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:99999;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:30px 40px; border-radius:4px; text-align:center; min-width:320px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<h3 style="margin:0 0 12px; color:#333; font-size:16px;">자동 로그아웃 안내</h3>
<p style="margin:0 0 22px; color:#666; font-size:14px;">
장시간 사용하지 않아 <strong><span id="autoLogoutSeconds">60</span>초</strong> 후 자동 로그아웃됩니다.
</p>
<button id="autoLogoutContinue" style="margin-right:8px; padding:8px 22px; background:#337ab7; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">계속 사용</button>
<button id="autoLogoutNow" style="padding:8px 22px; background:#d9534f; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">로그아웃</button>
</div>
</div>
<script type="text/javascript">
(function () {
var IDLE_MS = ${autoLogoutTimeout} * 60 * 1000; // 설정값(분) → ms
var WARN_MS = 60 * 1000; // 로그아웃 1분 전 경고
var CHECK_INTERVAL = 5000; // 5초 간격 체크
var countdownTimer = null;
/* sessionStorage: iframe 포함 모든 동일-origin 프레임과 공유 */
function getLastActivity() {
var t = sessionStorage.getItem('lastActivity');
return t ? parseInt(t, 10) : Date.now();
}
function markActivity() {
sessionStorage.setItem('lastActivity', String(Date.now()));
}
function resetTimer() {
markActivity();
if ($('#autoLogoutModal').is(':visible')) {
$('#autoLogoutModal').hide();
clearInterval(countdownTimer);
}
}
function showWarning(remainMs) {
var sec = Math.ceil(remainMs / 1000);
$('#autoLogoutSeconds').text(sec);
$('#autoLogoutModal').show();
clearInterval(countdownTimer);
countdownTimer = setInterval(function () {
sec--;
if (sec <= 0) {
clearInterval(countdownTimer);
doLogout();
} else {
$('#autoLogoutSeconds').text(sec);
}
}, 1000);
}
function doLogout() {
location.href = '<%=request.getContextPath()%>/rms/logout.do';
}
/* 상위 프레임(main.jsp)에서 발생하는 AJAX 요청만 활동으로 기록 (마우스 이동/클릭만으로는 갱신되지 않음) */
$(document).ajaxSend(markActivity);
/* 주기적 유휴 시간 체크 */
setInterval(function () {
var idle = Date.now() - getLastActivity();
var remain = IDLE_MS - idle;
if (remain <= 0) {
clearInterval(countdownTimer);
doLogout();
} else if (remain <= WARN_MS && !$('#autoLogoutModal').is(':visible')) {
showWarning(remain);
}
}, CHECK_INTERVAL);
/* 버튼 이벤트 */
$(document).on('click', '#autoLogoutContinue', function () {
resetTimer();
$.get('<%=request.getContextPath()%>/checkSession.do'); // 서버 세션도 갱신
});
$(document).on('click', '#autoLogoutNow', function () {
doLogout();
});
/* 페이지 로드 시 초기화 */
markActivity();
})();
</script>
</body>
</html> </html>
+60 -9
View File
@@ -9,6 +9,8 @@
<%@page import="com.eactive.eai.rms.common.util.StringUtils" %> <%@page import="com.eactive.eai.rms.common.util.StringUtils" %>
<%@page import="com.eactive.eai.rms.common.datasource.DataSourceType" %> <%@page import="com.eactive.eai.rms.common.datasource.DataSourceType" %>
<%@page import="com.eactive.eai.rms.common.datasource.DataSourceTypeManager" %> <%@page import="com.eactive.eai.rms.common.datasource.DataSourceTypeManager" %>
<%@page import="com.eactive.eai.common.util.ApplicationContextProvider" %>
<%@page import="com.eactive.eai.rms.data.entity.man.role.RoleMenuAuthService" %>
<%@ 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" %>
@@ -16,6 +18,12 @@
<% <%
String serviceKey = SessionManager.getServiceTypeKey(request); String serviceKey = SessionManager.getServiceTypeKey(request);
String serviceText = DataSourceTypeManager.getDataSourceType(serviceKey).getText(); String serviceText = DataSourceTypeManager.getDataSourceType(serviceKey).getText();
final String SITEMAP_MENU_ID = "0510013";
RoleMenuAuthService roleMenuAuthService = ApplicationContextProvider.getContext().getBean(RoleMenuAuthService.class);
String sitemapMenuAuth = roleMenuAuthService.getRoleList(SessionManager.getUserId(request), SITEMAP_MENU_ID, serviceKey);
boolean showSitemapLink = "R".equals(sitemapMenuAuth) || "W".equals(sitemapMenuAuth);
String roleScreenId = (String) request.getAttribute("roleScreenId"); String roleScreenId = (String) request.getAttribute("roleScreenId");
String mainPage = (String) request.getAttribute("mainPage"); String mainPage = (String) request.getAttribute("mainPage");
String menuId = (String) request.getAttribute("menuId"); String menuId = (String) request.getAttribute("menuId");
@@ -240,6 +248,12 @@
<link rel="stylesheet" type="text/css" media="screen" href="<c:url value="/css/web_ui.css"/>"/> <link rel="stylesheet" type="text/css" media="screen" href="<c:url value="/css/web_ui.css"/>"/>
<link rel="stylesheet" type="text/css" media="screen" href="<c:url value="/css/theme_${themeColor}.css"/>"/> <link rel="stylesheet" type="text/css" media="screen" href="<c:url value="/css/theme_${themeColor}.css"/>"/>
<style>
.topmenu_box .sitemap-link { display: inline-block; width: 80px; height: 80px; line-height: 80px; box-sizing: border-box; font-size: 14px; color: #666; text-align: center; opacity: 0.7; transition: all 0.3s; margin-top: 0px; }
.topmenu_box .sitemap-link:hover { opacity: 1; }
.topmenu_box .logout-link { line-height: 80px; margin-top:0; }
.topmenu_box > ul li:last-child > a:before { display: none; }
</style>
<script language="javascript" src="<c:url value="/js/jquery-1.12.1.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery-1.12.1.min.js"/>"></script>
<script language="javascript" src="<c:url value="/js/prefixfree.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/prefixfree.min.js"/>"></script>
<script language="javascript" src="<c:url value="/js/jquery.cookie.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.cookie.js"/>"></script>
@@ -303,11 +317,11 @@
} }
//왼쪽메뉴, 메인 페이지 이동 //왼쪽메뉴, 메인 페이지 이동
function goPage2(page1, page2, page2_id) { function goPage2(page1, page2, page2_id) {
parent.leftFrame.location.href = goNavUrl(page1); parent.leftFrame.location.href = goNavUrl(page1);
parent.mainFrame.location.href = goNavUrl(page2); parent.mainFrame.location.href = goNavUrl(page2);
} }
var sessionAjax = createAjaxRequest(); var sessionAjax = createAjaxRequest();
function getAjaxData() { function getAjaxData() {
@@ -529,6 +543,7 @@
console.log("selectLocaleValue : " + selectLocaleValue); console.log("selectLocaleValue : " + selectLocaleValue);
parent.changeLocale($(this).val()); parent.changeLocale($(this).val());
}); });
}); });
$(function() { $(function() {
@@ -540,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>
@@ -551,21 +594,29 @@
</h1><!-- /monitoring/images/top_logo.png --> </h1><!-- /monitoring/images/top_logo.png -->
<div class="topmenu_box"> <div class="topmenu_box">
<ul> <ul>
<li style="width:240px;" id="newDashboardShow"> <li style="width:240px;">
<a style="width:240px;" <a style="width:240px; cursor: default"
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</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> <li style="width:100px; line-height:80px; text-align:center;">
<select id="selectLocale" name="locale" style="margin-top: 20px; width:80px"> <select id="selectLocale" name="locale" style="width:60px; padding: 2px; text-align:center;">
<option value="en">English</option> <option value="en">English</option>
<option value="ko">한글</option> <option value="ko">한글</option>
</select> </select>
</li> </li>
<li onclick="logout()"><a href="#" class=""><img src="<c:url value="/img/icon_logout.png"/>" <% if (showSitemapLink) { %>
<li>
<a href="<c:url value="/common/acl/sitemap/sitemapMan.view"><c:param name="menuId" value="<%=SITEMAP_MENU_ID%>"/><c:param name="serviceType" value="<%=serviceKey%>"/></c:url>" target="mainFrame" class="sitemap-link">
<%= localeMessage.getString("screen.sitemap") %>
</a>
</li>
<% } %>
<li onclick="logout()"><a href="#" class="logout-link"><img src="<c:url value="/img/icon_logout.png"/>"
alt=""/><%= localeMessage.getString("screen.logout") %> alt=""/><%= localeMessage.getString("screen.logout") %>
</a></li> </a></li>
</ul> </ul>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
+40 -23
View File
@@ -1,5 +1,10 @@
/** /**
* 문자열 마스킹 처리를 위한 유틸리티 * 문자열 마스킹 처리를 위한 유틸리티
*
* 보안아키텍처 표준 마스킹 규칙(Java MaskingUtils)과 동일한 결과를 내도록 구현한다.
* 이름 2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라)
* 이메일 아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**)
* 전화 2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**)
*/ */
const StringMaskingUtil = { const StringMaskingUtil = {
@@ -8,41 +13,51 @@ const StringMaskingUtil = {
return str != null && str !== ''; return str != null && str !== '';
}, },
// 이름 마스킹 처리 // '*' 반복 생성
_stars: function(count) {
return count > 0 ? '*'.repeat(count) : '';
},
// 세그먼트 뒤 count 자리 마스킹
_maskSegmentTail: function(segment, count) {
if (segment.length <= count) {
return this._stars(segment.length);
}
return segment.substring(0, segment.length - count) + this._stars(count);
},
// 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체)
maskName: function(name) { maskName: function(name) {
if (!this.isValidString(name)) { if (!this.isValidString(name)) {
return name; return name;
} }
return name.length > 1 ? const len = name.length;
name.substring(0, name.length - 1) + "*" : if (len === 1) {
name; return name;
}
if (len === 2) {
return name.charAt(0) + '*';
}
return name.charAt(0) + this._stars(len - 2) + name.charAt(len - 1);
}, },
// 이메일 마스킹 처리 // 이메일 마스킹 처리 (아이디 앞2·뒤2, 4자 이하 전체)
maskEmail: function(email) { maskEmail: function(email) {
if (!this.isValidString(email) || !email.includes('@')) { if (!this.isValidString(email) || !email.includes('@')) {
return email; return email;
} }
const parts = email.split('@'); const atIdx = email.indexOf('@');
if (parts.length !== 2) { const local = email.substring(0, atIdx);
return email; const domain = email.substring(atIdx);
if (local.length <= 4) {
return this._stars(local.length) + domain;
} }
return this._stars(2) + local.substring(2, local.length - 2) + this._stars(2) + domain;
const localPart = parts[0];
let maskedLocal;
if (localPart.length <= 3) {
maskedLocal = localPart.substring(0, 1) +
'*'.repeat(localPart.length - 1);
} else {
maskedLocal = localPart.substring(0, localPart.length - 3) + '***';
}
return maskedLocal + '@' + parts[1];
}, },
// 휴대폰 번호 마스킹 처리 // 휴대폰/전화 번호 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리)
maskMobileNumber: function(number) { maskMobileNumber: function(number) {
if (!this.isValidString(number)) { if (!this.isValidString(number)) {
return number; return number;
@@ -50,8 +65,10 @@ const StringMaskingUtil = {
const parts = number.split('-'); const parts = number.split('-');
if (parts.length === 3) { if (parts.length === 3) {
return parts[0] + '-****-' + parts[2]; return parts[0] + '-' +
this._maskSegmentTail(parts[1], 2) + '-' +
this._maskSegmentTail(parts[2], 2);
} }
return number; return number;
} }
}; };
@@ -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");
@@ -82,8 +82,10 @@
apiDesc: api.eaiSvcDesc apiDesc: api.eaiSvcDesc
}; };
}); });
// addJSONData는 datatype:"local" 그리드의 p.data를 채우지 않아, 헤더 클릭 정렬 시 p.data(빈 배열) 기준으로 다시 그려지며 행이 사라짐.
// setGridParam({data:...}).trigger('reloadGrid')로 채워야 정렬이 정상 동작함.
$("#selectedApiGrid").jqGrid("clearGridData"); $("#selectedApiGrid").jqGrid("clearGridData");
$("#selectedApiGrid")[0].addJSONData(gridData); $("#selectedApiGrid").jqGrid('setGridParam', {data: gridData}).trigger('reloadGrid');
var data = json; var data = json;
$("#btn_gen_id").hide(); $("#btn_gen_id").hide();
@@ -133,15 +135,14 @@
var selectedApiGrid = $("#selectedApiGrid"); var selectedApiGrid = $("#selectedApiGrid");
selectedApiGrid.jqGrid({ selectedApiGrid.jqGrid({
datatype: "local", datatype: "local",
gridview: true, // 성능 향상 rownumbers: true, // 가상 인덱스
rownumbers: true, // 가상 인덱스
rownumWidth: 40, rownumWidth: 40,
colNames: ['API ID', 'API 이름'], colNames: ['API ID', 'API 이름'],
colModel: [ colModel: [
{name: 'apiId', index: 'apiId', width: 100, align: 'center'}, {name: 'apiId', index: 'apiId', width: 100, align: 'center'},
{name: 'apiDesc', index: 'apiDesc', width: 100, align: 'center'} {name: 'apiDesc', index: 'apiDesc', width: 100, align: 'center'}
], ],
rowNum: -1, rowNum: 9999,
// rowList: [10, 20, 30], // rowList: [10, 20, 30],
// pager: '#selectedApiGridPager', // pager: '#selectedApiGridPager',
// sortname: 'apiId', // sortname: 'apiId',
@@ -156,8 +157,8 @@
} }
}); });
selectedApiGrid.jqGrid('setLabel', 'rn', 'No'); // 가상 인덱스 ColNames selectedApiGrid.jqGrid('setLabel', 'rn', 'No'); // 가상 인덱스 ColNames
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: url, url: url,
@@ -189,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);
@@ -305,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);
@@ -505,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>
@@ -537,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>
@@ -554,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>
@@ -739,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;
@@ -769,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);
@@ -782,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 = "반영됨";
@@ -19,7 +19,6 @@
var url = '<c:url value="/onl/admin/authserver/scopeMan.json" />'; var url = '<c:url value="/onl/admin/authserver/scopeMan.json" />';
var url_view = '<c:url value="/onl/admin/authserver/scopeMan.view" />'; var url_view = '<c:url value="/onl/admin/authserver/scopeMan.view" />';
var api_url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />'; var api_url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />';
var mapping_url = '<c:url value="/onl/admin/authserver/apiScopeMan.json" />';
var isDetail = false; var isDetail = false;
function isValid(){ function isValid(){
@@ -65,9 +64,9 @@ function mappingInfo(key){
$.ajax({ $.ajax({
type : "POST", type : "POST",
url: mapping_url, url: url,
dataType: "json", dataType: "json",
data: {cmd: 'LIST', searchScopeId : key}, data: {cmd: 'API_LIST', searchScopeId : key},
success: function(json){ success: function(json){
var mappingData = json.rows; var mappingData = json.rows;
@@ -131,45 +130,6 @@ function init(key, callback){
} }
} }
function saveApiGridData(scopeId, returnUrl) {
var postData = [];
var rows = $("#selectedApiGrid").jqGrid('getRowData');
$.each(rows, function(index, item) {
item.scopeId = scopeId;
});
postData.push({
name: "apiList",
value: JSON.stringify(rows)
});
postData.push({ name: "cmd" , value:"INSERT_APILIST" });
postData.push({ name: "scopeId" , value:scopeId });
$.ajax({
type : "POST",
url:mapping_url,
data: postData,
beforeSend: function() {
$("[id^='btn_']").prop("disabled", true);
$("#btn_modify").text("처리중 . . . . .");
},
success:function(args){
alert("저장 되었습니다.");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
},
complete: function() {
$("[id^='btn_']").prop("disabled", false);
$("#btn_modify").text("수정");
}
});
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key ="${param.scopeId}"; var key ="${param.scopeId}";
@@ -182,28 +142,39 @@ $(document).ready(function() {
if (!isValid()){ if (!isValid()){
return; return;
} }
var scopeId = $('input[name=scopeId]').val();
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
var rows = $("#selectedApiGrid").jqGrid('getRowData');
$.each(rows, function(index, item) {
item.scopeId = scopeId;
});
postData.push({ name: "apiList" , value: JSON.stringify(rows) });
if (isDetail){ if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"}); postData.push({ name: "cmd" , value:"UPDATE"});
}else{ }else{
postData.push({ name: "cmd" , value:"INSERT"}); postData.push({ name: "cmd" , value:"INSERT"});
} }
$.ajax({ $.ajax({
type : "POST", type : "POST",
url:url, url:url,
data:postData, data:postData,
beforeSend: function() {
$("[id^='btn_']").prop("disabled", true);
$("#btn_modify").text("처리중 . . . . .");
},
success:function(args){ success:function(args){
alert("SCOPE 정보가 저장 되었습니다. 이어서 API 리스트를 저장합니다."); alert("저장 되었습니다.");
goNav(returnUrl);//LIST로 이동
if(!isDetail) { // INSERT 일 경우 ScopeId를 업데이트
key = $('input[name=scopeId]').val();
}
saveApiGridData(key, returnUrl); // API LIST 저장.
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
},
complete: function() {
$("[id^='btn_']").prop("disabled", false);
$("#btn_modify").text("수정");
} }
}); });
}); });
@@ -250,6 +250,29 @@ var url_view = '<c:url value="/onl/admin/common/propertyMan.view" />';
}); });
}); });
$("#btn_pop_encrypt").click(function() {
var prpty2Val = $("input[name=prpty2Val]").val();
if (prpty2Val == '') {
alert("프라퍼티 값을 입력하세요");
return;
}
var postData = {
cmd : "ENCRYPT",
prpty2Val : prpty2Val,
};
$.ajax({
type : "POST",
url : url,
data : postData,
success : function(args) {
$("input[name=prpty2Val]").val(args);
},
error : function(e) {
alert(e.responseText);
}
});
});
buttonControl(key); buttonControl(key);
titleControl(key); titleControl(key);
@@ -289,12 +312,17 @@ var url_view = '<c:url value="/onl/admin/common/propertyMan.view" />';
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyKey") %></th> <th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyKey") %></th>
<td><input type="text" name="prptyName" style="width:calc(100% - 85px);" /> <!-- <img src="<c:url value="/img/btn_pop_input.png"/>" id="btn_pop_input" /></td> --> <td>
<button type="button" class="cssbtn smallBtn2" id="btn_pop_input" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">input</i><%= localeMessage.getString("button.input") %></button> <input type="text" name="prptyName" style="width:calc(100% - 85px);" /> <!-- <img src="<c:url value="/img/btn_pop_input.png"/>" id="btn_pop_input" /></td> -->
<button type="button" class="cssbtn smallBtn2" id="btn_pop_input" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">input</i><%= localeMessage.getString("button.input") %></button>
</td>
</tr> </tr>
<tr> <tr>
<th><%= localeMessage.getString("propertyDetail.propertyValue") %></th> <th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyValue") %></th>
<td><input type="text" name="prpty2Val"/></td> <td>
<input type="text" name="prpty2Val" style="width:calc(100% - 85px);"/>
<button type="button" class="cssbtn smallBtn2" id="btn_pop_encrypt" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">lock</i><%= localeMessage.getString("button.encode") %></button>
</td>
</tr> </tr>
</table> </table>
<!-- grid --> <!-- grid -->
@@ -25,7 +25,7 @@ $(document).ready(function() {
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()}, postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
colNames:['<%= localeMessage.getString("infAdpConMan.adaNm") %>', colNames:['<%= localeMessage.getString("infAdpConMan.adaNm") %>',
'<%= localeMessage.getString("infAdpConMan.adaDes") %>', '<%= localeMessage.getString("infAdpConMan.adaDes") %>',
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>', '<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
@@ -62,6 +62,7 @@ $(document).ready(function() {
url2 += '&menuId='+'${param.menuId}'; url2 += '&menuId='+'${param.menuId}';
//검색값 //검색값
url2 += '&searchName='+$("input[name=searchName]").val(); url2 += '&searchName='+$("input[name=searchName]").val();
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
//key값 //key값
url2 += '&name='+name; url2 += '&name='+name;
goNav(url2); goNav(url2);
@@ -80,7 +81,7 @@ $(document).ready(function() {
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid"); $("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
}); });
$("#btn_new").click(function(){ $("#btn_new").click(function(){
var url2 = url_view; var url2 = url_view;
@@ -126,6 +127,16 @@ $(document).ready(function() {
<tr> <tr>
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th> <th style="width:180px;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="searchName" value="${param.searchName}"></td> <td><input type="text" name="searchName" value="${param.searchName}"></td>
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="searchUseYn">
<option value="">전체</option>
<option value="1">사용함</option>
<option value="0">사용안함</option>
</select>
</div>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -16,173 +16,486 @@
<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" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
$.ajax({ function updateThresholdLabel() {
type : "POST", var timeUnit = $('select[name=thresholdTimeUnit]').val();
url:url, var $input = $('#thresholdInput');
dataType:"json", var $unit = $('#thresholdUnit');
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){ var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') {
callback(url,key); if (typeof callback === 'function') {
callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return; function detail(url, key) {
$.ajax({ if (!isDetail) return;
type : "POST", $.ajax({
url:url, type: "POST",
dataType:"json", url: url,
data:{cmd: 'DETAIL', name : key}, dataType: "json",
success:function(json){ data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json; var data = json;
$("input[name=name]").attr('readonly',true); $("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly',true); $("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ $("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name"); var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); function switchTab(tabName) {
var key ="${param.name}"; $('.detail-tab').removeClass('active');
if (key != "" && key !="null"){ $('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
isDetail = true; $('.tab-content').removeClass('active');
} $('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
init(url,key,detail);
if (tabName === 'status' && isDetail) {
loadBucketStatus();
$("#btn_modify").click(function(){ }
var postData = $('#ajaxForm').serializeArray(); }
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"}); function loadBucketStatus() {
}else{ $('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
postData.push({ name: "cmd" , value:"INSERT"});
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', adapterId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
} }
$.ajax({ });
type : "POST", }
url:url,
data:postData, function formatDateTime(date) {
success:function(args){ var h = date.getHours();
alert("<%= localeMessage.getString("common.saveMsg") %>"); var m = date.getMinutes();
goNav(returnUrl);//LIST로 이동 var s = date.getSeconds();
}, return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
error:function(e){ }
alert(e.responseText);
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '어댑터가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 어댑터를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
}, $("#btn_delete").click(function() {
error:function(e){ $(this).blur();
alert(e.responseText); var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + adapterName + ']</strong> 어댑터를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
}); });
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<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="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></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_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%> </div>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<div class="title-wrap">
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip"><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,150 @@
<%@ 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/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
url: url,
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
colNames:['클라이언트 ID',
'클라이언트명',
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
'<%= localeMessage.getString("infAdpConMan.thr") %>',
'<%= localeMessage.getString("infAdpConMan.thrTimeUnit") %>',
'<%= localeMessage.getString("infAdpConMan.useYn") %>'
],
colModel:[
{ name : 'NAME' , align:'left' , width:'100' , sortable:false },
{ name : 'DESC' , align:'left' , width:'200'},
{ name : 'THRESHOLDPERSECOND' , align:'right' , width:'60'},
{ name : 'THRESHOLD' , align:'right' , width:'60'},
{ name : 'THRESHOLDTIMEUNIT' , align:'center' , width:'80'},
{ name : 'USEYN' , align:'center' , width:'60' , editoptions:{value:"0:사용안함;1:사용함"}, formatter:"select" }
],
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 name = rowData['NAME'];
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$(this).getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName='+$("input[name=searchName]").val();
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
//key값
url2 += '&name='+name;
goNav(url2);
},
gridComplete:function (d){
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
}
});
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
});
$("#btn_new").click(function(){
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$("#grid").getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName=';
url2 += '&searchUseYn='+$('select[name=searchUseYn]').val();
goNav(url2);
});
$("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">클라이언트(API Key) 유량제어를 관리한다</span></div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
<th style="width:180px;">클라이언트명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
<th style="width:180px;">사용여부</th>
<td>
<div class="select-style">
<select name="searchUseYn">
<option value="">전체</option>
<option value="1">사용함</option>
<option value="0">사용안함</option>
</select>
</div>
</td>
</tr>
</tbody>
</table>
<table id="grid" ></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,512 @@
<%@ 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"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false;
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
//버킷 현황 조회
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', clientId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
// 전체 합산 계산
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
// 요약 영역 표시
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 클라이언트를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
console.log('mod', args);
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function() {
$(this).blur();
var clientName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + clientName + ']</strong> 클라이언트를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
});
</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_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="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></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>
</div>
<div class="title-wrap">
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트 유량제어</span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>클라이언트 ID</th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th>클라이언트명</th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -47,27 +47,40 @@ function groupNameFormat(cellvalue, options, rowObject) {
return strVal; return strVal;
} }
$(document).ready(function() { $(document).ready(function() {
$("input[name=searchStartYYYYMMDD],input[name=searchEndYYYYMMDD]").inputmask("yyyy-mm-dd",{'autoUnmask':true}); $("input[name=searchStartYYYYMMDD],input[name=searchEndYYYYMMDD]").inputmask("yyyy-mm-dd",{'autoUnmask':true});
$("input[name=searchStartTime],input[name=searchEndTime]").inputmask("hh:mm:ss",{'autoUnmask':true});
$("input[name=searchStartYYYYMMDD]").each(function(){ $("input[name=searchStartYYYYMMDD]").each(function(){
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){ if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
$(this).val(getToday()); $(this).val(getToday());
} }
}); });
$("input[name=searchEndYYYYMMDD]").each(function(){ $("input[name=searchEndYYYYMMDD]").each(function(){
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){ if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
$(this).val(getToday()); $(this).val(getToday());
} }
}); });
$("input[name=searchStartTime]").each(function(){
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
$(this).val("000000");
}
});
$("input[name=searchEndTime]").each(function(){
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
$(this).val("235959");
}
});
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,""); var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,"");
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,""); var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,"");
gridPostData["searchStartDate"] = start; gridPostData["searchStartDate"] = start;
gridPostData["searchEndDate"] = end; gridPostData["searchEndDate"] = end;
@@ -132,18 +145,20 @@ $(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 로
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,""); var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,"");
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,""); var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,"");
var startTime = $("input[name=searchStartTime]").val();
if(start > end){ var endTime = $("input[name=searchEndTime]").val();
if((start + startTime) > (end + endTime)){
alert("조회기간을 확인해주세요."); alert("조회기간을 확인해주세요.");
return false; return false;
} }
$("input[name=searchStartDate]").val(start); $("input[name=searchStartDate]").val(start);
$("input[name=searchEndDate]").val(end); $("input[name=searchEndDate]").val(end);
postData["searchStartDate"] = start; postData["searchStartDate"] = start;
postData["searchEndDate"] = end; postData["searchEndDate"] = end;
@@ -197,14 +212,24 @@ $(document).ready(function() {
<tbody> <tbody>
<tr> <tr>
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.date") %></th> <th style="width:180px;"><%= localeMessage.getString("infConHstMan.date") %></th>
<td> <td colspan="5">
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:80px; border:1px solid #ebebec;"> <input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:80px; border:1px solid #ebebec;">
~ <input type="text" name="searchStartTime" value="" size="8" style="width:70px; border:1px solid #ebebec;">
~
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:80px; border:1px solid #ebebec;"> <input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:80px; border:1px solid #ebebec;">
<input type="text" name="searchEndTime" value="" size="8" style="width:70px; border:1px solid #ebebec;">
<input type="hidden" name="searchStartDate" value="" style="width:0px;"> <input type="hidden" name="searchStartDate" value="" style="width:0px;">
<input type="hidden" name="searchEndDate" value="" style="width:0px;"> <input type="hidden" name="searchEndDate" value="" style="width:0px;">
</td> </td>
</tr> </tr>
<tr>
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.instNm") %></th>
<td><input type="text" name="searchInstanceName" value="${param.searchInstanceName}"></td>
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.eaiSvcCd") %></th>
<td><input type="text" name="searchApiName" value="${param.searchApiName}"></td>
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.adpGrpNm") %></th>
<td><input type="text" name="searchAdapterGroupName" value="${param.searchAdapterGroupName}"></td>
</tr>
</tbody> </tbody>
</table> </table>
</form> </form>
@@ -88,7 +88,7 @@ $(document).ready(function() {
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val()}, postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
colNames:['그룹 ID', colNames:['그룹 ID',
'그룹명', '그룹명',
'초당 임계치', '초당 임계치',
@@ -125,6 +125,7 @@ $(document).ready(function() {
url2 += '&menuId='+'${param.menuId}'; url2 += '&menuId='+'${param.menuId}';
//검색값 //검색값
url2 += '&searchGroupName='+$("input[name=searchGroupName]").val(); url2 += '&searchGroupName='+$("input[name=searchGroupName]").val();
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
//key값 //key값
url2 += '&groupId='+groupId; url2 += '&groupId='+groupId;
goNav(url2); goNav(url2);
@@ -143,7 +144,7 @@ $(document).ready(function() {
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val()}, page:1 }).trigger("reloadGrid"); $("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
}); });
$("#btn_new").click(function(){ $("#btn_new").click(function(){
var url2 = url_view; var url2 = url_view;
@@ -188,6 +189,16 @@ $(document).ready(function() {
<tr> <tr>
<th style="width:180px;">그룹명</th> <th style="width:180px;">그룹명</th>
<td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td> <td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td>
<th style="width:180px;">사용여부</th>
<td>
<div class="select-style">
<select name="searchUseYn">
<option value="">전체</option>
<option value="1">사용함</option>
<option value="0">사용안함</option>
</select>
</div>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -25,7 +25,7 @@ $(document).ready(function() {
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()}, postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
colNames:['<%= localeMessage.getString("infIfConMan.ifNm") %>', colNames:['<%= localeMessage.getString("infIfConMan.ifNm") %>',
'<%= localeMessage.getString("infIfConMan.ifDesc") %>', '<%= localeMessage.getString("infIfConMan.ifDesc") %>',
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>', '<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
@@ -62,6 +62,7 @@ $(document).ready(function() {
url2 += '&menuId='+'${param.menuId}'; url2 += '&menuId='+'${param.menuId}';
//검색값 //검색값
url2 += '&searchName='+$("input[name=searchName]").val(); url2 += '&searchName='+$("input[name=searchName]").val();
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
//key값 //key값
url2 += '&name='+name; url2 += '&name='+name;
goNav(url2); goNav(url2);
@@ -80,7 +81,7 @@ $(document).ready(function() {
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid"); $("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
}); });
$("#btn_new").click(function(){ $("#btn_new").click(function(){
var url2 = url_view; var url2 = url_view;
@@ -126,6 +127,16 @@ $(document).ready(function() {
<tr> <tr>
<th style="width:180px;"><%= localeMessage.getString("infIfConMan.ifNm") %></th> <th style="width:180px;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="searchName" value="${param.searchName}"></td> <td><input type="text" name="searchName" value="${param.searchName}"></td>
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="searchUseYn">
<option value="">전체</option>
<option value="1">사용함</option>
<option value="0">사용안함</option>
</select>
</div>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -16,173 +16,486 @@
<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" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
$.ajax({ function updateThresholdLabel() {
type : "POST", var timeUnit = $('select[name=thresholdTimeUnit]').val();
url:url, var $input = $('#thresholdInput');
dataType:"json", var $unit = $('#thresholdUnit');
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){ var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') {
callback(url,key); if (typeof callback === 'function') {
callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return; function detail(url, key) {
$.ajax({ if (!isDetail) return;
type : "POST", $.ajax({
url:url, type: "POST",
dataType:"json", url: url,
data:{cmd: 'DETAIL', name : key}, dataType: "json",
success:function(json){ data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json; var data = json;
$("input[name=name]").attr('readonly',true); $("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly',true); $("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ $("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name"); var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); function switchTab(tabName) {
var key ="${param.name}"; $('.detail-tab').removeClass('active');
if (key != "" && key !="null"){ $('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
isDetail = true; $('.tab-content').removeClass('active');
} $('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
init(url,key,detail);
if (tabName === 'status' && isDetail) {
loadBucketStatus();
$("#btn_modify").click(function(){ }
var postData = $('#ajaxForm').serializeArray(); }
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"}); function loadBucketStatus() {
}else{ $('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
postData.push({ name: "cmd" , value:"INSERT"});
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', interfaceId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
} }
$.ajax({ });
type : "POST", }
url:url,
data:postData, function formatDateTime(date) {
success:function(args){ var h = date.getHours();
alert("<%= localeMessage.getString("common.saveMsg") %>"); var m = date.getMinutes();
goNav(returnUrl);//LIST로 이동 var s = date.getSeconds();
}, return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
error:function(e){ }
alert(e.responseText);
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '인터페이스가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 인터페이스를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
}, $("#btn_delete").click(function() {
error:function(e){ $(this).blur();
alert(e.responseText); var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + ifName + ']</strong> 인터페이스를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
}); });
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<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="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></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_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%> </div>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infIfConMan.title") %></span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<div class="title-wrap">
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip"><%= localeMessage.getString("infIfConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" style="width:300px;background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" style="width:300px;background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -34,7 +34,7 @@
, {id:'LLLVAR',name:'[ISO8583] LLLVAR'}, {id:'LLLLVAR',name:'[ISO8583] LLLLVAR'}, {id:'BINARY',name:'[ISO8583] BINARY'}, {id:'LLBIN',name:'[ISO8583] LLBIN'} , {id:'LLLVAR',name:'[ISO8583] LLLVAR'}, {id:'LLLLVAR',name:'[ISO8583] LLLLVAR'}, {id:'BINARY',name:'[ISO8583] BINARY'}, {id:'LLBIN',name:'[ISO8583] LLBIN'}
, {id:'LLLBIN',name:'[ISO8583] LLLBIN'}]; , {id:'LLLBIN',name:'[ISO8583] LLLBIN'}];
const basicDataTypes = [{id:'BigDecimal',name:'BigDecimal'},{id:'String',name:'String'}]; const basicDataTypes = [{id:'BigDecimal',name:'BigDecimal'},{id:'String',name:'String'},{id:'Boolean',name:'Boolean'}];
function gridRenderingInit() { function gridRenderingInit() {
$('#effectGrid').jqGrid({ $('#effectGrid').jqGrid({
@@ -71,18 +71,18 @@
$('#grid').children().remove(); $('#grid').children().remove();
const itemTypes = ['Field', 'Grid', 'Group', 'Attr']; const itemTypes = ['Field', 'Grid', 'Group', 'Attr', 'Array'];
columns = [ // for columnData prop columns = [ // for columnData prop
{ title: '<%= localeMessage.getString("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
{ title: '<%= localeMessage.getString("layoutMan.itemDesc") %>', name: 'loutItemDesc', type: 'text', width:280, resize:true, minWidth:120 }, { title: '<%= localeMessage.getString("layoutMan.itemDesc") %>', name: 'loutItemDesc', type: 'text', width:280, resize:true, minWidth:120 },
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'text', width:50 }, { title: '<%= localeMessage.getString("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'numeric', width:50 },
{ title: '<%= localeMessage.getString("standardLayout.itemType") %>', name: 'loutItemType', type: 'autocomplete', source: itemTypes, width:70 }, { title: '<%= localeMessage.getString("standardLayout.itemType") %>', name: 'loutItemType', type: 'autocomplete', source: itemTypes, width:70 },
{ title: '<%= localeMessage.getString("standardLayout.arraySize") %>', name: 'loutItemOccCnt', type: 'text', width:90 }, { title: '<%= localeMessage.getString("standardLayout.arraySize") %>', name: 'loutItemOccCnt', type: 'text', width:90 },
{ title: '<%= localeMessage.getString("standardLayout.refField") %>', name: 'loutItemOccRef', type: 'text', width:90 }, { title: '<%= localeMessage.getString("standardLayout.refField") %>', name: 'loutItemOccRef', type: 'text', width:90 },
{ title: '<%=localeMessage.getString("layoutMan.dataType")%>', name: 'loutItemDataType', type: 'autocomplete', source: basicDataTypes, width:160 }, { title: '<%=localeMessage.getString("layoutMan.dataType")%>', name: 'loutItemDataType', type: 'autocomplete', source: basicDataTypes, width:160 },
{ title: '<%=localeMessage.getString("layoutMan.dataLen")%>', name: 'loutItemLength', type: 'numeric', width: 70, align: 'center' }, { title: '<%=localeMessage.getString("layoutMan.dataLen")%>', name: 'loutItemLength', type: 'numeric', width: 70, align: 'center' },
{ title: '<%=localeMessage.getString("layoutMan.decpLen")%>', name: 'loutItemDecimal', type: 'text', width: 70 }, { title: '<%=localeMessage.getString("layoutMan.decpLen")%>', name: 'loutItemDecimal', type: 'numeric', width: 70 },
{ title: '<%=localeMessage.getString("layoutMan.defaultVal")%>', name: 'loutItemDefault', type: 'text', width: 70 }, { title: '<%=localeMessage.getString("layoutMan.defaultVal")%>', name: 'loutItemDefault', type: 'text', width: 70 },
{ title: '<%=localeMessage.getString("layoutMan.mask")%>', name: 'loutItemMaskYn', type: 'checkbox', width: 70 }, { title: '<%=localeMessage.getString("layoutMan.mask")%>', name: 'loutItemMaskYn', type: 'checkbox', width: 70 },
{ title: '<%=localeMessage.getString("layoutMan.moffset")%>', name: 'loutItemMaskOffset', type: 'text', width: 50 }, { title: '<%=localeMessage.getString("layoutMan.moffset")%>', name: 'loutItemMaskOffset', type: 'text', width: 50 },
@@ -182,6 +182,41 @@
$('#dataTotalLen').text(totalLen); $('#dataTotalLen').text(totalLen);
} }
function focusGridCell(colIndex, rowIndex) {
var cellName = jexcel.getColumnNameFromId([colIndex, rowIndex]);
var cell = grid.getCell(cellName);
grid.updateSelection(cell);
cell.scrollIntoView({block: 'center', inline: 'center'});
grid.openEditor(cell, false);
}
var INTEGER_REGEXP = /^(0|[1-9][0-9]*)$/;
function validateNumberColumns() {
var data = grid.getData();
var gridColumns = grid.options.columns;
for (var col = 0; col < gridColumns.length; col++) {
if (gridColumns[col].type !== 'numeric') {
continue;
}
for (var row = 0; row < data.length; row++) {
var value = data[row][col];
if (value == null) {
continue;
}
value = $.trim(String(value));
if (value !== '' && !INTEGER_REGEXP.test(value)) {
alert("숫자만 입력할 수 있습니다.");
focusGridCell(col, row);
return false;
}
}
}
return true;
}
function detail(key) { function detail(key) {
if (!isDetail) { if (!isDetail) {
return return
@@ -321,6 +356,10 @@
}); });
$("#btn_modify").click(function(){ $("#btn_modify").click(function(){
if (!validateNumberColumns()) {
return;
}
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
if (isDetail){ if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"}); postData.push({ name: "cmd" , value:"UPDATE"});
@@ -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>
@@ -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>
@@ -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"/>
@@ -54,6 +57,7 @@
const LAYOUT_ITEM_TYPE_GROUP = "Group"; const LAYOUT_ITEM_TYPE_GROUP = "Group";
const LAYOUT_ITEM_TYPE_GRID = "Grid"; const LAYOUT_ITEM_TYPE_GRID = "Grid";
const LAYOUT_ITEM_TYPE_FIELD = "Field"; const LAYOUT_ITEM_TYPE_FIELD = "Field";
const LAYOUT_ITEM_TYPE_ARRAY = "Array";
var isDetail = false; var isDetail = false;
@@ -550,6 +554,14 @@
return data; return data;
} }
// 항목 자신이 Array인 경우, 경로/변환명령 끝에 반복 표시 [*]를 추가
function addArrayTag(loutItemType, data) {
if (loutItemType == LAYOUT_ITEM_TYPE_ARRAY && !data.endsWith("[*]")) {
data = data + "[*]";
}
return data;
}
//변환명에 있는 서비스코드와 if서비스 코드 매칭 //변환명에 있는 서비스코드와 if서비스 코드 매칭
function validation(){ function validation(){
if ($('input[name=cnvsnName]').val() == '') { if ($('input[name=cnvsnName]').val() == '') {
@@ -879,7 +891,7 @@
} }
}); });
$("input[name=auto]").click(function(){ $("#auto").click(function(){
//debugger; //debugger;
jsPlumbInstance.deleteEveryConnection(); jsPlumbInstance.deleteEveryConnection();
@@ -889,10 +901,10 @@
let selectValue = ""; let selectValue = "";
let targetValue = ""; let targetValue = "";
for ( var i = 0; i < sourceData.length; i++) { for ( var i = 0; i < sourceData.length; i++) {
if (sourceData[i]['loutItemType'] != "Field" && sourceData[i]['loutItemType'] != "Attr") continue; if (sourceData[i]['loutItemType'] != "Field" && sourceData[i]['loutItemType'] != "Attr" && sourceData[i]['loutItemType'] != "Array") continue;
selectValue = (sourceData[i]['LOUTITEMPATH']).substr(sourceData[i]['loutName'].length+1); selectValue = (sourceData[i]['LOUTITEMPATH']).substr(sourceData[i]['loutName'].length+1);
for (var j = 0; j <targetData.length; j++) { for (var j = 0; j <targetData.length; j++) {
if (targetData[j]['loutItemType'] != "Field" && targetData[j]['loutItemType'] != "Attr") continue; if (targetData[j]['loutItemType'] != "Field" && targetData[j]['loutItemType'] != "Attr" && targetData[j]['loutItemType'] != "Array") continue;
targetValue = (targetData[j]['LOUTITEMPATH']).substr(targetData[j]['loutName'].length+1); targetValue = (targetData[j]['LOUTITEMPATH']).substr(targetData[j]['loutName'].length+1);
if (targetValue == selectValue ) { if (targetValue == selectValue ) {
var data = sourceData[i]['LOUTITEMPATH']; var data = sourceData[i]['LOUTITEMPATH'];
@@ -906,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" );
@@ -916,8 +928,8 @@
//field, ATTR 여부 판단 //field, ATTR 여부 판단
var sourceRow = sourceGrid.getRowData( srcRowId ); var sourceRow = sourceGrid.getRowData( srcRowId );
var targetRow = targetGrid.getRowData( tgtRowId ); var targetRow = targetGrid.getRowData( tgtRowId );
if ( (sourceRow["loutItemType"] == 'Field' || sourceRow["loutItemType"] == 'Attr') && if ( (sourceRow["loutItemType"] == 'Field' || sourceRow["loutItemType"] == 'Attr' || sourceRow["loutItemType"] == 'Array') &&
(targetRow["loutItemType"] == 'Field' || targetRow["loutItemType"] == 'Attr') ){ (targetRow["loutItemType"] == 'Field' || targetRow["loutItemType"] == 'Attr' || targetRow["loutItemType"] == 'Array') ){
; ;
}else{ }else{
alert('<%=localeMessage.getString("transformManDetail.alert3")%>'); alert('<%=localeMessage.getString("transformManDetail.alert3")%>');
@@ -936,15 +948,15 @@
var targetValue = ""; var targetValue = "";
var j=tgtIndex-1-1; var j=tgtIndex-1-1;
for ( var i = srcIndex-1; i < sourceData.length; i++) { for ( var i = srcIndex-1; i < sourceData.length; i++) {
if (sourceData[i]['loutItemType'] != LAYOUT_ITEM_TYPE_FIELD) continue; if (sourceData[i]['loutItemType'] != LAYOUT_ITEM_TYPE_FIELD && sourceData[i]['loutItemType'] != LAYOUT_ITEM_TYPE_ARRAY) continue;
j++; j++;
while (targetData.length -1 >= j &&targetData[j]["loutItemType"] != LAYOUT_ITEM_TYPE_FIELD){ while (targetData.length -1 >= j && targetData[j]["loutItemType"] != LAYOUT_ITEM_TYPE_FIELD && targetData[j]["loutItemType"] != LAYOUT_ITEM_TYPE_ARRAY){
j++; j++;
} }
if (targetData.length -1 < j) break; if (targetData.length -1 < j) break;
var targetRowId = targetData[j]['id']; var targetRowId = targetData[j]['id'];
targetGrid.jqGrid('setCell',targetRowId,'CNVSNCMDNAME',addGroupTag(srcGroup,sourceData[i]['LOUTITEMPATH'])); targetGrid.jqGrid('setCell',targetRowId,'CNVSNCMDNAME',addArrayTag(sourceData[i]['loutItemType'], addGroupTag(srcGroup,sourceData[i]['LOUTITEMPATH'])));
targetGrid.jqGrid('setCell',targetRowId,'CNVSNRSULTITEMPATHNAME',addGroupTag(tgtGroup,targetData[j]['LOUTITEMPATH'])); targetGrid.jqGrid('setCell',targetRowId,'CNVSNRSULTITEMPATHNAME',addArrayTag(targetData[j]['loutItemType'], addGroupTag(tgtGroup,targetData[j]['LOUTITEMPATH'])));
targetGrid.jqGrid('setCell',targetRowId,'CNVSNITEMSERNO',targetData[j]['loutItemSerno']); targetGrid.jqGrid('setCell',targetRowId,'CNVSNITEMSERNO',targetData[j]['loutItemSerno']);
const sourceEndpointElementId = sourceGrid.getEndpointElementId(sourceData[i]['id']); const sourceEndpointElementId = sourceGrid.getEndpointElementId(sourceData[i]['id']);
@@ -957,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
@@ -976,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" );
@@ -1015,7 +1070,7 @@
var targetData = targetGrid.getRowData(); var targetData = targetGrid.getRowData();
var gridData = new Array(); var gridData = new Array();
for (var i = 0; i <targetData.length; i++) { for (var i = 0; i <targetData.length; i++) {
if ((targetData[i]['loutItemType'] != "Field") && (targetData[i]['loutItemType'] != "Attr")) { if ((targetData[i]['loutItemType'] != "Field") && (targetData[i]['loutItemType'] != "Attr") && (targetData[i]['loutItemType'] != "Array")) {
continue; continue;
} }
if (targetData[i]['CNVSNCMDNAME']==null if (targetData[i]['CNVSNCMDNAME']==null
@@ -1158,7 +1213,7 @@
function srcformatterFunction(cellvalue,options,rowObject){ function srcformatterFunction(cellvalue,options,rowObject){
var rowId = options["rowId"]; var rowId = options["rowId"];
var loutItemType = rowObject["loutItemType"]; var loutItemType = rowObject["loutItemType"];
if (loutItemType == 'Field' || loutItemType == 'Attr'){ if (loutItemType == 'Field' || loutItemType == 'Attr' || loutItemType == 'Array'){
return "<div id='div_src_"+rowId+"' name='src_"+rowId+"' style='width:28px;height:17px;background-color: #D12F35; border-color: #ab1717; border-style: solid; border-width: 1px; border-radius: 3px;' />"; return "<div id='div_src_"+rowId+"' name='src_"+rowId+"' style='width:28px;height:17px;background-color: #D12F35; border-color: #ab1717; border-style: solid; border-width: 1px; border-radius: 3px;' />";
}else{ }else{
return "<div id='div_srcgroup_"+rowId+"' name='srcgroup_"+rowId+"' style='width:28px;height:17px;' />"; return "<div id='div_srcgroup_"+rowId+"' name='srcgroup_"+rowId+"' style='width:28px;height:17px;' />";
@@ -1170,7 +1225,7 @@
function tgtformatterFunction(cellvalue,options,rowObject){ function tgtformatterFunction(cellvalue,options,rowObject){
var rowId = options["rowId"]; var rowId = options["rowId"];
var loutItemType = rowObject["loutItemType"]; var loutItemType = rowObject["loutItemType"];
if (loutItemType == 'Field' || loutItemType == 'Attr'){ if (loutItemType == 'Field' || loutItemType == 'Attr' || loutItemType == 'Array'){
return "<div id='div_tgt_"+rowId+"_base' name='div_tgt_"+rowId+"_base' style='width:30px;height:20px; background-color:gray;' >" return "<div id='div_tgt_"+rowId+"_base' name='div_tgt_"+rowId+"_base' style='width:30px;height:20px; background-color:gray;' >"
+ "<div id='div_tgt_"+rowId+"' name='tgt_"+rowId+"' style='width:28px;height:17px; background-color: #3E7E9C; border-color: #217ca7; border-style: solid; border-width: 1px; border-radius: 3px;' onclick='jqGridOnTargetGridEndpointClick(this)'>" + "<div id='div_tgt_"+rowId+"' name='tgt_"+rowId+"' style='width:28px;height:17px; background-color: #3E7E9C; border-color: #217ca7; border-style: solid; border-width: 1px; border-radius: 3px;' onclick='jqGridOnTargetGridEndpointClick(this)'>"
+ "</div></div>"; + "</div></div>";
@@ -1669,8 +1724,8 @@
return; return;
} }
if ((sourceRow["loutItemType"] != 'Field' && sourceRow["loutItemType"] != 'Attr') if ((sourceRow["loutItemType"] != 'Field' && sourceRow["loutItemType"] != 'Attr' && sourceRow["loutItemType"] != 'Array')
|| (targetRow["loutItemType"] != 'Field' && targetRow["loutItemType"] != 'Attr')) { || (targetRow["loutItemType"] != 'Field' && targetRow["loutItemType"] != 'Attr' && targetRow["loutItemType"] != 'Array')) {
alert('<%=localeMessage.getString("transformManDetail.alert3")%>'); alert('<%=localeMessage.getString("transformManDetail.alert3")%>');
return ; return ;
} }
@@ -1684,7 +1739,7 @@
if (targetRow['CNVSNCMDNAME'] == '') { if (targetRow['CNVSNCMDNAME'] == '') {
// 변환 명령 값이 한번 설정 되면 변경 되지 않게 함. // 변환 명령 값이 한번 설정 되면 변경 되지 않게 함.
var cnvsnCmdName = addGroupTag(srcGroup,sourceRow["LOUTITEMPATH"]); var cnvsnCmdName = addArrayTag(sourceRow["loutItemType"], addGroupTag(srcGroup,sourceRow["LOUTITEMPATH"]));
// function까지 처리되도록 수정했으나, parameter가 하나인 경우에만 정상처리됨 // function까지 처리되도록 수정했으나, parameter가 하나인 경우에만 정상처리됨
if($('#functionCombo').val() != "") { if($('#functionCombo').val() != "") {
cnvsnCmdName = $('#functionCombo').val() + "(" + cnvsnCmdName + ")"; cnvsnCmdName = $('#functionCombo').val() + "(" + cnvsnCmdName + ")";
@@ -1693,7 +1748,7 @@
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNCMDNAME', cnvsnCmdName); targetGrid.jqGrid('setCell',tgtRowId,'CNVSNCMDNAME', cnvsnCmdName);
} }
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNRSULTITEMPATHNAME',addGroupTag(tgtGroup,targetRow["LOUTITEMPATH"])); targetGrid.jqGrid('setCell',tgtRowId,'CNVSNRSULTITEMPATHNAME',addArrayTag(targetRow["loutItemType"], addGroupTag(tgtGroup,targetRow["LOUTITEMPATH"])));
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNITEMSERNO',targetRow['loutItemSerno']); targetGrid.jqGrid('setCell',tgtRowId,'CNVSNITEMSERNO',targetRow['loutItemSerno']);
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNSRCITEMS', cnvsnSrcItems); targetGrid.jqGrid('setCell',tgtRowId,'CNVSNSRCITEMS', cnvsnSrcItems);
targetGrid.jqGrid('setCell',tgtRowId,'ISVALIDMAPPING', true); targetGrid.jqGrid('setCell',tgtRowId,'ISVALIDMAPPING', true);
@@ -1885,8 +1940,8 @@
if (loutItemType == LAYOUT_ITEM_TYPE_GRID || loutItemType == LAYOUT_ITEM_TYPE_GROUP) { if (loutItemType == LAYOUT_ITEM_TYPE_GRID || loutItemType == LAYOUT_ITEM_TYPE_GROUP) {
// group 인 경우 // group 인 경우
items = createGridGroupContextMenuItems(grid, rowId); items = createGridGroupContextMenuItems(grid, rowId);
} else if(loutItemType == LAYOUT_ITEM_TYPE_FIELD) { } else if(loutItemType == LAYOUT_ITEM_TYPE_FIELD || loutItemType == LAYOUT_ITEM_TYPE_ARRAY) {
// field 인 경우 // field, array 인 경우
items = createFieldContextMenuItems(grid, positionElement.attr('id')); items = createFieldContextMenuItems(grid, positionElement.attr('id'));
} else { } else {
return false; return false;
@@ -1938,8 +1993,8 @@
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";
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
@@ -1953,11 +2008,11 @@
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();
}); });
@@ -1989,7 +2044,7 @@
break; break;
} }
if (loutItemType == LAYOUT_ITEM_TYPE_FIELD) { if (loutItemType == LAYOUT_ITEM_TYPE_FIELD || loutItemType == LAYOUT_ITEM_TYPE_ARRAY) {
loutItemPath = loutItemPath.replace(new RegExp("^" + prefix), ""); loutItemPath = loutItemPath.replace(new RegExp("^" + prefix), "");
console.debug(grid.getId() + " : loutItemPath : " + loutItemPath); console.debug(grid.getId() + " : loutItemPath : " + loutItemPath);
rowData['GROUP_LOUTITEMPATH'] = loutItemPath; rowData['GROUP_LOUTITEMPATH'] = loutItemPath;
@@ -2152,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">
@@ -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/transform2/transform2Man.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,11 +98,11 @@ $(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']);
+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>
@@ -44,14 +44,16 @@
'<%=localeMessage.getString("stdDefaultMan.columDesc")%>', '<%=localeMessage.getString("stdDefaultMan.columDesc")%>',
'<%=localeMessage.getString("stdDefaultMan.columLen")%>', '<%=localeMessage.getString("stdDefaultMan.columLen")%>',
'<%=localeMessage.getString("stdDefaultMan.keyOr")%>', '<%=localeMessage.getString("stdDefaultMan.keyOr")%>',
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>' '<%=localeMessage.getString("stdDefaultMan.ifOr")%>',
'<%=localeMessage.getString("stdDefaultMan.orderSeq")%>'
], ],
colModel:[ colModel:[
{ name : 'COLUMNNAME' , align:'left' , sortable:false }, { name : 'COLUMNNAME' , align:'left' , width: '200px', sortable:false },
{ name : 'COLUMNDESC' , align:'left' }, { name : 'COLUMNDESC' , align:'left' , width: '200px' },
{ name : 'COLUMNLENGTH' , align:'right' }, { name : 'COLUMNLENGTH' , align:'center' , width: '100px' },
{ name : 'ISKEY' , align:'center' }, { name : 'ISKEY' , align:'center', width: '100px' },
{ name : 'ISINTERFACE' , align:'center' } { name : 'ISINTERFACE' , align:'center', width: '100px' },
{ name : 'ORDERSEQ' , align:'center', width: '100px' }
], ],
jsonReader: { jsonReader: {
repeatitems:false repeatitems:false
@@ -23,7 +23,7 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>'; var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>';
var url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view"/>'; var url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view"/>';
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecManPopup.view"/>'; var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
var isDetail = false; var isDetail = false;
let dialog; let dialog;
@@ -415,6 +415,12 @@
<input type="text" name="groupName" data-required data-warning="API 그룹 명을 입력하여 주십시오."> <input type="text" name="groupName" data-required data-warning="API 그룹 명을 입력하여 주십시오.">
</td> </td>
</tr> </tr>
<tr>
<th style="width:150px;">그룹 설명</th>
<td colspan="7">
<input type="text" name="groupDesc">
</td>
</tr>
<tr> <tr>
<th style="width:150px;">공개 여부</th> <th style="width:150px;">공개 여부</th>
<td style="width:100px;"> <td style="width:100px;">
@@ -461,14 +467,6 @@
<img id="mainIconPreview" src="#" alt="Main Icon Preview" style="display:none;height:20px"> <img id="mainIconPreview" src="#" alt="Main Icon Preview" style="display:none;height:20px">
</td> </td>
</tr> </tr>
<%--<tr>
<th>컨텐츠 내용</th>
<td colspan="5">
<textarea name="contentDetail" id="contentDetail" rows="10" cols="50"></textarea>
</td>
</tr>--%>
</table> </table>
<div style="margin-top:15px;"> <div style="margin-top:15px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px "> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px ">
@@ -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>
@@ -38,19 +38,34 @@
var loginUser = '<%= loginVo.getUserId() %>'; var loginUser = '<%= loginVo.getUserId() %>';
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />'; var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />'; var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
function dateFormat(cellvalue) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
$(document).ready(function () { $(document).ready(function () {
var key = "${param.id}"; var key = "${param.id}";
if (key) { if (key) {
detail(key); detail(key);
} }
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#btn_app_approve").click(function () { $("#btn_app_approve").click(function () {
if (confirm("승인하시겠습니까?")) { var isDeleteReq = (currentRequestType === 'DELETE');
var gwAction = (isDeleteReq && $('#chkGwDelete').is(':checked')) ? 'DELETE' : 'BLOCK';
var confirmMsg = isDeleteReq
? "해지 승인 시 포털 인증키(Credential)는 삭제되며,\n게이트웨이 클라이언트는 [" + (gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)") + "] 처리됩니다.\n승인하시겠습니까?"
: "승인하시겠습니까?";
if (confirm(confirmMsg)) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: url, url: url,
data: {cmd: "APPROVE", id: key}, data: {cmd: "APPROVE", id: key, gwAction: gwAction},
success: function (args) { success: function (args) {
<%--alert("<%= localeMessage.getString("approval.approveSuccess") %>");--%> <%--alert("<%= localeMessage.getString("approval.approveSuccess") %>");--%>
alert("승인되었습니다."); alert("승인되었습니다.");
@@ -91,6 +106,32 @@
} }
}); });
$("#btn_app_force_reject").click(function () {
if (!confirm("승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건입니다.\n강제 반려하시겠습니까?")) {
return;
}
var rejectReason = prompt("강제 반려 사유를 입력해 주세요");
if (rejectReason != null) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "FORCE_REJECT", id: key, rejectReason: rejectReason},
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(e.responseText);
}
});
}
});
$('#btn_app_redeploy').click(function () { $('#btn_app_redeploy').click(function () {
if (confirm("재배포 하시겠습니까?")) { if (confirm("재배포 하시겠습니까?")) {
$.ajax({ $.ajax({
@@ -112,12 +153,88 @@
}); });
} }
}); });
$("#btn_modify").click(function () {
if (confirm("수정하시겠습니까?")) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "UPDATE", id: key, expectEndDate: $("#expectEndDate").val().replaceAll('-','')},
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(e.responseText);
}
});
}
});
$("#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 처리방식 선택 노출용)
// 값이 있을 때만 행을 노출한다. (해지 신청처럼 해당 항목이 없는 유형에서 빈 행이 남지 않도록)
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",
@@ -126,17 +243,31 @@
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (json) { success: function (json) {
var data = json; var data = json;
if (data.approvalStatus.code == 'END') {
$('#btn_modify').hide();
$('#expectEndDate').hide();
$('#spExpectEndDate').show();
$('#spExpectEndDate').text(dateFormat(data.expectEndDate));
} else {
$('#btn_modify').show();
$('#expectEndDate').show();
$('#spExpectEndDate').hide();
}
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용"); $("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
$("#approvalStatus").text(data.approvalStatus.description); $("#approvalStatus").text(data.approvalStatus.description);
$("#requesterName").text(data.requester.userName); $("#requesterName").text(data.requester.userName);
$("#approvalSubject").text(data.approvalSubject); $("#approvalSubject").text(data.approvalSubject);
$("#approvalDetail").html(data.approvalDetail); $("#approvalDetail").html(data.approvalDetail);
$("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true}); $("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true});
$("#expectEndDate").val(data.expectEndDate);
if (data.approvers.some(approver => var isCurrentApprover = data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' && approver.approverStatus === 'CURRENT' &&
approver.user.USERID === loginUser approver.user && approver.user.USERID === loginUser
)) { );
if (isCurrentApprover) {
document.getElementById("btn_app_approve").style.display = ""; document.getElementById("btn_app_approve").style.display = "";
document.getElementById("btn_app_reject").style.display = ""; document.getElementById("btn_app_reject").style.display = "";
} else { } else {
@@ -144,7 +275,30 @@
document.getElementById("btn_app_reject").style.display = "none"; document.getElementById("btn_app_reject").style.display = "none";
} }
if (data.approvalStatus.description === '배포실패') { // API 이용해지(DELETE) 건: 승인 가능 상태의 현재 승인자에게만 GW 처리방식 선택 노출
currentRequestType = data.requestType;
var inProgress = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
if (data.requestType === 'DELETE' && isCurrentApprover && inProgress) {
$("#gwActionSection").show();
} else {
$("#gwActionSection").hide();
}
// 처리 완료된 해지 건은 어떤 방식으로 실행됐는지 표시
if (data.requestType === 'DELETE' && data.gwAction) {
$("#gwActionResult").text("GW 처리방식: " + (data.gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)")).show();
} else {
$("#gwActionResult").hide();
}
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
var hasDeletedApprover = data.approvers.some(approver =>
!approver.user || !approver.user.USERID
);
var isProgressStatus = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
document.getElementById("btn_app_force_reject").style.display =
(hasDeletedApprover && isProgressStatus && !isCurrentApprover) ? "" : "none";
if (data.approvalStatus.code === 'FAILED') {
document.getElementById("btn_app_redeploy").style.display = ""; document.getElementById("btn_app_redeploy").style.display = "";
} }
@@ -162,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];
@@ -216,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);
} }
}); });
} }
@@ -242,18 +413,43 @@
</tr> </tr>
<tr> <tr>
<th>상세 내용</th> <th>상세 내용</th>
<td colspan="3"> <td>
<div id="approvalDetail" style="white-space: pre-wrap;"></div> <div id="approvalDetail" style="white-space: pre-wrap;"></div>
</td> </td>
<th>예상완료일</th>
<td>
<input type="text" id="expectEndDate" style="width: 120px; text-align:center"/>
<span id="spExpectEndDate"></span>
</td>
</tr> </tr>
</table> </table>
<div class="title">Client 정보</div> <div class="title">Client 정보</div>
<table id="client" class="table_row" cellspacing="0"> <table id="client" class="table_row" cellspacing="0">
<colgroup>
<col style="width:20%"/>
<col style="width:80%"/>
</colgroup>
<tr> <tr>
<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">
@@ -281,13 +477,29 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div id="gwActionSection" style="display:none; margin: 12px 0 0 4px; color: #c0392b;">
<label style="cursor:pointer;">
<input type="checkbox" id="chkGwDelete"/>
GW 인증정보 완전 삭제 (미체크 시 차단 처리 &mdash; 인증정보 보존, 복구 가능)
</label>
</div>
<div id="gwActionResult" style="display:none; margin: 12px 0 0 4px; font-weight: bold;"></div>
<div class="popup_button"> <div class="popup_button">
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
class="material-icons">save</i>수정
</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i
class="material-icons">check</i>승인 class="material-icons">check</i>승인
</button> </button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_reject" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_reject" level="W"><i
class="material-icons">close</i>반려 class="material-icons">close</i>반려
</button> </button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_force_reject" level="W"><i
class="material-icons">block</i>강제 반려
</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>
@@ -17,10 +17,59 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />'; var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />'; var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
var hardDeleteEnabled = ${hardDeleteEnabled};
const APPROVAL_TYPE_DISPLAY = { const APPROVAL_TYPE_DISPLAY = {
'USER': '법인사용자', 'USER': '법인사용자',
'APP': 'API 사용' 'APP': 'API 사용'
}; };
function dateFormat(cellvalue, options, rowObject) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
function deleteSelectedApprovals() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
showAlert("삭제할 승인 요청을 선택해주세요.", {type: 'warning'});
return;
}
if (!hardDeleteEnabled) {
showAlert("완전삭제 기능이 비활성화되어 있습니다.", {type: 'error'});
return;
}
var confirmMsg = "선택된 " + selectedIds.length + "건을 완전삭제(DB 영구 삭제)하시겠습니까?";
showConfirm(confirmMsg, {
type: 'error',
onConfirm: function() {
showConfirm("최종 확인: 완전삭제는 되돌릴 수 없습니다. 정말 실행하시겠습니까?", {
type: 'error',
title: '최종 확인',
onConfirm: function() {
$.ajax({
type: "POST", url: url, dataType: "json",
data: { cmd: 'HARD_DELETE_MULTIPLE', ids: selectedIds.join(',') },
success: function(data) {
showAlert(data.message, {
type: data.status === 'success' ? 'success' : 'error',
onClose: function() { $("#grid").trigger("reloadGrid"); }
});
},
error: function(e) {
showAlert("완전삭제 오류: " + e.responseText, {type: 'error'});
}
});
}
});
}
});
}
$(document).ready(function() { $(document).ready(function() {
$('#grid').jqGrid({ $('#grid').jqGrid({
@@ -33,7 +82,7 @@
searchApprovalStatus: $('select[name=searchApprovalStatus]').val(), searchApprovalStatus: $('select[name=searchApprovalStatus]').val(),
searchApprovalSubject: $('input[name=searchApprovalSubject]').val() searchApprovalSubject: $('input[name=searchApprovalSubject]').val()
}, },
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일'], colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일', '예상완료일'],
colModel: [ colModel: [
{ name: 'rowNum', align: 'center', width: '30', sortable: false}, { name: 'rowNum', align: 'center', width: '30', sortable: false},
{ name: 'id', align: 'center', key: true, hidden: true }, { name: 'id', align: 'center', key: true, hidden: true },
@@ -42,7 +91,8 @@
{ name: 'approvalType', align: 'center', width: 80, hidden: true}, { name: 'approvalType', align: 'center', width: 80, hidden: true},
{ name: 'approvalStatus.description', align: 'center', width: 80 }, { name: 'approvalStatus.description', align: 'center', width: 80 },
{ name: 'requester.userName', align: 'center', width: 100 }, { name: 'requester.userName', align: 'center', width: 100 },
{ name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } } { name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } },
{ name: 'expectEndDate', align: 'center', width: 100, formatter: dateFormat},
], ],
jsonReader: { jsonReader: {
repeatitems: false repeatitems: false
@@ -53,6 +103,8 @@
viewrecords: true, viewrecords: true,
autowidth: true, autowidth: true,
height: 'auto', height: 'auto',
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) { ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId); var rowData = $(this).getRowData(rowId);
var id = rowData['id']; var id = rowData['id'];
@@ -79,6 +131,24 @@
// var reverseNumber = records - ((page - 1) * rowNum + i); // var reverseNumber = records - ((page - 1) * rowNum + i);
$(this).setCell(rows[i], 'rowNum', number); $(this).setCell(rows[i], 'rowNum', number);
} }
// 승인 상태별 행 배경색: 요청됨(#fff3cd) / 진행중(#d1ecf1) 강조
if (d && d.rows) {
for (var j = 0; j < d.rows.length; j++) {
var st = d.rows[j].approvalStatus || {};
var code = st.code || '';
var desc = st.description || '';
var bg = '';
if (code === 'REQUESTED' || desc === '요청됨') {
bg = '#fff3cd';
} else if (code === 'PROCESSING' || desc === '진행중') {
bg = '#d1ecf1';
}
if (bg) {
$('#grid tr#' + $.jgrid.jqID(d.rows[j].id)).css('background-color', bg);
}
}
}
}, },
loadError: function(jqXHR, textStatus, errorThrown) { loadError: function(jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/'; var location = '<%=request.getContextPath()%>/';
@@ -93,6 +163,10 @@
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid"); $("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
}); });
$("#btn_delete_selected").click(function() {
deleteSelectedApprovals();
});
$("select[name^=search], input[name^=search]").keydown(function(key) { $("select[name^=search], input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) { if (key.keyCode == 13) {
$("#btn_search").click(); $("#btn_search").click();
@@ -112,9 +186,17 @@
</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">
<c:if test="${hardDeleteEnabled}">
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</c:if>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button> <button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div> </div>
<div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div> <div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div>
<c:if test="${hardDeleteEnabled}">
<div style="background:#fff3cd; border:1px solid #ffc107; padding:8px 15px; margin:5px 0; border-radius:4px; color:#856404;">
<strong>[주의]</strong> 삭제 기능은 <strong>개발(dev) 환경에서만</strong> 동작합니다. 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
</div>
</c:if>
<form id="ajaxForm" onsubmit="return false;"> <form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
<tbody> <tbody>
@@ -134,8 +216,8 @@
<div class="select-style"> <div class="select-style">
<select name="searchApprovalStatus"> <select name="searchApprovalStatus">
<option value="">전체</option> <option value="">전체</option>
<option value="CREATED">생성됨</option>
<option value="REQUESTED">요청됨</option> <option value="REQUESTED">요청됨</option>
<option value="PROCESSING">진행중</option>
<option value="DENIED">반려됨</option> <option value="DENIED">반려됨</option>
<option value="END">승인됨</option> <option value="END">승인됨</option>
</select> </select>
@@ -45,6 +45,10 @@
padding: 2px 5px; padding: 2px 5px;
border-radius: 3px; border-radius: 3px;
} }
.cssbtn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
</style> </style>
<script language="javascript"> <script language="javascript">
<% <%
@@ -150,6 +154,55 @@
} }
}); });
$("#btn_user_force_reject").click(function() {
if (!confirm("승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건입니다.\n강제 반려하시겠습니까?")) {
return;
}
var rejectReason = prompt("강제 반려 사유를 입력해 주세요");
if (rejectReason != null) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "FORCE_REJECT", id: key, rejectReason: rejectReason},
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(e.responseText);
}
});
}
});
$("#btn_user_delete").click(function() {
if (!confirm("삭제된 요청자·사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?\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_user_redeploy').click(function() { $('#btn_user_redeploy').click(function() {
if (confirm("재배포 하시겠습니까?")) { if (confirm("재배포 하시겠습니까?")) {
$.ajax({ $.ajax({
@@ -217,20 +270,55 @@
// Display Portal User details // Display Portal User details
var portalUser = data.portalUser; var portalUser = data.portalUser;
$("#loginId").text(portalUser.loginId); var portalOrg = portalUser ? portalUser.portalOrgUIs : null;
$("#userName").text(portalUser.userName);
$("#mobileNumber").text(portalUser.mobileNumber); // 삭제된 대상 사용자/법인 판별
$("#userStatusDescription").text(portalUser.userStatusDescription); var userDeleted = !portalUser
$("#roleCodeDescription").text(portalUser.roleCodeDescription); || portalUser.userName === '반려된 사용자'
if (data.approvers.some(approver => || portalUser.userName === '삭제된 사용자';
approver.approverStatus === 'CURRENT' && var orgDeleted = !portalOrg
approver.user.USERID === loginUser || portalOrg.orgStatus === 'REMOVED'
)) { || portalOrg.orgName === '삭제된법인';
document.getElementById("btn_user_approve").style.display = ""; var deletedTarget = userDeleted || orgDeleted;
document.getElementById("btn_user_reject").style.display = "";
if (portalUser) {
$("#loginId").text(portalUser.loginId);
$("#userName").text(portalUser.userName);
$("#mobileNumber").text(portalUser.mobileNumber);
$("#userStatusDescription").text(portalUser.userStatusDescription);
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
} else { } else {
document.getElementById("btn_user_approve").style.display = "none"; $("#loginId, #userName, #mobileNumber, #userStatusDescription, #roleCodeDescription").text("삭제된 사용자");
document.getElementById("btn_user_reject").style.display = "none"; }
var isCurrentApprover = data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' &&
approver.user && approver.user.USERID === loginUser
);
if (deletedTarget) {
// 삭제된 사용자/법인 포함 → 승인·반려 불가. 버튼은 비활성(disable) 표기, 삭제 버튼 노출
$("#btn_user_approve, #btn_user_reject").show().prop("disabled", true);
document.getElementById("btn_user_force_reject").style.display = "none";
document.getElementById("btn_user_delete").style.display = "";
} else {
$("#btn_user_approve, #btn_user_reject").prop("disabled", false);
document.getElementById("btn_user_delete").style.display = "none";
if (isCurrentApprover) {
document.getElementById("btn_user_approve").style.display = "";
document.getElementById("btn_user_reject").style.display = "";
} else {
document.getElementById("btn_user_approve").style.display = "none";
document.getElementById("btn_user_reject").style.display = "none";
}
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
var hasDeletedApprover = data.approvers.some(approver =>
!approver.user || !approver.user.USERID
);
var isProgressStatus = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
document.getElementById("btn_user_force_reject").style.display =
(hasDeletedApprover && isProgressStatus && !isCurrentApprover) ? "" : "none";
} }
if (data.approvalStatus.description === '배포실패'){ if (data.approvalStatus.description === '배포실패'){
@@ -238,23 +326,27 @@
} }
// Display Portal Org details // Display Portal Org details
var portalOrg = portalUser.portalOrgUIs; if (portalOrg) {
$("#orgName").text(portalOrg.orgName); $("#orgName").text(portalOrg.orgName);
$("#orgStatusDescription").text(portalOrg.orgStatusDescription); $("#orgStatusDescription").text(portalOrg.orgStatusDescription);
$("#serviceName").text(portalOrg.serviceName); $("#serviceName").text(portalOrg.serviceName);
$("#corpRegNo").text(formatCorpRegNo(portalOrg.corpRegNo)); $("#corpRegNo").text(formatCorpRegNo(portalOrg.corpRegNo));
$("#compRegNo").text(formatCompanyRegNo(portalOrg.compRegNo)); $("#compRegNo").text(formatCompanyRegNo(portalOrg.compRegNo));
$("#scPhoneNumber").text(portalOrg.scPhoneNumber); // 고객센터 전화번호 $("#scPhoneNumber").text(portalOrg.scPhoneNumber); // 고객센터 전화번호
$("#orgPhoneNumber").text(portalOrg.orgPhoneNumber); // 회사 전화번호 $("#orgPhoneNumber").text(portalOrg.orgPhoneNumber); // 회사 전화번호
$("#ceoName").text(portalOrg.ceoName); // 대표자 성명 $("#ceoName").text(portalOrg.ceoName); // 대표자 성명
$("#orgIndustryType").text(portalOrg.orgIndustryType || ""); // 업종 $("#orgIndustryType").text(portalOrg.orgIndustryType || ""); // 업종
$("#orgAddr").text(portalOrg.orgAddr); // 소재지 $("#orgAddr").text(portalOrg.orgAddr); // 소재지
$("#orgSectors").text(portalOrg.orgSectors || ""); // 업태 $("#orgSectors").text(portalOrg.orgSectors || ""); // 업태
$("#orgCode").text(portalOrg.orgCode); $("#orgCode").text(portalOrg.orgCode);
$("#ipWhitelist").text(portalOrg.ipWhitelist || ""); $("#ipWhitelist").text(portalOrg.ipWhitelist || "");
} else {
$("#orgName").text("삭제된법인");
$("#orgStatusDescription, #serviceName, #corpRegNo, #compRegNo, #scPhoneNumber, #orgPhoneNumber, #ceoName, #orgIndustryType, #orgAddr, #orgSectors, #orgCode, #ipWhitelist").text("");
}
if (portalOrg.compRegFile) { if (portalOrg && portalOrg.compRegFile) {
$("#fileName").text(portalOrg.fileName); $("#fileName").text(portalOrg.fileName);
if (portalOrg.fileName) { if (portalOrg.fileName) {
$("#fileWrapper").show().off('click').on('click', function() { $("#fileWrapper").show().off('click').on('click', function() {
@@ -270,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);
@@ -294,15 +407,18 @@
// 사용자 정보 업데이트 // 사용자 정보 업데이트
var portalUser = data.portalUser; var portalUser = data.portalUser;
$("#loginId").text(portalUser.loginId); if (portalUser) {
$("#userName").text(portalUser.userName); $("#loginId").text(portalUser.loginId);
$("#mobileNumber").text(portalUser.mobileNumber); $("#userName").text(portalUser.userName);
$("#userStatusDescription").text(portalUser.userStatusDescription); $("#mobileNumber").text(portalUser.mobileNumber);
$("#roleCodeDescription").text(portalUser.roleCodeDescription); $("#userStatusDescription").text(portalUser.userStatusDescription);
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
} else {
$("#loginId, #userName, #mobileNumber, #userStatusDescription, #roleCodeDescription").text("삭제된 사용자");
}
// 법인 정보 업데이트 // 법인 정보 업데이트
var portalOrg = portalUser.portalOrgUIs; updateOrgInfo(portalUser ? portalUser.portalOrgUIs : null);
updateOrgInfo(portalOrg);
// 마스킹 상태에 따라 데이터를 다르게 표시 // 마스킹 상태에 따라 데이터를 다르게 표시
var isMasked = $("#btn_unmask").attr("status") === "DETAIL"; var isMasked = $("#btn_unmask").attr("status") === "DETAIL";
@@ -344,6 +460,11 @@
// 법인 정보 업데이트 함수 // 법인 정보 업데이트 함수
function updateOrgInfo(portalOrg) { function updateOrgInfo(portalOrg) {
if (!portalOrg) {
$("#orgName").text("삭제된법인");
$("#orgStatusDescription, #serviceName, #corpRegNo, #compRegNo, #scPhoneNumber, #orgPhoneNumber, #ceoName, #orgIndustryType, #orgAddr, #orgSectors, #orgCode, #ipWhitelist").text("");
return;
}
$("#orgName").text(portalOrg.orgName); $("#orgName").text(portalOrg.orgName);
$("#orgStatusDescription").text(portalOrg.orgStatusDescription); $("#orgStatusDescription").text(portalOrg.orgStatusDescription);
$("#serviceName").text(portalOrg.serviceName); $("#serviceName").text(portalOrg.serviceName);
@@ -392,15 +513,21 @@
<div class="title">사용자 정보</div> <div class="title">사용자 정보</div>
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col style="width:20%"/>
<col style="width:30%"/>
<col style="width:20%"/>
<col style="width:30%"/>
</colgroup>
<tr> <tr>
<th style="width:20%;">이메일 아이디</th> <th>이메일 아이디</th>
<td style="width:30%;" colspan="3"><span id="loginId"></span></td> <td colspan="3"><span id="loginId"></span></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;">이름</th> <th>이름</th>
<td style="width:30%;"><span id="userName"></span></td> <td><span id="userName"></span></td>
<th style="width:20%;">권한</th> <th>권한</th>
<td style="width:30%;"><span id="roleCodeDescription"></span></td> <td><span id="roleCodeDescription"></span></td>
</tr> </tr>
<tr> <tr>
<th>핸드폰 번호</th> <th>핸드폰 번호</th>
@@ -480,6 +607,8 @@
<div class="popup_button"> <div class="popup_button">
<button type="button" class="cssbtn" style="display: none;" id="btn_user_approve" level="W"><i class="material-icons">check</i>승인</button> <button type="button" class="cssbtn" style="display: none;" id="btn_user_approve" level="W"><i class="material-icons">check</i>승인</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_user_reject" level="W"><i class="material-icons">close</i>반려</button> <button type="button" class="cssbtn" style="display: none;" id="btn_user_reject" level="W"><i class="material-icons">close</i>반려</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_user_force_reject" level="W"><i class="material-icons">block</i>강제 반려</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_user_delete" level="W"><i class="material-icons">delete</i>삭제</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_user_redeploy" level="W"><i class="material-icons">close</i>재처리</button> <button type="button" class="cssbtn" style="display: none;" id="btn_user_redeploy" level="W"><i class="material-icons">close</i>재처리</button>
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %> <button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
</button> </button>
@@ -0,0 +1,240 @@
<%@ 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>
.mask-email { color:#1a73e8; font-weight:bold; }
.mask-phone { color:#d93025; font-weight:bold; }
.mask-digits { color:#9334e6; font-weight:bold; }
.msg-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/messagerequest/messageRequestMan.json" />';
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '&quot;'); }
// 제목: 없으면 messageCode, 마우스오버에 코드명(코드) 표기
function formatSubject(cellvalue, options, rowObject) {
var code = rowObject.messageCode || '';
var subject = (cellvalue && cellvalue !== '') ? cellvalue : '';
var title = rowObject.messageCodeName ? (rowObject.messageCodeName + ' (' + code + ')') : code;
if (subject) {
// 제목이 있으면 2줄: 제목 + 하단에 메세지코드
var codeLine = code ? '<br><span style="color:#888;font-size:0.85em;">' + escapeHtmlJs(code) + '</span>' : '';
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(subject) + codeLine + '</span>';
}
// 제목이 없으면 메세지코드만
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(code) + '</span>';
}
// 본문: 서버에서 마스킹+escape 된 안전 HTML 그대로 렌더
function formatMessage(cellvalue, options, rowObject) {
return '<span class="msg-cell">' + (rowObject.messageHtml || '') + '</span>';
}
// 수신자 정보: 휴대폰 / 이메일 / 메신저ID (모두 마스킹됨)
function formatReceiverInfo(cellvalue, options, rowObject) {
var parts = [];
if (rowObject.phone) parts.push(escapeHtmlJs(rowObject.phone));
if (rowObject.email) parts.push(escapeHtmlJs(rowObject.email));
if (rowObject.messengerId) parts.push(escapeHtmlJs(rowObject.messengerId));
return parts.join('<br>');
}
function formatRequestDate(cellvalue, options, rowObject) {
return '<span title="' + escapeAttr(rowObject.requestDateFull || '') + '">' + escapeHtmlJs(rowObject.requestDateShort || '') + '</span>';
}
function formatSentDate(cellvalue, options, rowObject) {
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() {
$("#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({
type: "POST", url: url, dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function (json) {
new makeOptions("CODE", "NAME").setObj($("select[name=searchRequestStatus]"))
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
.setData(json.statusList).rendering();
new makeOptions("CODE", "NAME").setObj($("select[name=searchMessageCode]"))
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
.setData(json.messageCodeList).rendering();
putSelectFromParam();
// 콤보 로드 후 그리드 생성 (경합 방지)
buildGrid();
},
error: function (e) { alert(e.responseText); }
});
}
function buildGrid() {
$('#grid').jqGrid({
datatype: "json", mtype: 'POST', url: url,
postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['id', 'No.', '제목', '메세지 내용', '수신자명', '수신자 정보', '유형', '상태', '요청시간', '발송시간'],
colModel: [
{name: 'id', align: 'center', key: true, hidden: true},
{name: 'rowNum', align: 'center', width: 45, sortable: false},
{name: 'subject', align: 'left', width: 160, formatter: formatSubject},
{name: 'messageHtml', align: 'left', width: 300, formatter: formatMessage},
{name: 'username', align: 'center', width: 80},
{name: 'receiverInfo', align: 'left', width: 150, formatter: formatReceiverInfo},
{name: 'messageType', align: 'center', width: 60},
{name: 'requestStatus', align: 'center', width: 70},
{name: 'requestDateShort', align: 'center', width: 110, formatter: formatRequestDate},
{name: 'sentDateShort', align: 'center', width: 110, formatter: formatSentDate}
],
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;
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 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");
$("#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;">
<col style="width:120px;"><col style="width:200px;">
</colgroup>
<tbody>
<tr>
<th>상태</th>
<td><div class="select-style"><select name="searchRequestStatus"></select></div></td>
<th>메세지 코드</th>
<td><div class="select-style"><select name="searchMessageCode"></select></div></td>
<th>수신자명</th>
<td><input type="text" name="searchRecipient" autocomplete="off"></td>
</tr>
<tr>
<th>유형</th>
<td>
<div class="select-style">
<select name="searchMessageType">
<option value="">전체</option>
<option value="EMAIL">EMAIL</option>
<option value="SMS">SMS</option>
<option value="MESSENGER">MESSENGER</option>
</select>
</div>
</td>
<th>요청기간</th>
<td colspan="3">
<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>
</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,155 @@
<%@ 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>
.mask-email { color:#1a73e8; font-weight:bold; }
.mask-phone { color:#d93025; font-weight:bold; }
.mask-digits { color:#9334e6; font-weight:bold; }
.msg-detail { white-space:pre-wrap; word-break:break-all; min-height:80px; line-height:1.6; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
var key = '${param.id}';
function detail() {
if (!key) return;
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'DETAIL', id: key},
success: function (data) {
var codeText = (data.messageCodeName || '') + (data.messageCode ? ' (' + data.messageCode + ')' : '');
$('#messageCodeName').text(codeText);
$('#subject').text((data.subject && data.subject !== '') ? data.subject : (data.messageCode || ''));
$('#messageType').text(data.messageType || '');
$('#requestStatus').text(data.requestStatus || '');
$('#username').text(data.username || '');
$('#email').text(data.email || '');
$('#phone').text(data.phone || '');
$('#messengerId').text(data.messengerId || '');
$('#umsUid').text(data.umsUid || '');
$('#eaiInterfaceId').text(data.eaiInterfaceId || '');
$('#serviceId').text(data.serviceId || '');
$('#requestDate').text(data.requestDateFull || '');
$('#sentDate').text(data.sentDateFull || '');
// 서버에서 마스킹+escape 된 안전 HTML
$('#messageHtml').html(data.messageHtml || '');
// 응답내용(RESPONSE_DATA) — 원문이므로 text 로만 출력
$('#responseData').text(data.responseData || '');
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
if (data.requestStatus !== 'PENDING') {
$('#btn_fail').hide();
}
},
error: function (e) { alert(e.responseText); }
});
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
buttonControl();
detail();
$('#btn_fail').click(function () {
if (!confirm('이 건을 FAILED(발송 실패) 상태로 변경하시겠습니까?')) return;
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'UPDATE_STATUS', id: key},
success: function () {
alert("변경되었습니다.");
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_fail" level="W" status="DETAIL" style="background-color:#dc3545;border-color:#dc3545;color:#fff;"><i class="material-icons">report</i> 실패 처리</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>
</div>
<div class="title" id="title">메세지 발송 내역 상세<span class="tooltip">개인정보는 마스킹되어 표시됩니다.</span></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>메세지 코드</th>
<td colspan="3"><span id="messageCodeName"></span></td>
</tr>
<tr>
<th>제목</th>
<td colspan="3"><span id="subject"></span></td>
</tr>
<tr>
<th>유형</th>
<td><span id="messageType"></span></td>
<th>상태</th>
<td><span id="requestStatus"></span></td>
</tr>
<tr>
<th>수신자명</th>
<td><span id="username"></span></td>
<th>메신저 ID</th>
<td><span id="messengerId"></span></td>
</tr>
<tr>
<th>이메일</th>
<td><span id="email"></span></td>
<th>휴대폰</th>
<td><span id="phone"></span></td>
</tr>
<tr>
<th>요청 시간</th>
<td><span id="requestDate"></span></td>
<th>발송 시간</th>
<td><span id="sentDate"></span></td>
</tr>
<tr>
<th>EAI 인터페이스 ID</th>
<td><span id="eaiInterfaceId"></span></td>
<th>서비스 ID</th>
<td><span id="serviceId"></span></td>
</tr>
<tr>
<th>UMS UID</th>
<td colspan="3"><span id="umsUid"></span></td>
</tr>
<tr>
<th>메세지 내용</th>
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
</tr>
<tr>
<th>응답내용</th>
<td colspan="3"><div id="responseData" class="msg-detail"></div></td>
</tr>
</table>
</div>
</div>
</body>
</html>
@@ -20,15 +20,35 @@
var select_orgId = ""; var select_orgId = "";
function formatInquiryStatus(cellvalue, options, rowObject) { function formatInquiryStatus(cellvalue, options, rowObject) {
return cellvalue === 'PENDING' if (cellvalue == 'PENDING') {
? '<span style="color: red;">문의중</span>' return '<span style="color: red;">문의중</span>';
: '<span>답변완료</span>'; } else if (cellvalue == 'REVIEWING') {
return '<span >문의검토중</span>'
} else if (cellvalue == 'RESPONDED') {
return '<span >답변완료</span>'
} else if (cellvalue == 'CLOSED') {
return '<span >답변종료</span>'
} else {
return cellvalue;
}
}
function formatVisibility(cellvalue, options, rowObject) {
if (cellvalue == 'PRIVATE') {
return '<span style="color:#c0392b; font-weight:bold;">비공개</span>';
} else if (cellvalue == 'ALL') {
return '전체공개';
} else if (cellvalue == 'ORG') {
return '법인공개';
}
return cellvalue;
} }
function formatFile(cellvalue, options, rowObject) { function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : ''; var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon; var commentCount = rowObject.commentCount > 0 ? ' <span style="color: #337ab7;">[' + rowObject.commentCount + ']</span>' : '';
return cellvalue + icon + commentCount;
} }
function deleteSelectedRows() { function deleteSelectedRows() {
@@ -92,13 +112,14 @@
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST' }, postData : { cmd : 'LIST' },
colNames:['No.', 'id', '제목', '법인명', '상태', '작성자', '등록일', '답변자','답변일'], colNames:['No.', 'id', '제목', '법인명', '상태', '공개범위', '작성자', '등록일', '답변자','답변일'],
colModel:[ colModel:[
{ name : 'rowNum' , align:'center', width:50, sortable:false }, { name : 'rowNum' , align:'center', width:50, sortable:false },
{ name : 'id' , align:'center', key:true, hidden:true}, { name : 'id' , align:'center', key:true, hidden:true},
{ name : 'inquirySubject' , align:'left' , width:200 , formatter: formatFile}, { name : 'inquirySubject' , align:'left' , width:200 , formatter: formatFile},
{ name : 'orgName' , align:'center' , width:100 , }, { name : 'orgName' , align:'center' , width:100 , },
{ name : 'inquiryStatus' , align:'center', width:70, formatter: formatInquiryStatus }, { name : 'inquiryStatus' , align:'center', width:70, formatter: formatInquiryStatus },
{ name : 'visibility' , align:'center', width:70, formatter: formatVisibility },
{ name : 'inquirer.userName', align:'center', width:70 }, { name : 'inquirer.userName', align:'center', width:70 },
{ name : 'createdDate' , align:'center', width:100, formatter: timeStampFormat }, { name : 'createdDate' , align:'center', width:100, formatter: timeStampFormat },
{ name : 'responderName' , align:'center', width:70 }, { name : 'responderName' , align:'center', width:70 },
@@ -200,7 +221,9 @@
<select name="searchInquiryStatus"> <select name="searchInquiryStatus">
<option value="">전체</option> <option value="">전체</option>
<option value="PENDING">문의중</option> <option value="PENDING">문의중</option>
<option value="REVIEWING">문의검토중</option>
<option value="RESPONDED">답변완료</option> <option value="RESPONDED">답변완료</option>
<option value="CLOSED">답변종료</option>
</select> </select>
</div> </div>
</td> </td>
@@ -36,6 +36,13 @@
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */ border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */
} }
.comment-section {
margin-bottom: 30px;
border: 1px solid #e0e0e0; /* 동일한 박스 스타일 */
padding: 20px;
border-radius: 5px;
}
.info-row { .info-row {
margin-bottom: 10px; margin-bottom: 10px;
@@ -57,17 +64,61 @@
width: 100%; width: 100%;
min-height: 200px; min-height: 200px;
} }
#commentDetail {
min-height: 100px;
border: 1px solid #00000032;
border-radius: 4px;
padding: 10px;
flex: 1;
font-family: '맑은고딕'
}
.required-mark { .required-mark {
color: red; color: red;
margin-left: 2px; margin-left: 2px;
font-weight: bold; font-weight: bold;
} }
.reason-modal-overlay {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1000;
}
.reason-modal-box {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 6px;
padding: 20px;
width: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.reason-modal-box h3 {
margin: 0 0 12px;
font-size: 16px;
}
.reason-modal-box textarea {
width: 100%;
min-height: 80px;
box-sizing: border-box;
}
.reason-modal-actions {
margin-top: 12px;
text-align: right;
}
</style> </style>
<script language="javascript"> <script language="javascript">
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />'; var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />'; var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
var file_download = '<c:url value="/file/download.do" />'; var file_download = '<c:url value="/file/download.do" />';
var inquiryStatus = 'PENDING';
var originalVisibility = 'ORG';
var reasonModalCallback = null;
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea'); var textArea = document.createElement('textarea');
@@ -82,10 +133,23 @@
dataType: "json", dataType: "json",
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (data) { success: function (data) {
$("#id").val(key); $("#id").val(key);
$("#inquirySubject").text(data.inquirySubject); $("#inquirySubject").text(data.inquirySubject);
$("#inquiryDetail").html(data.inquiryDetail); $("#inquiryDetail").html(data.inquiryDetail);
$("#inquiryStatus").text(data.inquiryStatus === 'PENDING' ? '문의중' : '답변완료');
inquiryStatus = data.inquiryStatus;
if (data.inquiryStatus == 'PENDING') {
$("#inquiryStatus").text('문의중');
} else if (data.inquiryStatus == 'REVIEWING') {
$("#inquiryStatus").text('문의검토중');
} else if (data.inquiryStatus == 'RESPONDED') {
$("#inquiryStatus").text('답변완료');
} else if (data.inquiryStatus == 'CLOSED') {
$("#inquiryStatus").text('답변종료');
}
$("#inquirer").text(data.inquirer.userName); $("#inquirer").text(data.inquirer.userName);
$("#createdDate").text(timeStampFormat(data.createdDate)); $("#createdDate").text(timeStampFormat(data.createdDate));
$("#responder").text(data.responderName || ''); $("#responder").text(data.responderName || '');
@@ -94,6 +158,26 @@
var decodedContent = decodeHTMLEntities(data.responseDetail); var decodedContent = decodeHTMLEntities(data.responseDetail);
$("#responseDetail").summernote('code', decodedContent || ''); $("#responseDetail").summernote('code', decodedContent || '');
// 공개범위 / 조회수
$('#visibilitySelect').val(data.visibility || 'ORG');
originalVisibility = data.visibility || 'ORG';
$('#viewCount').text(data.viewCount || 0);
// 조회수 증가 (sessionStorage dedup — 새로고침 시 중복 증가 억제)
var viewedKey = 'inquiry_viewed_' + key;
if (!sessionStorage.getItem(viewedKey)) {
$.ajax({
type: "POST",
url: url,
data: {cmd: 'INCREMENT_VIEW', id: key},
success: function () {
sessionStorage.setItem(viewedKey, 'true');
}
});
}
// 댓글 표시
listComment(key);
// 첨부 파일 표시 // 첨부 파일 표시
displayAttachFiles(data.fileInfo); displayAttachFiles(data.fileInfo);
@@ -104,6 +188,159 @@
} }
}); });
} }
function listComment(inquiryId) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_COMMENT', id: inquiryId},
success: function (data) {
var $commentList = $('#commentList');
$commentList.empty();
if (!data || data.length === 0) return;
$.each(data, function (i, comment) {
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
var isPrivate = comment.visibility === 'PRIVATE';
var row = $('<div>').addClass('info-row');
if (isPrivate) {
// 비공개 댓글은 색상으로 구별
row.css({'background-color': '#FFF5F5', 'border-radius': '4px', 'padding': '4px'});
}
var labelDiv = $('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '');
if (isPrivate) {
$('<span>').text(' 비공개').css({'color': '#c0392b', 'font-size': '0.85em', 'font-weight': 'bold'}).appendTo(labelDiv);
}
labelDiv.appendTo(row);
var content = $('<div>').addClass('info-content');
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(content);
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
$('<span>').text(date).appendTo(meta);
$('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '8px', 'font-size': '0.85em', 'padding': '1px 6px'})
.text('delete').on('click', (function(id, inquiryId) {
return function() { deleteComment(id, inquiryId); };
})(comment.id, comment.inquiryId))
.appendTo(meta);
meta.appendTo(content);
// 댓글 공개범위 변경 컨트롤 (사유 필수)
var visCtrl = $('<div>').css({'margin-top': '4px', 'font-size': '0.85em'});
var sel = $('<select>').append('<option value="ALL">공개</option>').append('<option value="PRIVATE">비공개</option>');
sel.val(comment.visibility || 'ALL');
var reasonInput = $('<input>').attr({'type': 'text', 'placeholder': '변경 사유'}).css({'margin-left': '4px'});
var applyBtn = $('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '4px', 'font-size': '0.85em', 'padding': '1px 6px'})
.text('공개범위변경').on('click', (function(id, inquiryId, selEl, reasonEl) {
return function() { changeCommentVisibility(id, inquiryId, selEl.val(), reasonEl.val()); };
})(comment.id, comment.inquiryId, sel, reasonInput));
visCtrl.append(sel).append(reasonInput).append(applyBtn);
visCtrl.appendTo(content);
content.appendTo(row);
row.appendTo($commentList);
});
},
error: function (e) {
alert(e.responseText);
}
});
}
function openReasonModal(callback) {
reasonModalCallback = callback;
$('#reasonModalInput').val('');
$('#reasonModal').show();
$('#reasonModalInput').focus();
}
function closeReasonModal() {
$('#reasonModal').hide();
reasonModalCallback = null;
}
// 답변(summernote) 내용이 실제로 입력됐는지 확인
function hasAnswerContent() {
var v = $('#responseDetail').summernote('code');
var text = $('<div>').html(v || '').text().replace(/ /g, ' ').trim();
return text.length > 0;
}
function saveInquiryAnswer(returnUrl) {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "INSERT"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);
},
error: function (e) {
alert(e.responseText);
}
});
}
// 게시물 공개범위 변경(사유 필수). 성공 후 답변 내용이 있으면 이어서 저장.
function changeInquiryVisibility(newVis, reason, returnUrl, thenSaveAnswer) {
$.ajax({
type: "POST",
url: url,
data: {cmd: 'VISIBILITY', serviceType: 'APIGW', targetType: 'INQUIRY', id: $('#id').val(),
visibility: newVis, auditReason: reason},
success: function () {
originalVisibility = newVis;
if (thenSaveAnswer) {
saveInquiryAnswer(returnUrl);
} else {
alert('변경되었습니다.');
goNav(returnUrl);
}
},
error: function (e) {
alert(e.responseText);
}
});
}
function changeCommentVisibility(commentId, inquiryId, visibility, reason) {
if (!reason || !reason.trim()) {
alert('공개범위 변경 사유를 입력하세요.');
return;
}
if (!confirm('댓글 공개범위를 변경하시겠습니까?')) return;
$.ajax({
type: "POST",
url: url,
data: {cmd: 'VISIBILITY', serviceType: 'APIGW', targetType: 'COMMENT', id: commentId, visibility: visibility, auditReason: reason},
success: function () {
alert('변경되었습니다.');
listComment(inquiryId);
},
error: function (e) {
alert(e.responseText);
}
});
}
function deleteComment(commentId, inquiryId) {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 삭제할 수 없습니다');
return;
}
if (!confirm("댓글을 삭제하시겠습니까?")) return;
$.ajax({
type: "POST",
url: url,
data: {cmd: 'DELETE_COMMENT', id: commentId},
success: function () {
listComment(inquiryId);
},
error: function (e) {
alert(e.responseText);
}
});
}
$(document).ready(function () { $(document).ready(function () {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
@@ -114,25 +351,70 @@
placeholder: '여기에 답변을 입력하세요.', placeholder: '여기에 답변을 입력하세요.',
height: 200 height: 200
}); });
if (key) { if (key) {
detail(key); detail(key);
} }
$("#btn_modify").click(function () { $("#btn_modify").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 수정할 수 없습니다');
return;
}
var newVis = $('#visibilitySelect').val();
var visChanged = (newVis !== originalVisibility);
if (visChanged) {
// 공개범위 변경 감지 → 사유 입력 모달 (답변 내용이 있으면 함께 저장)
var saveAnswerToo = hasAnswerContent();
openReasonModal(function (reason) {
changeInquiryVisibility(newVis, reason, returnUrl, saveAnswerToo);
});
return;
}
// 공개범위 변경 없음 → 기존 답변 저장
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
saveInquiryAnswer(returnUrl);
});
var postData = $('#ajaxForm').serializeArray(); $("#reasonModalOk").click(function () {
postData.push({name: "cmd", value: "INSERT"}); var reason = ($('#reasonModalInput').val() || '').trim();
if (!reason) {
alert('변경 사유를 입력하세요.');
return;
}
var cb = reasonModalCallback;
closeReasonModal();
if (cb) cb(reason);
});
$("#reasonModalCancel").click(function () {
closeReasonModal();
});
$("#btn_comment").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 댓글을 등록할 수 없습니다');
return;
}
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: url, url: url,
data: postData, data: {
cmd: "INSERT_COMMENT",
inquiryId: $('#id').val(),
commentDetail: $('#commentDetail').val(),
},
success: function (json) { success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl); $('#commentDetail').val('');
listComment(key);
}, },
error: function (e) { error: function (e) {
alert(e.responseText); alert(e.responseText);
@@ -194,7 +476,22 @@
<div class="info-label">작성일</div> <div class="info-label">작성일</div>
<div id="createdDate" class="info-content"></div> <div id="createdDate" class="info-content"></div>
</div> </div>
<div class="info-row" id="attachFileRow" style="display: none;"> <div class="info-row">
<div class="info-label">조회수</div>
<div id="viewCount" class="info-content">0</div>
</div>
<div class="info-row">
<div class="info-label">공개범위</div>
<div class="info-content" style="display:flex; gap:8px; align-items:center;">
<select id="visibilitySelect">
<option value="ALL">전체공개</option>
<option value="ORG">법인공개</option>
<option value="PRIVATE">비공개</option>
</select>
<span style="font-size:0.85em; color:#999;">변경 후 상단 [수정] 버튼을 누르면 사유 입력창이 표시됩니다.</span>
</div>
</div>
<div class="info-row" id="attachFileRow" style="display: none;">
<div class="info-label">첨부파일</div> <div class="info-label">첨부파일</div>
<div id="attachFiles" class="info-content"></div> <div id="attachFiles" class="info-content"></div>
</div> </div>
@@ -206,6 +503,10 @@
<!-- 답변 작성 부분 --> <!-- 답변 작성 부분 -->
<div class="response-section"> <div class="response-section">
<div class="info-row">
<div class="info-label">상태</div>
<div id="inquiryStatus" class="info-content"></div>
</div>
<div class="info-row"> <div class="info-row">
<div class="info-label">답변자</div> <div class="info-label">답변자</div>
<div id="responder" class="info-content"></div> <div id="responder" class="info-content"></div>
@@ -221,9 +522,34 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 댓글 작성 부분 -->
<div class="comment-section">
<div id="commentList"></div>
<div class="info-row">
<div class="info-label">${sessionScope.login.userName}</div>
<div style="flex:1; display: flex; gap: 8px; align-items: stretch;">
<textarea id="commentDetail" name="commentDetail" data-required data-warning="댓글 내용을 입력하여 주십시오."></textarea>
<button type="button" class="cssbtn" id="btn_comment" level="W" style="width:100px; height: 100px; text-align:center; ">댓글등록</button>
</div>
</div>
</div>
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
</form> </form>
</div> </div>
</div> </div>
<!-- 공개범위 변경 사유 입력 모달 -->
<div id="reasonModal" class="reason-modal-overlay">
<div class="reason-modal-box">
<h3>공개범위 변경 사유</h3>
<textarea id="reasonModalInput" placeholder="변경 사유를 입력하세요."></textarea>
<div class="reason-modal-actions">
<button type="button" class="cssbtn" id="reasonModalOk">확인</button>
<button type="button" class="cssbtn" id="reasonModalCancel">취소</button>
</div>
</div>
</div>
</body> </body>
</html> </html>
@@ -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>
@@ -0,0 +1,636 @@
<%@ 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" %>
<%
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"/>
<jsp:include page="/jsp/common/include/portal.jsp"/>
<style>
/* ── 스윔레인 보드 ── */
.menu-board { display: flex; gap: 12px; align-items: flex-start; }
.menu-lane-strip { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 10px; flex: 1 1 auto; }
.menu-lane { background: #f1f3f5; border-radius: 10px; min-width: 230px; max-width: 230px; flex: 0 0 230px; }
.menu-lane.unplaced { background: #fff4e6; border: 1px dashed #fd7e14; }
.lane-header { display: flex; align-items: center; gap: 6px; padding: 10px 12px;
border-bottom: 1px solid #dee2e6; cursor: grab; }
.menu-lane.unplaced .lane-header { cursor: default; }
.lane-title { font-weight: 700; font-size: 14px; flex: 1 1 auto; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; }
.lane-body { list-style: none; margin: 0; padding: 8px; min-height: 60px; }
.menu-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px;
padding: 8px 10px; margin-bottom: 8px; cursor: grab;
box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
.menu-card.hidden-item, .menu-lane.hidden-item > .lane-header { opacity: 0.45; }
.menu-card .card-name { font-size: 13px; font-weight: 600; }
.menu-card .card-id { font-size: 11px; color: #868e96; }
.menu-card .card-path { font-size: 11px; color: #495057; word-break: break-all; }
.card-actions, .lane-actions { display: flex; gap: 4px; }
.icon-btn { border: none; background: transparent; padding: 1px 3px; cursor: pointer;
font-size: 14px; color: #495057; }
.icon-btn:hover { color: #0d6efd; }
.icon-btn.danger:hover { color: #dc3545; }
.badge-src { font-size: 10px; }
.role-check-group { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.role-check-group .form-check { min-width: 45%; }
.dflt-hint { font-size: 11px; color: #868e96; }
.board-toolbar { display: flex; gap: 6px; margin-bottom: 10px; align-items: center; }
.board-toolbar .spacer { flex: 1 1 auto; }
.sortable-placeholder { border: 2px dashed #adb5bd; border-radius: 8px; height: 48px;
margin-bottom: 8px; background: #e9ecef; }
.lane-placeholder { border: 2px dashed #adb5bd; border-radius: 10px; min-width: 230px; }
</style>
<script language="javascript">
// AuthorizeInterceptor 는 serviceType 을 "요청 파라미터"에서 읽으므로
// 모든 .json 호출 URL 에 serviceType 을 부착한다 (누락 시 권한 조회 실패 → Access denied)
var url = urlAddServiceType('<c:url value="/onl/apim/portalmenu/portalMenuMan.json" />');
// 화면 상태 (저장 버튼 클릭 시에만 서버 반영 — 드래그/토글은 클라이언트 상태만 변경)
var state = { itemsById: {}, lanes: [], unplaced: [], roles: [] };
var dirty = false;
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function loadAll(afterFn) {
$.post(url, { cmd: 'LIST_ALL' }, function (data) {
buildState(data);
render();
dirty = false;
if (afterFn) afterFn();
}, 'json').fail(ajaxFail);
}
function buildState(data) {
state.itemsById = {};
state.roles = data.roles || [];
(data.items || []).forEach(function (it) { state.itemsById[it.menuId] = it; });
var placedTop = [], childrenByParent = {}, unplaced = [];
(data.items || []).forEach(function (it) {
if (!it.placed) { unplaced.push(it.menuId); return; }
if (!it.parentId) {
placedTop.push(it);
} else {
(childrenByParent[it.parentId] = childrenByParent[it.parentId] || []).push(it);
}
});
placedTop.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); });
state.lanes = placedTop.map(function (top) {
var kids = (childrenByParent[top.menuId] || [])
.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); })
.map(function (c) { return { menuId: c.menuId, visibleYn: c.visibleYn || 'Y' }; });
delete childrenByParent[top.menuId];
return { menuId: top.menuId, visibleYn: top.visibleYn || 'Y', children: kids };
});
// 부모가 미배치/부재인 고아 배치는 미배치로 강등
Object.keys(childrenByParent).forEach(function (pid) {
childrenByParent[pid].forEach(function (c) { unplaced.push(c.menuId); });
});
state.unplaced = unplaced;
}
// ── 렌더 ──
function cardHtml(menuId, visibleYn, inLane) {
var it = state.itemsById[menuId];
if (!it) return '';
var hidden = visibleYn === 'N';
var srcBadge = it.sourceType === 'PORTAL'
? '<span class="badge text-bg-secondary badge-src">기본</span>'
: '<span class="badge text-bg-warning badge-src">커스텀</span>';
var grpBadge = it.groupYn === 'Y' ? ' <span class="badge text-bg-info badge-src">그룹</span>' : '';
var placeBtn = (!inLane && it.groupYn === 'Y')
? '<button type="button" class="icon-btn" title="최상위 레인으로 배치" onclick="placeAsLane(\'' + menuId + '\')"><i class="bi bi-box-arrow-in-up"></i></button>'
: '';
var eyeBtn = inLane
? '<button type="button" class="icon-btn" title="노출/숨김" onclick="toggleVisible(\'' + menuId + '\')"><i class="bi ' + (hidden ? 'bi-eye-slash' : 'bi-eye') + '"></i></button>'
: '';
return '<li class="menu-card' + (hidden ? ' hidden-item' : '') + '" data-menu-id="' + escapeHtml(menuId) + '">'
+ '<div class="d-flex align-items-start">'
+ '<div class="flex-grow-1">'
+ '<div class="card-name">' + escapeHtml(it.menuName) + ' ' + srcBadge + grpBadge + '</div>'
+ '<div class="card-id">' + escapeHtml(it.menuId) + '</div>'
+ (it.menuPath ? '<div class="card-path">' + escapeHtml(it.menuPath) + '</div>' : '')
+ '</div>'
+ '<div class="card-actions">'
+ placeBtn + eyeBtn
+ '<button type="button" class="icon-btn" title="수정" onclick="openModal(\'' + menuId + '\')"><i class="bi bi-pencil"></i></button>'
+ '<button type="button" class="icon-btn danger" level="W" title="' + (it.sourceType === 'PORTAL' ? '미배치로 이동' : '삭제') + '" onclick="minusItem(\'' + menuId + '\')"><i class="bi bi-dash-circle"></i></button>'
+ '</div></div></li>';
}
function laneHtml(lane) {
var it = state.itemsById[lane.menuId];
if (!it) return '';
var hidden = lane.visibleYn === 'N';
var sectionBadge = it.menuSection === 'MYPAGE'
? '<span class="badge text-bg-primary badge-src">마이페이지</span>'
: '<span class="badge text-bg-success badge-src">GNB</span>';
var srcBadge = it.sourceType === 'PORTAL' ? '' : ' <span class="badge text-bg-warning badge-src">커스텀</span>';
var cards = lane.children.map(function (c) { return cardHtml(c.menuId, c.visibleYn, true); }).join('');
return '<div class="menu-lane' + (hidden ? ' hidden-item' : '') + '" data-menu-id="' + escapeHtml(lane.menuId) + '">'
+ '<div class="lane-header lane-drag-handle">'
+ '<span class="lane-title">' + escapeHtml(it.menuName) + '</span>'
+ sectionBadge + srcBadge
+ '<div class="lane-actions">'
+ '<button type="button" class="icon-btn" title="노출/숨김" onclick="toggleVisible(\'' + lane.menuId + '\')"><i class="bi ' + (hidden ? 'bi-eye-slash' : 'bi-eye') + '"></i></button>'
+ '<button type="button" class="icon-btn" title="수정" onclick="openModal(\'' + lane.menuId + '\')"><i class="bi bi-pencil"></i></button>'
+ '<button type="button" class="icon-btn danger" level="W" title="레인 미배치" onclick="unplaceLane(\'' + lane.menuId + '\')"><i class="bi bi-dash-circle"></i></button>'
+ '</div></div>'
+ '<ul class="lane-body" data-lane-id="' + escapeHtml(lane.menuId) + '">' + cards + '</ul>'
+ '</div>';
}
function render() {
var strip = state.lanes.map(laneHtml).join('');
$('#laneStrip').html(strip);
$('#unplacedBody').html(state.unplaced.map(function (id) { return cardHtml(id, 'Y', false); }).join(''));
initSortables();
buttonControl();
}
function initSortables() {
$('#laneStrip').sortable({
items: '.menu-lane',
handle: '.lane-drag-handle',
placeholder: 'lane-placeholder',
tolerance: 'pointer',
update: function () { syncFromDom(); }
});
$('.lane-body, #unplacedBody').sortable({
connectWith: '.lane-body, #unplacedBody',
items: '.menu-card',
placeholder: 'sortable-placeholder',
tolerance: 'pointer',
stop: function () { syncFromDom(); }
});
}
// 드래그 종료 후 DOM 순서 → 상태 동기화 (visible 값은 기존 상태 유지)
function syncFromDom() {
var visibleMap = {};
state.lanes.forEach(function (l) {
visibleMap[l.menuId] = l.visibleYn;
l.children.forEach(function (c) { visibleMap[c.menuId] = c.visibleYn; });
});
var lanes = [];
$('#laneStrip .menu-lane').each(function () {
var laneId = $(this).data('menu-id');
var children = [];
$(this).find('.lane-body .menu-card').each(function () {
var id = $(this).data('menu-id');
children.push({ menuId: id, visibleYn: visibleMap[id] || 'Y' });
});
lanes.push({ menuId: laneId, visibleYn: visibleMap[laneId] || 'Y', children: children });
});
var unplaced = [];
$('#unplacedBody .menu-card').each(function () { unplaced.push($(this).data('menu-id')); });
state.lanes = lanes;
state.unplaced = unplaced;
dirty = true;
render();
}
// ── 카드/레인 조작 ──
function toggleVisible(menuId) {
state.lanes.forEach(function (l) {
if (l.menuId === menuId) l.visibleYn = (l.visibleYn === 'N' ? 'Y' : 'N');
l.children.forEach(function (c) {
if (c.menuId === menuId) c.visibleYn = (c.visibleYn === 'N' ? 'Y' : 'N');
});
});
dirty = true;
render();
}
function placeAsLane(menuId) {
state.unplaced = state.unplaced.filter(function (id) { return id !== menuId; });
state.lanes.push({ menuId: menuId, visibleYn: 'Y', children: [] });
dirty = true;
render();
}
function minusItem(menuId) {
var it = state.itemsById[menuId];
if (!it) return;
if (it.sourceType === 'PORTAL') {
removeFromBoard(menuId);
if (state.unplaced.indexOf(menuId) < 0) state.unplaced.push(menuId);
dirty = true;
render();
} else {
if (!confirm('커스텀 항목은 미배치 시 삭제됩니다.\n[' + it.menuName + '] 을(를) 삭제하시겠습니까?')) return;
$.post(url, { cmd: 'DELETE', menuId: menuId }, function () {
removeFromBoard(menuId);
state.unplaced = state.unplaced.filter(function (id) { return id !== menuId; });
delete state.itemsById[menuId];
dirty = true;
render();
}, 'json').fail(ajaxFail);
}
}
function unplaceLane(laneId) {
var lane = null;
state.lanes = state.lanes.filter(function (l) {
if (l.menuId === laneId) { lane = l; return false; }
return true;
});
if (!lane) return;
lane.children.forEach(function (c) {
if (state.unplaced.indexOf(c.menuId) < 0) state.unplaced.push(c.menuId);
});
var it = state.itemsById[laneId];
if (it && it.sourceType !== 'PORTAL') {
if (confirm('커스텀 항목은 미배치 시 삭제됩니다.\n[' + it.menuName + '] 항목을 삭제하시겠습니까?')) {
$.post(url, { cmd: 'DELETE', menuId: laneId }, function () {
delete state.itemsById[laneId];
render();
}, 'json').fail(ajaxFail);
} else {
state.unplaced.push(laneId);
}
} else {
if (state.unplaced.indexOf(laneId) < 0) state.unplaced.push(laneId);
}
dirty = true;
render();
}
function removeFromBoard(menuId) {
state.lanes = state.lanes.filter(function (l) { return l.menuId !== menuId; });
state.lanes.forEach(function (l) {
l.children = l.children.filter(function (c) { return c.menuId !== menuId; });
});
}
// ── 저장 / 초기화 / 캐시 Reload ──
function savePlacements() {
// 미배치 상태의 커스텀 항목은 저장 시 삭제 처리 (스펙: 커스텀은 미배치=삭제)
var unplacedAdmin = state.unplaced.filter(function (id) {
var it = state.itemsById[id];
return it && it.sourceType !== 'PORTAL';
});
if (unplacedAdmin.length > 0) {
var names = unplacedAdmin.map(function (id) { return state.itemsById[id].menuName; }).join(', ');
if (!confirm('미배치된 커스텀 항목은 저장 시 삭제됩니다: ' + names + '\n계속하시겠습니까?')) return;
}
var rows = [];
state.lanes.forEach(function (l, i) {
rows.push({ menuId: l.menuId, parentId: null, sortOrder: (i + 1) * 10, visibleYn: l.visibleYn });
l.children.forEach(function (c, j) {
rows.push({ menuId: c.menuId, parentId: l.menuId, sortOrder: (j + 1) * 10, visibleYn: c.visibleYn });
});
});
var deleteCalls = unplacedAdmin.map(function (id) {
return $.post(url, { cmd: 'DELETE', menuId: id });
});
$.when.apply($, deleteCalls).always(function () {
$.ajax({
url: url + '&cmd=TRANSACTION_PLACEMENT',
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(rows)
}).done(function () {
dirty = false;
loadAll(function () {
if (confirm('배치가 저장되었습니다.\n포탈 메뉴 캐시를 즉시 반영(Reload)하시겠습니까?\n(미반영 시 캐시 TTL 후 자동 반영)')) {
reloadPortalCache();
}
});
}).fail(ajaxFail);
});
}
function initializeMenus() {
if (!confirm('메뉴 초기화 시 포탈 기본(menu.yml) 값으로 복원되고\n커스텀 항목은 모두 삭제됩니다. 계속하시겠습니까?')) return;
$.post(url, { cmd: 'INITIALIZE' }, function () {
loadAll(function () { alert('메뉴가 초기화되었습니다.'); });
}, 'json').fail(ajaxFail);
}
// ── 캐시 Reload ──
var reloadModal;
var reloadBtnHtml;
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) {
showReloadResult(result);
}, 'json').fail(ajaxFail).always(function () {
$btn.prop('disabled', false).html(reloadBtnHtml);
});
}
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) {
var msg = '처리 중 오류가 발생했습니다.';
try {
var body = JSON.parse(xhr.responseText);
if (body.errorMsg) msg = body.errorMsg;
else if (body.message) msg = body.message;
} catch (e) { /* ignore */ }
alert(msg + ' (HTTP ' + xhr.status + ')');
}
// ── 추가/수정 모달 ──
var menuModal;
function roleChecksHtml(namePrefix, checkedCsv) {
var checked = (checkedCsv || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
var html = '';
var all = [{ roleCode: 'AUTHENTICATED', roleName: '로그인 사용자', roleType: '-' }].concat(state.roles);
all.forEach(function (r) {
var id = namePrefix + '_' + r.roleCode;
html += '<div class="form-check form-check-sm">'
+ '<input class="form-check-input" type="checkbox" name="' + namePrefix + '" id="' + id + '" value="' + escapeHtml(r.roleCode) + '"'
+ (checked.indexOf(r.roleCode) >= 0 ? ' checked' : '') + '>'
+ '<label class="form-check-label" for="' + id + '" style="font-size:12px;">'
+ escapeHtml(r.roleName) + ' <span class="text-muted">(' + escapeHtml(r.roleCode) + ')</span></label>'
+ '</div>';
});
return html;
}
function openModal(menuId) {
var isNew = !menuId;
var it = isNew ? { sourceType: 'ADMIN', groupYn: 'N', menuSection: 'GNB', newWindowYn: 'N' }
: state.itemsById[menuId];
if (!it) return;
var isPortal = it.sourceType === 'PORTAL';
$('#modalTitle').text(isNew ? '메뉴 추가' : '메뉴 수정' + (isPortal ? ' (기본 항목)' : ''));
$('#fMenuId').val(it.menuId || '').prop('readonly', !isNew);
$('#fMenuName').val(it.menuName || '');
$('#fMenuPath').val(it.menuPath || '');
$('#fIconClass').val(it.iconClass || '');
$('#fMenuSection').val(it.menuSection || 'GNB').prop('disabled', isPortal);
$('#fGroupYn').prop('checked', it.groupYn === 'Y').prop('disabled', isPortal);
$('#fNewWindowYn').prop('checked', it.newWindowYn === 'Y');
$('#exposeRoleChecks').html(roleChecksHtml('exposeRole', it.exposeRoles));
$('#accessRoleChecks').html(roleChecksHtml('accessRole', it.accessRoles));
// 기본 항목: 기본값(DFLT_*) 병기
if (isPortal) {
$('#hintMenuName').text('기본값: ' + (it.dfltMenuName || '-'));
$('#hintMenuPath').text('기본값: ' + (it.dfltMenuPath || '-'));
$('#hintExposeRoles').text('기본값: ' + (it.dfltExposeRoles || '전체 노출'));
$('#hintAccessRoles').text('기본값: ' + (it.dfltAccessRoles || '제한 없음'));
$('.dflt-hint').show();
} else {
$('.dflt-hint').hide();
}
$('#modalMode').val(isNew ? 'INSERT' : 'UPDATE');
menuModal.show();
}
function submitModal() {
var cmd = $('#modalMode').val();
var data = {
cmd: cmd,
menuId: $('#fMenuId').val().trim(),
menuName: $('#fMenuName').val().trim(),
menuPath: $('#fMenuPath').val().trim(),
iconClass: $('#fIconClass').val().trim(),
menuSection: $('#fMenuSection').val(),
groupYn: $('#fGroupYn').is(':checked') ? 'Y' : 'N',
newWindowYn: $('#fNewWindowYn').is(':checked') ? 'Y' : 'N',
exposeRoles: $('input[name=exposeRole]:checked').map(function () { return this.value; }).get().join(','),
accessRoles: $('input[name=accessRole]:checked').map(function () { return this.value; }).get().join(',')
};
if (!data.menuId) { alert('메뉴 ID 를 입력하세요.'); return; }
if (cmd === 'INSERT' && !/^[a-z0-9-]+$/.test(data.menuId)) {
alert('메뉴 ID 는 kebab-case(소문자/숫자/하이픈)만 허용합니다.');
return;
}
if (!data.menuName) { alert('노출명을 입력하세요.'); return; }
$.post(url, data, function () {
menuModal.hide();
loadAll();
}, 'json').fail(ajaxFail);
}
$(document).ready(function () {
menuModal = new bootstrap.Modal(document.getElementById('menuModal'));
loadAll();
window.addEventListener('beforeunload', function (e) {
if (dirty) { e.preventDefault(); e.returnValue = ''; }
});
});
</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="title">포탈 메뉴 관리</div>
<div class="board-toolbar">
<button type="button" class="btn btn-sm btn-primary" level="W" onclick="openModal()">
<i class="bi bi-plus-lg"></i> 메뉴 추가</button>
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
<span class="spacer"></span>
<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>
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
<i class="bi bi-save"></i> 저장</button>
</div>
<div style="background:#e7f1ff; border:1px solid #0d6efd33; padding:8px 15px; margin-bottom:10px; border-radius:6px; font-size:12px; color:#31507a;">
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
저장 후 <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 class="menu-board">
<div class="menu-lane-strip" id="laneStrip"><!-- 렌더링 --></div>
<div class="menu-lane unplaced">
<div class="lane-header">
<span class="lane-title">미배치</span>
<span class="badge text-bg-warning badge-src">보관함</span>
</div>
<ul class="lane-body" id="unplacedBody"><!-- 렌더링 --></ul>
</div>
</div>
<!-- 추가/수정 모달 -->
<div class="modal fade" id="menuModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">메뉴 추가</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
</div>
<div class="modal-body">
<input type="hidden" id="modalMode" value="INSERT">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="fMenuId">메뉴 ID (kebab-case)</label>
<input type="text" class="form-control form-control-sm" id="fMenuId" placeholder="ex) custom-link">
</div>
<div class="col-md-6">
<label class="form-label" for="fMenuName">노출명</label>
<input type="text" class="form-control form-control-sm" id="fMenuName">
<div class="dflt-hint" id="hintMenuName"></div>
</div>
<div class="col-md-6">
<label class="form-label" for="fMenuPath">Path (그룹은 생략 가능)</label>
<input type="text" class="form-control form-control-sm" id="fMenuPath" placeholder="/example 또는 https://...">
<div class="dflt-hint" id="hintMenuPath"></div>
</div>
<div class="col-md-6">
<label class="form-label" for="fIconClass">아이콘 클래스 (마이페이지용, ex: fa-users)</label>
<input type="text" class="form-control form-control-sm" id="fIconClass">
</div>
<div class="col-md-4">
<label class="form-label" for="fMenuSection">섹션</label>
<select class="form-select form-select-sm" id="fMenuSection">
<option value="GNB">GNB (상단 메뉴)</option>
<option value="MYPAGE">마이페이지</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fGroupYn">
<label class="form-check-label" for="fGroupYn">그룹(상위 메뉴)</label>
</div>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fNewWindowYn">
<label class="form-check-label" for="fNewWindowYn">새 창 열기</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label">노출 권한 (미선택 = 전체 노출)</label>
<div class="role-check-group" id="exposeRoleChecks"></div>
<div class="dflt-hint" id="hintExposeRoles"></div>
</div>
<div class="col-md-6">
<label class="form-label">접근 권한 (미선택 = 제한 없음)</label>
<div class="role-check-group" id="accessRoleChecks"></div>
<div class="dflt-hint" id="hintAccessRoles"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="button" class="btn btn-sm btn-primary" level="W" onclick="submitModal()">저장</button>
</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 right_box -->
</body>
</html>
@@ -17,12 +17,57 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />'; var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />'; var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
var combo;
// 코드 상수 — PortalNoticeUI 와 동기화
var NOTICE_TYPE_INCIDENT = '3';
// IncidentState 라벨 — 상세 화면과 동일하게 유지
var STATE_LABELS = {
'INVESTIGATING': '조사중',
'IDENTIFIED': '원인 파악',
'MONITORING': '모니터링',
'RESOLVED': '해소',
'CANCELED': '취소'
};
function formatIncidentState(cellvalue, options, rowObject) {
if (!cellvalue) return '';
var label = STATE_LABELS[cellvalue] || cellvalue;
var closed = (cellvalue === 'RESOLVED' || cellvalue === 'CANCELED');
return closed
? '<span title="' + cellvalue + '">' + label + '</span>'
: '<span style="color: red;" title="' + cellvalue + '">' + label + '</span>';
}
function formatNoticeType(cellvalue, options, rowObject) {
var name = cellvalue;
if (combo && combo.noticeTypeList) {
for (var i = 0; i < combo.noticeTypeList.length; i++) {
if (combo.noticeTypeList[i].CODE == cellvalue) {
name = combo.noticeTypeList[i].NAME;
break;
}
}
}
// 장애인데 종료 시각이 없으면 아직 진행 중이다
if (cellvalue == NOTICE_TYPE_INCIDENT && rowObject && !rowObject.endAt) {
return '<span style="color: red;">' + name + '-진행중</span>';
}
return name;
}
function formatuseYn(cellvalue, options, rowObject) { function formatuseYn(cellvalue, options, rowObject) {
return cellvalue === 'N' return cellvalue === 'N'
? '<span style="color: red;">미사용</span>' ? '<span style="color: red;">미사용</span>'
: '<span>사용</span>'; : '<span>사용</span>';
} }
function formatFixYn(cellvalue, options, rowObject) {
return cellvalue === 'Y'
? '고정'
: '';
}
function formatFile(cellvalue, options, rowObject) { function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
@@ -30,22 +75,81 @@
return cellvalue + icon; return cellvalue + icon;
} }
$(document).ready(function() { function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
function init(){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
console.log('init', json);
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=searchNoticeType]")).setNoValueInclude(true).setNoValue('','<%=localeMessage.getString("combo.all")%>').setData(json.noticeTypeList).rendering();
putSelectFromParam();
// 콤보(combo)가 채워진 뒤에 그리드를 생성해야 formatNoticeType 경합이 발생하지 않음
buildGrid();
},
error:function(e){
alert(e.responseText);
}
});
}
function buildGrid(){
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST', postData : { cmd : 'LIST',
searchNoticeType: $('select[name=searchNoticeType]').val(),
searchUseYn: $('select[name=searchUseYn]').val(), searchUseYn: $('select[name=searchUseYn]').val(),
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(), searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
searchNoticeDetail: $('input[name=searchNoticeDetail]').val() searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
}, },
colNames:['id', 'No.', '제목', '사용', '마지막 수정일', '등록일', '등록자', '조회수'], colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '장애상태', '마지막 수정일', '등록일', '등록자', '조회수'],
colModel:[ colModel:[
{ name : 'id' , align:'center', key:true, hidden:true}, { name : 'id' , align:'center', key:true, hidden:true},
{ name : 'rowNum' , align:'center', width:80 }, { name : 'rowNum' , align:'center', width:60 },
{ name : 'noticeSubject' , align:'left' , width:200 , formatter: formatFile }, { name : 'noticeType' , align:'center', width:100, formatter: formatNoticeType },
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn }, { name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
{ name : 'state' , align:'center', width:90, sortable:false, formatter: formatIncidentState },
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat}, { name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat }, { name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
{ name : 'inquirerName' , align:'center', width:100 }, { name : 'inquirerName' , align:'center', width:100 },
@@ -62,6 +166,8 @@
autowidth: true, autowidth: true,
viewrecords: true, viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'), rowList : eval('[${rmsDefaultRowList}]'),
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) { ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId); var rowData = $(this).getRowData(rowId);
var id = rowData['id']; var id = rowData['id'];
@@ -99,6 +205,12 @@
}); });
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
// 콤보 로드 → 콤보 성공 콜백에서 buildGrid() 호출 (경합 방지)
init();
$("#btn_search").click(function(){ $("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); var postData = getSearchForJqgrid("cmd","LIST");
@@ -114,6 +226,10 @@
goNav(url2); goNav(url2);
}); });
$("#btn_delete_selected").click(function(){
deleteSelectedRows();
});
$("select[name=searchUseYn], input[name^=search]").keydown(function(key){ $("select[name=searchUseYn], input[name^=search]").keydown(function(key){
if (key.keyCode == 13){ if (key.keyCode == 13){
$("#btn_search").click(); $("#btn_search").click();
@@ -136,13 +252,30 @@
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button> <button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></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>
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</div> </div>
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div> <div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;"> <form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
</colgroup>
<tbody> <tbody>
<tr> <tr>
<th style="width:180px;">사용여부</th> <th>게시유형</th>
<td>
<div class="select-style">
<select name="searchNoticeType"></select>
</div>
</td>
<th>제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
<th>사용여부</th>
<td> <td>
<div class="select-style"> <div class="select-style">
<select name="searchUseYn"> <select name="searchUseYn">
@@ -152,8 +285,6 @@
</select> </select>
</div> </div>
</td> </td>
<th style="width:180px;">제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -19,12 +19,139 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />'; var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />'; var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
var apiPopupUrl = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />';
var file_download = '<c:url value="/file/download.do" />'; var file_download = '<c:url value="/file/download.do" />';
var isDetail = false; var isDetail = false;
var fileInfo = null; var fileInfo = null;
// 코드 상수 — PortalNoticeUI 와 동기화
var NOTICE_TYPE_NORMAL = '1';
var NOTICE_TYPE_MAINTENANCE = '2';
var NOTICE_TYPE_INCIDENT = '3';
// 영향 API 목록 (UI 상태)
var affectedApis = [];
// 장애 타임라인 (UI 상태)
var currentIncidentId = null;
var currentIncidentState = null;
// 종결 상태 — 재전이 불가 (재발은 새 장애로 등록)
var TERMINAL_STATES = ['RESOLVED', 'CANCELED'];
// IncidentState 라벨 — 상태 콤보 문구와 동일하게 유지
var STATE_LABELS = {
'INVESTIGATING': '조사중',
'IDENTIFIED': '원인 파악',
'MONITORING': '모니터링',
'RESOLVED': '해소',
'CANCELED': '취소'
};
function isIncidentType(t) {
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
}
function stateLabel(code) {
if (!code) return '';
return (STATE_LABELS[code] || code) + ' (' + code + ')';
}
function escapeHtml(text) {
return $('<div>').text(text == null ? '' : text).html();
}
function init() { function init() {
// 초기화 로직 var key = "${param.id}";
isDetail = key != "" && key != "null";
if (isDetail) {
$("#title").append(" 수정");
buttonControl(true);
} else {
$("#title").append(" 등록");
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false);
}
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
toggleIncidentFields();
if (key) {
detail(key);
}
},
error:function(e){
alert(e.responseText);
}
});
}
function toggleIncidentFields() {
var t = $('#noticeType').val();
if (isIncidentType(t)) {
$('.incident-row').show();
// INCIDENT 만 상태 영역 표시
if (t === NOTICE_TYPE_INCIDENT) {
$('.incident-state-row').show();
$('.incident-endat-hint').show();
} else {
$('.incident-state-row').hide();
$('.incident-endat-hint').hide();
}
// 장애는 종결(해소·취소) 후에만 종료 시각을 손볼 수 있다. 진행 중에는 상태를 따른다.
var closed = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
$('#endAt').prop('readonly', t === NOTICE_TYPE_INCIDENT && !closed);
$('.incident-endat-hint').text(closed
? '※ 종결된 장애입니다. 실제 복구 시각으로 수정할 수 있습니다.'
: '※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.');
} else {
$('.incident-row').hide();
$('.incident-state-row').hide();
$('.incident-endat-hint').hide();
$('#endAt').prop('readonly', false);
}
// 양식(템플릿)은 장애 공지에만 쓴다 - 점검은 문구 체계가 다르다
$('.template-row').toggle(t === NOTICE_TYPE_INCIDENT);
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
$('#timelineSection').show();
} else {
$('#timelineSection').hide();
}
}
/**
* 장애 공지 양식(제목·본문)을 서버에서 받아 채운다.
* 문구는 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) {
@@ -44,67 +171,281 @@
function displayAttachFile(file) { function displayAttachFile(file) {
var $attachFiles = $('#attachFiles'); var $attachFiles = $('#attachFiles');
$attachFiles.empty(); $attachFiles.empty();
if (file) { if (file) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var fileItem = $('<div>').addClass('file-item'); var fileItem = $('<div>').addClass('file-item');
$('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem); $('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem);
var fileNameSpan = $('<span>').addClass('fileName')
var fileNameSpan = $('<span>') .text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
.addClass('fileName') .css('cursor', 'pointer')
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')') .on('click', function() { if (file.fileId) downloadFile(file.fileId); });
.css('cursor', 'pointer')
.on('click', function() {
if (file.fileId) {
downloadFile(file.fileId);
}
});
fileNameSpan.appendTo(fileItem); fileNameSpan.appendTo(fileItem);
$('<button>').text('X').addClass('delete-file').css('marginLeft', '15px') $('<button>').text('X').addClass('delete-file').css('marginLeft', '15px')
.on('click', function() { .on('click', function() {
fileInfo = null; fileInfo = null;
displayAttachFile(null); displayAttachFile(null);
// 폼에 삭제 플래그 추가 $('<input>').attr({type: 'hidden', name: 'fileDeleted', value: 'true'}).appendTo('#ajaxForm');
$('<input>').attr({ }).appendTo(fileItem);
type: 'hidden',
name: 'fileDeleted',
value: 'true'
}).appendTo('#ajaxForm');
})
.appendTo(fileItem);
fileItem.appendTo($attachFiles); fileItem.appendTo($attachFiles);
} }
} }
function downloadFile(fileId) { function downloadFile(fileId) {
if (fileId) { if (fileId) {
var fileSn = 1; // 단일 파일이므로 시리얼 넘버를 1로 고정 window.location.href = file_download + '?fileId=' + fileId + '&fileSn=1';
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=' + fileSn;
} else {
console.log("File not yet uploaded or doesn't have an ID");
} }
} }
function renderAffectedApis() {
var $tb = $('#affectedApiTbody');
$tb.empty();
if (!affectedApis.length) {
$tb.append('<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>');
return;
}
$.each(affectedApis, function(idx, api) {
var $tr = $('<tr>');
$tr.append('<td style="text-align:center;width:40px;"><input type="checkbox" class="affected-api-check" data-idx="' + idx + '"></td>');
$tr.append('<td>' + (api.apiId || '') + '</td>');
$tr.append('<td>' + (api.apiName || '') + '</td>');
$tb.append($tr);
});
}
function openApiPopup() {
// showModal 헬퍼는 urlAddServiceType + &menuId=getMenuId() + &pop=true 를 자동 부여 →
// AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생.
// 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어
// frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별.
// 공지 영향 API 는 포탈에 실제 노출되는 것만 고르면 되므로 두 조건을 켠 채로 연다.
// (타 화면에서 여는 팝업은 파라미터가 없어 해제 상태 — 팝업 기본값은 건드리지 않는다)
var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(',');
var url2 = apiPopupUrl + '?cmd=POPUP&searchApiSpecYn=Y&searchPortalDisplayYn=Y'
+ '&selectedApiIds=' + encodeURIComponent(selectedIds);
var args = {};
showModal(url2, args, 1280, 860, function () {
var doc = args.self && args.self.document;
if (!doc) return;
var popupFrame = null;
var iframes = doc.querySelectorAll('iframe');
for (var i = 0; i < iframes.length; i++) {
var src = iframes[i].getAttribute('src') || '';
if (src.indexOf('apiGroupMan') >= 0) {
popupFrame = iframes[i].contentWindow;
break;
}
}
if (!popupFrame) return;
var rv = popupFrame.returnValue;
if (!rv) return;
var rows = Array.isArray(rv) ? rv : [rv];
$.each(rows, function(_, item) {
if (!item || !item.apiId) return;
var exists = affectedApis.some(function(a){ return a.apiId === item.apiId; });
if (!exists) {
affectedApis.push({apiId: item.apiId, apiName: item.apiDesc || item.apiName || ''});
}
});
renderAffectedApis();
});
}
function removeSelectedAffectedApis() {
var idxes = [];
$('.affected-api-check:checked').each(function() {
idxes.push(parseInt($(this).data('idx'), 10));
});
if (!idxes.length) {
alert('삭제할 항목을 선택해주세요.');
return;
}
// 역순 삭제
idxes.sort(function(a,b){return b-a;}).forEach(function(i){ affectedApis.splice(i, 1); });
renderAffectedApis();
}
// ───────────────────── 장애 처리 타임라인 ─────────────────────
/** 현재 상태 기준으로 전이 가능한 선택지만 남긴다. 실제 차단은 서버가 한다. */
function applyStateChangeGuard() {
var terminal = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
$('#timelineStateChange').prop('disabled', terminal);
if (terminal) {
$('#timelineStateChange').prop('checked', false);
$('#timelineNextState').prop('disabled', true).val('');
}
$('#timelineClosedHint').toggle(terminal);
$('#timelineResolveHint').toggle(!terminal);
// 현재와 동일한 상태로는 전이할 수 없다
$('#timelineNextState option').each(function() {
var v = $(this).val();
$(this).prop('disabled', v !== '' && v === currentIncidentState);
});
if ($('#timelineNextState').val() === currentIncidentState) {
$('#timelineNextState').val('');
}
}
function loadTimeline() {
if (!currentIncidentId) {
renderTimeline([]);
return;
}
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'TIMELINE_LIST', incidentId: currentIncidentId},
success: function (rows) { renderTimeline(rows || []); },
error: function (e) { alert(e.responseText); }
});
}
function renderTimeline(rows) {
var $tb = $('#timelineTbody');
$tb.empty();
if (!rows.length) {
$tb.append('<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>');
return;
}
$.each(rows, function(_, row) {
var author = row.authorType === 'SYSTEM' ? 'System' : 'Admin';
var visible = row.visibleYn === 'Y';
var $tr = $('<tr>');
$tr.append('<td style="text-align:center;white-space:nowrap;">' + escapeHtml(row.eventAt) + '</td>');
$tr.append('<td style="text-align:center;">' + author + '</td>');
$tr.append('<td style="text-align:center;white-space:nowrap;">' + (row.stateAfter ? escapeHtml(stateLabel(row.stateAfter)) : '-') + '</td>');
$tr.append('<td>' + escapeHtml(row.body).replace(/\n/g, '<br>') + '</td>');
// 현재 상태(라벨) + 눌렀을 때 바뀔 상태(버튼) 를 나눠 표기 — 버튼 하나면 의미가 모호하다
var $visibleTd = $('<td style="text-align:center;white-space:nowrap;">');
$('<span>')
.text(visible ? '공개중' : '비공개')
.css({color: visible ? '#1B8C4A' : '#999', 'margin-right': '5px'})
.appendTo($visibleTd);
$('<button type="button" class="cssbtn" level="W" status="DETAIL" style="min-width:0;padding:3px 8px;">')
.text(visible ? '비공개로' : '공개로')
.click(function() { toggleTimelineVisible(row.timelineId, visible ? 'N' : 'Y'); })
.appendTo($visibleTd);
$tr.append($visibleTd);
var $deleteTd = $('<td style="text-align:center;">');
$('<button type="button" class="cssbtn smallBtn" level="W" status="DETAIL" style="min-width:0;width:90%;">')
.text('삭제')
.click(function() { deleteTimeline(row.timelineId); })
.appendTo($deleteTd);
$tr.append($deleteTd);
$tb.append($tr);
});
buttonControl(true);
}
function addTimeline() {
if (!currentIncidentId) {
alert('장애 정보를 먼저 저장하여 주십시오.');
return;
}
var body = $('#timelineBody').val();
if (!body || !$.trim(body)) {
alert('타임라인 메세지를 입력하여 주십시오.');
$('#timelineBody').focus();
return;
}
var stateChange = $('#timelineStateChange').is(':checked');
var nextState = $('#timelineNextState').val();
if (stateChange && !nextState) {
alert('변경할 상태를 선택하여 주십시오.');
return;
}
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'TIMELINE_ADD', incidentId: currentIncidentId, body: body,
stateChange: stateChange, nextState: nextState},
success: function (data) {
$('#timelineBody').val('');
$('#timelineStateChange').prop('checked', false).trigger('change');
// 상태 전이 시 서버가 상태/이전 상태/종료 시각을 함께 갱신한다
$('#stateText').text(data.state ? stateLabel(data.state) : '');
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
$('#endAt').val(data.endAt || '');
currentIncidentState = data.state || null;
applyStateChangeGuard();
// 종결 전이 직후부터 종료 시각을 손볼 수 있어야 한다
toggleIncidentFields();
loadTimeline();
},
error: function (e) { alert(e.responseText); }
});
}
function toggleTimelineVisible(timelineId, visibleYn) {
$.ajax({
type: "POST", url: url,
data: {cmd: 'TIMELINE_VISIBLE', timelineId: timelineId, visibleYn: visibleYn},
success: loadTimeline,
error: function (e) { alert(e.responseText); }
});
}
function deleteTimeline(timelineId) {
if (confirm("<%= localeMessage.getString("common.checkDelete")%>") != true) return;
$.ajax({
type: "POST", url: url,
data: {cmd: 'TIMELINE_DELETE', timelineId: timelineId},
success: loadTimeline,
error: function (e) { alert(e.responseText); }
});
}
/** 장애/점검 공지는 최초 게시 후 게시유형을 바꿀 수 없다. */
function lockNoticeType(noticeType) {
if (!isIncidentType(noticeType)) return;
$('#noticeType').prop('disabled', true);
if (!$('#noticeTypeHidden').length) {
$('#ajaxForm').append('<input type="hidden" id="noticeTypeHidden" name="noticeType">');
}
$('#noticeTypeHidden').val(noticeType);
}
function detail(key) { function detail(key) {
if (!isDetail) return; if (!isDetail) return;
$.ajax({ $.ajax({
type: "POST", type: "POST", url: url, dataType: "json",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (data) { success: function (data) {
$("#id").val(key); $("#id").val(key);
$('#noticeType').val(data.noticeType);
$("#noticeSubject").val(data.noticeSubject); $("#noticeSubject").val(data.noticeSubject);
$("#useYn").prop("checked", data.useYn === "Y"); $("#useYn").prop("checked", data.useYn === "Y");
$("#fixYn").prop("checked", data.fixYn === "Y");
var decodedContent = decodeHTMLEntities(data.noticeDetail); var decodedContent = decodeHTMLEntities(data.noticeDetail);
$('#contents').summernote('code', decodedContent); $('#contents').summernote('code', decodedContent);
// 파일 정보 표시 // 게시유형은 최초 저장 후 변경 불가 (disabled 는 전송되지 않아 hidden 으로 값을 유지)
lockNoticeType(data.noticeType);
// 장애/점검 필드
$('#summary').val(data.summary || '');
$('#startedAt').val(data.startedAt || '');
$('#endAt').val(data.endAt || '');
// 상태/이전 상태는 타임라인 게시로만 바뀐다 (직접 수정 불가)
$('#stateText').text(data.state ? stateLabel(data.state) : '');
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
affectedApis = (data.affectedApis || []).map(function(a){
return {apiId: a.apiId, apiName: a.apiName || ''};
});
renderAffectedApis();
currentIncidentId = data.incidentId || null;
currentIncidentState = data.state || null;
applyStateChangeGuard();
toggleIncidentFields();
if (data.noticeType === NOTICE_TYPE_INCIDENT && currentIncidentId) {
loadTimeline();
}
if (data.fileInfo) { if (data.fileInfo) {
fileInfo = { fileInfo = {
originalFileName: data.fileInfo.fileDetails[0].originalFileName, originalFileName: data.fileInfo.fileDetails[0].originalFileName,
@@ -115,37 +456,37 @@
displayAttachFile(fileInfo); displayAttachFile(fileInfo);
} }
}, },
error: function (e) { error: function (e) { alert(e.responseText); }
alert(e.responseText);
}
}); });
} }
$(document).ready(function () { $(document).ready(function () {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.id}";
isDetail = key != "" && key != "null";
if (isDetail) {
$("#title").append(" 수정");
buttonControl(true);
} else {
$("#title").append(" 등록");
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false);
}
init(); init();
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원) initSummernote('#contents', { placeholder: '여기에 내용을 입력하세요.', height: 300 });
initSummernote('#contents', {
placeholder: '여기에 내용을 입력하세요.',
height: 300
});
if (key) { $('#noticeType').on('change', toggleIncidentFields);
detail(key);
} // 신규 등록에서 장애를 고르면 빈 본문에 한해 양식을 자동으로 채운다.
// (수정 모드나 이미 쓴 본문은 건드리지 않는다 - 덮어쓰려면 '양식 적용' 버튼)
$('#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_removeAffectedApi').click(removeSelectedAffectedApis);
$('#timelineStateChange').on('change', function() {
var checked = $(this).is(':checked');
$('#timelineNextState').prop('disabled', !checked);
if (!checked) $('#timelineNextState').val('');
});
$('#btn_addTimeline').click(addTimeline);
$('#fileInput').change(function(e) { $('#fileInput').change(function(e) {
var file = e.target.files[0]; var file = e.target.files[0];
@@ -153,51 +494,75 @@
fileInfo = { fileInfo = {
originalFileName: file.name.split('.').slice(0, -1).join('.'), originalFileName: file.name.split('.').slice(0, -1).join('.'),
fileExtension: file.name.split('.').pop(), fileExtension: file.name.split('.').pop(),
file: file, file: file, size: file.size
size: file.size
}; };
displayAttachFile(fileInfo); displayAttachFile(fileInfo);
} }
this.value = ''; // 입력 필드 초기화 this.value = '';
});
$('#addFile').click(function() {
$('#fileInput').click();
}); });
$('#addFile').click(function() { $('#fileInput').click(); });
$("#btn_modify").click(function () { $("#btn_modify").click(function () {
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) var noticeType = $('#noticeType').val();
return; if (isIncidentType(noticeType)) {
if (!$('#startedAt').val()) {
alert('시작 시각을 입력해 주십시오.');
return;
}
if (noticeType === NOTICE_TYPE_MAINTENANCE && !$('#endAt').val()) {
alert('점검 종료 시각을 입력해 주십시오.');
return;
}
// datetime-local 값은 'YYYY-MM-DDTHH:mm' 이라 문자열 비교로 선후가 가려진다
if ($('#endAt').val() && $('#endAt').val() < $('#startedAt').val()) {
alert('종료 시각은 시작 시각보다 빠를 수 없습니다.');
return;
}
if (affectedApis.length === 0) {
alert('영향 API를 1건 이상 선택해 주십시오.');
return;
}
}
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
var formData = new FormData($("#ajaxForm")[0]); 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("noticeDetail", $('#contents').summernote('code')); formData.set("noticeDetail", $('#contents').summernote('code'));
// 시작/종료 시각이 비어 있으면 전송하지 않음 (서버 파싱 오류 방지)
if (!$('#startedAt').val()) formData.delete('startedAt');
if (!$('#endAt').val()) formData.delete('endAt');
// 영향 API → affectedApis[i].apiId/apiName 으로 전송
$.each(affectedApis, function(i, api) {
formData.set('affectedApis[' + i + '].apiId', api.apiId);
formData.set('affectedApis[' + i + '].apiName', api.apiName || '');
});
formData.append("cmd", isDetail ? "UPDATE" : "INSERT"); formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
// 파일 처리
if (fileInfo && fileInfo.file) { if (fileInfo && fileInfo.file) {
formData.set("files", fileInfo.file); formData.set("files", fileInfo.file);
formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension); formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension);
} }
$.ajax({ $.ajax({
type: "POST", type: "POST", url: url, data: formData,
url: url, processData: false, contentType: false,
data: formData, success: function () {
processData: false,
contentType: false,
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl); goNav(returnUrl);
}, },
error: function (e) { error: function (e) { alert(e.responseText); }
alert(e.responseText);
}
}); });
}); });
@@ -205,25 +570,18 @@
if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) { if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"}); postData.push({name: "cmd", value: "DELETE"});
$.ajax({ $.ajax({
type: "POST", type: "POST", url: url, data: postData,
url: url, success: function () {
data: postData,
success: function (args) {
alert("<%= localeMessage.getString("common.deleteMsg") %>"); alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl); goNav(returnUrl);
}, },
error: function (e) { error: function (e) { alert(e.responseText); }
alert(e.responseText);
}
}); });
} }
}); });
$("#btn_previous").click(function () { $("#btn_previous").click(function () { goNav(returnUrl); });
goNav(returnUrl);
});
}); });
</script> </script>
</head> </head>
@@ -233,7 +591,7 @@
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div>
<div class="content_middle"> <div class="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
@@ -244,34 +602,146 @@
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8"> <form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col style="width: 10%"/>
<col style="width: 40%"/>
<col style="width: 10%"/>
<col style="width: 40%"/>
</colgroup>
<tr> <tr>
<th style="width:20%;">사용여부</th> <th>게시유형 <font color="red">*</font></th>
<td> <td colspan="3">
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용</td> <div class="select-style">
</tr> <select name="noticeType" id="noticeType"></select>
<tr> </div>
<th style="width:20%;">제목 <font color="red">*</font></th>
<td><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th style="width:20%;">본문 <font color="red">*</font></th>
<td>
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;">첨부파일</th> <th>제목 <font color="red">*</font></th>
<td colspan="3"><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th>사용여부</th>
<td> <td>
<div style="margin: 5px 0px; display: inline-block;"> <input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
<input type="file" id="fileInput" name="files" style="display:none;" /> </td>
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button> <th>상단고정</th>
<td>
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
</td>
</tr>
<tr>
<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> </div>
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div> </th>
<td colspan="3">
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td>
</tr>
<!-- ───── 장애/점검 전용 영역 ───── -->
<tr class="incident-row" style="display:none;">
<th>시작 <font color="red">*</font></th>
<td>
<input type="datetime-local" id="startedAt" name="startedAt" style="width:90%">
</td>
<th>종료</th>
<td>
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
<div class="incident-endat-hint" style="display:none;color:#888;font-size:12px;margin-top:3px;">
※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.
</div>
</td>
</tr>
<!-- 상태/이전 상태는 타임라인 게시로만 전이한다 (직접 수정 불가) -->
<tr class="incident-row incident-state-row" style="display:none;">
<th>상태</th>
<td><span id="stateText"></span></td>
<th>이전 상태</th>
<td><span id="previousState">없음</span></td>
</tr>
<tr class="incident-row" style="display:none;">
<th>영향 API 선택</th>
<td colspan="3">
<div style="margin-bottom:5px;">
<button type="button" class="cssbtn smallBtn" id="btn_removeAffectedApi"><i class="material-icons">delete</i> 삭제</button>
<button type="button" class="cssbtn smallBtn" id="btn_addAffectedApi"><i class="material-icons">add</i> 추가</button>
</div>
<table class="table_row" cellspacing="0" style="width:100%;">
<thead>
<tr>
<th style="width:40px;text-align:center;"><input type="checkbox" id="affectedApiAll" onclick="$('.affected-api-check').prop('checked', this.checked);"></th>
<th>API ID</th>
<th>API 명</th>
</tr>
</thead>
<tbody id="affectedApiTbody">
<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>
</tbody>
</table>
</td> </td>
</tr> </tr>
</table> </table>
</form> </form>
<!-- 타임라인은 [수정] 저장과 별개로 게시 즉시 반영되므로 폼 밖에 둔다 -->
<div id="timelineSection" style="display:none;margin-top:20px;">
<div class="title">타임라인</div>
<table class="table_row" cellspacing="0" style="width:100%;">
<colgroup>
<col style="width: 130px"/>
<col style="width: 80px"/>
<col style="width: 140px"/>
<col/>
<col style="width: 160px"/>
<col style="width: 80px"/>
</colgroup>
<thead>
<tr>
<th>시간</th>
<th>작성자</th>
<th>상태</th>
<th>메세지</th>
<th>개발자포탈 공개</th>
<th>삭제</th>
</tr>
</thead>
<tbody id="timelineTbody">
<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>
</tbody>
</table>
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
<label>
<input type="checkbox" id="timelineStateChange"> 상태 변경
</label>
<div class="select-style" style="width:220px;">
<select id="timelineNextState" disabled>
<option value="">(현재 상태 유지)</option>
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
<option value="MONITORING">MONITORING - 모니터링</option>
<option value="RESOLVED">RESOLVED - 해소</option>
<option value="CANCELED">CANCELED - 취소</option>
</select>
</div>
<span id="timelineResolveHint" style="color:#888;font-size:12px;">※ 해소(RESOLVED)·취소(CANCELED)로 전이하면 종료 시각이 자동 입력됩니다.</span>
<span id="timelineClosedHint" style="display:none;color:#C0392B;font-size:12px;">
※ 종결된 장애입니다. 상태 변경 불가 — 재발 시 새 장애로 등록하십시오.
</span>
</div>
<div style="margin-top:5px;display:flex;gap:5px;">
<textarea id="timelineBody" style="flex:1;height:60px;"
placeholder="타임라인 메세지 입력&#10;예 : 모니터링 5분간 정상 응답 확인 후 완료 상태 전이 예정"></textarea>
<button type="button" class="cssbtn" id="btn_addTimeline" level="W" status="DETAIL">
<i class="material-icons">send</i> 게시
</button>
</div>
</div>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
@@ -312,7 +312,9 @@ $(document).ready(function() {
<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_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button> <button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
<c:if test="${hardDeleteEnabled}">
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button> <button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</c:if>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button> <button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div> </div>
<%--법인정보관리--%> <%--법인정보관리--%>
@@ -1,5 +1,7 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.io.*"%> <%@ page import="java.io.*"%>
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
<%@ 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" %>
@@ -7,6 +9,9 @@
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 reverseProxyUrl = monitoringContext.getStringProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL, "");
%> %>
<%! <%!
public String getRequiredLabel(String label) { public String getRequiredLabel(String label) {
@@ -133,10 +138,15 @@
$("#btn_delete").hide(); $("#btn_delete").hide();
} }
$("#approvalStatus").val(portalOrgData.approvalStatus); $("select[name=approvalStatus]").val(portalOrgData.approvalStatus);
// 승인상태는 읽기전용(수정 불가) - 값은 그대로 제출되도록 disabled 대신 잠금 처리
$("select[name=approvalStatus]")
.css({'pointer-events':'none', 'background-color':'#eee'})
.attr('tabindex', '-1');
$("#compRegNo").val(formatCompanyRegNo(portalOrgData.compRegNo)); $("#compRegNo").val(formatCompanyRegNo(portalOrgData.compRegNo));
$("#corpRegNo").val(formatCorpRegNo(portalOrgData.corpRegNo)); $("#corpRegNo").val(formatCorpRegNo(portalOrgData.corpRegNo));
$("#createdDate").inputmask("9999-99-99 99:99:99", {'autoUnmask': true}); $("#createdDate").inputmask("9999-99-99 99:99:99", {'autoUnmask': true});
$("#reverseProxyPath").val(portalOrgData.reverseProxyPath);
// 전화번호 처리 // 전화번호 처리
if (portalOrgData.scPhoneNumber) { if (portalOrgData.scPhoneNumber) {
@@ -404,47 +414,6 @@
downloadFile(fileId, '1'); // fileSn은 '1'로 가정 downloadFile(fileId, '1'); // fileSn은 '1'로 가정
}); });
// OBP 파트너코드 조회 버튼 클릭
$("#btn_query_obp").click(function() {
var compRegNo = $("#compRegNo").val();
if (!compRegNo) {
jSuites.notification({ name: '알림', message: '사업자등록번호를 먼저 입력해주세요.', error: true });
$("#compRegNo").focus();
return;
}
// 하이픈 제거
compRegNo = compRegNo.replace(/-/g, '');
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'QUERY_OBP_PARTNER_CODE',
compRegNo: compRegNo
},
beforeSend: function() {
$("#btn_query_obp").prop("disabled", true).html('<i class="material-icons">hourglass_empty</i> 조회중...');
},
success: function(json) {
if (json.success) {
$("#orgCode").val(json.partnerCode);
jSuites.notification({ name: '성공', message: '파트너코드가 조회되었습니다: ' + json.partnerCode });
} else {
jSuites.notification({ name: '실패', message: json.message, error: true });
}
},
error: function(e) {
jSuites.notification({ name: '오류', message: '파트너코드 조회 중 오류가 발생했습니다.', error: true });
console.error(e.responseText);
},
complete: function() {
$("#btn_query_obp").prop("disabled", false).html('<i class="material-icons">search</i> 파트너코드 조회');
}
});
});
$("#btn_modify").click(function(){ $("#btn_modify").click(function(){
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
@@ -515,8 +484,7 @@
} }
} }
if (confirm("<%= localeMessage.getString("common.checkSave")%>") !== true) // 저장 확인/사유 입력은 유효성 검사 및 FormData 구성 후 처리 (하단 submit 분기)
return;
// FormData 생성 전에 분리된 전화번호 필드 제거 // FormData 생성 전에 분리된 전화번호 필드 제거
$("#scPhonePrefix, #scPhoneMiddle, #scPhoneLast, #orgPhonePrefix, #orgPhoneMiddle, #orgPhoneLast").removeAttr('name'); $("#scPhonePrefix, #scPhoneMiddle, #scPhoneLast, #orgPhonePrefix, #orgPhoneMiddle, #orgPhoneLast").removeAttr('name');
@@ -551,23 +519,47 @@
} }
// cmd 파라미터 추가 // cmd 및 사유(auditReason) 첨부 후 전송
formData.append("cmd", isDetail ? "UPDATE" : "INSERT"); function submitOrg(cmd, auditReason) {
formData.append("cmd", cmd);
$.ajax({ // 감사로그(AUDIT_LOG) 발화를 위한 systemCode. 법인/가입자 리포지토리는 고정 EMS 스키마라 스키마 전환 영향 없음
type : "POST", formData.append("serviceType", "APIGW");
url:url, if (auditReason) {
data: formData, formData.append("auditReason", auditReason);
processData: false,
contentType: false,
success:function(json){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);
},
error:function(e){
alert(e.responseText);
} }
});
$.ajax({
type : "POST",
url:url,
data: formData,
processData: false,
contentType: false,
success:function(json){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);
},
error:function(e){
alert(e.responseText);
}
});
}
if (isDetail) {
// 수정: 상태를 REMOVED로 바꾼 경우 삭제(cmd=DELETE)로 감사 기록 + 익명화
var targetCmd = ($("#orgStatus").val() === 'REMOVED') ? "DELETE" : "UPDATE";
var isRemove = (targetCmd === "DELETE");
showReasonPrompt(
isRemove ? "해당 법인을 삭제(탈퇴) 처리합니다. 사유를 입력해 주세요." : "법인 정보를 수정합니다. 사유를 입력해 주세요.",
{
title: isRemove ? "삭제 사유" : "수정 사유",
onConfirm: function(reason) { submitOrg(targetCmd, reason); }
}
);
} else {
// 신규 등록: 사유 불필요
if (confirm("<%= localeMessage.getString("common.checkSave")%>") !== true) return;
submitOrg("INSERT", null);
}
}); });
$("#btn_previous").click(function() { $("#btn_previous").click(function() {
@@ -576,25 +568,28 @@
$("#btn_delete").click(function(){ $("#btn_delete").click(function(){
if(confirm("<%= localeMessage.getString("common.confirmMsg")%>")){ showReasonPrompt("해당 법인을 삭제 처리합니다. 사유를 입력해 주세요.", {
var postData = $('#ajaxForm').serializeArray(); title: "삭제 사유",
postData.push({name: "cmd" , value:"DELETE"}); onConfirm: function(reason) {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd" , value:"DELETE"});
postData.push({name: "serviceType", value: "APIGW"});
postData.push({name: "auditReason", value: reason});
$.ajax({ $.ajax({
type : "POST", type : "POST",
url:url, url:url,
data:postData, data:postData,
success:function(args){ success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>"); alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl); goNav(returnUrl);
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
} }
}); });
}else{ }
return false; });
}
}); });
@@ -630,10 +625,7 @@
<tr> <tr>
<th style="width:15%;"><%= localeMessage.getString("portalOrg.orgCode") %></th> <th style="width:15%;"><%= localeMessage.getString("portalOrg.orgCode") %></th>
<td colspan="3" style="width:35%;"> <td colspan="3" style="width:35%;">
<div style="display: flex; align-items: center;"> <input type="text" name="orgCode" id="orgCode"/>
<input type="text" name="orgCode" id="orgCode" style="width: 200px; margin-right: 10px;"/>
<button type="button" class="cssbtn smallBtn" id="btn_query_obp"><i class="material-icons">search</i> 파트너코드 조회</button>
</div>
</td> </td>
<th><%= localeMessage.getString("portalOrg.orgIndustryType") %></th> <%--업종--%> <th><%= localeMessage.getString("portalOrg.orgIndustryType") %></th> <%--업종--%>
<td><input type="text" name="orgIndustryType" id="orgIndustryType" maxlength="50" /></td> <td><input type="text" name="orgIndustryType" id="orgIndustryType" maxlength="50" /></td>
@@ -738,6 +730,16 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr>
<th><%= localeMessage.getString("portalOrg.reverseProxyPath") %></th>
<td colspan="3">
<%=reverseProxyUrl %>/<input type="text" name="reverseProxyPath" id="reverseProxyPath" style="width:100px; "/>
</td>
<th></th>
<td>
</td>
</tr>
<tr><%--사업자등록증--%> <tr><%--사업자등록증--%>
<th><%= localeMessage.getString("portalOrg.compRegFile") %></th> <th><%= localeMessage.getString("portalOrg.compRegFile") %></th>
<td colspan="5" style="padding-top: 5px; padding-bottom: 5px;"> <td colspan="5" style="padding-top: 5px; padding-bottom: 5px;">
@@ -26,7 +26,38 @@ function formatFile(cellvalue, options, rowObject) {
return cellvalue + icon; return cellvalue + icon;
} }
$(document).ready(function() { function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
$(document).ready(function() {
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
@@ -45,7 +76,7 @@ $(document).ready(function() {
], ],
jsonReader: { jsonReader: {
repeatitems:false repeatitems:false
}, },
pager : $('#pager'), pager : $('#pager'),
page : '${param.page}', page : '${param.page}',
rowNum : '${rmsDefaultRowNum}', rowNum : '${rmsDefaultRowNum}',
@@ -54,6 +85,8 @@ $(document).ready(function() {
autowidth: true, autowidth: true,
viewrecords: true, viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'), rowList : eval('[${rmsDefaultRowList}]'),
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) { ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId); var rowData = $(this).getRowData(rowId);
var id = rowData['id']; var id = rowData['id'];
@@ -94,10 +127,14 @@ $(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 로
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid"); $("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
}); });
$("#btn_delete_selected").click(function(){
deleteSelectedRows();
});
$("input[name^=search]").keydown(function(key){ $("input[name^=search]").keydown(function(key){
if (key.keyCode == 13){ if (key.keyCode == 13){
$("#btn_search").click(); $("#btn_search").click();
@@ -120,8 +157,9 @@ $(document).ready(function() {
<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_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>
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</div> </div>
<div class="title">사업제휴 신청 목록</div> <div class="title">피드백/개선요청 신청 목록</div>
<form id="ajaxForm" onsubmit="return false;"> <form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<tbody> <tbody>
@@ -92,6 +92,27 @@ $(document).ready(function() {
goNav(returnUrl);//LIST로 이동 goNav(returnUrl);//LIST로 이동
}); });
$("#btn_delete").click(function(){
if (!confirm('선택한 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: { cmd: 'DELETE', id: key },
success: function(data) {
alert('삭제되었습니다.');
var returnUrl = getReturnUrlForReturn();
goNav(returnUrl);//LIST로 이동
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
});
$("#btn_unmask").click(function () { $("#btn_unmask").click(function () {
const reason = prompt("마스킹 해제 사유를 입력하세요:"); const reason = prompt("마스킹 해제 사유를 입력하세요:");
@@ -134,9 +155,10 @@ $(document).ready(function() {
<div class="content_middle"> <div class="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_unmask" level="W" status="DETAIL"><i class="material-icons">lock_open</i> 마스킹해제</button> <button type="button" class="cssbtn" id="btn_unmask" level="W" status="DETAIL"><i class="material-icons">lock_open</i> 마스킹해제</button>
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 삭제</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>
</div> </div>
<div class="title">사업제휴 신청 상세</div> <div class="title">피드백/개선요청 신청 상세</div>
<table id="grid" ></table> <table id="grid" ></table>
<div id="pager"></div> <div id="pager"></div>
@@ -20,6 +20,7 @@
var url = '<c:url value="/onl/apim/portalproperty/propertyMan.json" />'; var url = '<c:url value="/onl/apim/portalproperty/propertyMan.json" />';
var url_view = '<c:url value="/onl/apim/portalproperty/propertyMan.view" />'; var url_view = '<c:url value="/onl/apim/portalproperty/propertyMan.view" />';
var lastsel2; var lastsel2;
var clickedCol; //클릭한 셀의 컬럼 인덱스(포커스 이동용)
function isValid() { function isValid() {
if ($('input[name=prptyGroupName]').val() == "") { if ($('input[name=prptyGroupName]').val() == "") {
@@ -39,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;
} }
@@ -86,9 +87,23 @@
editurl: "clientArray", editurl: "clientArray",
colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>', colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>',
'<%= localeMessage.getString("propertyDetail.propertyValue") %>', '<%= localeMessage.getString("propertyDetail.propertyValue") %>',
'설명',
'<%= 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: 200, 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: 'DELETEYN', name: 'DELETEYN',
width: 20, width: 20,
@@ -101,6 +116,10 @@
}, },
loadComplete: function () { loadComplete: function () {
}, },
onCellSelect: function (rowid, iCol) {
//onSelectRow보다 먼저 발화 → 클릭한 컬럼 인덱스 저장
clickedCol = iCol;
},
onSelectRow: function (rowid, status) { onSelectRow: function (rowid, status) {
if (lastsel2 != undefined) { if (lastsel2 != undefined) {
if ($("#grid tr#" + lastsel2).attr("editable") == 1) { //editable=1 means row in edit mode if ($("#grid tr#" + lastsel2).attr("editable") == 1) { //editable=1 means row in edit mode
@@ -108,7 +127,8 @@
} }
} }
$('#grid').restoreRow(lastsel2); $('#grid').restoreRow(lastsel2);
$('#grid').editRow(rowid, true); //focusField로 클릭한 셀에 포커스(미지정 시 첫 편집 컬럼으로 감)
$('#grid').editRow(rowid, {keys: true, focusField: clickedCol});
lastsel2 = rowid; lastsel2 = rowid;
}, },
onSortCol: function () { onSortCol: function () {
@@ -257,7 +277,8 @@
} }
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();
var rows = $("#grid")[0].rows; var rows = $("#grid")[0].rows;
var index = Number($(rows[rows.length - 1]).attr("id")); var index = Number($(rows[rows.length - 1]).attr("id"));
@@ -337,7 +358,13 @@
<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>
</tr>
<tr>
<th>설명</th>
<td colspan="3"><input type="text" name="prptyDesc"/>
</td> </td>
</tr> </tr>
</table> </table>
@@ -29,6 +29,16 @@
// 원본 행 데이터를 ID로 조회하기 위한 맵 // 원본 행 데이터를 ID로 조회하기 위한 맵
var gridRowDataMap = {}; var gridRowDataMap = {};
// 휴대폰번호 중복 조회 모드 여부
var isDupView = false;
// 현재 검색조건 + 중복 조회 모드 플래그를 합쳐 LIST 요청 파라미터를 생성
function buildListPostData() {
var postData = getSearchForJqgrid("cmd", "LIST");
postData.onlyDuplicateMobile = isDupView;
return postData;
}
/* 선택된 사용자 삭제 - 상태에 따라 Soft/Hard Delete 자동 분기 */ /* 선택된 사용자 삭제 - 상태에 따라 Soft/Hard Delete 자동 분기 */
function deleteSelectedUsers() { function deleteSelectedUsers() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow'); var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
@@ -166,6 +176,13 @@
$('#grid').jqGrid('setColProp', 'approvalStatus', {editoptions: {value: select_approvalStatus}}); $('#grid').jqGrid('setColProp', 'approvalStatus', {editoptions: {value: select_approvalStatus}});
$('#grid').jqGrid('setColProp', 'userStatus', {editoptions: {value: select_userStatus}}); $('#grid').jqGrid('setColProp', 'userStatus', {editoptions: {value: select_userStatus}});
// 휴대폰번호 중복 허용안함(기능 ON) + 중복 사용자 존재 시 배너 노출
if (json.mobileDuplicateCheckEnabled && json.mobileDuplicateUserCount > 0) {
$("#dupMobileCount").text(json.mobileDuplicateUserCount);
$("#dupMobileBanner").show();
}
$('#grid').trigger('reloadGrid'); $('#grid').trigger('reloadGrid');
}, },
@@ -176,7 +193,7 @@
} }
$(document).ready(function () { $(document).ready(function () {
var gridPostData = getSearchForJqgrid("cmd", "LIST"); var gridPostData = buildListPostData();
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype: "json", datatype: "json",
@@ -188,7 +205,7 @@
'<%= localeMessage.getString("portalUser.orgName") %>', '<%= localeMessage.getString("portalUser.orgName") %>',
'<%= localeMessage.getString("portalUser.name") %>', '<%= localeMessage.getString("portalUser.name") %>',
'<%= localeMessage.getString("portalUser.userId") %>', '<%= localeMessage.getString("portalUser.userId") %>',
<%--'<%= localeMessage.getString("portalUser.mobile") %>',--%> '<%= localeMessage.getString("portalUser.mobileNumber") %>',
'<%= localeMessage.getString("portalUser.roleName") %>', '<%= localeMessage.getString("portalUser.roleName") %>',
'<%= localeMessage.getString("portalUser.userStatus") %>', '<%= localeMessage.getString("portalUser.userStatus") %>',
'<%= localeMessage.getString("portalUser.createOn") %>', '<%= localeMessage.getString("portalUser.createOn") %>',
@@ -201,7 +218,7 @@
{name: 'portalOrgUIs.orgName', align: 'center', width: "120"}, {name: 'portalOrgUIs.orgName', align: 'center', width: "120"},
{name: 'userName', align: 'center', width: "80" }, {name: 'userName', align: 'center', width: "80" },
{name: 'loginId', align: 'center', width: "150" }, {name: 'loginId', align: 'center', width: "150" },
// {name: 'mobileNumber', align: 'center', width: "100" }, {name: 'mobileNumber', align: 'center', width: "110", hidden: true},
{name: 'roleCodeDescription', align: 'center', width: "80"}, {name: 'roleCodeDescription', align: 'center', width: "80"},
{name: 'userStatusDescription', align: 'center', width: "50"}, {name: 'userStatusDescription', align: 'center', width: "50"},
{name: 'createdDate', align: 'center', width: "100", formatter: timeStampFormat}, {name: 'createdDate', align: 'center', width: "100", formatter: timeStampFormat},
@@ -274,9 +291,20 @@
init(); init();
$("#btn_search").click(function () { $("#btn_search").click(function () {
var postData = getSearchForJqgrid("cmd", "LIST"); $("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
});
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid"); // 중복 휴대폰번호 사용자만 보기 / 전체 보기 토글
$("#btn_dup_toggle").click(function () {
isDupView = !isDupView;
if (isDupView) {
$(this).text("전체 보기");
$("#grid").jqGrid('showCol', 'mobileNumber');
} else {
$(this).text("중복만 보기");
$("#grid").jqGrid('hideCol', 'mobileNumber');
}
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
}); });
$("#btn_new").click(function () { $("#btn_new").click(function () {
@@ -319,9 +347,11 @@
<button type="button" class="cssbtn" id="btn_new" level="W"><i <button type="button" class="cssbtn" id="btn_new" level="W"><i
class="material-icons">add</i> <%= localeMessage.getString("button.new") %> class="material-icons">add</i> <%= localeMessage.getString("button.new") %>
</button> </button>
<c:if test="${hardDeleteEnabled}">
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i <button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i
class="material-icons">delete_forever</i> 선택 삭제 class="material-icons">delete_forever</i> 선택 삭제
</button> </button>
</c:if>
<button type="button" class="cssbtn" id="btn_search" level="R"><i <button type="button" class="cssbtn" id="btn_search" level="R"><i
class="material-icons">search</i> <%= localeMessage.getString("button.search") %> class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
@@ -334,6 +364,11 @@
</div> </div>
<% } %> <% } %>
<div id="dupMobileBanner" style="display:none; background:#f8d7da; border:1px solid #dc3545; padding:8px 15px; margin:5px 0; border-radius:4px; color:#721c24;">
<strong>&#9888; 중복 휴대폰번호 사용자 <span id="dupMobileCount">0</span>건 발견</strong>
<button type="button" class="cssbtn" id="btn_dup_toggle" style="margin-left:10px;">중복만 보기</button>
</div>
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<tbody> <tbody>
<tr> <tr>
@@ -365,15 +400,16 @@
<td><input type="text" name="searchUserName"></td> <td><input type="text" name="searchUserName"></td>
<th style="width:10%; min-width:100px;"> <th style="width:10%; min-width:100px;">
<%= localeMessage.getString("portalUser.userId") %> <%= localeMessage.getString("portalUser.userId") %><span style="color:#dc3545;">(*)</span>
</th> </th>
<td><input type="text" name="searchUserId" value="${param.searchUserId}"></td> <td><input type="text" name="searchUserId" value="${param.searchUserId}"></td>
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %> <th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %><span style="color:#dc3545;">(*)</span>
</th> </th>
<td><input type="text" name="searchMobileNumber" value="${param.searchMobileNumber}"></td> <td><input type="text" name="searchMobileNumber" value="${param.searchMobileNumber}"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div style="color:#dc3545; font-size:12px; margin:4px 2px;">(*) 암호화 항목 · 부분검색 불가(전체 일치)</div>
<table id="grid"></table> <table id="grid"></table>
<div id="pager"></div> <div id="pager"></div>
@@ -165,6 +165,10 @@
$("#userStatus").val(data.userStatus); $("#userStatus").val(data.userStatus);
$("#roleCode").val(data.roleCode).trigger('change'); $("#roleCode").val(data.roleCode).trigger('change');
$("#approvalStatus").val(data.approvalStatus); $("#approvalStatus").val(data.approvalStatus);
// 승인상태는 읽기전용(수정 불가) - 값은 그대로 제출되도록 disabled 대신 잠금 처리
$("#approvalStatus")
.css({'pointer-events':'none', 'background-color':'#eee'})
.attr('tabindex', '-1');
// 탈퇴 상태 처리 // 탈퇴 상태 처리
if (originalUserStatus === STATUS.REMOVED) { if (originalUserStatus === STATUS.REMOVED) {
@@ -281,7 +285,7 @@
} }
const fullLoginId = id + "@" + domain; const fullLoginId = id + "@" + domain;
const emailPattern = /^[A-Za-z0-9._-]*@[A-Za-z0-9][-A-Za-z0-9.]+\.[A-Za-z]{2,}$/; const emailPattern = /^[A-Za-z0-9._%+-]*@[A-Za-z0-9][-A-Za-z0-9.]+\.[A-Za-z]{2,}$/;
if (!emailPattern.test(fullLoginId)) { if (!emailPattern.test(fullLoginId)) {
alert(fullLoginId + " 은 올바른 이메일 형식이 아닙니다."); alert(fullLoginId + " 은 올바른 이메일 형식이 아닙니다.");
return; return;
@@ -313,7 +317,7 @@
} }
function validateEmailId(input) { function validateEmailId(input) {
input.value = input.value.replace(/[^A-Za-z0-9._-]/g, ''); input.value = input.value.replace(/[^A-Za-z0-9._%+-]/g, '');
} }
function validateDomainInput(input) { function validateDomainInput(input) {
@@ -488,29 +492,49 @@
formData.push({name: 'mobileNumber', value: mobileNumber}); formData.push({name: 'mobileNumber', value: mobileNumber});
} }
formData.push({ // cmd 및 사유(auditReason) 첨부 후 전송
name: 'cmd', function submitUser(cmd, auditReason) {
value: isDetail ? "UPDATE" : "INSERT" formData.push({ name: 'cmd', value: cmd });
}); // 감사로그(AUDIT_LOG) 발화를 위한 systemCode. 가입자 리포지토리는 고정 EMS 스키마라 스키마 전환 영향 없음
formData.push({ name: 'serviceType', value: 'APIGW' });
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; if (auditReason) {
formData.push({ name: 'auditReason', value: auditReason });
$.ajax({
type: "POST",
url: url,
data: formData,
success: function (json) {
if (json.status === "fail") {
alert(json.errorMsg || "저장에 실패했습니다.");
return;
}
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);
},
error: function (e) {
alert(e.responseText);
} }
});
$.ajax({
type: "POST",
url: url,
data: formData,
success: function (json) {
if (json.status === "fail") {
alert(json.errorMsg || "저장에 실패했습니다.");
return;
}
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);
},
error: function (e) {
alert(e.responseText);
}
});
}
if (isDetail) {
// 수정: 상태를 REMOVED로 바꾼 경우 삭제(cmd=DELETE)로 감사 기록 + 익명화
var targetCmd = ($("#userStatus").val() === 'REMOVED') ? "DELETE" : "UPDATE";
var isRemove = (targetCmd === "DELETE");
showReasonPrompt(
isRemove ? "해당 가입자를 삭제(탈퇴) 처리합니다. 사유를 입력해 주세요." : "가입자 정보를 수정합니다. 사유를 입력해 주세요.",
{
title: isRemove ? "삭제 사유" : "수정 사유",
onConfirm: function(reason) { submitUser(targetCmd, reason); }
}
);
} else {
// 신규 등록: 사유 불필요. 초기 패스워드는 비밀번호 초기화와 동일(이메일ID + subfix)
if (!confirm("가입자를 등록하시겠습니까?\n초기 패스워드는 '이메일ID${passwordInitSubfix}' 입니다.")) return;
submitUser("INSERT", null);
}
}); });
$("#btn_previous").click(function () { $("#btn_previous").click(function () {
@@ -518,27 +542,32 @@
}); });
$("#btn_delete").click(function () { $("#btn_delete").click(function () {
if (!confirm("<%= localeMessage.getString("common.confirmMsg")%>")) return; showReasonPrompt("해당 가입자를 삭제 처리합니다. 사유를 입력해 주세요.", {
title: "삭제 사유",
$.ajax({ onConfirm: function(reason) {
type: "POST", $.ajax({
url: url, type: "POST",
data: { url: url,
cmd: "DELETE", data: {
id: $('input[name=id]').val() cmd: "DELETE",
}, id: $('input[name=id]').val(),
success: function () { serviceType: "APIGW",
alert("<%= localeMessage.getString("common.deleteMsg") %>"); auditReason: reason
goNav(returnUrl); },
}, success: function () {
error: function (e) { alert("<%= localeMessage.getString("common.deleteMsg") %>");
alert(e.responseText); goNav(returnUrl);
},
error: function (e) {
alert(e.responseText);
}
});
} }
}); });
}); });
$("#btn_password_reset").click(function () { $("#btn_password_reset").click(function () {
if (!confirm("패스워드를 초기화 하시겠습니까?\n초기 패스워드는 '!' + 이메일ID 입니다.")) return; if (!confirm("패스워드를 초기화 하시겠습니까?\n초기 패스워드는 '이메일ID${passwordInitSubfix}' 입니다.")) return;
$.ajax({ $.ajax({
type: "POST", type: "POST",
@@ -172,6 +172,9 @@ $(document).ready(function() {
<form id="ajaxForm"> <form id="ajaxForm">
<input type="hidden" name="id"> <input type="hidden" name="id">
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col width="200px">
</colgroup>
<tr> <tr>
<th><%= localeMessage.getString("messageTemplate.messageCode") %> <font color="red"> *</font></th> <th><%= localeMessage.getString("messageTemplate.messageCode") %> <font color="red"> *</font></th>
<td><input type="text" name="messageCode" data-required data-warning="메세지 코드는 필수입니다"/></td> <td><input type="text" name="messageCode" data-required data-warning="메세지 코드는 필수입니다"/></td>
@@ -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>
@@ -0,0 +1,243 @@
<%@ 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; }
.succ-y { color:#1a73e8; font-weight:bold; }
.succ-n { color:#d93025; font-weight:bold; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/webhook/webhookSendLogMan.json" />';
var url_view = '<c:url value="/onl/apim/webhook/webhookSendLogMan.view" />';
var combo;
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>';
}
// 이벤트유형 코드 → 모니터링 공통코드(EVENT_TYPE) 코드명
function formatEventType(cellvalue, options, rowObject) {
if (!combo || !combo.eventTypeList) {
return escapeHtmlJs(cellvalue);
}
for (var i = 0; i < combo.eventTypeList.length; i++) {
if (combo.eventTypeList[i].CODE == cellvalue) {
return escapeHtmlJs(combo.eventTypeList[i].NAME);
}
}
return escapeHtmlJs(cellvalue);
}
function formatSuccess(cellvalue, options, rowObject) {
if (cellvalue === true) return '<span class="succ-y">성공</span>';
if (cellvalue === false) return '<span class="succ-n">실패</span>';
return '';
}
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);
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function (json) {
combo = json;
new makeOptions("CODE", "NAME").setObj($("select[name=searchEventType]"))
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
.setData(json.eventTypeList).rendering();
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
putSelectFromParam();
// 콤보(이벤트유형) 로드 후 그리드 생성 — formatEventType 경합 방지
buildGrid();
},
error: function (e) { alert(e.responseText); }
});
}
function buildGrid() {
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
url: url,
postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'],
colModel: [
{name: 'id', align: 'center', key: true, hidden: true},
{name: 'rowNum', align: 'center', width: 45, sortable: false},
{name: 'orgName', align: 'left', width: 140},
{name: 'eventType', align: 'center', width: 120, formatter: formatEventType},
{name: 'targetUrl', align: 'left', width: 300, formatter: formatTargetUrl},
{name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl},
{name: 'statusCode', align: 'center', width: 60},
{name: 'success', align: 'center', width: 70, formatter: formatSuccess},
{name: 'retryCount', align: 'center', width: 60},
{name: 'sendBy', align: 'center', width: 100},
{name: 'sentAt', 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 start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "");
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "");
if (start && start > getToday()) {
alert("시작일이 오늘 이후입니다. 시작일을 확인해주세요.");
$("input[name=searchStartYYYYMMDD]").focus();
return false;
}
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");
});
$("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;">
<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>
<div class="select-style">
<select name="searchEventType"></select>
</div>
</td>
<th>성공여부</th>
<td>
<div class="select-style">
<select name="searchSuccess">
<option value="">전체</option>
<option value="Y">성공</option>
<option value="N">실패</option>
</select>
</div>
</td>
</tr>
<tr>
<th>Target URL</th>
<td><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
<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="">
</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,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" %>
<%@ 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; min-height:60px; line-height:1.5; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/webhook/webhookSendLogMan.json" />';
var url_view = '<c:url value="/onl/apim/webhook/webhookSendLogMan.view" />';
var key = '${param.id}';
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
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 || '');
$('#eventType').text(data.eventTypeName || data.eventType || '')
.attr('title', data.eventType || '');
$('#targetUrl').text(data.targetUrl || '');
$('#proxyUrl').text(data.proxyUrl || '');
$('#statusCode').text(data.statusCode != null ? data.statusCode : '');
$('#retryCount').text(data.retryCount != null ? data.retryCount : '');
$('#sendBy').text(data.sendBy || '');
$('#sentAt').text(data.sentAt || '');
$('#signature').text(data.signature || '');
$('#userSecret').text(data.userSecret || '');
$('#errorMessage').text(data.errorMessage || '');
$('#payload').text(data.payload || '');
$('#responseBody').text(data.responseBody || '');
if (data.success === true) {
$('#success').html('<span class="succ-y">성공</span>');
} else if (data.success === false) {
$('#success').html('<span class="succ-n">실패</span>');
} else {
$('#success').text('');
}
},
error: function (e) { alert(e.responseText); }
});
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
buttonControl();
detail();
$('#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><span id="id"></span></td>
<th>기관명</th>
<td><span id="orgName"></span></td>
</tr>
<tr>
<th>이벤트유형</th>
<td><span id="eventType"></span></td>
<th>Status Code</th>
<td><span id="statusCode"></span></td>
</tr>
<tr>
<th>Target URL</th>
<td><span id="targetUrl"></span></td>
<th>Proxy URL</th>
<td><span id="proxyUrl"></span></td>
</tr>
<tr>
<th>성공여부</th>
<td><span id="success"></span></td>
<th>재시도횟수</th>
<td><span id="retryCount"></span></td>
</tr>
<tr>
<th>발송서버</th>
<td><span id="sendBy"></span></td>
<th>발송일시</th>
<td><span id="sentAt"></span></td>
</tr>
<tr>
<th>Signature</th>
<td><span id="signature"></span></td>
<th>User Secret</th>
<td><span id="userSecret"></span></td>
</tr>
<tr>
<th>Payload</th>
<td colspan="3"><div id="payload" class="pre-wrap"></div></td>
</tr>
<tr>
<th>Response Body</th>
<td colspan="3"><div id="responseBody" class="pre-wrap"></div></td>
</tr>
<tr>
<th>에러 메세지</th>
<td colspan="3"><div id="errorMessage" class="pre-wrap"></div></td>
</tr>
</table>
</div>
</div>
</body>
</html>
@@ -82,6 +82,54 @@
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript"> <script language="javascript">
function executeAggregationHourly() {
var targetDate = $("#targetHour").val().replace(/-/g, "");
var resultDiv = $("#hourlyResult");
// 날짜 입력 검증
if (!targetDate || targetDate.trim() === '') {
alert('대상 날짜를 입력하세요. (예: 20250120)');
$("#targetHour").focus();
return;
}
resultDiv.hide();
$("#btn_execute_hourly").prop("disabled", true).text("실행중...");
$.ajax({
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
type: 'POST',
data: {
cmd: 'AGGREGATION_HOUR',
targetDate: targetDate,
serviceType: '${param.serviceType}'
},
success: function(response) {
resultDiv.removeClass("error").addClass("success");
var message = response.message + "\n";
message += "대상 날짜: " + response.targetDate + "\n";
message += "처리 건수: " + response.processedCount;
resultDiv.find("pre").text(message);
resultDiv.show();
},
error: function(xhr) {
resultDiv.removeClass("success").addClass("error");
var message = "집계 실행 실패\n";
try {
var error = JSON.parse(xhr.responseText);
message += error.message;
} catch(e) {
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
}
resultDiv.find("pre").text(message);
resultDiv.show();
},
complete: function() {
$("#btn_execute_hourly").prop("disabled", false).text("집계 실행");
}
});
}
function executeHourlyToDaily() { function executeHourlyToDaily() {
var targetDate = $("#targetDate").val().replace(/-/g, ""); var targetDate = $("#targetDate").val().replace(/-/g, "");
var resultDiv = $("#dailyResult"); var resultDiv = $("#dailyResult");
@@ -241,6 +289,7 @@
var year = yesterday.getFullYear(); var year = yesterday.getFullYear();
var month = String(yesterday.getMonth() + 1).padStart(2, '0'); var month = String(yesterday.getMonth() + 1).padStart(2, '0');
var day = String(yesterday.getDate()).padStart(2, '0'); var day = String(yesterday.getDate()).padStart(2, '0');
$("#targetHour").val(year + month + day);
$("#targetDate").val(year + month + day); $("#targetDate").val(year + month + day);
// 전월 // 전월
@@ -255,6 +304,12 @@
$("#targetYear").val(lastYear); $("#targetYear").val(lastYear);
// 버튼 이벤트 // 버튼 이벤트
$("#btn_execute_hourly").click(function() {
if (confirm("시간별 통계 집계를 실행하시겠습니까?")) {
executeAggregationHourly();
}
});
$("#btn_execute_daily").click(function() { $("#btn_execute_daily").click(function() {
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) { if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
executeHourlyToDaily(); executeHourlyToDaily();
@@ -297,9 +352,26 @@
</ul> </ul>
</div> </div>
<!-- 거래로그 → 시간별 집계 -->
<div class="aggregation-section">
<h3>1. 거래로그 → 시간별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 날짜:</label>
<input type="text" id="targetHour" placeholder="yyyyMMdd" maxlength="8">
<span style="color: #666; font-size: 12px;">(예: 20250120)</span>
<button type="button" class="cssbtn" id="btn_execute_hourly" level="W">
<i class="material-icons">play_arrow</i> 집계 실행
</button>
</div>
<div id="hourlyResult" class="result-area">
<strong>실행 결과:</strong>
<pre></pre>
</div>
</div>
<!-- 시간별 → 일별 집계 --> <!-- 시간별 → 일별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>1. 시간별 → 일별 통계 집계</h3> <h3>2. 시간별 → 일별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 날짜:</label> <label>대상 날짜:</label>
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8"> <input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
@@ -316,7 +388,7 @@
<!-- 일별 → 월별 집계 --> <!-- 일별 → 월별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>2. 일별 → 월별 통계 집계</h3> <h3>3. 일별 → 월별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 월:</label> <label>대상 월:</label>
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6"> <input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
@@ -333,7 +405,7 @@
<!-- 월별 → 연별 집계 --> <!-- 월별 → 연별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>3. 월별 → 연별 통계 집계</h3> <h3>4. 월별 → 연별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 연도:</label> <label>대상 연도:</label>
<input type="text" id="targetYear" placeholder="yyyy" maxlength="4"> <input type="text" id="targetYear" placeholder="yyyy" maxlength="4">
@@ -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,7 +52,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,12 +66,11 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -68,22 +79,27 @@
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b}: {c} ({d}%)' formatter: function (params) {
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
}
}, },
legend: { legend: {
orient: 'horizontal', orient: 'vertical',
bottom: 10, right: 30,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] top: 'middle',
data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
type: 'pie', type: 'pie',
radius: ['40%', '70%'], radius: ['38%', '65%'],
center: ['50%', '50%'], center: ['44%', '55%'],
avoidLabelOverlap: true, avoidLabelOverlap: true,
label: { label: {
show: true, show: true,
formatter: '{b}: {c}' formatter: function (params) {
return params.name + ': ' + params.value.toLocaleString();
}
}, },
emphasis: { emphasis: {
label: { label: {
@@ -96,90 +112,20 @@
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' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}] }]
}; };
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } }, title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
@@ -191,32 +137,13 @@
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,33 +195,12 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}] }]
}); });
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -302,24 +208,13 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -404,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 + ')');
@@ -534,21 +451,17 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -567,6 +480,9 @@
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 });
} }
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
} }
}); });
} }
@@ -578,11 +494,10 @@
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', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -596,15 +511,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -620,6 +529,9 @@
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 });
} }
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
} }
}); });
} }
@@ -629,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, '');
@@ -668,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);
}); });
@@ -685,9 +607,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -708,43 +628,51 @@
</button> </button>
</div> </div>
<div class="title" id="title">API 일별 통계</div> <div class="title" id="title">API 일별 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0" style="table-layout:fixed;">
<colgroup>
<col style="width:150px;">
<col>
<col style="width:150px;">
<col>
<col style="width:150px;">
<col>
</colgroup>
<tbody> <tbody>
<tr> <tr>
<th style="width:100px;">조회기간</th> <th>조회기간</th>
<td colspan="3"> <td colspan="5">
<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 style="width:100px;">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>
<th style="width:100px;">인스턴스</th> <th>업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th>인스턴스</th>
<td> <td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}"> <input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:100px;">업무구분</th> <th>클라이언트ID</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td> <td>
<input type="text" name="searchClientId" value="${param.searchClientId}"> <input type="text" name="searchClientId" value="${param.searchClientId}">
</td> </td>
</tr> <th>Inbound Adapter</th>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td> <td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}"> <input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td> </td>
<th style="width:100px;">Outbound Adapter</th> <th>Outbound Adapter</th>
<td> <td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}"> <input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td> </td>
@@ -753,19 +681,17 @@
</table> </table>
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div class="chart" style="position:relative; padding:0;">
<div id="seq900DonutChart" class="chart"></div> <div id="totalDonutChart" style="width:100%; height:100%;"></div>
</div> <div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) --> </div>
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" 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,8 +52,10 @@
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, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -55,12 +69,11 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
color: ['#5470C6', '#FAC858', '#EE6666'],
title: { title: {
text: '총 건수 분포', text: '총 건수 분포',
left: 'center', left: 'center',
@@ -69,22 +82,27 @@
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b}: {c} ({d}%)' formatter: function (params) {
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
}
}, },
legend: { legend: {
orient: 'horizontal', orient: 'vertical',
bottom: 10, right: 30,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] top: 'middle',
data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
type: 'pie', type: 'pie',
radius: ['40%', '70%'], radius: ['38%', '65%'],
center: ['50%', '50%'], center: ['44%', '55%'],
avoidLabelOverlap: true, avoidLabelOverlap: true,
label: { label: {
show: true, show: true,
formatter: '{b}: {c}' formatter: function (params) {
return params.name + ': ' + params.value.toLocaleString();
}
}, },
emphasis: { emphasis: {
label: { label: {
@@ -97,135 +115,42 @@
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' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}] }]
}; };
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } }, title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
// 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동) // 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동)
callChart.on('click', function(params) { callChart.on('click', function(params) {
drillDownToMinute(callChart, params.dataIndex); drillDownToMinute(callChart, params.dataIndex);
}); });
respChart.on('click', function(params) {
drillDownToMinute(respChart, params.dataIndex);
});
} }
function drillDownToMinute(chart, dataIndex) { function drillDownToMinute(chart, dataIndex) {
@@ -239,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;
} }
@@ -308,33 +230,12 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}] }]
}); });
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -342,24 +243,13 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -414,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 + ')');
@@ -540,21 +449,17 @@
mtype: 'POST', mtype: 'POST',
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API ID', 'API 명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -573,6 +478,9 @@
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 });
} }
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
} }
}); });
} }
@@ -583,12 +491,11 @@
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', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -602,15 +509,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -626,6 +527,9 @@
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 });
} }
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
} }
}); });
} }
@@ -634,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()) {
@@ -652,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);
}); });
@@ -669,9 +583,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -692,41 +604,49 @@
</button> </button>
</div> </div>
<div class="title" id="title">API 시간별 통계</div> <div class="title" id="title">API 시간별 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0" style="table-layout:fixed;">
<colgroup>
<col style="width:150px;">
<col>
<col style="width:150px;">
<col>
<col style="width:150px;">
<col>
</colgroup>
<tbody> <tbody>
<tr> <tr>
<th style="width:100px;">조회일자</th> <th>조회일자</th>
<td colspan="3"> <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 style="width:100px;">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>
<th style="width:100px;">인스턴스</th> <th>업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th>인스턴스</th>
<td> <td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}"> <input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:100px;">업무구분</th> <th>클라이언트ID</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td> <td>
<input type="text" name="searchClientId" value="${param.searchClientId}"> <input type="text" name="searchClientId" value="${param.searchClientId}">
</td> </td>
</tr> <th>Inbound Adapter</th>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td> <td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}"> <input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td> </td>
<th style="width:100px;">Outbound Adapter</th> <th>Outbound Adapter</th>
<td> <td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}"> <input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td> </td>
@@ -735,24 +655,22 @@
</table> </table>
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div class="chart" style="position:relative; padding:0;">
<div id="seq900DonutChart" class="chart"></div> <div id="totalDonutChart" style="width:100%; height:100%;"></div>
</div> <div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) --> </div>
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" 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>
</div> </div>
<table id="gridSummary"></table> <table id="gridSummary"></table>
<div id="pagerSummary"></div> <div id="pagerSummary"></div>
</div> </div>

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