230 Commits

Author SHA1 Message Date
Rinjae 0b6ee10439 Jenkinsfile.sonar: SonarQube 설정 이름 보정 및 스캐너 탐색 폴백
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- SONAR_ENV_NAME 을 실제 등록명 SonarQube-Community 로 수정
- 스캐너 툴 이름 미지정 시 후보 이름 탐색 후 PATH 의 sonar-scanner 폴백
- 스캐너를 못 찾으면 원인이 드러나는 메시지와 함께 실패

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:35:12 +09:00
Rinjae 1a95f015dc Jenkinsfile.sonar: 컴파일 전 생성 소스 정리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
clean 이 build/generated 잠금으로 실패하는 사례가 있어 rm -rf 를 선행한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:32:15 +09:00
Rinjae 92589530d1 Merge branch 'master' into feats/ci-test
# Conflicts:
#	Jenkinsfile.deploy
#	Jenkinsfile.test-build
#	src/test/java/com/eactive/apim/portal/apps/user/AccountControllerTest.java
2026-08-13 15:30:31 +09:00
Rinjae be40973aa3 SonarQube 정적 보안 분석 파이프라인 추가
- Jenkinsfile.sonar: 분석 전용 파이프라인 (컴파일 JDK8 / 스캐너 JDK17 분리,
  Quality Gate 실패는 UNSTABLE 처리, 토큰은 환경변수로만 전달)
- sonar-project.properties: 분석 대상/제외 규칙, Java 8 소스 설정
- ci/sonar-classpath.gradle: build.gradle 무수정으로 sonar.java.libraries 덤프
  (타입 해석용 컴파일 클래스패스가 없으면 보안 룰 다수가 동작하지 않음)
- .gitignore: .scannerwork/ 제외

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:29:21 +09:00
Rinjae 1e21b85de1 - 소수 자릿수 처리 개선 - DECIMAL/BIGDECIMAL 정수/숫자 구분
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 샘플 생성 로직 리팩토링 - 길이 및 소수 자릿수 기준 상세화
- 공통 스키마 처리 함수 추가 - 코드 중복 제거 및 가독성 향상
2026-08-13 14:42:44 +09:00
Rinjae 7b8e0575c5 Actuator 인터셉터 엔드포인트 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- `/actuator/mappings`에 누락된 Interceptor 목록 읽기용 엔드포인트 구현
- 내부 사용자 이메일 도메인 동적 주입 로직 UserTypeConfigurer 클래스에 추가
- 비동기 스타일 개선 및 CSS 메인 스타일 일부 수정
2026-08-13 13:36:09 +09:00
Rinjae 7c0cd371f0 클라이언트 표현 수정 및 내부 사용자 이메일 도메인 설정 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- "앱" -> "클라이언트"로 명칭 일괄 수정 (HTML/DTO/JS)
- 내부 사용자 이메일 도메인 동적 설정 로직 및 기본값 추가
2026-08-13 12:50:50 +09:00
Rinjae eb2cce8002 Actuator/SBA 로컬 제한 설정 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- application.yml 주석 및 build.gradle 로컬 전용 라이브러리 처리
- 배포 산출물에서 로컬 전용 리소스/라이브러리 제외
2026-08-12 11:48:37 +09:00
Rinjae 634c97c7ca 첨부파일 업로드 제약 조건 추가 및 APIM 서비스 파일 컨텍스트 처리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 첨부파일 업로드 시 파일 크기/형식 조건 동적 적용
- FileTypeContext.clear 호출 추가, 내부 사용자 컨텍스트 정리
2026-08-12 09:41:59 +09:00
Rinjae e32acfa215 UserLog 엔티티 필드 추가 및 관련 API User-Agent 처리 지원
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- UserLog에 userAgent(500자)와 duplicate(Boolean) 컬럼 추가
- 로그인/실패/로그아웃 로깅 시 User-Agent와 중복 세션 여부 기록
- TwoFactorService/Controller Request 객체 전달 및 처리 반영
2026-08-11 19:10:39 +09:00
Rinjae 8356fc3029 - Swagger spec 서버 주소 GW 기준 치환 통합 - UI/다운로드 동일 기준
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- ApiTesterFilter: gw 호출 시 base-url 재조립 처리 추가
- admin 스펙 저장 시 실주소 고정 제거 - 재치환 유연성 확보
2026-08-11 19:00:18 +09:00
Rinjae d6186de009 DEFAULT spec 서버 치환 제거:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- GW/sentinel 치환 로직 삭제 및 serverRewriter 호출 제거
- mock path 분리/servers 덮어쓰기 로직 및 공통 유틸 추가
2026-08-11 16:56:17 +09:00
Rinjae 2a9980ab16 - API Tester 프록시 응답 헤더 전달 로직 개선 - BLOCKED 헤더 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- API ID 기반 명세 조회 기능 추가 - GW/Mock path 매칭 문제 해결
- APISender에 ApiResponse 클래스 도입 - 응답 상태코드/본문/헤더 반환
2026-08-11 16:38:46 +09:00
Rinjae 919b4a0b06 이메일 인증 템플릿 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 제주은행/DJ Bank 이메일 템플릿 신규 추가
- Base64 임베디드 이미지 및 링크형 리소스 분리
2026-08-11 15:03:37 +09:00
Rinjae b0ecb26a9d 변경/해지 신청 개선 및 사전검사 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 변경/해지 상태 표시 및 취소 기능 추가
- 해지 신청 사전검사 및 경고 메시지 처리 구현
- 관리자 승인 이전 허용되는 취소 정책 지원
2026-08-11 14:15:33 +09:00
Rinjae 0ad5bffa4f DJB 커스텀 소스 납품 스크립트 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Windows, Linux/macOS 지원 배치/셸 스크립트 구현
- 소스 필터 규칙 및 수집 파일/모듈 매니페스트 작성
2026-08-11 11:47:31 +09:00
Rinjae 766341540d 개발가이드 Base URL 동적 처리, Mock 응답 인증 로직 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- GuideProperty 이용 개발가이드 GW URL 동적 주입
- sample/mock 응답 유형의 인증 절차 생략 로직 추가
- Webhook 이벤트 코드명/JSON 값 수정
2026-08-11 11:46:49 +09:00
Rinjae 63d4d7268c Merge branch 'master' into design
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-07 10:19:52 +09:00
Rinjae c42a278c8e 수신자 이름 지정 인증번호 발송 기능 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- %USER_NAME% 지원 위한 createMessageRecipient 수정
- GUEST_USER_NAME 상수 및 관련 기본값 처리 로직 추가
2026-08-07 09:20:39 +09:00
hong 83352e3669 fix : css 수정 2026-08-06 13:21:41 +09:00
hong 317b8d781d Merge branch 'design' of http://172.30.1.50:3000/djb-eapim/eapim-portal into design 2026-08-06 13:20:27 +09:00
hong 206e72edde fix : 디자인 수정 2026-08-06 13:20:21 +09:00
Rinjae 34dce308dc 메뉴 내부 API 허용 IP 검증 로직 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- IpAddressMatcher로 CIDR/와일드카드 지원
- 허용 IP 목록 설명 필드 추가
- RemoteAddr 정규화 로직 제거
2026-08-06 09:59:34 +09:00
Rinjae b3fc5c06bf webhook 수정 화면에서 이벤트 코드 출력 제거.
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-06 09:24:48 +09:00
Rinjae eb74a99a5a - PersonalDataEncryptConverter: isFakeMode 메서드 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- LegacyEncryptionMigration: 허용 IP 검증 로직 추가 - 스킵 내역 로그 강화
- 암호화 대상 컬럼 추가 및 DamoMode 구분 지원
2026-08-05 17:47:37 +09:00
Rinjae 38e5a7f13c API 카드 레이아웃 수정:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 1행 4열 고정 및 반응형 flex-wrap 적용
- 카드 간격/정렬 방식 및 max-width 설정 추가
2026-08-05 15:42:48 +09:00
Rinjae 3b7c2e5a8f 푸터 "관련 사이트" 조회 및 렌더링 기능 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- RelatedSite/RelatedSiteService 생성 및 PortalProperty 연계
- GlobalControllerAdvice에서 셀렉트 라벨/목록 주입
- footer.html 템플릿 및 관련 JS 수정
2026-08-05 15:23:59 +09:00
Rinjae 47099ec485 Merge branch 'feats/menu-control'
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-05 14:21:22 +09:00
Rinjae 6dbf6af3de 서비스 소개 페이지 텍스트 간소화:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 'DJBank API Portal 소개'로 제목 및 내용 정리
2026-08-05 14:19:58 +09:00
Rinjae 5585df9133 메뉴 기능 추가 - menu.yml GNB 정의 - 접근 인터셉터/뷰 주입/내부 API 구현 2026-08-05 14:19:33 +09:00
Rinjae 2c63fa1557 개발자포탈 미게시 GW 인터페이스 표시 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- hiddenApiCount 필드로 미게시 인터페이스 집계 노출
- API Pills 및 관련 CSS/JS 수정
- 영향 인터페이스 노출 로직 개선 및 VisibleApis 클래스 적용
2026-08-05 10:44:50 +09:00
Rinjae d71af34950 공지 연계 기능 추가 및 UI 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 진행 중, 과거 장애/점검 카드에 공지 제목 및 본문 추가.
- 공지 본문 2열 레이아웃 및 세로 스크롤 스타일 적용.
- CSS 반응형 수정(768px → 1024px).
2026-08-05 09:33:22 +09:00
Rinjae fbc145d145 로고 워드마크 정렬 수정:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 모바일 로고 텍스트 및 링크 baseline 맞춤.
- CSS 수정 및 스타일 정렬 개선.
2026-08-04 19:27:54 +09:00
Rinjae f36b1a6478 Merge branch 'master' into design
# Conflicts:
#	src/main/resources/static/css/main.css.map
2026-08-04 19:23:45 +09:00
Rinjae 8f03209f8a UI 문구 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 공지 상세 화면 "끝" → "종료"로 수정
- Webhook 가이드 "장애 시작" → "장애 발생"으로 변경
2026-08-04 16:20:11 +09:00
hong 8743592fde fix : footer 수정 2026-08-04 14:59:49 +09:00
hong a0685c8689 merge 충돌 해결 2026-08-04 14:53:33 +09:00
hong c0ae60f738 fix : 부분적으로 수정 2026-08-04 14:50:27 +09:00
Rinjae b45a7a162a - IncidentTimelineUI VO 추가 - 장애 타임라인 화면 구현 지원
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- PortalNoticeManService 장애 상태 타임라인 로직 추가
- 메인 CSS에 타임라인 스타일 추가
2026-08-04 13:10:31 +09:00
Rinjae 07fbe1ba54 장애·지연 구분 및 집계 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 지연 유형 로직 및 UI/스타일 업데이트
- JPQL 조회와 공개 조건 로직 전면 개정
2026-08-03 16:57:57 +09:00
Rinjae 4916bdf4af API 상태 지연 필터 및 UI/로직 개선:
- 장애·지연 구분 및 집계 로직 추가 대응
- 지연 유형 반영 색상/태그/레이블/UI 경로 업데이트
2026-08-03 15:33:21 +09:00
Rinjae 35621b5174 Merge branch 'master' into feats/api-status 2026-07-31 17:35:11 +09:00
Rinjae b97b0fcf7b "SBOM 생성 및 변환 추가 - CycloneDX -> Excel(xlsx) 지원 - Jenkins 단계에 SBOM 빌드 및 아티팩트 보관 연결"
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-07-31 17:31:45 +09:00
Rinjae 6cb98d36bb API Status 개선 및 조회 기간 설정 추가 2026-07-31 16:22:43 +09:00
Rinjae fd229fca43 API Status 페이지 기능 및 UI 업데이트:
- 상태 모니터링 시각 및 세부 정보 노출 관련 로직 추가
- 지연 필터 및 상대 시간 표기 기능 구현
- 점검/장애 필터 옵션 및 탐지 메타 정보 조회 로직 보완
2026-07-31 14:42:09 +09:00
Rinjae 5e8d08f1af API Status 페이지 포탈 미게시 API 안내 추가:
- 포탈 미게시 API 필터 안내 배너 HTML 및 JS 추가
- 조회 시 API 명 표시 및 API 옵션 외 필터 유지 로직 변경
2026-07-31 14:10:39 +09:00
Rinjae e7945233fe Merged from master 2026-07-31 13:41:25 +09:00
Rinjae 815e2bdd04 API Status 페이지 UI 및 주요 기능 추가:
- API Status 페이지 SCSS 스타일 및 반응형 대응 추가
- 진행 중 장애 DTO 및 영향 API DTO 구현
- 진행 중 장애 및 점검 사항 관련 JS 렌더링 로직 추가
2026-07-31 13:38:37 +09:00
Rinjae 0629e842a3 푸터 외부 링크 스타일 추가 및 SCSS 리팩토링
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- footer-link--external 스타일 추가로 외부 링크 시각화
- 불필요한 CSS 줄바꿈 및 공백 제거로 파일 최적화
2026-07-31 13:33:41 +09:00
Rinjae 6fa2167378 Swing 알림 체계 도입 - 기존 Q&A 관리자 알림 제거 - Swing Notifier 구현 2026-07-30 19:33:11 +09:00
Rinjae 84e873a08b Merge branch 'design' into feats/api-status 2026-07-30 17:42:29 +09:00
Rinjae 6c231c4189 API 해지 팝업 추가 및 사용자 오류 메시지 처리 개선
- API 해지 신청 팝업 UI 및 로직 구현
- 사용자 친화적 예외 처리 헬퍼(UserErrorMessageResolver) 추가
- Webhook 신청/수정, 글로벌 예외 처리에 사용자 메시지 연동
2026-07-30 16:22:45 +09:00
Rinjae e3b5a44270 Merge branch 'master' into feats/api-status 2026-07-30 13:44:50 +09:00
Rinjae 829630ae5c API 스펙 관리 문구 및 로직 개선
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 게이트웨이 관련 문구 수정 및 그룹 설정 안내 추가
- 비 로그인 사용자 공개 관련 문구 수정
- description 필드 저장/복원 로직 추가
2026-07-30 11:24:05 +09:00
Rinjae 2a9694cd85 API 스펙 관리 문구 및 로직 개선
- 게이트웨이 관련 문구 수정 및 그룹 설정 안내 추가
- 비 로그인 사용자 공개 관련 문구 수정
- description 필드 저장/복원 로직 추가
2026-07-30 11:24:05 +09:00
hong 456b635a32 merge 충돌 해결 2026-07-30 10:07:00 +09:00
hong faaf9e8cd6 feat: 로그인 퍼블리싱 2026-07-30 10:01:38 +09:00
Rinjae 4749613947 - PortalOrg 초기화 필드 추가(orgDesc, ipWhitelist 등)
- ApiTesterFilter 디버깅 로직/유틸 추가(maskHeaders 등)
- jackson-core 취약점(CVSS 7.5) 수정 버전 업그레이드(2.18.6)
2026-07-29 20:20:43 +09:00
Rinjae 5125dab897 Merge branch 'design' of ssh://172.30.1.50:2222/djb-eapim/eapim-portal into design
# Conflicts:
#	src/main/resources/static/css/main.css.map
2026-07-29 19:32:08 +09:00
Rinjae 02eb81af67 클라이언트 생성/수정 흐름 및 관련 UI 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 클라이언트 등록/수정 URL 구조 리팩토링 (myapikey → clients)
- 로그인 직후 클라이언트 미보유 사용자 대상 추가 안내 팝업
- 클라이언트 Secret 조회 및 관련 2FA 단계 로직 개선
2026-07-29 19:25:10 +09:00
Rinjae c02a5ff047 전역 토스트 알림 추가 및 UI/로직 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 전역 토스트 알림용 JS 및 SCSS 추가 (djbToast)
- 신규 토스트 연동 레이아웃(js 포함) 및 스타일 확장 대응
- 클라이언트 신청/수정 화면 개선, IP 등록 검증 로직 추가
2026-07-29 15:32:05 +09:00
hong 4181d114c1 style: SASS 빌드 결과 main.css 업데이트 2026-07-28 18:21:56 +09:00
hong daff07c006 merge후 충돌 해결 2026-07-28 18:18:23 +09:00
hong 1f51c9b21e feat: 로그인과 회원가입 퍼블리싱 2026-07-28 18:09:20 +09:00
Rinjae 929980c089 중복 로그인 확인 및 처리 로직 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 동시 접속 확인 API(DuplicateLoginController) 및 서비스 구현
- 중복 세션 확인 후 로그인 확정/취소 처리 로직 추가
- 2FA 흐름과 연계된 프론트엔드 수정 및 UI 개선
2026-07-28 17:00:10 +09:00
Rinjae d4d5d66727 앱 수정 최종 반영(2FA) 로직 추가:
- 최종 저장 시 2FA 통과권 요구 로직 추가 및 중복 인증 방지
- Step2 2FA 팝업 및 프론트엔드 2FA 검증 로직 반영
- 불필요한 CallBack URL 필드 제거 및 UI 문구 수정
2026-07-28 16:38:44 +09:00
Rinjae d3e6249a41 앱 수정 최종 반영(2FA) 로직 추가:
- 최종 저장 시 2FA 통과권 요구 로직 추가 및 중복 인증 방지
- Step2 2FA 팝업 및 프론트엔드 2FA 검증 로직 반영
- 불필요한 CallBack URL 필드 제거 및 UI 문구 수정
2026-07-28 16:38:37 +09:00
Rinjae 68f8c07fc0 Merge branch 'master' into design 2026-07-28 16:07:51 +09:00
Rinjae 87369ce7f7 법인 로그인 차단 로직 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- ACTIVE 외 상태(휴면/탈퇴/준비) 로그인 제한 처리 추가
- 승인 미완료 예외 케이스 방어 로직 강화
2026-07-28 15:38:18 +09:00
Rinjae a2f6151a08 인증 관련 PTL_PROPERTY 및 로그인 차단 로직 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 개발/테스트 환경 인증번호 노출 설정 AuthNoticeProperties 추가
- 삭제된 법인 소속 사용자 로그인 차단 처리 로직 반영
2026-07-28 15:31:21 +09:00
Rinjae b29882c0b2 이메일 인증 흐름 개선 및 테스트 환경 대응:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 가입 시 이메일 인증 여부 검증 로직 추가
- 테스트 환경에서 인증번호 UI 출력 및 관리 개선
- 리다이렉트 시 기본 모델 노출 방지 Bean 설정 추가
2026-07-28 15:13:55 +09:00
hong 80740a9543 merge 충돌 해결 2026-07-28 14:35:21 +09:00
hong cdd68f48f0 fix: 마이페이지 이용 통계 , 내정보 관리 , 비밀전호 변경 & 로그인 반응형 퍼블리싱 2026-07-28 14:30:03 +09:00
Rinjae d2bc61dc31 2FA 서비스 및 속성 추가 - 테스트 모듈 적용
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 비밀번호 변경 경로 리팩토링 및 엔드포인트 변경
- 테스트 경로 및 목(mock) 객체 추가
2026-07-28 12:41:02 +09:00
Rinjae 73deb7f02e WebLogic WebSocket 충돌 방지 설정 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Tomcat WebSocket 모듈(tomcat-embed-websocket) 제외 추가
2026-07-28 11:27:58 +09:00
Rinjae fedd3e4c50 본인 인증 추가 기능 및 UI 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Step-up 비밀번호 확인 페이지 구현
- 기존 비밀번호 확인 로직 제거 및 반영 직전 2FA 추가
- 보호 경로 완화 정책(PASSWORD/TWO_FACTOR) 및 경로 관리 확장
2026-07-28 11:04:24 +09:00
Rinjae 6c13b6bc63 design → master 병합: 2FA·Step-up 인증 및 디자인 반영본 통합 2026-07-28 10:09:14 +09:00
Rinjae b54fedc280 feats/security → design 병합: 2FA·Step-up 인증 통합
- SuccessHandler 충돌 해결: LoginFinalizer 리팩터 버전 채택
- LoginFinalizer 로그 마스킹 재적용(design 변경 보존)
- CSS 생성물 충돌은 SASS 재컴파일로 해소(.tfa- 포함)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:55:57 +09:00
Rinjae a4316545dc 2FA 팝업 UI 개선 및 기능 추가:
- 디자인 가이드 기반 레이아웃/스타일 재정의(SASS)
- 인증 흐름 개선: 타이머/프로그레스바/버튼 상태 업데이트(JS)
- 사용자 친화적 안내 메시지 및 입력 상태 검증 추가
2026-07-28 09:50:45 +09:00
Rinjae b5ffa69eba 2FA (추가 인증) 기능 추가:
- Step-up 인증 Interceptor 및 보호 경로 관리 로직 도입
- 2FA 팝업 모듈 및 스타일(SASS, JS) 추가
- 로그인 실패 사유 Enum 및 2FA 관련 처리 로직 공통화
2026-07-27 20:12:13 +09:00
Rinjae 2106142d93 - UserRoleHistoryService 추가: 역할 변경 감사 이력 서비스 구현
- `ROLE_API_KEY_REQUEST` → `ROLE_WEBHOOK` 변경: Webhook 관리 권한 분리 및 강화
- "이용자" → "개발자" 용어 통일: 템플릿, 컨트롤러 및 예외 메시지 수정
2026-07-27 20:11:09 +09:00
Rinjae 0f58f043a8 Merge remote-tracking branch 'origin/design' into design 2026-07-27 18:20:43 +09:00
Rinjae 61be6bf108 - API 선택 검증 로직 수정 - 선택 사항으로 변경
- '인증 키 관리' → '앱 관리' 용어 변경
- API 미선택 시 확인 팝업 로직 추가
2026-07-27 18:20:37 +09:00
hong 96d6dd3df1 충돌 해결 2026-07-27 17:44:23 +09:00
hong f9b53810d9 fix: 인증키 관리 & webhook 수정및 반응형 퍼블리싱 2026-07-27 17:32:33 +09:00
Rinjae 41be827b81 비밀번호 정책 및 변경 강제 적용 기능 추가:
- 비밀번호 규칙 클라이언트 검증 JS/SASS 추가
- 세션 기반 비밀번호 변경 강제 Interceptor 구현 (`PasswordChangeEnforcementInterceptor`)
- URL 리팩토링 및 엔드포인트 변경 (`/password/*`)
- 비밀번호 유효성 검사 공통화 (`PasswordRuleValidator`)
- Logback ERROR 전용 파일 로그 추가
2026-07-27 17:25:42 +09:00
Rinjae 78898ab5a8 Merge dfc859c (승인 요청 처리 개선) into design
- 승인정보 없는 요청 목록 노출 및 삭제 로직 (MyAppController, AppServiceFacade)
- '승인대기' → '승인정보 없음' 텍스트 (apiKeyList.html, design 뱃지 클래스 유지)
- 내부 결재 중 취소 불가 안내 스타일 (_apikey-detail.scss)
- 충돌 CSS는 design(ours) 채택 후 SASS 재컴파일
2026-07-27 16:14:53 +09:00
hong 19c17f4e91 Merge remote-tracking branch 'origin/master' into design 2026-07-27 14:40:12 +09:00
hong c2d84bb024 마이페이지 이용자 관리 , 인증키 관리 반응형으로 수정 2026-07-27 14:31:51 +09:00
hong 6434d48b05 Merge branch 'design' of http://172.30.1.50:3000/djb-eapim/eapim-portal into design 2026-07-24 17:41:43 +09:00
hong d5f0f3aab4 fix: FAQ , Q&A , 피드백 반응형 퍼블리싱 2026-07-24 17:41:39 +09:00
Rinjae dfc859c9cf 승인 요청 처리 개선 및 UI/로직 추가:
- 승인정보 없는 요청 목록 노출 및 삭제 로직 추가
- 내부 결재 진행 중 취소 불가 안내 메시지 및 스타일 추가
- '승인대기' -> '승인정보 없음' 텍스트 수정
2026-07-24 17:22:40 +09:00
Rinjae 24c669b0be 접근 권한 처리 및 권한 설정 개선:
- 미로그인 사용자 접근 시 로그인 페이지 리다이렉트 처리
- Webhook 관리 접근 권한 `ROLE_API_KEY_REQUEST`로 변경
- 오류 페이지 스타일 수정: `white-space: pre-line` 추가
2026-07-24 13:14:17 +09:00
hong 3eedc522e2 merge 충돌수정 2026-07-24 10:24:07 +09:00
hong a361cc3dec fix : open api , 공지사항 반응형 퍼블리싱 2026-07-24 10:19:18 +09:00
Rinjae 1ac9fa5407 "SwaggerUI - 실행 전 응답 UI 노출 방지 로직 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Execute 클릭 여부 확인 pendingExecute 플래그 도입
- 네트워크 에러/응답 처리 시 플래그 초기화 추가"
2026-07-24 09:44:28 +09:00
Rinjae 9981459691 "SwaggerUI - 실행 전 응답 UI 노출 방지 로직 추가
- Execute 클릭 여부 확인 pendingExecute 플래그 도입
- 네트워크 에러/응답 처리 시 플래그 초기화 추가"
2026-07-24 09:44:28 +09:00
Rinjae a2878f9cee Merge branch 'master' into design 2026-07-23 20:58:03 +09:00
Rinjae 1f10dda993 - API 감사 로거 추가 - APITesterAuditLogger 클래스 생성
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 회원가입 링크 수정 - href 속성 추가
- Testbed UI 스타일 추가 - CSS/SASS 코드 업데이트
2026-07-23 20:28:12 +09:00
Rinjae a6807a37c2 - API 감사 로거 추가 - APITesterAuditLogger 클래스 생성
- 회원가입 링크 수정 - href 속성 추가
- Testbed UI 스타일 추가 - CSS/SASS 코드 업데이트
2026-07-23 20:28:12 +09:00
Rinjae 6c10ae12c7 Merge branch 'master' into design 2026-07-23 20:26:55 +09:00
Rinjae 01c4a80182 API 테스트베드 개선 및 감사 로그 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Gateway 프로퍼티 mock 토큰 관련 항목 추가
- API 요청/응답 감사 로그 작성 로직 구현 및 로깅 설정 추가
- Swagger UI 전용 spec 제공 및 서버 주소 치환 로직 분리
2026-07-23 20:26:40 +09:00
Rinjae a5611cf775 - 서비스 가이드 컬럼 레이아웃 개선: 1열 배치로 변경
- 다국어 코드 블럭 간격 문제 수정 및 SASS 재컴파일
- 버튼 및 배지 hover 컬러 RGB % 표기로 변경
2026-07-23 18:12:12 +09:00
Rinjae c733e6b200 Merge branch 'master' into design
# Conflicts:
#	src/main/resources/templates/views/apps/service/oauth2-guide.html
2026-07-23 18:09:00 +09:00
Rinjae a2813f391c - OAuth2 가이드 scope 고정값 "api" 반영(master 이식)
- 인증 헤더 표기 X-AUTH-TOKEN 으로 수정
- 가이드 히어로/사전준비/엔드포인트 1열 배치 전환
- webhook 개발가이드 문구 정리, SASS 재컴파일

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:07:58 +09:00
Rinjae f221c20ece - 세션 타임아웃 10분 고정: 서버/DB 설정 통일 및 관리 단순화
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- CSRF 토큰 조회/재발급 API 추가 - 익명 세션 유지 핑 로직 구현
- OAuth2 가이드 scope 고정값 "api"로 수정
2026-07-23 18:05:57 +09:00
Rinjae 4a3b5b43f3 불필요 주석 블록 처리:
- HTML 템플릿 주석 CSS 규격으로 통일
- guide.html 외 10개 파일 변경
- editor-content 클래스 관련 설명 추가
2026-07-22 15:51:43 +09:00
Rinjae 8de0d94063 API 관련 HTML 템플릿 파일 제거 및 상세 뷰 클래스 추가:
- api_lnb.html, head.html 템플릿 삭제
- API 상세 페이지에 editor-content 클래스 추가
2026-07-22 14:40:12 +09:00
Rinjae 57d4efb4eb Merge branch 'master' into design 2026-07-22 11:09:13 +09:00
Rinjae c865359819 사용자 이메일 마스킹 처리 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- PortalConfigLog에 StringMaskingUtil 적용
- 로그인 사용자 아이디 마스킹 처리(Log 개선)
2026-07-22 09:39:09 +09:00
Rinjae 41391c8df0 - Webhook 신청 및 관리 UI 전면 개편 - 신규 가이드 반영
- Webhook 수정/삭제 기능 UX 개선 - 단계별 디자인 업데이트
- Webhook 등록/수정 CSS 및 JS 로직 최적화 및 재사용 코드 추가
2026-07-20 19:53:54 +09:00
Rinjae 9f4874fe44 Merge branch 'feats/api-status' into design
# Conflicts:
#	src/main/resources/static/css/main.css.map
#	src/main/resources/static/css/main.min.css
#	src/main/resources/static/css/main.min.css.map
#	src/main/resources/templates/views/apps/mypage/apiKeyModifyStep2.html
#	src/main/resources/templates/views/apps/mypage/apiKeyRegisterStep2.html
#	src/main/resources/templates/views/fragment/djbank/header_container.html
2026-07-20 19:13:38 +09:00
Rinjae 43d915342e 모니터링 코드 엔티티 제거 및 API 선택 모듈 추가:
- MonitoringCode 엔티티, 리포지토리, 복합키 클래스 삭제
- API 선택 공용 모듈 JS 및 HTML 파일 추가 (api_selector)
- 웹훅 개발 가이드 페이지 신규 추가 (webhook-dev-guide)
2026-07-20 19:10:05 +09:00
Rinjae ba00b80bfe Webhook 신청/관리 및 EventType 제공 기능 추가:
- Webhook 신청/수정 3단계 UI 및 관련 서비스 구현
- TSEAIRM28 공통코드 기반 EventType 조회 기능 추가
- 신규/수정 세션 관리 및 비밀번호 재인증 로직 포함
2026-07-20 17:36:17 +09:00
Rinjae 87763ba325 CSS 메인 파일 업데이트:
- "main.min.css"에서 디자인 스타일 및 애니메이션 수정 및 최적화.
2026-07-20 16:48:12 +09:00
Rinjae 5b7a4a108d design-merge 강제 병합 - 디자인 반영본 전체 통합
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:32:18 +09:00
Rinjae 0f581cae23 design merge 2026-07-20 16:27:49 +09:00
Rinjae 923bf56e21 design merge 2026-07-20 16:27:38 +09:00
hong 9b9bcb5be4 패딩 수정 2026-07-20 14:05:43 +09:00
Rinjae bdb249b74b Merge branch 'master' into feats/api-status 2026-07-20 10:57:55 +09:00
Rinjae 1cb9606602 API 통계 도메인 구조 개편 및 조회 기능 확장:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- API 통계 엔티티/리포지토리 다층 나눔 (분/시/일/월)
- API 상세/요약 통계 및 기간별 누적 통계 DTO 추가
- 고객센터 연락처 조회 로직 추가 (@ModelAttribute) - 글로벌 푸터 적용
- @EMSDataSource 지원 엔티티-패키지 다중 검색 적용
- 기존 ApiStatisticsRepository 삭제, 분리된 구성으로 대체
2026-07-16 11:20:44 +09:00
hong 93f7c210bc 11 2026-07-14 15:52:10 +09:00
hong bb79b55f08 fix: 회원가입 , oauth 반응형 수정 2026-07-14 14:54:53 +09:00
Rinjae 227d4d8abd 파트너십 신청 요약 DTO 추가 - 로그아웃 확인 팝업 로직 구현:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- PartnershipApplicationSummaryDTO 생성 및 필드 정의
- 로그아웃 클릭 시 확인 팝업 노출 로직 추가(js-logout)
2026-07-14 11:12:14 +09:00
hong 23bd0c1207 fix : 메인페이지 반응형 수정 2026-07-14 10:35:14 +09:00
hong 28a9ff1a7f feat: 마이페이지 퍼블리싱 2026-07-14 10:34:31 +09:00
Rinjae 4395757d98 사용자 최근 작성 글 조회 기능 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 최근 3건 조회 서비스/페이사드/리포지토리 구현
- HTML 아코디언 UI 및 스타일 추가
2026-07-13 17:27:06 +09:00
Rinjae 778d579ada # Conflicts:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
#	src/main/resources/static/css/main.css.map
2026-07-13 13:29:54 +09:00
Rinjae 3e403788e5 비운영 환경 Hibernate SQL 로그 비활성화:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- show_sql, format_sql 설정 false로 수정
2026-07-13 11:50:01 +09:00
Rinjae 6c5ba6765d 조회수 비정상 증가 방지 로직 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- sessionStorage 활용 중복 증가 억제
- CSRF 토큰 기반 조회수 증가 API 호출 처리
2026-07-13 11:37:06 +09:00
Rinjae a97378b84d Q&A 문의 공개범위 기능 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 공개범위 설정 기능 개발(ALL/ORG/PRIVATE) - 게시물/댓글 정교화
- 요청 및 DB 처리 확장 - UI, DTO, Service 계층 업데이트
- 감사 로그 및 조회수 처리 로직 추가
2026-07-13 11:32:34 +09:00
Rinjae 9c01db9d00 weblogic.xml 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- XML 태그 단순화 및 불필요한 속성 제거
- session-descriptor에 cookie-name 추가
2026-07-10 17:48:34 +09:00
Rinjae 08bd705dec 세션 유지 기능 추가 및 FAQ 관리 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 비운영 환경 + DB 설정 활성 시 세션 유지(타임아웃 무시) 지원
- FAQ 단건 조회 API 추가 (`FaqFacade`, `FaqFacadeImpl`)
- 헤더 세션 타이머 UI 개선 및 유지 옵션 체크박스 추가
2026-07-10 16:36:19 +09:00
Rinjae 99f765507b 세션 유지 기능 추가 및 FAQ 관리 개선:
- 비운영 환경 + DB 설정 활성 시 세션 유지(타임아웃 무시) 지원
- FAQ 단건 조회 API 추가 (`FaqFacade`, `FaqFacadeImpl`)
- 헤더 세션 타이머 UI 개선 및 유지 옵션 체크박스 추가
2026-07-10 16:36:11 +09:00
Rinjae 7d6aabe122 문의 상태값 'REVIEWING' 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 상태 코드/배지 지원 로직(DjbInquiryStatus) 반영
- Inquiry 댓글 작성자명 관리자 구분 로직 수정
2026-07-10 14:36:23 +09:00
Rinjae f08da86eb3 - Jenkinsfile 경로 수정: WL_HOME, WL_DEPLOY_DIR 디렉토리 변경
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- credentialDetail.html: 비밀값 1회 표시 경고 메시지 추가
2026-07-09 15:26:01 +09:00
hong 045f354e5c Q&A 리스트 퍼블리싱 2026-07-09 10:45:20 +09:00
hong 4f577c8c14 Faq , q&a 상세 , 피드백 , 마이페이지 이용자 관리 퍼블리싱 2026-07-09 09:47:16 +09:00
Rinjae 2a2505d5f7 관리자 게이트웨이 연동 및 보안 기능 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- AdminGatewayClient 클래스 추가: GW 인증 차단/리로드 처리
- Client Secret 1회 노출+삭제 로직(AppServiceFacade, MyAppController) 추가
- credentialDetail.html에 비밀정보 분실 대응 UI 및 JS 로직 구현
2026-07-08 18:34:33 +09:00
Rinjae cad574324e Jenkinsfile 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- skipDefaultCheckout 옵션 추가 (Deploy 노드 Git 접근 불필요)
2026-07-06 21:05:00 +09:00
Rinjae 4b4ef4196e Jenkinsfile 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- eapim-portal.war 해시 생성 방식 단순화 (sha256만 유지)
- archiveArtifacts 대상 및 스탠징 파일 축소
2026-07-06 20:49:10 +09:00
Rinjae 5402a5354a Jenkinsfile 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 빌드 및 배포 환경을 WebLogic으로 변경
- 다단계 파이프라인 구성 정비 및 에이전트 설정 추가
2026-07-06 20:42:14 +09:00
Rinjae c5b23c3873 PortalProperty 부팅 시 중복키 점검 기능 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- (PROPERTY_GROUP_NAME, PROPERTY_NAME) 유일성 검증
- 중복 발견 시 ERROR 로그 출력 및 수동 정리 안내
- JSESSIONID 쿠키 이름 변경 (PORTAL_JSESSIONID → JSESSIONID_PORTAL)
2026-07-06 17:47:48 +09:00
hong cc4c1a6b53 feat : 오픈 API & 공지사항 퍼블리싱 2026-07-06 17:21:58 +09:00
Rinjae 83d267faed 클라이언트 하드닝 기능 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- DevTools 감지 및 화면 삭제, 우클릭 차단 기능 구현
- PortalProperty 기반 설정 관리 서비스 추가
- 클라이언트 가드 스크립트 및 토글 로직 템플릿 연동
2026-07-06 15:32:18 +09:00
Rinjae b02e598d62 수수료 관리 기능 제거:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 관련 Controller, DTO 클래스 삭제
- commissionManage.html 및 스타일 파일 제거
2026-07-03 18:30:34 +09:00
hong 551a2da717 feat: 회원가입 안내 및 oauth 2 페이지 퍼블리싱 2026-07-03 16:41:24 +09:00
hong 1c57d348e6 디자인 시안 적용 및 애니메이션 개선 2026-07-02 17:52:59 +09:00
Rinjae 4f9fbefbcc OAuth2 가이드 문서 업데이트:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 인증 헤더 `X-AUTH-TOKEN`으로 변경
- `scope` 필수값 "api"로 수정
- 오류 코드 및 설명 표준화 (`E.GW.AUTH_FAIL`)
2026-07-01 15:58:21 +09:00
Rinjae c403118289 - StartupInfoPrinter 클래스 추가 - 서비스 시작 시 배너 출력
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- MessageCode 채널 필드 @Deprecated 추가 및 기본값 설정
- MessageCode 신규 생성자 추가 - channel 파라미터 제거
2026-07-01 10:37:08 +09:00
Rinjae a91b56ed11 - 법인 탈퇴 시 민감정보 삭제 로직 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- CSS 수정: 폰트 크기 제거 및 스타일 변경 적용
- 예상완료일 요일 계산 로직 추가
2026-06-30 15:38:40 +09:00
Rinjae 8d232734b1 IP 등록 UI 및 검증 로직 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- `.ip-input-row`/`.btn-add-ip` 정렬 및 버튼 크기 수정
- Callback URL 관련 입력 및 검증 로직 주석 처리
- 화이트리스트 필수 입력 항목 검증 로직 추가
2026-06-30 10:54:22 +09:00
Rinjae 06c18e437b 회원가입 제출 필드 개선: 선택 입력 포함 모든 항목 전송 처리 2026-06-30 10:54:08 +09:00
Rinjae 97b9d2161a IP 입력 필드 스타일 개선: 정렬 및 버튼 크기 수정
- `.ip-input-row` 정렬 기준 추가 및 버튼 폭 고정
- `.btn-add-ip` 및 제거 버튼 스타일/반응형 조정
2026-06-30 10:54:00 +09:00
Rinjae 0c3b63a5b1 - UserSessionService 목 객체 추가 및 관련 의존성 주입
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 초대 조회 시 휴대폰 번호 형식 통일 (`01012345678` → `010-1234-5678`)
- AccountControllerTest 테스트 코드 수정 및 누락 보완
2026-06-30 09:45:08 +09:00
Rinjae 582a3e0f69 회원가입 필수 항목 검증 강화:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 입력/미충족 항목 구체적 수집 및 에러 메시지 개선
- 시나리오별/공통 항목 검증 로직 추가
2026-06-30 09:42:34 +09:00
Rinjae c1ae12e1ca 전화번호 정규화 유틸 추가 및 적용:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- PhoneNumberUtil 도입: 한국번호 정규화/숫자 추출 지원
- 유틸 적용: 모든 저장 및 조회에 정규형 강제
- 초대 매칭 오류 수정: 휴대폰 형식 차이 대응
2026-06-29 18:26:15 +09:00
Rinjae 77aea79363 콘솔 로그 설정 추가 및 레벨 제어 기능 구현
- JVM 파라미터 기반 CONSOLE_LOG_ENABLED, LOG_LEVEL 제어 추가
- Stage 프로파일 정리 및 콘솔 로그 필터 설정 적용
2026-06-29 18:24:37 +09:00
Rinjae aa5e769148 JTA 설정 비활성화 및 신규 회원가입 경로 추가
- `spring.jta.enabled` 값 false로 변경 (stage/dev/prod 공통)
- 법인회원 초대 경로(`/signup/decision_process`) 신규 추가
2026-06-29 18:24:26 +09:00
Rinjae ee8e9e3529 CycloneDX BOM 플러그인 추가 - build.gradle에 org.cyclonedx.bom 적용 2026-06-29 18:24:07 +09:00
Rinjae 82948a13ce CSRF 만료 세션 처리 및 정적 리소스 캐싱 제어 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- CSRF 만료 시 기존 세션 만료 안내로 리다이렉트 처리
- 정적 리소스 캐싱 토글 기능 추가 (개발 환경 즉시 반영 지원)
- 회원가입 페이지 및 세션 설정 스타일 수정
2026-06-26 15:05:19 +09:00
Rinjae f79e528fec 이메일 인증 재발송 기능 제거 및 관련 코드 정리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- `emailResendPopup` 및 팝업 호출 로직 삭제
- `InquiryAdminNotifier` → `CommunityAdminNotifier`로 클래스 명칭 변경
- 제휴 신청 시 관리자 알림 로직 추가
2026-06-24 19:45:37 +09:00
Rinjae 6f776ed72a 로그아웃 핸들러 추가: 세션 무효화 전 DB 정리 및 로깅 처리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-24 16:52:09 +09:00
Rinjae e242d3ed06 - 모바일 약관 탭 UI 개선 - 스크롤 추가 및 스타일 조정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 필수 입력 경고 메세지 추가 및 인증 미완료 경고 추가
2026-06-24 15:32:18 +09:00
Rinjae d7abe2c2e5 - 테스트 데이터에 휴대폰 번호 추가 및 관련 로직 수정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 초대 조회 조건: 이메일 → 휴대폰 번호로 변경
2026-06-23 21:06:28 +09:00
Rinjae 84d92fced9 - 초대 로직 변경: 이메일→휴대폰 기반 초대 전환 및 UI 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 알림 수신 동의 추가: 초대/취소 시 알림 발송 옵션 탑재
- 관리자 기능 추가: 소속 제외 및 권한 조정 기능 구현
2026-06-23 20:58:27 +09:00
Rinjae 53468c3b83 WAR 파일 해시 파일 확장자 변경 및 MD5SUM 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-23 13:58:02 +09:00
Rinjae 198f6fc82a - 회원가입 이메일 인증 페이지 추가 및 이메일 인증 흐름 구현
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 휴대폰 번호 중복 검증 로직 추가 및 프로퍼티 기반 설정 활성화
- 알림 수신 동의 항목 및 뷰 로직 추가
2026-06-22 19:20:03 +09:00
Rinjae 21c500610c Damo 적용 준비 코드
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- KjbNotRnnoJpaAttributeConverter 삭제 및 PersonalDataEncryptConverter 추가
- damo-manager 지원을 위한 JSP 추가 및 레거시 암호화 마이그레이션 컨트롤러 구현
- 기존 엔티티 암호화 로직 변경 및 build.gradle에 damo-manager 의존성 추가
2026-06-22 16:29:45 +09:00
Rinjae 687d4798d4 - error 페이지 개발 환경용 상세 오류 정보 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 스택 트레이스 및 예외 클래스 노출 로직 구현
- activeProfile 표시 및 스타일 업데이트
2026-06-22 15:39:59 +09:00
Rinjae 5adfd42751 - CSRF 토큰 저장소 쿠키 → 세션 전환 및 클라이언트 로직 수정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- error 페이지 텍스트 동적 처리 추가 (prod 환경 구분)
- CSRF 메타 데이터 추가 및 JS/AJAX CSRF 로직 일원화
2026-06-22 14:19:32 +09:00
Rinjae 6694715195 Merge branch 'feature/session-timeout'
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
# Conflicts:
#	src/main/resources/static/css/main.css.map
2026-06-19 18:09:47 +09:00
Rinjae fc96c13df2 - 로그인 중복 세션 확인 및 기존 세션 강제 로그아웃 처리 추가
- 세션 타이머 데스크톱·모바일 표시 및 경고 스타일 동기화
2026-06-19 18:06:40 +09:00
Rinjae 7265ad6589 - 세션 타이머·유휴 자동 로그아웃 기능 추가
- 만료 직전 연장 확인 모달, PortalProperty로 시간 설정
- PTL_USER_SESSION 기반 세션 검증 필터/API 도입
- 로그인 시 중복 세션 강제 로그아웃 처리
- 세션 테이블 DDL 및 로그인 안내 메시지 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:45:37 +09:00
Rinjae 02f9d41334 OAuth2 개발가이드 추가 및 테이블 UI 개선
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- OAuth2 개발가이드 링크 추가
- 테이블 컬럼 스타일 조정: 모바일 대응 및 오버플로 방지
- Jackson 직렬화 대응 날짜 포맷 수정
2026-06-19 17:22:33 +09:00
Rinjae fe4b176512 CSS source map 경로 수정 및 매핑 데이터 규칙 정비
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-19 10:27:17 +09:00
Rinjae 17c27e4398 문의 상태값 수정: IN_PROGRESS → RESPONDED
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 문의 상태 필드 및 관련 로직/뷰 업데이트
- Validation 패턴 및 테스트 코드 반영
- 뷰 상태 배지 및 응답 섹션 표시 로직 수정
2026-06-19 10:16:16 +09:00
Rinjae 10ff5d6f2f 마스킹 표준화 적용 (eapim-admin/portal/JS)
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- admin: elink-online-core 표준 위임(e메일/이름/전화/IP)으로 규칙 일원화
- portal/JS: 표준 로직 직접 구현 + 기존 owner 비마스킹 유지
- 호출처 무수정/인라인 중복 제거/테스트 통과
2026-06-18 17:56:32 +09:00
Rinjae-gf63 f671f491b7 - PortalUser-테이블명 대소문자 수정
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Credential-EqualsAndHashCode 어노테이션 추가
- 사업제휴신청 메시지 수정 및 파일 null 검사 추가
2026-06-18 16:28:22 +09:00
Rinjae-gf63 3d68589ae7 - 불필요 로그 설정 및 springProfile(gf63) 제거
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- "사업 제휴 문의" 텍스트를 "피드백/개선요청"으로 변경
- 과금 관리 관련 링크 제거
2026-06-18 16:03:03 +09:00
rinjae 7268aef211 Jenkinsfile.deploy 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 13:51:14 +09:00
rinjae 85b2f01ce0 Jenkinsfile.deploy 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 13:43:24 +09:00
rinjae 0f255d7337 Jenkinsfile.deploy 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 12:17:31 +09:00
rinjae a8323e78f5 Jenkinsfile.deploy 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 12:11:00 +09:00
Rinjae fa8fce4f2d Jenkinsfile 추가 및 CI/CD 스크립트 통합
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- 기존 *.sh 스크립트 제거 및 Jenkinsfile.deploy로 통합
- 테스트 전용 Jenkinsfile.test-build 추가
2026-06-18 11:35:12 +09:00
rinjae 67ec6d2103 Jenkinsfile.deploy 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 11:12:57 +09:00
rinjae b024f8e86f Jenkinsfile.test-build 업데이트
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 11:09:38 +09:00
rinjae fd16300553 Jenkinsfile.test-build 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-18 10:51:00 +09:00
Rinjae 4e7044b9fe 폰트 패밀리 정의 분리 및 웹폰트 리소스 경로 정비
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- OneShinhan, Spoqa Han Sans Neo 등 역할별 폰트 패밀리 분리
- 제목/본문 영역에 맞는 폰트 설정 및 재정의
- 웹폰트 리소스 URL 경로 및 성능 최적화
2026-06-17 20:47:01 +09:00
Rinjae bddd7e3f85 공지사항/Q&A 개선 - 장애 점검 정보 노출 및 상단고정 정렬 - Q&A 이벤트 정리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-06-16 14:54:02 +09:00
Rinjae 2c29c8a466 공지사항 목록 정렬 및 UI 개선
- 상단고정(fixYn) 우선 정렬 로직 추가
- 고정 뱃지 UI 템플릿 추가
2026-06-16 14:46:25 +09:00
Rinjae d2052d0e16 Q&A 이벤트 리팩토링
- InquiryCommentCreatedAdminEvent 클래스 제거
- build.gradle 리소스 처리 코드 주석 처리
2026-06-16 10:53:39 +09:00
Rinjae 9c4f3cfb04 공지사항 상세 - 장애/점검 정보 노출 추가
- PortalNoticeDTO 장애/점검 필드 확장
- Facade에서 djb_incident 데이터 결합
- 목록/상세에 유형 뱃지 시작 종료 영향 API 표시

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 09:11:28 +09:00
Rinjae 71f4cb6f79 Q&A 이벤트 리팩토링
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- InquiryCommentCreatedAdminEvent 삭제 및 경로 변경
- userName 정보 추가 및 description 업데이트
- inquirySubject 처리 로직 개선
2026-06-15 18:57:51 +09:00
Rinjae 171feee9ea Q&A 댓글 관련 코드 리팩토링 및 역할 기반 알림 기능 개선
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- InquiryComment 코드 구조 리팩토링 및 역할 분리
- portal-admin 알림 기능 추가 및 예외 처리 개선
- InquiryComment 관련 불필요 클래스/메서드 제거
2026-06-15 18:49:14 +09:00
Rinjae 3f664bcd5c Q&A 댓글 기능 추가 및 첨부파일 제거
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- djb 패키지 분리 - 댓글 CRUD/권한/알림 일체
- 같은 법인 공유 - 상세/댓글 조회 정책 일원화
- 작성자명 매핑 - PortalUser/TSEAIRM02 양쪽 조회
- 목록 - 댓글 수 표시 및 첨부 UI/로직 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 12:09:39 +09:00
Rinjae c77c0a2f91 ci - 워크플로 정비 및 단위테스트 안정화
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Gitea Generic Package 업로드 단계 추가
- 단위테스트, e2e 워크플로 분리
- systemd 유닛 동기화 단계 제거
- checkout step 이름·주석 정리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 21:06:11 +09:00
Rinjae 815c329e3c CI - 단위테스트 안정화 및 e2e 분리 워크플로 도입
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- AccountControllerTest 현 컨트롤러 API 기준 재작성
- Selenium 4종·SignupFileTest @Tag("e2e") 부여
- BaseWebTest headless 옵션, e2e 워크플로 신설

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 20:21:01 +09:00
Rinjae 7fdd70dbc7 ci - checkout step 이름·주석 정리
- 자신 모듈은 this branch, 의존은 master 명시
- step 이름과 주석으로 의도 가시화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 19:50:58 +09:00
Rinjae 3dd9edc849 eapim-portal CI 테스트 워크플로 추가 2026-06-12 19:40:20 +09:00
Rinjae 6cff96f40a CI - systemd 유닛 동기화 및 리로드 단계 제거 2026-06-12 18:45:55 +09:00
Rinjae a97609d533 Gitea Generic Package 업로드 워크플로 추가
- WAR 파일 Gitea Generic Package 업로드 단계 추가
- 패키지 버전 및 업로드 실패 시 오류 처리 로직 포함
2026-06-12 18:43:18 +09:00
Rinjae 4482f6f807 CI - systemd 기반 워크플로 보강
eapim-portal CI / build (push) Has been cancelled
- systemd 인스턴스 정지/정리 로직 추가
- 사용자 systemd unit 파일 동기화 및 리로드 처리
- Tomcat 실행 방식을 systemd로 전환
2026-06-12 18:36:00 +09:00
Rinjae 34f5a89f78 readiness probe 추가 및 워크플로 보강
- ReadinessController로 DB 연결 여부 확인 엔드포인트 추가
- CI 워크플로에 readiness probe 검증 로직 추가 (120초 제한)
- develop, stage 브랜치 push 이벤트 트리거 추가
2026-06-12 18:23:13 +09:00
Rinjae 9238584cb9 ci - 폐쇄망 액션 경로 사내 Gitea로 변경
- actions/checkout, upload-artifact를
  http://172.30.1.50:3000/actions 로 교체
- github.com 접근 불가로 빌드 실패 해결

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 17:28:48 +09:00
Rinjae 23235bb854 2-Legged 가이드 삭제 완료
- 관련 HTML 제거
- 메뉴 및 경로 수정
2026-06-12 17:11:13 +09:00
Rinjae 2d67647c8f Merge branch 'feats/ui' 2026-06-12 16:15:24 +09:00
Rinjae 4824149ba9 Merge branch 'feats/ci-test' 2026-06-12 16:12:17 +09:00
Rinjae 4927c301f3 개발가이드 페이지 추가 2026-06-10 19:29:24 +09:00
Rinjae d9830fb47d eapim-portal CI 워크플로 및 JNDI 전환
- gitea actions self-hosted runner 워크플로
- EMS/Gateway 데이터소스 JNDI 적용 및 보강
- 부팅/로깅 경로 /prod/eapim, /logs/eapim 으로 정리
2026-06-09 19:12:47 +09:00
Rinjae 6a1fade68d log - jndi-name 미설정시 warn 추가
- portal / gateway datasource 둘다 적용
- 직접 접속 모드 진입 사유 명시
2026-06-09 19:08:19 +09:00
Rinjae 357d609552 fix - jndi lookup 에 resourceRef true 적용
- java:comp/env 네임스페이스 자동 prepend
- tomcat ContextResource 매칭 위함
2026-06-09 18:58:04 +09:00
Rinjae cce9753e36 ci - 로깅 경로 /logs → /logs/eapim 수정 2026-06-09 16:38:10 +09:00
Rinjae 06fdcb8bb6 ci - 로깅 경로 통일성 확보 및 수정
- 로그 디렉토리 /Log/App → /logs/eapim 으로 변경
2026-06-09 16:36:04 +09:00
Rinjae efb88caab5 ci - tomcat 경로 /prod/eapim 으로 이동
- CATALINA_BASE / CATALINA_HOME / CATALINA_PID 갱신
2026-06-09 16:19:16 +09:00
Rinjae fb28a31da1 - EMS/Gateway 데이터소스 JNDI 설정으로 전환
- Hibernate Dialect 및 NamingStrategy 적용
- 불필요한 Proxy 및 로깅 설정 제거
2026-06-09 16:16:48 +09:00
Rinjae b38ca6fe0b - KJB-SafeDB 잔여 설정 제거
- Atomikos 경로 및 로깅 경로 수정
2026-06-09 15:48:08 +09:00
Rinjae 86336d52f0 ci - tomcat 만 jdk17 로 실행
- 빌드는 jdk8, 톰캣 startup/shutdown 만 jdk17
- JAVA_HOME_TOMCAT 환경변수로 분리
2026-06-09 15:41:50 +09:00
Rinjae 53ca4a2a26 ci - tomcat 배포 및 헬스체크 추가
- ROOT.war 로 /prod/devportal/webapps 배포
- startup.sh 후 90초 내 GET / 200 기대
- post-clean 으로 shutdown 보장
2026-06-09 15:35:39 +09:00
Rinjae 36a59b2b64 ci - secret 이름을 CI_TOKEN 으로 변경
- gitea 가 GITEA_ 접두어를 예약어로 차단
2026-06-09 12:22:41 +09:00
Rinjae cdf57c130c ci - actions/checkout 다중 호출 회피
- 두 의존 repo는 raw git clone 으로 체크아웃
- act_runner 0.6.1 액션 캐시 race 우회
2026-06-09 12:20:39 +09:00
Rinjae 7e123c088f ci - eapim-portal gitea actions 워크플로 추가
- master / feats/ci-test push 시 트리거
- 세 repo composite 체크아웃 후 빌드
- war 산출물 업로드
2026-06-09 12:00:28 +09:00
Rinjae daafbb596a ApiTesterFilter 개선 및 KJBank 흔적 제거
- "/api/call-api" 경로 필터링 로직 추가
- KJBank 관련 문자열 DJBank로 변경
2026-06-04 11:03:41 +09:00
Rinjae 0ffee7a101 폰트 기본 설정에 '원신한' 추가 및 SCSS/CSS 파일 업데이트 2026-06-02 15:00:06 +09:00
Rinjae b47f76a783 Merge commit '157c66ca9d4eda83720f3ae35a3e3585408326ab' 2026-06-02 09:34:53 +09:00
Rinjae f591c5c81d KJBank 흔적 삭제 2026-06-02 09:28:44 +09:00
Rinjae a0ea62f8cc KJBank 흔적 삭제 2026-06-02 09:27:16 +09:00
eastargh 157c66ca9d DB스키마 변경 AGWADM->AGWAPP, EMSADM->EMSAPP 2026-05-28 10:57:50 +09:00
Rinjae ba5b2e61a5 - logback 관련 XML 설정 파일 삭제
- KJBank 레이아웃, 프래그먼트 파일 DJBank로 변경
- HTML 레이아웃 파일 KJBank에서 DJBank로 업데이트
2026-05-24 22:21:57 +09:00
Rinjae 5deda2a004 - K8sUtil 파일 삭제
- 승인관리 완전삭제 기능 추가
- DJBank로 명칭 업데이트
2026-05-21 09:52:34 +09:00
604 changed files with 62409 additions and 31434 deletions
+180
View File
@@ -0,0 +1,180 @@
name: eapim-portal CI
on:
push:
branches:
- master
- develop
- stage
- feats/ci-test
jobs:
build:
runs-on: [self-hosted, linux, eapim-portal]
env:
JAVA_HOME: /apps/opts/jdk8
JAVA_HOME_TOMCAT: /apps/opts/jdk17
CATALINA_BASE: /prod/eapim/devportal
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
CATALINA_PID: /prod/eapim/devportal/temp/catalina.pid
DEPLOY_HTTP_PORT: "39130"
defaults:
run:
working-directory: eapim-portal
steps:
- name: Checkout eapim-portal (this branch)
# ref 미지정 — 트리거된 브랜치/commit(github.sha) 그대로 빌드.
uses: http://172.30.1.50:3000/actions/checkout@v4
with:
path: eapim-portal
- name: Checkout elink-portal-common (master, dependency)
# 의존 모듈은 항상 master 고정.
working-directory: ${{ github.workspace }}
run: |
rm -rf elink-portal-common
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-portal-common.git \
elink-portal-common
- name: Checkout elink-online-core-jpa (master, dependency)
# 의존 모듈은 항상 master 고정.
working-directory: ${{ github.workspace }}
run: |
mkdir -p eapim-online
rm -rf eapim-online/elink-online-core-jpa
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-online-core-jpa.git \
eapim-online/elink-online-core-jpa
- name: Verify toolchain
run: |
java -version
gradle --version
- name: Gradle build (no tests)
run: gradle clean build -x test --no-daemon
- name: Upload WAR artifacts
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
with:
name: eapim-portal-wars-${{ github.sha }}
path: |
eapim-portal/build/libs/eapim-portal.war
eapim-portal/build/libs/eapim-portal-boot.war
if-no-files-found: error
- name: Publish WAR to Gitea Generic Package
working-directory: ${{ github.workspace }}
env:
GITEA_URL: http://172.30.1.50:3000
PKG_OWNER: djb-eapim
PKG_NAME: eapim-portal
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
VERSION="$(TZ=Asia/Seoul date +%Y-%m-%d-%H%M)-${SHORT_SHA}"
BASE="$GITEA_URL/api/packages/$PKG_OWNER/generic/$PKG_NAME/$VERSION"
echo "package version: $VERSION"
echo "endpoint base : $BASE"
for WAR in eapim-portal.war eapim-portal-boot.war; do
SRC="eapim-portal/build/libs/$WAR"
if [ ! -f "$SRC" ]; then
echo "::error::missing artifact: $SRC"
exit 1
fi
HTTP=$(curl -sS -o "/tmp/pkg-upload-$WAR.log" -w '%{http_code}' \
--user "oauth2:${{ secrets.CI_TOKEN }}" \
--upload-file "$SRC" \
"$BASE/$WAR")
if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then
echo "::error::upload failed for $WAR (HTTP $HTTP)"
cat "/tmp/pkg-upload-$WAR.log" || true
exit 1
fi
echo " uploaded: $WAR (HTTP $HTTP)"
done
echo "published: $GITEA_URL/$PKG_OWNER/-/packages/generic/$PKG_NAME/$VERSION"
- name: Stop tomcat (pre-clean)
working-directory: ${{ github.workspace }}
run: |
# 1) systemd 관리 인스턴스 정지 (이번 워크플로가 띄운 것)
systemctl --user stop eapim-portal 2>/dev/null || true
# 2) systemd 외부 detached 인스턴스 (eapim-boot-start.sh 등) 정리
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 ROOT
working-directory: ${{ github.workspace }}
run: |
DEPLOY_DIR="$CATALINA_BASE/webapps"
rm -rf "$DEPLOY_DIR/ROOT" "$DEPLOY_DIR/ROOT.war"
cp eapim-portal/build/libs/eapim-portal.war "$DEPLOY_DIR/ROOT.war"
ls -la "$DEPLOY_DIR"
- name: Start tomcat and readiness probe (timeout 120s)
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-portal
DEADLINE=$(($(date +%s) + 120))
LIVE_OK=0
STATUS=000
BODY=""
while [ $(date +%s) -lt $DEADLINE ]; do
# Stage 1: liveness — servlet 응답 가능?
if [ "$LIVE_OK" = "0" ]; then
LIVE=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
http://localhost:$DEPLOY_HTTP_PORT/health 2>/dev/null || echo "000")
if [ "$LIVE" = "200" ]; then
echo "Liveness OK (/health)"
LIVE_OK=1
fi
fi
# Stage 2: readiness — DB(EMS, Gateway)까지 검증
if [ "$LIVE_OK" = "1" ]; then
BODY=$(curl -sS -o /tmp/ready-body.json -w '%{http_code}' --max-time 3 \
http://localhost:$DEPLOY_HTTP_PORT/health/ready 2>/dev/null || echo "000")
STATUS=$BODY
if [ "$STATUS" = "200" ]; then
echo "Readiness OK (/health/ready)"
cat /tmp/ready-body.json
echo
break
fi
fi
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
if [ "$STATUS" != "200" ]; then
echo "::error::Readiness probe failed within 120s (last HTTP status: $STATUS)"
[ -f /tmp/ready-body.json ] && echo "--- last readiness body ---" && cat /tmp/ready-body.json && echo
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100
exit 1
fi
echo "--- key boot log lines ---"
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
+60
View File
@@ -0,0 +1,60 @@
name: eapim-portal E2E Test
# 수동 트리거 전용. e2e 태그가 붙은 Selenium/통합 테스트만 실행.
# 요구 인프라:
# - Chrome + libgbm/libxkbcommon-x11 (Selenium 4종)
# - Oracle JDBC 또는 testcontainers (SignupFileTest)
# 미설치 환경에서는 SessionNotCreated/DB 에러로 일부 케이스가 실패할 수 있음.
on:
workflow_dispatch:
jobs:
e2e:
runs-on: [self-hosted, linux, eapim-portal]
env:
JAVA_HOME: /apps/opts/jdk8
defaults:
run:
working-directory: eapim-portal
steps:
- name: Checkout eapim-portal (this branch)
uses: http://172.30.1.50:3000/actions/checkout@v4
with:
path: eapim-portal
- name: Checkout elink-portal-common (master, dependency)
working-directory: ${{ github.workspace }}
run: |
rm -rf elink-portal-common
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-portal-common.git \
elink-portal-common
- name: Checkout elink-online-core-jpa (master, dependency)
working-directory: ${{ github.workspace }}
run: |
mkdir -p eapim-online
rm -rf eapim-online/elink-online-core-jpa
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-online-core-jpa.git \
eapim-online/elink-online-core-jpa
- name: Verify toolchain
run: |
java -version
gradle --version
- name: Gradle e2e test
run: gradle test --no-daemon -PincludeE2E=true
- name: Upload test results
if: always()
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
with:
name: eapim-portal-e2e-${{ github.sha }}
path: |
eapim-portal/build/reports/tests/test/
eapim-portal/build/test-results/test/
if-no-files-found: warn
+63
View File
@@ -0,0 +1,63 @@
name: eapim-portal Test
on:
push:
branches:
- master
- develop
- stage
- feats/ci-test
jobs:
test:
runs-on: [self-hosted, linux, eapim-portal]
env:
JAVA_HOME: /apps/opts/jdk8
defaults:
run:
working-directory: eapim-portal
steps:
- name: Checkout eapim-portal (this branch)
# ref 미지정 — 트리거된 브랜치/commit(github.sha) 그대로 빌드.
uses: http://172.30.1.50:3000/actions/checkout@v4
with:
path: eapim-portal
- name: Checkout elink-portal-common (master, dependency)
# 의존 모듈은 항상 master 고정.
working-directory: ${{ github.workspace }}
run: |
rm -rf elink-portal-common
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-portal-common.git \
elink-portal-common
- name: Checkout elink-online-core-jpa (master, dependency)
# 의존 모듈은 항상 master 고정.
working-directory: ${{ github.workspace }}
run: |
mkdir -p eapim-online
rm -rf eapim-online/elink-online-core-jpa
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/elink-online-core-jpa.git \
eapim-online/elink-online-core-jpa
- name: Verify toolchain
run: |
java -version
gradle --version
- name: Gradle test
run: gradle test --no-daemon
- name: Upload test results
# 실패해도 결과 리포트는 올려서 원인 분석 가능하게.
if: always()
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
with:
name: eapim-portal-test-${{ github.sha }}
path: |
eapim-portal/build/reports/tests/test/
eapim-portal/build/test-results/test/
if-no-files-found: warn
+6 -1
View File
@@ -83,6 +83,9 @@ file_upload/
# Ignore Gradle build output directory
build
# SonarScanner 작업 디렉터리
.scannerwork/
build_knbank.sh
@@ -107,4 +110,6 @@ TODO.txt
diff
*rinjae*
*gf63*
*gf63*
*obsidian*
design-backup
+39 -4
View File
@@ -8,7 +8,7 @@
**기술 스택:**
- Spring Boot 2.7.18 with Spring MVC and Thymeleaf
- Java 8 (targetCompatibility: 1.8)
- Java 8 (sourceCompatibility/targetCompatibility: 1.8)
- Gradle 8.7 빌드 시스템
- Oracle 19c 데이터베이스 (JNDI를 통한 이중 데이터소스)
- Spring Security (커스텀 인증)
@@ -59,7 +59,7 @@ git push origin jenkins_with_weblogic
### 애플리케이션 실행
- gradlew 사용 금지 - offline gradle 단독 실행
- msi-gf63 컴퓨터에서는 /c/eactive/workspaces/shell-scripts/kjb-gradle.sh 로 gradle 실행 (jdk, gradle 등 환경 변수 세팅 후 gradle 실행하는 스크립트)
- JDK/Gradle 환경 변수는 저장소 루트의 `.envrc`(direnv)로 자동 설정됨 (JAVA_HOME → Zulu JDK 8, GRADLE_USER_HOME → 격리 디렉터리)
```bash
# dev 프로파일로 실행 (기본값)
@@ -173,13 +173,13 @@ com.eactive.apim.portal/
1. **EMS Database** (Portal/Admin)
- JNDI: `jdbc/dsOBP_EMS`
- Schema: `EMSADM`
- Schema: `EMSAPP`
- Entities: `com.eactive.apim.portal.apps.*`
- 목적: 포털 사용자, 조직, API 키, 승인
2. **Gateway Database** (API Specs)
- JNDI: `jdbc/dsOBP_AGW`
- Schema: `AGWADM`
- Schema: `AGWAPP`
- Entities: `com.eactive.eai.data.entity.onl.*`
- 목적: API 명세, 서비스, 메시지
@@ -265,6 +265,41 @@ API 키는 승인 워크플로우와 함께 관리됩니다:
8. `src/main/resources/templates/views/newfeature/`에 Thymeleaf 템플릿 추가
9. `application.yml``pages` 섹션에 라우트 설정 (또는 `@GetMapping`/`@PostMapping`으로 직접 매핑)
### 메뉴/네비게이션 항목 추가하기
**중요:** 메뉴 정의가 **두 곳으로 분리**되어 있다. yml 한 곳만 고치면 헤더 드롭다운에 안 보이고, 헤더만 고치면 브레드크럼이 비어 보인다. 메뉴 추가/이름 변경/순서 변경 시 **반드시 두 파일을 같이 수정**한다.
1. **`application.yml``page:` 트리** (브레드크럼·페이지 메타용)
- `PageService`가 URL → breadcrumb 매핑에 사용
- 부모 메뉴(`service`, `apis`, `community` 등) 아래 `children:`에 새 키를 추가
```yaml
page:
home:
children:
service:
children:
oauth2_guide:
name: "OAuth2 인증가이드"
path: "/service/oauth2-guide"
```
2. **`templates/views/fragment/djbank/header_container.html`** (상단 글로벌 네비)
- `<nav class="header-center"><ul class="nav-menu">` 블록에 정적 `<li>`로 하드코딩되어 있다 — yml 트리를 iterate 하지 **않음**
- 부모 메뉴의 `<ul class="sub-menu">`에 `<li><a th:href="@{/...}">메뉴명</a></li>` 직접 추가
3. **라우트도 같이 등록** (`application.yml`의 `portal.pages` 매핑 섹션, 또는 `@GetMapping`)
```yaml
portal:
pages:
- path-pattern: /service/oauth2-guide
method: GET
view-name: apps/service/oauth2-guide
```
4. **검증**: 변경 후 (a) 헤더 드롭다운에 항목이 보이는지, (b) 새 페이지 진입 시 브레드크럼에 메뉴명이 보이는지 둘 다 확인.
5. **재시작**: `application.yml` 변경은 `PortalPageHandlerMapper`가 `@PostConstruct`에서 한 번만 등록하므로 **자동 라이브 리로드 안 됨** — 수동 재시작 필요. HTML/CSS만 라이브 반영됨.
### MapStruct DTO 매핑
타입 안전한 DTO 변환을 위해 MapStruct 사용:
-83
View File
@@ -1,83 +0,0 @@
# JDK 21로 eapim-portal 기동 가이드
## 환경
- JDK 21 경로: `D:\installs\jdk-21` (jdk-21.0.10+7 폴더를 복사하여 이름 변경)
- Gradle 8.7 (JDK 21 지원)
- Spring Boot 2.7.18
> **주의:** 원본 폴더명(`jdk-21.0.10+7`)에 `+` 문자가 포함되어 있어 CMD에서 JAVA_HOME 설정 시 오류 발생.
> 탐색기에서 `D:\installs\jdk-21`로 복사하여 사용.
---
## build.gradle 변경 사항
JDK 17+ 모듈 시스템(JPMS) 호환을 위해 `bootRun` 블록에 `--add-opens` JVM 인수 추가.
(Hibernate 5.6.x, Atomikos, JAXB 등의 리플렉션 접근 허용)
```groovy
bootRun {
jvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.math=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED',
'--add-opens', 'java.base/java.net=ALL-UNNAMED',
'--add-opens', 'java.base/java.text=ALL-UNNAMED',
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
'--add-opens', 'java.base/sun.util.calendar=ALL-UNNAMED',
'--add-opens', 'java.rmi/sun.rmi.transport=ALL-UNNAMED',
'--add-opens', 'java.sql/java.sql=ALL-UNNAMED',
]
sourceResources sourceSets.main
args = ["--spring.profiles.active=dev"]
}
```
> `sourceCompatibility = "1.8"` 은 유지. `var` 키워드 관련 warning이 발생하지만 빌드/실행에는 영향 없음.
---
## WAR 빌드 및 실행 (bootWarDev)
### 1. JAVA_HOME 설정
```cmd
set JAVA_HOME=D:\installs\jdk-21
set PATH=%JAVA_HOME%\bin;%PATH%
```
### 2. bootWarDev 빌드
```cmd
gradle bootWarDev
```
출력 파일: `build\libs\eapim-portal-dev.war`
### 3. 실행
```cmd
chcp 65001
D:\installs\jdk-21\bin\java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -jar build\libs\eapim-portal-dev.war --spring.profiles.active=dev
```
---
## 한글 인코딩 설정
| 항목 | 설정 |
|---|---|
| CMD 코드페이지 | `chcp 65001` (UTF-8) |
| JVM 인수 | `-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8` |
---
## bootWarDev vs bootWar 차이
| | `bootWar` | `bootWarDev` |
|---|---|---|
| 포함 yml | stage, prod | application.yml + application-dev.yml |
| 출력 파일 | `eapim-portal-boot.war` | `eapim-portal-dev.war` |
| 용도 | 운영/스테이징 배포 | 로컬 dev 실행 |
+174
View File
@@ -0,0 +1,174 @@
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/.."
if [ ! -d elink-portal-common/.git ]; then
rm -rf elink-portal-common
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git elink-portal-common
else
git -C elink-portal-common fetch --depth=1 origin master
git -C elink-portal-common reset --hard origin/master
git -C elink-portal-common clean -fdx
fi
mkdir -p eapim-online
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
rm -rf eapim-online/elink-online-core-jpa
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git eapim-online/elink-online-core-jpa
else
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
git -C eapim-online/elink-online-core-jpa clean -fdx
fi
'''
}
}
stage('Verify toolchain') {
steps { sh 'set -eu; java -version; gradle --version' }
}
stage('Test') {
steps { sh 'gradle clean test --no-daemon -Pprofile=weblogic' }
post {
always {
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
}
}
}
stage('Build WAR') {
steps { sh 'gradle build -x test --no-daemon -Pprofile=weblogic' }
post {
success {
sh '''
set -eu
cd build/libs
sha256sum eapim-portal.war > eapim-portal.war.sha256
'''
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal.war.sha256', fingerprint: true
stash name: 'war', includes: 'build/libs/eapim-portal.war'
}
}
}
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-portal-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/devportal'
WL_DEPLOY_DIR = '/app/eapim/devportal'
WL_WAR_NAME = 'eapim-portal.war'
WL_HTTP_PORT = '39130'
WL_NOHUP = '/logs/weblogic/domains/eapimDomain/nohup.devSvr11.out'
}
stages {
stage('Stop WebLogic') {
steps {
sh '"$WL_HOME/stopDev11.sh"' // 동기: 완전 종료까지 블록, 미기동 시에도 안전
}
}
stage('Deploy WAR') {
steps {
unstash 'war'
sh '''
set -eu
cp build/libs/eapim-portal.war "$WL_DEPLOY_DIR/$WL_WAR_NAME"
ls -la "$WL_DEPLOY_DIR"
'''
}
}
stage('Start WebLogic and readiness') {
steps {
sh '''
set -eu
"$WL_HOME/startDev11.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/health/ready" 2>/dev/null || echo "000")
[ "$STATUS" = "200" ] && { echo "Readiness OK (/health/ready)"; break; }
sleep 3
done
if [ "$STATUS" != "200" ]; then
echo "Readiness failed within 300s, last HTTP=$STATUS"
[ -f "$WL_NOHUP" ] && tail -120 "$WL_NOHUP"
exit 1
fi
'''
}
}
}
}
}
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"
'''
}
}
}
}
+267
View File
@@ -0,0 +1,267 @@
// SonarQube 정적 보안 분석 전용 파이프라인.
//
// 전제 (Jenkins 쪽 설정 이름이 다르면 environment 블록의 SONAR_* 만 고치면 된다)
// 1) Manage Jenkins > System > SonarQube servers 에 서버 등록 (이름: SONAR_ENV_NAME)
// 2) Manage Jenkins > Tools > SonarQube Scanner 에 스캐너 등록
// (SONAR_SCANNER_TOOL 이 비어 있으면 흔한 이름을 탐색하고, 없으면 PATH 의 sonar-scanner 사용)
// 3) Quality Gate 대기를 쓰려면 SonarQube 서버에
// Administration > Configuration > Webhooks 에 <JENKINS_URL>/sonarqube-webhook/ 등록
// 4) Job 의 Git 설정에서 shallow clone 을 끄고 tags/history 를 받아와야 blame 기반 "새 코드" 판정이 정확하다.
//
// 빌드는 JDK 8, 스캐너 실행은 JDK 17 로 분리한다.
// (SonarScanner CLI 5.x / SonarQube 10.x 이상은 JRE 17 필수. JDK 8 로 실행하면 UnsupportedClassVersionError)
pipeline {
agent { label 'djb-vm' }
triggers {
pollSCM('H/30 * * * *')
}
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters {
booleanParam(
name: 'RUN_TESTS',
defaultValue: false,
description: '단위 테스트를 함께 실행해 JUnit 결과를 Sonar 로 전송한다(분석 시간 증가).'
)
}
environment {
// --- Jenkins 설정 이름 (환경에 맞게 수정) ---
// Manage Jenkins > System > SonarQube servers 의 Name
SONAR_ENV_NAME = 'SonarQube-Community'
// Manage Jenkins > Tools > SonarQube Scanner 에 등록한 이름.
// 비워두면 아래 후보 이름들을 순서대로 탐색하고, 그래도 없으면 PATH 의 sonar-scanner 를 쓴다.
SONAR_SCANNER_TOOL = ''
// --- 툴체인 ---
JAVA_HOME = '/apps/opts/jdk8' // gradle 컴파일용 (프로젝트는 Java 8)
JAVA_HOME_SCANNER = '/apps/opts/jdk17' // sonar-scanner 실행용
GRADLE_HOME = '/apps/opts/gradle-8.7'
GRADLE_USER_HOME = '/apps/opts/gradle-home'
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/bin:${env.PATH}"
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
// eapim-portal 은 elink-portal-common / elink-online-core-jpa 를 subproject 로 참조한다.
// 이 둘이 없으면 컴파일 자체가 불가능하고, 컴파일 산출물이 없으면 Sonar Java 분석이 무력화된다.
stage('Checkout dependencies') {
steps {
sh '''
set -eu
cd "$WORKSPACE/.."
if [ ! -d elink-portal-common/.git ]; then
rm -rf elink-portal-common
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
elink-portal-common
else
git -C elink-portal-common fetch --depth=1 origin master
git -C elink-portal-common reset --hard origin/master
git -C elink-portal-common clean -fdx
fi
mkdir -p eapim-online
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
rm -rf eapim-online/elink-online-core-jpa
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
eapim-online/elink-online-core-jpa
else
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
git -C eapim-online/elink-online-core-jpa clean -fdx
fi
'''
}
}
stage('Verify toolchain') {
steps {
sh '''
set -eu
echo "--- build JDK ---"
java -version
gradle --version
echo "--- scanner JDK ---"
if [ ! -x "$JAVA_HOME_SCANNER/bin/java" ]; then
echo "JDK 17 이 없다: $JAVA_HOME_SCANNER"
echo "SonarScanner 는 JRE 17 이상을 요구한다. 노드에 설치하거나 JAVA_HOME_SCANNER 를 수정할 것."
exit 1
fi
"$JAVA_HOME_SCANNER/bin/java" -version
'''
}
}
// build/generated 잔여 산출물이 남으면 MapStruct Impl / QueryDSL Q클래스가
// 중복 생성되어 컴파일이 깨진다. clean 이 파일 잠금 등으로 실패하는 경우가 있어
// 생성 소스 디렉터리는 별도로 먼저 지운다.
stage('Compile') {
steps {
sh '''
set -eu
rm -rf build/generated build/classes
gradle clean classes testClasses --no-daemon
'''
}
}
stage('Test') {
when { expression { return params.RUN_TESTS } }
steps {
sh 'gradle test --no-daemon'
}
post {
always {
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
}
}
}
// sonar.java.libraries 용 클래스패스 덤프.
// 없으면 타입 해석이 안 돼 보안 룰 다수가 침묵하므로 실패 시 UNSTABLE 로만 넘기고 분석은 계속한다.
stage('Export analysis classpath') {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh 'gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon'
}
}
}
stage('SonarQube analysis') {
steps {
script {
// 스캐너 위치 결정: 지정 이름 > 흔한 등록 이름 후보 > PATH 의 sonar-scanner
def candidates = env.SONAR_SCANNER_TOOL?.trim()
? [env.SONAR_SCANNER_TOOL.trim()]
: ['SonarScanner', 'SonarQube Scanner', 'sonar-scanner', 'SonarScanner CLI', 'sonarqube-scanner']
def scannerHome = null
for (name in candidates) {
try {
scannerHome = tool name: name, type: 'hudson.plugins.sonar.SonarRunnerInstallation'
echo "SonarScanner tool: '${name}' -> ${scannerHome}"
break
} catch (ignored) {
// 등록되지 않은 이름은 건너뛴다
}
}
if (scannerHome == null) {
echo 'Jenkins Tools 에 등록된 SonarQube Scanner 를 찾지 못했다. PATH 의 sonar-scanner 로 진행한다.'
echo '(Manage Jenkins > Tools 에 등록한 뒤 environment 의 SONAR_SCANNER_TOOL 에 그 이름을 넣으면 확실하다.)'
}
withSonarQubeEnv(env.SONAR_ENV_NAME) {
withEnv(["SONAR_SCANNER_HOME=${scannerHome ?: ''}"]) {
sh '''
set -eu
# 스캐너는 JDK 17 로 실행한다 (빌드용 JDK 8 과 분리)
export JAVA_HOME="$JAVA_HOME_SCANNER"
export PATH="$JAVA_HOME/bin:$PATH"
# 토큰은 커맨드라인(-Dsonar.token)에 노출시키지 않고 환경변수로만 넘긴다.
# 플러그인 버전에 따라 SONAR_AUTH_TOKEN 만 주입되는 경우가 있다.
if [ -z "${SONAR_TOKEN:-}" ] && [ -n "${SONAR_AUTH_TOKEN:-}" ]; then
export SONAR_TOKEN="$SONAR_AUTH_TOKEN"
fi
LIBS=""
if [ -f build/sonar/java-libraries.txt ]; then
LIBS=$(cat build/sonar/java-libraries.txt)
else
echo "WARN: build/sonar/java-libraries.txt 없음 - 타입 해석 정확도 저하"
fi
TEST_LIBS=""
if [ -f build/sonar/java-test-libraries.txt ]; then
TEST_LIBS=$(cat build/sonar/java-test-libraries.txt)
fi
JUNIT_ARG=""
if [ -d build/test-results/test ]; then
JUNIT_ARG="-Dsonar.junit.reportPaths=build/test-results/test"
fi
# Tools 에 등록된 스캐너가 없으면 PATH 에서 찾는다
if [ -n "${SONAR_SCANNER_HOME:-}" ] && [ -x "$SONAR_SCANNER_HOME/bin/sonar-scanner" ]; then
SCANNER="$SONAR_SCANNER_HOME/bin/sonar-scanner"
elif command -v sonar-scanner >/dev/null 2>&1; then
SCANNER="$(command -v sonar-scanner)"
else
echo "sonar-scanner 실행 파일을 찾지 못했다."
echo "Manage Jenkins > Tools > SonarQube Scanner 에 등록하거나 노드 PATH 에 설치할 것."
exit 1
fi
echo "scanner: $SCANNER"
"$SCANNER" \
-Dsonar.projectVersion="${BUILD_NUMBER}" \
-Dsonar.java.libraries="$LIBS" \
-Dsonar.java.test.libraries="$TEST_LIBS" \
$JUNIT_ARG
'''
}
}
}
}
}
// SonarQube 웹훅으로 게이트 결과를 받아 판정한다.
// 정책: 게이트 실패는 UNSTABLE 로만 표시하고 빌드는 실패시키지 않는다.
stage('Quality Gate') {
steps {
script {
def qg = null
try {
timeout(time: 15, unit: 'MINUTES') {
qg = waitForQualityGate abortPipeline: false
}
} catch (err) {
unstable("Quality Gate 결과 대기 실패/타임아웃: ${err}")
return
}
if (qg == null) {
unstable('Quality Gate 결과를 받지 못했다. SonarQube 웹훅 설정을 확인할 것.')
} else if (qg.status != 'OK') {
unstable("Quality Gate ${qg.status}")
} else {
echo 'Quality Gate OK'
}
}
}
}
}
post {
always {
archiveArtifacts allowEmptyArchive: true, artifacts: '.scannerwork/report-task.txt'
script {
def report = '.scannerwork/report-task.txt'
if (fileExists(report)) {
def url = readFile(report).readLines().find { it.startsWith('dashboardUrl=') }
if (url) {
echo "SonarQube: ${url.substring('dashboardUrl='.length())}"
}
}
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
pipeline {
agent { label 'djb-vm' }
triggers {
pollSCM('H/10 * * * *')
}
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
}
environment {
JAVA_HOME = '/apps/opts/jdk8'
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
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}"
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Checkout dependencies') {
steps {
sh '''
set -eu
cd "$WORKSPACE/.."
if [ ! -d elink-portal-common/.git ]; then
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
elink-portal-common
else
git -C elink-portal-common fetch --depth=1 origin master
git -C elink-portal-common reset --hard origin/master
git -C elink-portal-common clean -fdx
fi
mkdir -p eapim-online
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
eapim-online/elink-online-core-jpa
else
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
git -C eapim-online/elink-online-core-jpa clean -fdx
fi
'''
}
}
stage('Verify toolchain') {
steps {
sh '''
set -eu
java -version
gradle --version
node --version || true
npm --version || true
'''
}
}
stage('Test') {
steps {
sh 'gradle clean test --no-daemon'
}
post {
always {
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
}
}
}
stage('Build WAR') {
steps {
sh 'gradle build -x test --no-daemon'
}
post {
success {
sh '''
set -eu
cd build/libs
for f in eapim-portal.war eapim-portal-boot.war; do
sha1sum "$f" > "$f.sha1"
sha256sum "$f" > "$f.sha256"
md5sum "$f" > "$f.md5"
done
'''
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal-boot.war,build/libs/eapim-portal.war.sha1,build/libs/eapim-portal.war.sha256,build/libs/eapim-portal.war.md5,build/libs/eapim-portal-boot.war.sha1,build/libs/eapim-portal-boot.war.sha256,build/libs/eapim-portal-boot.war.md5', fingerprint: true
}
}
}
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-portal-sbom.xlsx 를 아티팩트로 보관.
// 실패해도 빌드는 진행하도록 UNSTABLE 로만 표시한다.
stage('SBOM') {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh 'gradle sbomXlsx --no-daemon'
}
}
post {
always {
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/sbom/*.xlsx', fingerprint: true
}
}
}
}
}
+16 -55
View File
@@ -69,10 +69,17 @@ eapim-portal
### 사전 요구사항
- JDK 8 이상
- Gradle 8.7
아래 도구 설치 및 검증 절차는 별도 문서로 분리되어 있습니다. 신규 개발자는 먼저 이 문서를 끝까지 진행하세요.
📄 **[개발환경 준비 사항](djb-docs/개발환경-준비-사항.md)** — JDK 8 · Gradle 8.7 · Git · Oracle 19c · Node.js & SASS · direnv · IDE 설정
요약:
- JDK 8 (Zulu 권장)
- Gradle 8.7 (gradlew 사용 금지)
- Git
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
- Oracle 19c 접근 (개발: `192.168.240.177:1599`)
- Node.js 18 LTS 이상 (SCSS 컴파일용)
### Git Bash에서 초기 세팅
@@ -90,10 +97,7 @@ export GIT_REPO_BASE="ssh://git@192.168.240.178:18081/eapim"
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
# Gradle 명령어 설정
# 일반 환경: gradle
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
export GRADLE_CMD="gradle"
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
# ====================================
# 2. 작업 디렉토리 생성
@@ -128,12 +132,6 @@ git clone ${GIT_REPO_BASE}/elink-portal-common.git
cd elink-portal-common
git checkout master # 또는 필요한 브랜치
# kjb-safedb 클론
cd $WORKSPACE_DIR
git clone ${GIT_REPO_BASE}/kjb-safedb.git
cd kjb-safedb
git checkout master # 또는 필요한 브랜치
# ====================================
# 6. 최종 폴더 구조 확인
# ====================================
@@ -142,7 +140,6 @@ git checkout master # 또는 필요한 브랜치
# ├── eapim-online/
# │ └── elink-online-core-jpa/
# ├── elink-portal-common/
# └── kjb-safedb/
# ====================================
# 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
@@ -169,10 +166,7 @@ set GIT_REPO_BASE=ssh://git@192.168.240.178:18081/eapim
REM set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
REM Gradle 명령어 설정
REM 일반 환경: gradle
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
set GRADLE_CMD=gradle
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
REM ====================================
REM 2. 작업 디렉토리 생성
@@ -207,12 +201,6 @@ git clone %GIT_REPO_BASE%/elink-portal-common.git
cd elink-portal-common
git checkout master
REM kjb-safedb 클론
cd /d "%WORKSPACE_DIR%"
git clone %GIT_REPO_BASE%/kjb-safedb.git
cd kjb-safedb
git checkout master
REM ====================================
REM 6. 최종 폴더 구조 확인
REM ====================================
@@ -221,7 +209,6 @@ REM ├── eapim-portal\
REM ├── eapim-online\
REM │ └── elink-online-core-jpa\
REM ├── elink-portal-common\
REM └── kjb-safedb\
REM ====================================
REM 7. eapim-portal 프로젝트로 이동 및 디펜던시 다운로드
@@ -253,30 +240,19 @@ REM Gradle 디펜던시 다운로드
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
- **SafeDB**: 암호화 기능이 필요한 경우 kjb-safedb 모듈 설정 확인
## 빌드 및 실행
### Gradle 명령어 설정
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
**Git Bash:**
```bash
# 일반 환경
export GRADLE_CMD="gradle"
# msi-gf63 환경
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
```
**Windows CMD:**
```cmd
REM 일반 환경
set GRADLE_CMD=gradle
REM msi-gf63 환경
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
```
### 빌드 및 실행 명령어
@@ -310,7 +286,6 @@ kjb-eapim/
├── eapim-online/ # Online 프로젝트
│ └── elink-online-core-jpa/ # Gateway 데이터 모델
├── elink-portal-common/ # 공통 유틸리티
└── kjb-safedb/ # SafeDB 암호화 라이브러리
```
### eapim-portal 내부 구조
@@ -327,7 +302,6 @@ src/main/java/com/eactive/apim/portal/
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조):
- `eapim-online/elink-online-core-jpa`: Gateway 데이터 모델 (../eapim-online/elink-online-core-jpa)
- `elink-portal-common`: 공통 유틸리티 (../elink-portal-common)
- `kjb-safedb`: SafeDB 암호화 라이브러리 (../kjb-safedb)
## SASS / CSS 스타일 개발
@@ -339,8 +313,11 @@ src/main/java/com/eactive/apim/portal/
### 설치
Node.js 및 SASS 설치 절차는 [개발환경 준비 사항 § 6. Node.js & SASS 빌드 환경](djb-docs/개발환경-준비-사항.md#6-nodejs--sass-빌드-환경) 을 참고하세요.
Node.js가 이미 설치되어 있다면 프로젝트 의존성만 설치하면 됩니다:
```bash
# Node.js가 설치된 상태에서
npm install
```
@@ -532,8 +509,6 @@ ls -lh src/main/resources/static/css/main.min.css # minified
- 사용자 관리 (내부/기업)
- API 키 관리 및 승인 워크플로우
- API 카탈로그 및 문서화
- API 테스트 프록시
- 감사 추적 (Hibernate Envers)
## 배포
@@ -543,24 +518,10 @@ ls -lh src/main/resources/static/css/main.min.css # minified
- WebLogic
- Tomcat (내장)
### Docker
```bash
./build_docker.sh
docker run -p 8080:8080 eapim-portal:latest
```
## 관련 프로젝트
프로젝트 구조 (kjb-eapim 디렉토리 기준):
- `eapim-portal/` - Portal 웹 애플리케이션 (현재 프로젝트)
- `eapim-online/` - Online 프로젝트 (elink-online-core-jpa 포함)
- `elink-portal-common/` - 공통 유틸리티 라이브러리
- `kjb-safedb/` - SafeDB 암호화 라이브러리
- `eapim-admin/` - Admin 포털 (선택사항)
- `kjb-eapim-sql/` - SQL 스크립트 (선택사항)
## 문서
- **개발환경 준비 사항**: [`djb-docs/개발환경-준비-사항.md`](djb-docs/개발환경-준비-사항.md) — JDK·Gradle·Node.js·SASS 설치 가이드
- **메뉴 관리 개발 가이드**: [`readme-docs/메뉴-관리-개발-가이드.md`](readme-docs/메뉴-관리-개발-가이드.md) — menu.yml/roles.yml 스키마·시딩 규칙·캐시 리로드·admin 포탈메뉴관리 연동
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
- **사용자 가이드**: `개발자포탈.md` (한글)
- **빌드 스크립트**: `build-gf63.sh`, `deploy_portal.sh`
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
echo "========================"
java -version
javac -version
echo "========================"
gradle --version
echo "========================"
gradle clean build -x test --no-daemon
+96 -64
View File
@@ -3,6 +3,7 @@ plugins {
id 'war'
id 'eclipse'
id 'idea'
id 'org.cyclonedx.bom' version '3.2.4'
id 'org.springframework.boot' version '2.7.18'
id 'io.spring.dependency-management' version '1.1.3'
}
@@ -23,15 +24,36 @@ allprojects {
}
}
// 로컬 전용 라이브러리 — WebLogic 배포 산출물(war/bootWar)에서 제외한다.
// 정보보호 점검에서 Actuator/모니터링 라이브러리가 배포본에 실려 있으면 불필요하게 탐지되므로
// 로컬(bootRun/IDE)에서만 classpath 에 오르게 한다. 제외 로직은 아래 war/bootWar 블록.
//
// ※ Spring Boot 의 developmentOnly 를 쓰지 않는 이유
// developmentOnly 는 bootJar/bootWar 에서만 제외된다. 표준 war task 는 runtimeClasspath 를
// 그대로 쓰므로 산출물에 실린다 (실측: devtools 가 eapim-portal.war 에 포함되어 있었음).
// ※ 별도 configuration(localOnly)을 쓰지 않는 이유
// runtimeClasspath 에서 빠지면 IntelliJ 가 모듈 classpath 를 구성할 때도 빠져
// IDE 로 기동할 때 Actuator/SBA 가 동작하지 않는다.
def localOnlyLibPrefixes = [
'spring-boot-devtools',
'spring-boot-starter-actuator',
'spring-boot-actuator', // spring-boot-actuator, -autoconfigure 모두 매칭
'micrometer-', // actuator 전용(runtimeClasspath 상 다른 출처 없음 — 확인함)
'spring-boot-admin-',
]
dependencies {
annotationProcessor "org.projectlombok:lombok:1.18.28"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
implementation 'com.eactive.elink.common:elink-common-data:4.5.5'
// implementation 'io.swagger.core.v3:swagger-core:2.2.25'
// implementation 'io.swagger.parser.v3:swagger-parser:2.1.23'
implementation fileTree(dir: 'libs/swagger', include: ['*.jar'])
// OpenAPI 3.0/3.1 spec 지원 (Nexus/Maven 해석). 기존 libs/swagger fileTree 대체.
// swagger 최신 릴리스(2.2.52/2.1.45)도 SpecVersion 은 V30/V31 까지만 — OpenAPI 3.2 는
// 아직 swagger-core/parser 어느 버전도 미지원. 지원 추가 시 버전만 상향하면 됨. JDK8 호환.
implementation 'io.swagger.core.v3:swagger-core:2.2.52' // io.swagger.v3.oas.models.*, io.swagger.v3.core.util.Json
implementation 'io.swagger.parser.v3:swagger-parser:2.1.45' // io.swagger.parser.OpenAPIParser, io.swagger.v3.parser.*
implementation 'io.swagger:swagger-annotations:1.6.14' // io.swagger.annotations.ApiModelProperty (CommissionSearch)
implementation project(':elink-online-core-jpa')
implementation project(':elink-portal-common')
@@ -41,6 +63,23 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-validation')
// ↓ 로컬 전용. war/bootWar 산출물에서는 localOnlyLibPrefixes 로 제외된다.
// runtimeOnly 인 이유: compileClasspath 에서 빠지므로 자바 코드가 이 API 를 참조하면
// 컴파일 단계에서 막힌다. implementation 이면 참조가 컴파일에 통과해버리고,
// 배포본(actuator/SBA 제외됨)에서 NoClassDefFoundError 로 터진다.
// IntelliJ / bootRun 은 runtimeClasspath 기준이라 로컬 기동에는 정상 포함된다.
// 설정(application-local*.yml)으로만 사용한다.
runtimeOnly('org.springframework.boot:spring-boot-starter-actuator')
// Spring Boot Admin client. 2.7.16 = Spring Boot 2.7.x 대응 마지막 계열(Java 8 호환).
runtimeOnly('de.codecentric:spring-boot-admin-starter-client:2.7.16')
// 위 runtimeOnly 원칙의 유일한 예외. InterceptorsEndpointConfig 가 actuator API
// (@Endpoint / InfoContributor)를 참조해야 하므로 compileClasspath 에만 올린다.
// compileOnly 는 runtimeClasspath 에 포함되지 않으므로 war/bootWar 산출물에는 영향이 없다.
// 해당 클래스는 @ConditionalOnClass 로 actuator 부재 시 로드되지 않으니
// 배포본(actuator 제외)에서 NoClassDefFoundError 가 나지 않는다.
compileOnly('org.springframework.boot:spring-boot-actuator')
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1'
@@ -76,9 +115,10 @@ dependencies {
// exclude group: 'commons-collections', module: 'commons-collections'
}
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.3'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.3'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
// WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정. JDK8 호환.
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.6'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.6'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.6'
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
@@ -123,36 +163,41 @@ bootRun {
// 디버깅 사용시에는 아래 내용 주석 해제 하세요
// jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
// JDK 17+ (모듈 시스템) 호환을 위한 설정 - Hibernate 5.x, Atomikos, JAXB 등
jvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.math=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED',
'--add-opens', 'java.base/java.net=ALL-UNNAMED',
'--add-opens', 'java.base/java.text=ALL-UNNAMED',
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
'--add-opens', 'java.base/sun.util.calendar=ALL-UNNAMED',
'--add-opens', 'java.rmi/sun.rmi.transport=ALL-UNNAMED',
'--add-opens', 'java.sql/java.sql=ALL-UNNAMED',
]
sourceResources sourceSets.main // processResources 필터 우회 (dev yml 직접 참조)
args = ["--spring.profiles.active=dev"]
// 로컬 기동 기본 프로파일.
// dev/stage/prod 는 리버스프록시(OHS) 뒤 WAR 배포용이라 server.forward-headers-strategy=framework 가
// 걸려 있다. framework 는 신뢰 프록시 목록 없이 X-Forwarded-* 를 그대로 신뢰하므로 앞단이 없는
// 로컬 기동에는 쓰지 않는다(로컬은 공통 기본값 native = Tomcat RemoteIpValve, 사설대역만 신뢰).
// 다른 프로파일로 띄우려면: gradle bootRun -PbootProfile=dev
// ('profile' 이 아니라 'bootProfile' 인 이유: 위 ext 블록이 profile='local' 을 이미 점유하고 있어
// findProperty('profile') 은 -P 지정 여부와 무관하게 항상 'local' 을 돌려준다.)
def bootRunProfile = (project.findProperty('bootProfile') ?: 'local_rinjaemac').toString()
args = ["--spring.profiles.active=" + bootRunProfile]
}
// annotation processor 출력 디렉토리는 IntelliJ/Gradle 통합이 자동 등록함.
// 명시적으로 srcDir 추가하면 IntelliJ idea 모드에서 MapStruct가 같은 파일을
// 중복 생성하려다 javax.annotation.processing.FilerException 발생함.
sourceSets {
main {
java {
srcDir 'src/main/java'
srcDir 'build/generated/sources/annotationProcessor/java/main'
}
}
}
configurations {
annotationProcessor
// WebLogic 배포 시 Tyrus WebSocket 필터(weblogic.websocket.tyrus.TyrusServletFilter)와
// 충돌 방지: WAR 에 번들된 Tomcat WsSci 가 javax.websocket.server.ServerContainer 속성을
// WsServerContainer 로 등록 → WebLogic Tyrus 필터가 TyrusServerContainer 로 캐스팅하다 실패.
// 앱은 WebSocket 미사용이므로 Tomcat WebSocket 모듈 제외.
all {
exclude group: 'org.apache.tomcat.embed', module: 'tomcat-embed-websocket'
}
}
compileJava {
@@ -162,48 +207,49 @@ compileJava {
}
processResources {
exclude { details ->
details.file.name.startsWith('application-') &&
details.file.name.endsWith('.yml') &&
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
}
// exclude { details ->
// details.file.name.startsWith('application-') &&
// details.file.name.endsWith('.yml') &&
// !(details.file.name in ['application-stage.yml', 'application-prod.yml'])
// }
}
test {
useJUnitPlatform()
useJUnitPlatform {
if (project.hasProperty('includeE2E') && project.includeE2E.toBoolean()) {
includeTags 'e2e'
} else {
excludeTags 'e2e'
}
}
enabled = true
}
// 로컬 전용 설정 파일. 배포 산출물(WAR)에 실리면 Actuator/SBA 설정이 그대로 노출되어
// 정보보호 점검에 불필요하게 걸린다. processResources 는 건드리지 않는다
// (bootRun 이 build/resources/main 을 그대로 쓰므로 로컬 기동이 깨진다).
def localOnlyResources = ['**/application-local*.yml']
// 배포 산출물에서 로컬 전용 라이브러리를 걸러낸다. localOnlyLibPrefixes 는 파일 상단 정의.
def excludeLocalOnlyLibs = { org.gradle.api.file.FileCollection cp ->
cp.filter { f -> !localOnlyLibPrefixes.any { p -> f.name.startsWith(p) } }
}
bootWar {
archiveFileName = "eapim-portal-boot.war"
mainClass = 'com.eactive.apim.portal.PortalApplication'
rootSpec.exclude(localOnlyResources)
classpath = excludeLocalOnlyLibs(classpath)
}
task bootWarDev(type: org.springframework.boot.gradle.tasks.bundling.BootWar) {
group = 'build'
description = 'dev 프로파일용 실행 가능 WAR (application.yml + application-dev.yml 포함)'
dependsOn compileJava
mainClass = bootWar.mainClass
classpath = sourceSets.main.runtimeClasspath
// processResources 결과물 포함 (stage, prod yml 제외된 상태)
from(processResources.destinationDir)
// application.yml, application-dev.yml 추가 (중복 시 srcDirs 파일 우선)
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from(sourceSets.main.resources.srcDirs) {
include 'application.yml'
include 'application-dev.yml'
}
archiveFileName = "eapim-portal-dev.war"
}//gradlew clean bootWarDev, java -jar build/libs/eapim-portal-dev.war --spring.profiles.active=dev
war {
archiveFileName = "eapim-portal.war"
from('src/main/resources/jeus-web-dd.xml') { into 'WEB-INF' }
from('src/main/resources/weblogic.xml') { into 'WEB-INF' }
rootSpec.exclude(localOnlyResources)
classpath = excludeLocalOnlyLibs(classpath)
}
task printSourceSets {
@@ -216,19 +262,5 @@ task printSourceSets {
}
}
}
// CryptoCli 실행 task (bcrypt 지원)
task cryptoCli(type: JavaExec) {
mainClass = 'com.eactive.ext.kjb.safedb.CryptoCli'
classpath = sourceSets.main.runtimeClasspath
standardInput = System.in
// SafeDB 라이브러리 경로 추가 (리눅스)
classpath += files("/safedb/JavaAPI/config")
classpath += fileTree(dir: "/safedb/JavaAPI/lib", include: ["*.jar"])
// CLI 인자 전달: ./gradlew cryptoCli --args="bcrypt hash password123"
if (project.hasProperty('args')) {
args project.args.split('\\s+')
}
}
// CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx)
apply from: "$projectDir/gradle/sbom-xlsx.gradle"
-3
View File
@@ -1,3 +0,0 @@
./gradlew bootWar
docker build -t eactive-portal .
+41
View File
@@ -0,0 +1,41 @@
// SonarScanner 의 sonar.java.libraries / sonar.java.test.libraries 로 넘길
// 컴파일 클래스패스를 파일로 덤프한다.
//
// build.gradle 을 건드리지 않기 위해 init script(-I) 로만 주입한다.
// gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon
//
// 출력(콤마 구분, 절대경로):
// build/sonar/java-libraries.txt main 컴파일 클래스패스
// build/sonar/java-test-libraries.txt main + test 컴파일 클래스패스
//
// 이 파일이 없으면 Sonar Java 분석기가 타입을 해석하지 못해
// 보안 룰(SQL Injection, XSS 등) 상당수가 침묵한다. 선택 사항이 아니다.
rootProject { project ->
project.plugins.withId('java') {
project.tasks.register('exportSonarClasspath') {
group = 'verification'
description = 'SonarScanner 용 컴파일 클래스패스를 build/sonar 에 덤프한다'
doLast {
def outDir = new File(project.layout.buildDirectory.get().asFile, 'sonar')
outDir.mkdirs()
def toLine = { files ->
files.findAll { it.exists() }
.collect { it.absolutePath }
.unique()
.join(',')
}
def mainCp = project.sourceSets.main.compileClasspath.files
def testCp = project.sourceSets.test.compileClasspath.files
new File(outDir, 'java-libraries.txt').text = toLine(mainCp)
new File(outDir, 'java-test-libraries.txt').text = toLine(mainCp + testCp)
logger.lifecycle("sonar classpath dumped: ${outDir}")
}
}
}
}
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
# Check if the war file is provided
if [ -z "$1" ]; then
echo "Usage: $0 /path/to/DEVPortal.war"
exit 1
fi
# Variables
WAR_FILE="$1"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DEST_DIR="/app/apim/deploy/DVPWeb"
BACKUP_DIR="/app/apim/backup"
# Create backup directory if not exists
mkdir -p $BACKUP_DIR
# Backup the existing directory
if [ -d "$DEST_DIR" ]; then
echo "Backing up existing directory..."
mv $DEST_DIR "${BACKUP_DIR}/dvp_${TIMESTAMP}"
fi
# Create destination directory if not exists
mkdir -p $DEST_DIR
# Unzip the file to the dedicated location
echo "Extracting WAR file to $DEST_DIR..."
unzip $WAR_FILE -d $DEST_DIR
echo "deploy successful"
-40
View File
@@ -1,40 +0,0 @@
#!/bin/bash
# Check if the war file is provided
if [ -z "$1" ]; then
echo "Usage: $0 /path/to/DEVPortal.war"
exit 1
fi
# Variables
WAR_FILE="$1"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DEST_DIR="/app/apim/deploy/DVPWeb"
STATIC_DIR="/app/apim/deploy/static"
BACKUP_DIR="/app/apim/backup"
# Create backup directory if not exists
mkdir -p $BACKUP_DIR
# Backup the existing directory
if [ -d "$STATIC_DIR" ]; then
echo "Backing up existing static directory..."
mv $STATIC_DIR "${BACKUP_DIR}/dvp_static_${TIMESTAMP}"
fi
if [ -d "$DEST_DIR" ]; then
echo "Backing up existing directory..."
mv $DEST_DIR "${BACKUP_DIR}/dvp_${TIMESTAMP}"
fi
# Create destination directory if not exists
mkdir -p $DEST_DIR
mkdir -p $STATIC_DIR
# unzip the file to the dedicated location
echo "Extracting WAR file to $DEST_DIR..."
unzip $WAR_FILE -d $DEST_DIR
mv $DEST_DIR/static/* $STATIC_DIR/
echo "deploy successful"
+273
View File
@@ -0,0 +1,273 @@
# 개발환경 준비 사항
EAPIM Portal 개발에 필요한 도구와 런타임을 설치하고 검증하는 절차입니다.
처음 환경을 세팅하는 신규 개발자는 이 문서를 끝까지 따라 진행한 뒤 [README.md](../README.md)의 "초기 세팅 가이드" 단계를 이어서 수행하세요.
---
## 0. 한눈에 보는 요구사항
| 구분 | 도구 | 권장 버전 | 비고 |
|------|------|-----------|----------------------------------|
| 백엔드 | JDK | 1.8 (Zulu 8 권장) | `sourceCompatibility=1.8` |
| 빌드 | Gradle | 8.7 | gradlew 사용 금지 — 시스템 gradle 직접 사용 |
| 형상관리 | Git | 2.30 이상 | SSH 키 등록 권장 |
| DB | Oracle Client/접근 | 19c | 개발은 `192.168.240.177:1598` |
| 프론트엔드 빌드 | Node.js | 18 LTS 이상 | SCSS 컴파일 전용 |
| 프론트엔드 빌드 | npm | Node.js 함께 설치 | `sass` 패키지 사용 |
| (선택) 환경 변수 | direnv | 최신 | `.envrc` 자동 로드 |
| (선택) IDE | IntelliJ IDEA | 2023.x 이상 | Lombok / MapStruct 플러그인 |
---
## 1. JDK 8 설치
Java 8 (1.8) 호환이 필요합니다. Azul Zulu JDK 8 사용을 권장합니다.
### macOS (Homebrew)
```bash
brew install --cask zulu@8
# 설치 후 경로 확인
/usr/libexec/java_home -v 1.8
```
### Windows
1. [Azul Zulu JDK 8 다운로드](https://www.azul.com/downloads/?version=java-8-lts&package=jdk)
2. MSI 설치 후 `JAVA_HOME` 환경변수 등록
3. `PATH``%JAVA_HOME%\bin` 추가
### 검증
```bash
java -version # 1.8.x
javac -version # 1.8.x
echo $JAVA_HOME # macOS/Linux
echo %JAVA_HOME% # Windows
```
---
## 2. Gradle 8.7 설치
이 프로젝트는 **gradlew 사용을 금지**합니다. 시스템에 설치된 gradle 8.7을 직접 사용합니다.
### macOS (Homebrew + SDKMAN 권장)
```bash
# SDKMAN 설치 (이미 있으면 생략)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
# Gradle 8.7 설치
sdk install gradle 8.7
sdk default gradle 8.7
```
### Windows
1. [Gradle 8.7 Binary-only 다운로드](https://gradle.org/releases/)
2. `C:\gradle\gradle-8.7` 경로에 압축 해제
3. `GRADLE_HOME=C:\gradle\gradle-8.7` 환경변수 등록
4. `PATH``%GRADLE_HOME%\bin` 추가
### 검증
```bash
gradle -v
# Gradle 8.7
# JVM: 1.8.x
```
---
## 3. (권장) direnv 사용
저장소 루트의 `.envrc``JAVA_HOME`, `GRADLE_USER_HOME` 등을 자동 주입합니다.
디렉터리 진입만으로 환경이 격리되므로 다른 프로젝트와 JDK가 섞이지 않습니다.
### 설치
```bash
# macOS
brew install direnv
# Windows (scoop)
scoop install direnv
```
### 셸 훅 등록 (한 번만)
```bash
# zsh
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
# bash
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
```
### 프로젝트에서 허용
```bash
cd /path/to/eapim-portal
direnv allow
```
---
## 4. Git 설치
### macOS
```bash
brew install git
```
### Windows
[Git for Windows](https://git-scm.com/download/win) 설치. Git Bash가 함께 설치되어 README의 Bash 스니펫을 그대로 사용할 수 있습니다.
### SSH 키 등록 (사내 Git 서버용)
```bash
ssh-keygen -t ed25519 -C "your.email@djbank.co.kr"
cat ~/.ssh/id_ed25519.pub
# 출력된 공개키를 사내 Git 서버(192.168.240.178:18081)에 등록
```
### 검증
```bash
git --version
ssh -T git@192.168.240.178 -p 18081
```
---
## 5. Oracle 19c 접근 정보
개발 환경에서는 별도 Oracle 설치 없이 사내 개발 DB에 접속합니다.
- **Host**: `192.168.240.177`
- **Port**: `1599`
- **Schemas**: `EMSADM` (Portal), `AGWADM` (Gateway)
- 상세 계정/패스워드는 사내 비밀번호 관리자에게 문의
스테이징/운영은 JNDI를 통해 접근합니다 — `jdbc/dsOBP_EMS`, `jdbc/dsOBP_AGW`.
DB 클라이언트 도구는 자유 선택 (DBeaver, DataGrip, SQL Developer 등).
---
## 6. Node.js & SASS 빌드 환경
SCSS 파일을 CSS로 컴파일하기 위해 Node.js 환경이 필요합니다.
`src/main/resources/static/css/main.css` 는 SASS 컴파일 결과물이므로 **반드시 SCSS 파일을 수정한 뒤 컴파일**해야 합니다.
### 6.1 Node.js 설치 (LTS 18 이상)
#### macOS (Homebrew)
```bash
brew install node
```
#### macOS / Linux (nvm 권장 — 버전 관리 용이)
```bash
# nvm 설치
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
exec $SHELL -l
# Node.js LTS 설치
nvm install --lts
nvm use --lts
```
#### Windows
1. [Node.js LTS 다운로드](https://nodejs.org/en/download) 후 MSI 설치
2. 또는 [nvm-windows](https://github.com/coreybutler/nvm-windows/releases) 사용
#### 검증
```bash
node -v # v18.x 이상
npm -v # 9.x 이상
```
### 6.2 프로젝트 SASS 의존성 설치
```bash
cd /path/to/eapim-portal
npm install
```
`package.json``devDependencies.sass` (Dart Sass) 가 `node_modules/` 에 설치됩니다.
### 6.3 SASS 빌드 명령어
```bash
# 1회 컴파일 (개발용, 들여쓰기 유지)
npm run sass:build
# 1회 컴파일 (배포용, minified)
npm run sass:build:minified
# 파일 변경 감지 후 자동 컴파일 (개발 중 권장)
npm run sass:watch
# 빌드 + minified 동시 생성
npm run build
```
### 6.4 SASS 컴파일 검증
```bash
ls -lh src/main/resources/static/css/main.css
ls -lh src/main/resources/static/css/main.min.css
```
파일이 갱신되었고 브라우저에서 스타일이 정상 적용되면 성공입니다.
### 6.5 SASS 작업 시 주의사항
- `src/main/resources/static/css/main.css` **직접 수정 금지** — 재컴파일 시 덮어씌워짐
- SCSS 파일은 `src/main/resources/static/sass/` 하위에서만 작성
- 새 파일 추가 시 파일명 앞에 언더스코어(`_`) 필수 (`_filename.scss`)
- `@import` 대신 `@use` 사용 (Dart Sass 3.0 호환)
---
## 7. (선택) IDE 설정
### IntelliJ IDEA
1. **Lombok 플러그인** 설치 → `Settings → Plugins → Lombok`
2. **Annotation Processing 활성화**`Settings → Build → Compiler → Annotation Processors → Enable annotation processing`
3. **Gradle JVM** 설정 → `Settings → Build → Build Tools → Gradle → Gradle JVM` 을 JDK 1.8 (Zulu) 로 지정
4. **MapStruct** 는 별도 플러그인 불필요 (annotation processor 만 활성화되어 있으면 됨)
### VS Code
- `Extension Pack for Java`
- `Lombok Annotations Support`
- `Sass` (Syler) — SCSS 문법 하이라이팅
---
## 8. 최종 체크리스트
아래 명령이 모두 정상 동작하면 개발환경 준비가 완료된 것입니다.
```bash
java -version # 1.8.x
gradle -v # 8.7 / JVM 1.8
git --version
node -v # v18 이상
npm -v
direnv version # (선택)
```
이후 [README.md](../README.md) 의 "초기 세팅 가이드" 로 이동해 프로젝트 클론과 의존 모듈 설치를 진행하세요.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>DJBank API Portal 이메일 인증</title>
<!--[if mso]>
<style type="text/css">
table, td { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; }
</style>
<![endif]-->
</head>
<body style="margin:0;padding:0;width:100%;background-color:#eef0f3;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<!-- 미리보기 텍스트 (받은편지함 목록에 노출, 본문에서는 숨김) -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#eef0f3;">DJBank 개발자포탈 이메일 인증번호 안내입니다.</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#eef0f3;">
<tr>
<td align="center" style="padding:30px 12px;">
<!-- ============ 카드 (흰 배경 · 테두리 #dfdfdf · 라운드 20) ============ -->
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;background-color:#ffffff;border:1px solid #dfdfdf;border-radius:20px;">
<!-- ===== Header : DJ Bank 로고 + API Portal ===== -->
<tr>
<td style="padding:26px 27px 0 27px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td width="20" style="width:20px;font-size:0;line-height:0;">&nbsp;</td>
<td valign="middle" style="line-height:0;">
<img src="images/logo-djbank.png" width="144" height="40" alt="DJ Bank" style="display:block;width:144px;height:40px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
</td>
<td width="16" style="width:16px;font-size:0;line-height:0;">&nbsp;</td>
<td valign="middle" style="line-height:0;">
<img src="images/logo-apiportal.png" width="124" height="21" alt="API Portal" style="display:block;width:124px;height:21px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
</td>
</tr>
</table>
</td>
</tr>
<!-- ===== 본문 : 상단 파란선(#2A69DE) + 그림자 ===== -->
<tr>
<td style="padding:7px 27px 0 27px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-top:3px solid #2a69de;box-shadow:0px 4px 12px 0px rgba(85,95,108,0.25);">
<tr>
<td style="padding:26px 29px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<!-- 제목 (subject) : ##변수## 치환 토큰 -->
<tr>
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:15px;line-height:25px;color:#000000;font-weight:600;padding-bottom:20px;">##subject:이메일인증##</td>
</tr>
<!-- 인증번호 박스 (#ECF1FF · 라운드 10) -->
<tr>
<td style="padding-bottom:20px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#ecf1ff;border-radius:10px;">
<tr>
<td align="center" bgcolor="#ecf1ff" style="background-color:#ecf1ff;border-radius:10px;padding:16px 20px;font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:14px;line-height:24px;color:#000000;font-weight:700;text-align:center;letter-spacing:2px;"><!-- 동적값: 인증번호 -->123456</td>
</tr>
</table>
</td>
</tr>
<!-- 안내 문구 -->
<tr>
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:12px;line-height:25px;color:#000000;padding-bottom:20px;">
안녕하세요, <!-- 동적값: 회원명 -->김철수 님.<br>
DJBank 개발자포탈 회원가입 시 입력하신 이메일을 인증해 주세요.<br>
아래 인증번호를 회원가입 화면에 입력하시면 인증이 완료됩니다.
</td>
</tr>
<!-- 경고 문구 (#FF2727) -->
<tr>
<td style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;font-size:12px;line-height:25px;color:#ff2727;font-weight:600;">※ 본 메일을 요청하지 않으셨다면 이 메일을 무시해 주시기 바랍니다.</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- ===== Footer : 제주은행 로고 + 안내 (#F1F1ED) ===== -->
<tr>
<td style="padding:7px 27px 26px 27px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f1f1ed" style="background-color:#f1f1ed;border-radius:4px;">
<tr>
<td style="padding:12px 14px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td valign="middle" width="86" style="width:86px;padding-right:16px;line-height:0;">
<img src="images/logo-jejubank.png" width="86" height="24" alt="제주은행" style="display:block;width:86px;height:24px;border:0;outline:none;-ms-interpolation-mode:bicubic;">
</td>
<td valign="middle" style="font-family:'Apple SD Gothic Neo','Malgun Gothic','맑은 고딕','Noto Sans KR',sans-serif;color:#0d0e11;">
<p style="margin:0;font-size:10px;line-height:20px;color:#0d0e11;">본 메일은 발신 전용으로 회신되지 않습니다.</p>
<p style="margin:0 0 9px 0;font-size:10px;line-height:20px;color:#0d0e11;">관련 문의사항은 고객센터 (????-????) 또는 홈페이지를 이용하시기 바랍니다.</p>
<p style="margin:0 0 9px 0;font-size:10px;line-height:16px;color:#0d0e11;">제주 제주시 1100로 3351 (노형동)<span style="color:#c2c2bc;">&nbsp;|&nbsp;</span>고객센터 : ????-?????<span style="color:#c2c2bc;">&nbsp;|&nbsp;</span>사업자 등록번호 : 616-81-00615</p>
<p style="margin:0;font-size:10px;line-height:16px;color:#454545;font-weight:500;">Copyright JEJUBANK. ALL Rights Reserved.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- ============ /카드 ============ -->
</td>
</tr>
</table>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

-11
View File
@@ -1,11 +0,0 @@
#!/bin/bash
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
# gradle 명령을 대체합니다.
command gradle $@
+326
View File
@@ -0,0 +1,326 @@
/*
* CycloneDX SBOM(bom.json) -> Excel(xlsx) 변환 태스크.
*
* gradle sbomXlsx # cyclonedxBom 실행 후 변환
* gradle sbomXlsx -PsbomJson=path.json # 기존 bom.json 사용(cyclonedxBom 생략)
* gradle sbomXlsx -PsbomOut=out.xlsx # 출력 경로 지정
*
* buildscript 블록이 이 스크립트에만 적용되므로 POI 의존성이 메인 빌드
* classpath 나 WAR 산출물에는 포함되지 않는다.
*
* 시트: 요약 / WAR 기준
* 산출 기준은 war 태스크의 classpath(= runtimeClasspath) 이므로
* test·annotationProcessor·developmentOnly·compileOnly 의존은 모두 제외된다.
* bom.json 은 라이선스/해시/설명/직접-전이 판별을 위한 메타 소스로만 쓴다.
*/
buildscript {
repositories {
maven {
url "https://nexus.eactive.synology.me:8090/repository/maven-public/"
allowInsecureProtocol = true
}
mavenCentral()
}
dependencies {
classpath 'org.apache.poi:poi-ooxml:3.17'
}
}
import groovy.json.JsonSlurper
import org.apache.poi.ss.usermodel.BorderStyle
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.HorizontalAlignment
import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.xssf.usermodel.XSSFWorkbook
// 엑셀 셀 문자열 상한(32767)보다 여유를 둔 절단 길이
ext.SBOM_CELL_LIMIT = 32000
task sbomXlsx {
group = 'sbom'
description = 'CycloneDX bom.json 을 WAR 수록 기준 xlsx 로 변환한다'
// -PsbomJson 으로 기존 산출물을 지정하면 재생성하지 않는다
if (!project.hasProperty('sbomJson')) {
dependsOn 'cyclonedxBom'
}
doLast {
File src = resolveBomJson(project)
File out = project.hasProperty('sbomOut')
? project.file(project.property('sbomOut'))
: new File(project.buildDir, "reports/sbom/${sbomFileName(project)}")
out.parentFile.mkdirs()
def bom = new JsonSlurper().parse(src, 'UTF-8')
def deploy = collectDeployJars(project)
def warRows = joinWarRows(deploy.jars, indexComponents(bom))
def wb = new XSSFWorkbook()
def st = createStyles(wb)
writeSummarySheet(wb, st, bom, warRows, src, deploy.label)
writeWarSheet(wb, st, warRows)
out.withOutputStream { os -> wb.write(os) }
wb.close()
int unmatched = warRows.count { it.matched == 'N' }
logger.lifecycle("SBOM xlsx 생성: ${out.absolutePath} " +
"(배포 수록 ${warRows.size()}개, SBOM 미매칭 ${unmatched}개, 원본 ${src.name})")
}
}
/** 산출 파일명: 배포 패키지명 기준 (war 있으면 war 파일명, 없으면 project 이름[-버전]) */
String sbomFileName(Project p) {
def warTask = p.tasks.findByName('war')
if (warTask != null) {
String archive = warTask.archiveFileName.get()
return archive.replaceAll(/\.(war|jar|ear)$/, '') + '-sbom.xlsx'
}
String ver = (p.version == null || p.version.toString() in ['', 'unspecified']) ? '' : "-${p.version}"
return "${p.name}${ver}-sbom.xlsx"
}
/** bom.json 위치 결정: -PsbomJson > cyclonedxBom 산출 경로 후보 */
File resolveBomJson(Project p) {
if (p.hasProperty('sbomJson')) {
File f = p.file(p.property('sbomJson'))
if (!f.exists()) {
throw new GradleException("bom.json 없음: ${f.absolutePath}")
}
return f
}
def candidates = [
new File(p.buildDir, 'reports/cyclonedx/bom.json'),
new File(p.buildDir, 'reports/bom.json'),
]
File found = candidates.find { it.exists() }
if (found == null) {
throw new GradleException(
"bom.json 을 찾지 못했다. 확인한 경로: " + candidates*.absolutePath.join(', ') +
"\n'gradle cyclonedxBom' 실행 후 재시도하거나 -PsbomJson=<경로> 로 지정한다.")
}
return found
}
/**
* 실제 배포물에 packaging 되는 jar 목록.
* war 프로젝트는 war 태스크 classpath(= runtimeClasspath), 그 외는 runtimeClasspath 를
* 기준으로 하므로 test/annotationProcessor/developmentOnly/compileOnly 는 자동으로 빠진다.
*
* @return [label: 기준 설명, jars: 행 목록]
*/
Map collectDeployJars(Project p) {
def cfg = p.configurations.findByName('runtimeClasspath')
if (cfg == null) {
p.logger.warn("[${p.name}] runtimeClasspath 가 없어 배포 기준 시트를 비운다")
return [label: '(없음)', jars: []]
}
def warTask = p.tasks.findByName('war')
def files
String label
if (warTask != null) {
files = warTask.classpath.files
label = 'WAR WEB-INF/lib (war 태스크 classpath)'
} else {
files = cfg.files
label = 'runtimeClasspath (war 태스크 없음)'
}
def coordByFile = [:]
cfg.resolvedConfiguration.resolvedArtifacts.each { a ->
def id = a.moduleVersion.id
coordByFile[a.file] = [group: id.group, name: id.name, version: id.version]
}
def jars = files.findAll { it.name.endsWith('.jar') }.collect { f ->
def c = coordByFile[f]
[
file : f.name,
group : c?.group ?: '',
name : c?.name ?: f.name.replaceAll(/\.jar$/, ''),
version: c?.version ?: '',
coord : c ? "${c.group}:${c.name}:${c.version}".toString() : '',
]
}.sort { it.file }
return [label: label, jars: jars]
}
/** bom.json 컴포넌트를 'group:name:version' 키로 색인 (라이선스/해시/설명/직접-전이) */
Map indexComponents(bom) {
String rootRef = bom.metadata?.component?.'bom-ref'
Set directRefs = (bom.dependencies?.find { it.ref == rootRef }?.dependsOn ?: []) as Set
def index = [:]
bom.components?.each { c ->
def hashes = [:]
c.hashes?.each { h -> hashes[h.alg] = h.content }
def licenses = (c.licenses ?: []).collect { l ->
l.license?.id ?: l.license?.name ?: l.expression ?: ''
}.findAll { it }
index["${c.group ?: ''}:${c.name ?: ''}:${c.version ?: ''}".toString()] = [
direct : directRefs.contains(c.'bom-ref') ? '직접' : '전이',
licenses : licenses.join('; '),
licenseList: licenses.isEmpty() ? ['(미상)'] : licenses,
purl : c.purl ?: '',
sha256 : hashes['SHA-256'] ?: '',
sha1 : hashes['SHA-1'] ?: '',
description: c.description ?: '',
]
}
return index
}
/** WAR jar 목록에 SBOM 메타를 좌표로 결합 */
List joinWarRows(List warJars, Map index) {
def result = []
warJars.eachWithIndex { j, i ->
def m = j.coord ? index[j.coord] : null
result << [
no : i + 1,
file : j.file,
group : j.group,
name : j.name,
version : j.version,
direct : m?.direct ?: '',
licenses : m?.licenses ?: '',
licenseList: m?.licenseList ?: ['(미상)'],
purl : m?.purl ?: '',
sha256 : m?.sha256 ?: '',
sha1 : m?.sha1 ?: '',
matched : (m != null) ? 'Y' : 'N',
description: m?.description ?: '',
]
}
return result
}
Map createStyles(wb) {
def headFont = wb.createFont()
headFont.setBold(true)
headFont.setColor(IndexedColors.WHITE.getIndex())
def head = wb.createCellStyle()
head.setFont(headFont)
head.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex())
head.setFillPattern(FillPatternType.SOLID_FOREGROUND)
head.setAlignment(HorizontalAlignment.CENTER)
head.setVerticalAlignment(VerticalAlignment.CENTER)
head.setBorderBottom(BorderStyle.THIN)
def body = wb.createCellStyle()
body.setVerticalAlignment(VerticalAlignment.TOP)
def wrap = wb.createCellStyle()
wrap.setVerticalAlignment(VerticalAlignment.TOP)
wrap.setWrapText(true)
def labelFont = wb.createFont()
labelFont.setBold(true)
def label = wb.createCellStyle()
label.setFont(labelFont)
return [head: head, body: body, wrap: wrap, label: label]
}
/** 헤더 행 생성 + 폭 지정 + 틀고정 */
def writeHeader(sheet, style, List<String> headers, List<Integer> widths) {
def row = sheet.createRow(0)
row.setHeightInPoints(20f)
headers.eachWithIndex { h, i ->
def cell = row.createCell(i)
cell.setCellValue(h)
cell.setCellStyle(style)
sheet.setColumnWidth(i, widths[i] * 256)
}
sheet.createFreezePane(0, 1)
}
def cellOf(row, int idx, value, style) {
def cell = row.createCell(idx)
String s = (value == null) ? '' : value.toString()
if (s.length() > SBOM_CELL_LIMIT) {
s = s.substring(0, SBOM_CELL_LIMIT) + '…(생략)'
}
cell.setCellValue(s)
cell.setCellStyle(style)
return cell
}
def writeSummarySheet(wb, st, bom, List warRows, File src, String basisLabel) {
def sheet = wb.createSheet('요약')
def comp = bom.metadata?.component ?: [:]
def tool = bom.metadata?.tools?.components?.getAt(0)
Set licenseKinds = warRows.collectMany { it.licenseList } as Set
def items = [
['대상 프로젝트', "${comp.group ?: ''}:${comp.name ?: ''}:${comp.version ?: ''}"],
['산출 기준', "${basisLabel} — test/annotationProcessor/compileOnly 제외"],
['BOM 포맷', "${bom.bomFormat ?: ''} ${bom.specVersion ?: ''}"],
['serialNumber', bom.serialNumber ?: ''],
['생성 시각', bom.metadata?.timestamp ?: ''],
['생성 도구', tool ? "${tool.name} ${tool.version}" : ''],
['원본 파일', src.absolutePath],
['배포 수록 jar', warRows.size()],
[' └ 직접 의존', warRows.count { it.direct == '직접' }],
[' └ 전이 의존', warRows.count { it.direct == '전이' }],
[' └ SBOM 미매칭', warRows.count { it.matched == 'N' }],
['라이선스 종류', licenseKinds.size()],
['라이선스 미상', warRows.count { it.licenses.isEmpty() }],
]
writeHeader(sheet, st.head, ['항목', '값'], [30, 90])
items.eachWithIndex { item, i ->
def row = sheet.createRow(i + 1)
cellOf(row, 0, item[0], st.label)
cellOf(row, 1, item[1], st.body)
}
// 라이선스 분포 (요약 하단)
def byLicense = [:].withDefault { 0 }
warRows.each { r -> r.licenseList.each { lic -> byLicense[lic] = byLicense[lic] + 1 } }
def sorted = byLicense.entrySet().sort { a, b -> (b.value <=> a.value) ?: (a.key <=> b.key) }
int base = items.size() + 2
def hdr = sheet.createRow(base)
cellOf(hdr, 0, '라이선스', st.head)
cellOf(hdr, 1, 'jar 수', st.head)
sorted.eachWithIndex { e, i ->
def row = sheet.createRow(base + 1 + i)
cellOf(row, 0, e.key, st.body)
cellOf(row, 1, e.value, st.body)
}
}
/** 실제 배포물(WAR WEB-INF/lib) 기준 시트 */
def writeWarSheet(wb, st, List warRows) {
def sheet = wb.createSheet('WAR 기준')
def headers = ['No', 'jar 파일명', 'Group', 'Name', 'Version', '구분',
'License', 'purl', 'SHA-256', 'SHA-1', 'SBOM매칭', 'Description']
def widths = [6, 46, 32, 34, 16, 7, 30, 60, 40, 30, 10, 60]
writeHeader(sheet, st.head, headers, widths)
warRows.eachWithIndex { r, i ->
def row = sheet.createRow(i + 1)
cellOf(row, 0, r.no, st.body)
cellOf(row, 1, r.file, st.body)
cellOf(row, 2, r.group, st.body)
cellOf(row, 3, r.name, st.body)
cellOf(row, 4, r.version, st.body)
cellOf(row, 5, r.direct, st.body)
cellOf(row, 6, r.licenses, st.body)
cellOf(row, 7, r.purl, st.body)
cellOf(row, 8, r.sha256, st.body)
cellOf(row, 9, r.sha1, st.body)
cellOf(row, 10, r.matched, st.body)
cellOf(row, 11, r.description, st.wrap)
}
if (!warRows.isEmpty()) {
sheet.setAutoFilter(new CellRangeAddress(0, warRows.size(), 0, headers.size() - 1))
}
}
Vendored Regular → Executable
View File
-91
View File
@@ -1,91 +0,0 @@
pipeline {
// 25.10.01 : 기본 브랜치 staging -> jenkins_with_weblogic 로 변경
agent { label 'apipod01' }
triggers {
pollSCM('H/5 * * * *')
}
environment {
// Java 환경 설정
JAVA_HOME = '/App/jenkins/jdks/jdk1.8.0_441'
PATH = "${JAVA_HOME}/bin:${env.PATH}"
// Gradle 환경 설정
GRADLE_HOME = '/App/jenkins/gradle-8.7'
GRADLE_USER_HOME = '/App/jenkins/gradle-home'
// GRADLE_OPTS = '--offline'
// Git 저장소 정보
GIT_SERVER = '192.168.240.178:18081'
GIT_CREDENTIALS_ID = 'git-ssh-credentials'
// 프로젝트 디렉토리 구조
PROJECT_HOME = '/Data/jenkins/eapim'
BRANCH = 'jenkins_with_weblogic'
}
stages {
stage('Git pull') {
steps {
echo "[portal] Git pull from branch: ${BRANCH}"
dir("${PROJECT_HOME}/eapim-portal") {
git url: "ssh://git@${GIT_SERVER}/eapim/eapim-portal", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
}
echo "[online-core-jpa] Git pull from branch: ${BRANCH}"
dir("${PROJECT_HOME}/eapim-online/elink-online-core-jpa") {
git url: "ssh://git@${GIT_SERVER}/eapim/elink-online-core-jpa", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
}
echo "[portal-common] Git pull from branch: ${BRANCH}"
dir("${PROJECT_HOME}/elink-portal-common") {
git url: "ssh://git@${GIT_SERVER}/eapim/elink-portal-common", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
}
echo "[kjb-safedb] Git pull from branch: ${BRANCH}"
dir("${PROJECT_HOME}/kjb-safedb") {
git url: "ssh://git@${GIT_SERVER}/eapim/kjb-safedb", branch: "${BRANCH}", credentialsId: "${GIT_CREDENTIALS_ID}"
}
}
}
stage('Build') {
steps {
tool name: 'Oracle-JDK-1.8', type: 'hudson.model.JDK'
// tool name: 'Gradle-8.7', type: 'hudson.tasks.Gradle'
// echo "[online-core-jpa] Build project"
// dir("${PROJECT_HOME}/elink-online-multi/elink-online-core-jpa") {
// sh '${GRADLE_HOME}/bin/gradle clean build -x test'
// }
// echo "[portal-common] Build project"
// dir("${PROJECT_HOME}/elink-portal-common") {
// sh '${GRADLE_HOME}/bin/gradle clean build -x test'
// }
echo "[portal] Build project"
dir("${PROJECT_HOME}/eapim-portal") {
sh '${GRADLE_HOME}/bin/gradle clean build -x test'
}
}
}
stage('Archive WAR') {
steps {
echo "[portal] Archive WAR file"
dir("${PROJECT_HOME}/eapim-portal/build/libs") {
archiveArtifacts artifacts: '*.war', fingerprint: true
}
}
}
}
}
-25
View File
@@ -1,25 +0,0 @@
# 광주은행 eAPIM
## 커스텀 항목
### 포탈 Property (EMS 프로퍼티)
- `disable_features.user_email_verify' == true
이메일 인증 기능 비활성화(회원 가입시 이메일 인증은 한걸로 처리)
### 디비 접속 방법 Direct -> JNDI 로 변경
- 은행 내부 환경에선 모두 JNDI 사용하기 때문에 서버환경에서 동작하는 profile 내 application.yaml 에서 디비 접속 정보 삭제
## 서버 운영 중 긴급 로깅이 필요할 경우 JVM 파라미터 추가 하여 재기동
- DEBUG 레벨 root 로그 / debug.log 로 파일 작성
```
-Dlogging.config=classpath:logback-debug.xml
```
## 참고자료
### 개발용 파라미터
```
-Dlogging.config=classpath:logback-rinjae.xml
-Dinst.Name=devSvr98
-Duser.language=en
-Duser.country=US
-Dkjb_safedb.mode=fake
```
-113
View File
@@ -1,113 +0,0 @@
# 사업자등록번호 중복 체크 구현 작업 계획서
## 1. 현황 분석
### 문제점
- 법인 등록 시 사업자등록번호(compRegNo) 중복 검증이 없음
- `PortalOrgRepository.findByCompRegNo()` 메서드는 존재하나 사용되지 않음
- 동일한 사업자등록번호로 여러 법인이 등록될 수 있는 상태
### 검증 시점
1. **실시간 검증**: 사용자가 사업자등록번호 입력 시 `/check-business-number` API 호출
2. **등록 시 검증**: 실제 법인 생성 시점 (`registerOrgFromDTOWithFile`)
---
## 2. 수정 대상 파일
| 파일 경로 | 수정 내용 |
|-----------|-----------|
| `src/main/java/.../apps/user/service/PortalOrgService.java` | 사업자등록번호 중복 체크 메서드 추가 |
| `src/main/java/.../apps/user/facade/OrgRegisterFacadeImpl.java` | `checkBusinessNumber()` 중복 체크 로직 추가, 등록 전 중복 체크 추가 |
---
## 3. 구현 상세
### 3.1 PortalOrgService.java
```java
// 추가할 메서드
public boolean existsByCompRegNo(String compRegNo) {
return portalOrgRepository.findByCompRegNo(compRegNo).isPresent();
}
```
### 3.2 OrgRegisterFacadeImpl.java
#### checkBusinessNumber 메서드 수정
- 형식 검증 + 중복 검증 동시 수행
- 중복 시 "이미 등록된 사업자등록번호입니다." 메시지 반환
```java
@Override
public ResponseEntity<ValidationResponse> checkBusinessNumber(String compRegNo) {
// 1. 형식 검증
boolean isValidFormat = validationService.isValidBusinessNumber(compRegNo);
if (!isValidFormat) {
return ResponseEntity.ok(new ValidationResponse(false, "유효하지 않은 사업자 등록번호입니다."));
}
// 2. 중복 검증
boolean isDuplicate = portalOrgService.existsByCompRegNo(compRegNo);
if (isDuplicate) {
return ResponseEntity.ok(new ValidationResponse(false, "이미 등록된 사업자등록번호입니다."));
}
return ResponseEntity.ok(new ValidationResponse(true, "사용 가능한 사업자 등록번호입니다."));
}
```
#### 법인 생성 메서드들에 중복 체크 추가
- `registerNewCorporateUser()`
- `convertExistingUserToCorporate()`
- `registerNewCorporateUserWithExistingData()`
각 메서드에서 `portalOrgService.registerOrgFromDTOWithFile()` 호출 전 중복 체크 수행
---
## 4. 검증 흐름
```
[사용자 입력]
[실시간 검증] /check-business-number API
├── 형식 검증 (10자리 숫자)
└── 중복 검증 (DB 조회)
[등록 요청]
[등록 시 검증] registerOrgFromDTOWithFile 호출 전
└── 중복 체크 (이중 방어)
[법인 생성]
```
---
## 5. 기존 중복 데이터 처리
- 기존에 중복 등록된 데이터는 그대로 유지
- 신규 등록 시에만 중복 체크 적용
- 필요시 관리자 화면에서 중복 데이터 조회/정리 기능 별도 개발 가능
---
## 6. 테스트 시나리오
| 시나리오 | 예상 결과 |
|----------|-----------|
| 형식이 잘못된 사업자등록번호 입력 | "유효하지 않은 사업자 등록번호입니다." |
| 이미 등록된 사업자등록번호 입력 | "이미 등록된 사업자등록번호입니다." |
| 사용 가능한 사업자등록번호 입력 | "사용 가능한 사업자 등록번호입니다." |
| 중복된 번호로 법인 등록 시도 | 등록 실패, 에러 메시지 표시 |
---
## 7. 작업 완료
- [x] PortalOrgService에 existsByCompRegNo 메서드 추가
- [x] OrgRegisterFacadeImpl.checkBusinessNumber 수정
- [x] 법인 등록 메서드들에 중복 체크 로직 추가
- [x] 빌드 검증 완료
-165
View File
@@ -1,165 +0,0 @@
# 네비게이션 바 디자인 Survey 기능 가이드
## 개요
직원 대상 네비게이션 바 디자인 선호도 조사를 위한 UI 기능입니다.
사용자가 A, B, C 등의 버튼을 클릭하면 실시간으로 네비게이션 바 디자인이 변경됩니다.
## 기능 활성화/비활성화
### PortalProperty 설정
DB의 `ptl_property` 테이블에서 `ui.design-survey` 값을 설정합니다.
```sql
-- 활성화
UPDATE ptl_property
SET property_value = 'true'
WHERE property_group_name = 'Portal' AND property_name = 'ui.design-survey';
-- 비활성화
UPDATE ptl_property
SET property_value = 'false'
WHERE property_group_name = 'Portal' AND property_name = 'ui.design-survey';
```
> 최초 페이지 로드 시 속성이 자동 생성됩니다 (기본값: `false`)
---
## 디자인 옵션 추가/수정 방법
### 파일 위치
`src/main/resources/templates/views/fragment/kjbank/header_container.html`
### DESIGN_OPTIONS 배열
디자인 옵션은 JavaScript의 `DESIGN_OPTIONS` 배열에서 관리됩니다:
```javascript
const DESIGN_OPTIONS = [
{
id: 'A', // 고유 식별자 (필수)
label: 'A (현재)', // 버튼에 표시될 텍스트 (필수)
isDefault: true, // 기본 선택 옵션 (선택, 하나만 true)
styles: {} // 적용할 CSS 스타일 (필수)
},
{
id: 'B',
label: 'B',
styles: {
'.logo-text': { 'font-size': '18px' },
'.nav-link': { 'margin': '0 20px' }
}
},
{
id: 'C',
label: 'C',
styles: {
'.nav-link': { 'margin': '0 20px', 'font-weight': 'bold' }
}
}
];
```
### 새 디자인 옵션 추가 예시
```javascript
// D 옵션 추가
{
id: 'D',
label: 'D',
styles: {
'.logo-text': {
'font-size': '16px',
'color': '#333333'
},
'.nav-link': {
'padding': '10px 24px',
'font-weight': '600'
}
}
}
```
### styles 객체 구조
```javascript
styles: {
'CSS 셀렉터': {
'CSS 속성': '값',
'CSS 속성2': '값2'
},
'다른 셀렉터': {
'CSS 속성': '값'
}
}
```
---
## 자주 사용되는 CSS 셀렉터
| 셀렉터 | 설명 |
|--------|------|
| `.logo-text` | 로고 텍스트 "API Portal" |
| `.nav-link` | 네비게이션 메뉴 링크 |
| `.nav-menu` | 네비게이션 메뉴 컨테이너 |
| `.header-content` | 헤더 콘텐츠 영역 |
| `.global-header` | 전체 헤더 |
| `.logo-wrapper` | 로고 래퍼 (이미지 + 텍스트) |
## 자주 사용되는 CSS 속성
| 속성 | 예시 값 | 설명 |
|------|---------|------|
| `font-size` | `18px`, `1.2rem` | 글자 크기 |
| `font-weight` | `400`, `500`, `600`, `bold` | 글자 두께 |
| `margin` | `0 20px`, `10px 15px` | 바깥 여백 |
| `padding` | `8px 16px` | 안쪽 여백 |
| `color` | `#333333`, `#0049b4` | 글자 색상 |
| `background` | `#ffffff`, `#f5f5f5` | 배경 색상 |
| `gap` | `12px`, `20px` | flex 아이템 간격 |
---
## 사용자 선택 저장
사용자의 디자인 선택은 브라우저의 `localStorage`에 저장됩니다.
- Key: `design-survey-selection`
- Value: 선택한 디자인 ID (예: `A`, `B`, `C`)
```javascript
// 저장된 선택 확인 (개발자 도구 콘솔)
localStorage.getItem('design-survey-selection')
// 선택 초기화
localStorage.removeItem('design-survey-selection')
```
---
## 관련 파일
| 파일 | 역할 |
|------|------|
| `GlobalControllerAdvice.java` | `designSurveyEnabled` ModelAttribute 제공 |
| `header_container.html` | Survey 바 HTML 및 DESIGN_OPTIONS 설정 |
| `_header.scss` | Survey 바 스타일 |
---
## 주의사항
1. **디자인 옵션 ID는 고유해야 합니다** - 중복 ID 사용 시 오동작
2. **isDefault는 하나의 옵션에만 설정** - 여러 개 설정 시 첫 번째만 적용
3. **CSS 속성명은 kebab-case 사용** - `fontSize` (X) → `font-size` (O)
4. **스타일 적용 시 `!important` 자동 추가** - 기존 스타일 덮어씀
5. **Survey 기능 종료 후** - `ui.design-survey``false`로 설정하여 비활성화
---
## 문의
기능 관련 문의는 개발팀에 연락해주세요.
File diff suppressed because it is too large Load Diff
@@ -1,105 +0,0 @@
flowchart TD
Start([법인 관리자가 사용자 초대]) --> GenerateToken[8글자 토큰 생성<br/>예: K7MNPQ2R<br/>UserInvitation 저장<br/>상태: PENDING]
GenerateToken --> SendEmail[이메일 발송<br/>- 링크 포함<br/>- 8글자 토큰 표시]
SendEmail --> UserType{사용자 유형}
%% ========================================
%% 신규 회원 플로우
%% ========================================
UserType -->|신규 회원<br/>미가입자| NewUserPath{초대 확인 방법}
NewUserPath -->|이메일 링크| EmailLink1[이메일 링크 클릭<br/>/signup/portalUser?invitation=CODE]
NewUserPath -->|일반 회원가입| DirectSignup[일반 회원가입 진행<br/>ROLE_USER로 생성]
EmailLink1 --> SignupPage1[회원가입 페이지<br/>- 이메일 자동 입력됨<br/>- 법인명 표시]
SignupPage1 --> RegisterForm[가입 정보 입력<br/>초대 수락]
RegisterForm --> RegisterComplete[회원가입 완료<br/>ROLE_CORP_USER로 생성<br/>상태: COMPLETED]
RegisterComplete --> NewUserEnd([가입 완료 안내])
DirectSignup --> Login4[로그인]
Login4 --> AfterLogin4[로그인 성공]
AfterLogin4 --> CheckInvitation
%% ========================================
%% 기존 회원 플로우
%% ========================================
UserType -->|기존 회원<br/>ROLE_USER| ExistingUserPath{초대 확인 방법}
ExistingUserPath -->|이메일 링크<br/>비로그인 상태| EmailLink2[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
ExistingUserPath -->|이메일 링크<br/>로그인 상태| EmailLinkLoggedIn[이메일 링크 클릭<br/>/signup/decision?invitation=CODE]
ExistingUserPath -->|직접 로그인| DirectLogin[로그인 페이지에서 로그인]
%% 비로그인 상태에서 이메일 링크 클릭
EmailLink2 --> DecisionPagePublic[초대 수락 페이지<br/>비로그인 상태]
DecisionPagePublic --> RedirectLogin2[로그인 유도]
RedirectLogin2 --> Login2[로그인]
Login2 --> AfterLogin2[로그인 성공]
%% 로그인 상태에서 이메일 링크 클릭 → 바로 수락 페이지
EmailLinkLoggedIn --> DecisionPageDirect[초대 수락 페이지<br/>/signup/decision_process<br/>초대 수락]
DecisionPageDirect --> UserDecision
%% 직접 로그인
DirectLogin --> Login3[로그인]
Login3 --> AfterLogin3[로그인 성공]
%% ========================================
%% 로그인 후 초대 확인 (PortalAuthenticationSuccessHandler)
%% ========================================
AfterLogin2 --> CheckInvitation[PortalAuthenticationSuccessHandler<br/>PENDING 초대 확인<br/>findFirstByInvitationEmailAndStatus]
AfterLogin3 --> CheckInvitation
CheckInvitation --> HasInvitation{PENDING 초대 존재?}
HasInvitation -->|없음| NormalHome[메인 페이지 이동]
HasInvitation -->|있음| SaveToSession[세션에 초대 정보 저장<br/>pendingInvitation=true<br/>pendingInvitationToken<br/>pendingInvitationOrgName]
SaveToSession --> RedirectHome[메인 페이지로 이동]
%% ========================================
%% 메인 페이지 팝업 (index.html)
%% ========================================
RedirectHome --> ShowPopup[팝업 표시<br/>OOO에서 초대가 도착했습니다<br/>초대를 확인하시겠습니까?]
ShowPopup --> PopupChoice{사용자 선택}
PopupChoice -->|확인| GoDecisionPage[초대 수락 페이지 이동<br/>/signup/decision?invitation=TOKEN]
PopupChoice -->|취소| StayHome[메인 페이지 유지<br/>다음 로그인 시 팝업 재표시]
StayHome --> NextLogin[다음 로그인]
NextLogin --> CheckInvitation
GoDecisionPage --> DecisionPage[초대 수락 페이지<br/>/signup/decision_process<br/>초대 수락]
DecisionPage --> UserDecision{사용자 선택}
UserDecision -->|수락| AcceptProcess[초대 수락 처리<br/>1. PortalOrg 변경<br/>2. ROLE_CORP_USER 전환<br/>3. 약관 재동의<br/>상태: COMPLETED]
UserDecision -->|거절| RejectProcess[초대 거절 처리<br/>상태: REJECTED]
AcceptProcess --> LogoutPrompt[로그아웃 후 재로그인 안내]
LogoutPrompt --> ExistingUserEnd([초대 수락 완료])
RejectProcess --> RejectEnd([초대 거절 완료<br/>다음 로그인 시 팝업 안 뜸])
%% ========================================
%% 스타일링
%% ========================================
style Start fill:#e1f5e1
style NewUserEnd fill:#c8e6c9
style ExistingUserEnd fill:#c8e6c9
style RejectEnd fill:#fff9c4
style NormalHome fill:#e3f2fd
style GenerateToken fill:#bbdefb
style AcceptProcess fill:#c5cae9
style RejectProcess fill:#ffecb3
style CheckInvitation fill:#ffe0b2
style SaveToSession fill:#ffe0b2
style ShowPopup fill:#f8bbd0
style PopupChoice fill:#f8bbd0
style StayHome fill:#e1bee7
style NextLogin fill:#e1bee7
-44
View File
@@ -1,44 +0,0 @@
flowchart TD
Start([법인 관리자가 사용자 초대 시작]) --> CheckUser{가입된 사용자인가?}
CheckUser -->|아니오| Error1[에러: 가입된 사용자만 초대 가능]
CheckUser -->|예| CheckRole{이미 법인 사용자인가?}
CheckRole -->|예| Error2[에러: 이미 기관에 등록된 사용자]
CheckRole -->|아니오 ROLE_USER| CreateInvitation[8글자 토큰 생성<br/>UserInvitation 저장<br/>상태: PENDING]
CreateInvitation --> SendEmail[이메일 발송<br/>UserInvitationEvent]
SendEmail --> UserReceive{사용자가 토큰 받음}
UserReceive -->|폐쇄망| ManualInput[8글자 토큰 직접 입력<br/>예: K7MNPQ2R]
UserReceive -->|이메일 링크| EmailLink[Base64 인코딩된<br/>링크 클릭]
ManualInput --> DecodeToken[토큰 디코딩<br/>decodeInvitationToken]
EmailLink --> DecodeToken
DecodeToken --> ValidateToken{토큰 유효성 검증}
ValidateToken -->|무효| Error3[에러: 유효하지 않은 초대]
ValidateToken -->|유효 & PENDING| ShowDecision[약관 동의 페이지<br/>/signup/decision]
ShowDecision --> UserDecision{사용자 선택}
UserDecision -->|수락| ProcessAccept[processInvitation: accept]
UserDecision -->|거절| ProcessReject[processInvitation: reject]
ProcessAccept --> UpdateUser[1. PortalOrg 변경<br/>2. ROLE_CORP_USER로 전환<br/>3. 약관 재동의 PRIVACY_COLLECT_ORG]
UpdateUser --> CompleteInvitation[Invitation 상태:<br/>COMPLETED + completeDate]
CompleteInvitation --> Success([초대 수락 완료<br/>로그아웃 후 재로그인])
ProcessReject --> RejectInvitation[Invitation 상태:<br/>REJECTED + completeDate]
RejectInvitation --> End([초대 거절 완료])
style Start fill:#e1f5e1
style Success fill:#c8e6c9
style End fill:#fff9c4
style Error1 fill:#ffcdd2
style Error2 fill:#ffcdd2
style Error3 fill:#ffcdd2
style CreateInvitation fill:#bbdefb
style UpdateUser fill:#c5cae9
-122
View File
@@ -1,122 +0,0 @@
sequenceDiagram
actor 법인관리자
participant Portal as Portal UI
participant UserManFacade
participant UserInvitationRepo as UserInvitation Repository
participant MessageService as Message Handler
participant Email as Email System
actor 초대받은사용자
participant AuthHandler as PortalAuthenticationSuccessHandler
participant IndexController
participant MainPage as 메인 페이지 (index.html)
participant UserRegisterController
participant UserRegisterFacade
participant PortalUserRepo as PortalUser Repository
participant PortalOrgRepo as PortalOrg Repository
%% ========================================
%% 1. 초대 발송
%% ========================================
법인관리자->>Portal: 사용자 이메일 입력<br/>초대 요청
Portal->>UserManFacade: sendInvitation(sender, emailAddr)
UserManFacade->>PortalUserRepo: findByEmailAddr(emailAddr)
alt 사용자가 없거나 이미 법인 사용자
UserManFacade-->>Portal: 에러 반환
Portal-->>법인관리자: 초대 불가 알림
else 일반 사용자 ROLE_USER 또는 비회원
UserManFacade->>UserManFacade: generateEightCharToken()<br/>예: K7MNPQ2R
UserManFacade->>UserInvitationRepo: save(UserInvitation)<br/>status: PENDING<br/>expiresOn: 7일 후
UserManFacade->>MessageService: publishEvent(UserInvitationEvent)
MessageService->>Email: 초대 이메일 발송<br/>(법인명, 관리자명, 토큰)
UserManFacade-->>Portal: 초대 완료
Portal-->>법인관리자: 초대 성공 알림
end
Email->>초대받은사용자: 초대 이메일 수신
%% ========================================
%% 2-A. 로그인 상태에서 이메일 링크 클릭 (바로 수락 페이지)
%% ========================================
alt 로그인 상태에서 이메일 링크 클릭
초대받은사용자->>Portal: 이메일 링크 클릭<br/>/signup/decision?invitation=TOKEN
Portal->>UserRegisterController: showDecisionPage(invitationToken)
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
UserRegisterController-->>Portal: 약관 동의 페이지
Portal-->>초대받은사용자: 초대 수락/거절 화면<br/>(바로 표시, 팝업 없음)
Note over 초대받은사용자: 6. 초대 수락/거절 처리로 이동
end
%% ========================================
%% 2-B. 로그인 (기존 회원 또는 신규 가입 후)
%% ========================================
alt 기존 회원 (비로그인 상태)
초대받은사용자->>Portal: 로그인
else 비회원이 일반 회원가입 후
초대받은사용자->>Portal: 회원가입 (ROLE_USER)
초대받은사용자->>Portal: 로그인
end
%% ========================================
%% 3. 로그인 성공 처리 (AuthenticationSuccessHandler)
%% ========================================
Portal->>AuthHandler: onAuthenticationSuccess()
AuthHandler->>AuthHandler: 이메일 인증, 휴면계정,<br/>비밀번호 만료 체크
AuthHandler->>UserInvitationRepo: findFirstByInvitationEmailAndStatus<br/>(loginId, PENDING)
alt PENDING 초대 없음
AuthHandler->>Portal: redirect to /
Portal-->>초대받은사용자: 메인 페이지 (팝업 없음)
else PENDING 초대 있음 & 만료 안됨
AuthHandler->>PortalOrgRepo: findById(invitation.orgId)
PortalOrgRepo-->>AuthHandler: PortalOrg (기관명)
AuthHandler->>AuthHandler: session.setAttribute<br/>("pendingInvitation", true)<br/>("pendingInvitationToken", token)<br/>("pendingInvitationOrgName", orgName)
AuthHandler->>Portal: redirect to /
end
%% ========================================
%% 4. 메인 페이지 팝업 표시
%% ========================================
Portal->>IndexController: GET /
IndexController->>MainPage: render index.html
MainPage->>MainPage: 세션에서 pendingInvitation 확인
alt pendingInvitation == true
MainPage-->>초대받은사용자: 팝업 표시<br/>"[기관명]에서 초대가 도착했습니다.<br/>초대를 확인하시겠습니까?"
end
%% ========================================
%% 5. 사용자 팝업 선택
%% ========================================
alt 팝업에서 "확인" 클릭
초대받은사용자->>Portal: /signup/decision?invitation=TOKEN
Portal->>UserRegisterController: showDecisionPage(invitationToken)
UserRegisterController->>UserInvitationRepo: findByToken(decodedToken)
UserRegisterController-->>Portal: 약관 동의 페이지
Portal-->>초대받은사용자: 초대 수락/거절 화면
else 팝업에서 "취소" 클릭
Note over 초대받은사용자,MainPage: 메인 페이지 유지<br/>다음 로그인 시 팝업 재표시
end
%% ========================================
%% 6. 초대 수락/거절 처리
%% ========================================
초대받은사용자->>Portal: 수락 or 거절 선택
Portal->>UserRegisterController: processInvitation(action)
UserRegisterController->>UserRegisterFacade: processInvitation(action, invitation)
alt 수락
UserRegisterFacade->>PortalUserRepo: findByEmailAddr()
UserRegisterFacade->>PortalUserRepo: updateUserToCorpUser()<br/>(PortalOrg 변경<br/>ROLE_CORP_USER 전환)
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: COMPLETED<br/>completeDate: now()
UserRegisterFacade-->>Portal: 수락 성공
Portal-->>초대받은사용자: 성공 메시지<br/>로그아웃 후 재로그인 안내
Note over 초대받은사용자: 다음 로그인 시<br/>PENDING 초대 없음 → 팝업 안 뜸
else 거절
UserRegisterFacade->>UserInvitationRepo: save(invitation)<br/>status: REJECTED<br/>completeDate: now()
UserRegisterFacade-->>Portal: 거절 처리 완료
Portal-->>초대받은사용자: 거절 완료 메시지
Note over 초대받은사용자: 다음 로그인 시<br/>REJECTED 상태 → 팝업 안 뜸
end
-44
View File
@@ -1,44 +0,0 @@
stateDiagram-v2
[*] --> 초대생성: 법인관리자가 초대 시작
초대생성 --> PENDING: 토큰 생성 및 저장
PENDING --> 이메일발송: 초대 이메일 발송
state 로그인후팝업 {
이메일발송 --> 로그인: 사용자 로그인
로그인 --> 초대확인: AuthSuccessHandler
초대확인 --> 세션저장: 세션에 초대 정보 저장
세션저장 --> 팝업표시: 메인 페이지 팝업
팝업표시 --> 팝업확인: 확인 클릭
팝업표시 --> 팝업취소: 취소 클릭
팝업취소 --> 로그인: 다음 로그인 시 재표시
}
팝업확인 --> 초대수락: 수락 페이지 이동
초대수락 --> COMPLETED: 수락
초대수락 --> REJECTED: 거절
COMPLETED --> [*]: 초대 완료
REJECTED --> [*]: 초대 거절
PENDING --> CANCELED: 관리자 취소
PENDING --> EXPIRED: 만료일 경과
CANCELED --> [*]: 초대 취소
EXPIRED --> [*]: 초대 만료
note right of PENDING
PENDING 상태일 때만
로그인 시 팝업 표시
end note
note right of REJECTED
REJECTED 상태가 되면
더 이상 팝업 표시 안 함
end note
note right of COMPLETED
COMPLETED 상태가 되면
더 이상 팝업 표시 안 함
ROLE_CORP_USER로 전환됨
end note
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+84 -466
View File
@@ -1,33 +1,30 @@
{
"name": "eapim-portal",
"version": "1.0.0",
"lockfileVersion": 2,
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "eapim-portal",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"playwright": "^1.57.0"
},
"devDependencies": {
"sass": "^1.69.5"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^1.0.3",
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
@@ -37,25 +34,25 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
@@ -74,9 +71,9 @@
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
@@ -95,9 +92,9 @@
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
@@ -116,9 +113,9 @@
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
@@ -137,9 +134,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
@@ -158,9 +155,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
@@ -179,9 +176,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
@@ -200,9 +197,9 @@
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
@@ -221,9 +218,9 @@
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
@@ -242,9 +239,9 @@
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
@@ -263,9 +260,9 @@
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
@@ -284,9 +281,9 @@
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
@@ -305,9 +302,9 @@
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
@@ -325,82 +322,37 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 14.16.0"
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true,
"license": "MIT"
},
@@ -429,32 +381,6 @@
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -464,57 +390,27 @@
"optional": true
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8.6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
@@ -522,21 +418,21 @@
}
},
"node_modules/sass": {
"version": "1.97.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"version": "1.100.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz",
"integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"chokidar": "^5.0.0",
"immutable": "^5.1.5",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
"node": ">=20.19.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
@@ -551,284 +447,6 @@
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
}
},
"dependencies": {
"@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"optional": true,
"requires": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
}
},
"@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"dev": true,
"optional": true
},
"@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"dev": true,
"optional": true
},
"@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"dev": true,
"optional": true
},
"@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"dev": true,
"optional": true
},
"@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"dev": true,
"optional": true
},
"@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"dev": true,
"optional": true
},
"@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"dev": true,
"optional": true
},
"@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"dev": true,
"optional": true
},
"@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"dev": true,
"optional": true
},
"@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"dev": true,
"optional": true
},
"@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"dev": true,
"optional": true
},
"@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"dev": true,
"optional": true
},
"@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"dev": true,
"optional": true
},
"braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"optional": true,
"requires": {
"fill-range": "^7.1.1"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"optional": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"dev": true
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"optional": true
},
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"optional": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"optional": true
},
"micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"optional": true,
"requires": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
"node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"optional": true
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"optional": true
},
"playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.57.0"
}
},
"playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="
},
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true
},
"sass": {
"version": "1.97.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"dev": true,
"requires": {
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"optional": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
}
+1 -4
View File
@@ -13,8 +13,5 @@
"sass": "^1.69.5"
},
"author": "",
"license": "ISC",
"dependencies": {
"playwright": "^1.57.0"
}
"license": "ISC"
}
@@ -0,0 +1,85 @@
# 메뉴 관리 개발 가이드
포탈 GNB/마이페이지 메뉴는 `menu.yml` → DB(PTL_MENU_*) → 캐시 → 템플릿 렌더 구조로 동작하며,
노출/배치 관리는 eapim-admin **포탈메뉴관리**(파트너포탈 > 포탈관리 > 메뉴 관리)에서 수행한다.
## 구성 요소
| 구성 | 위치 | 역할 |
|---|---|---|
| `menu.yml` | `src/main/resources/menu.yml` | 기본 메뉴 정의 (id/노출명/path/권한/기본 배치) |
| `roles.yml` | `src/main/resources/roles.yml` | 역할 정의 (`portal.portal_security` 이동분) |
| 엔티티/공유 서비스 | `elink-portal-common` `com.eactive.apim.portal.menu.*` | PTL_MENU_ITEM·PTL_MENU_PLACEMENT·PTL_ROLE(+AUTHORITY), `PortalMenuDataService` |
| 시더 | `djb/menu/MenuSeeder.java` | 부팅 시 yml→DB 적재 (ApplicationReadyEvent) |
| 캐시 | `djb/menu/MenuService.java` | role 비의존 트리 스냅샷, TTL 1시간(PTL_PROPERTY) |
| 렌더 | `djb/menu/MenuModelAdvice.java` → 모델 `menuView` | 요청별 노출(EXPOSE_ROLES) 필터 |
| 접근 제어 | `djb/menu/MenuAccessInterceptor.java` | ACCESS_ROLES 서버측 집행 (경로 정확 일치) |
| 내부 API | `djb/menu/MenuInternalController.java` | `POST /internal/menu/reload` (admin 캐시 리로드 수신) |
메뉴를 소비하는 템플릿: `fragment/djbank/header_container.html`(데스크톱 nav·마이페이지 드롭다운·모바일 drawer),
`fragment/djbank/service_sidebar.html`. 모두 `${menuView}` 를 반복 렌더하므로 **메뉴 추가 시 템플릿 수정 불필요**.
## menu.yml 스키마
```yaml
portal-menu:
items:
- id: support # kebab-case 필수 (^[a-z0-9-]+$). 변경 금지(변경=신규 항목)
name: "고객지원"
group: true # 상위 그룹. path 생략 시 클릭 없음(자식 있어야 노출)
section: GNB # GNB(기본) | MYPAGE. 자식은 부모 섹션 상속
expose-roles: [] # 생략=전체(익명 포함), AUTHENTICATED=로그인자, 그 외 역할코드 any-of
children:
- { id: support-faq, name: "FAQ", path: /faq_list }
- { id: my-page-webhook, name: "Webhook 관리", path: /webhook, icon: fa-bell,
expose-roles: [ROLE_WEBHOOK], access-roles: [ROLE_WEBHOOK] }
```
- `expose-roles` = 메뉴 **노출** 조건, `access-roles` = URL **접근** 조건(인터셉터 차단, redirect).
- `icon` 은 마이페이지 드롭다운 전용(FontAwesome 클래스).
- 정렬은 yml 나열 순서(기본 배치 sort = index×10).
## 시딩 규칙 (MenuSeeder)
1. **항목**: id 기준 upsert. yml 값이 바뀌면 DFLT_*(기본값 스냅샷)를 갱신하고,
**관리자가 수정하지 않은 필드(현재값==구 기본값)만** 새 기본값을 따라간다.
구조 필드(`group`/`section`/`icon`/`new-window`)는 항상 yml 이 이긴다.
2. **배치**: `PTL_MENU_PLACEMENT`**비어있을 때만** 기본 배치로 최초 시딩.
이후 배치는 admin 이 소유한다 — 재배포/재기동에도 보존됨.
3. yml 에서 항목을 제거해도 DB 는 삭제하지 않고 경고 로그만 남긴다(수동 정리).
4. 부팅 시딩 주체는 인증 사용자가 없으므로 `CREATED_BY=SYSTEM`.
## 캐시와 리로드
- 스냅샷 TTL: PTL_PROPERTY `Portal / menu.cache.ttl-seconds` (기본 3600초).
- 즉시 반영: `curl -X POST http://127.0.0.1:39130/internal/menu/reload`
(admin 포탈메뉴관리의 [캐시 Reload] 버튼이 동일 호출 수행).
- 내부 API 가드: `Portal / menu.internal.allow-ips` 허용 IP 목록(기본 loopback)
+ X-Forwarded-For 동반 요청 거부. CSRF 면제(`/internal/menu/**`).
- admin 측 호출 URL: `Portal / portal.internal.menu-reload-url`.
## 새 메뉴 추가 절차
**기본 메뉴(코드 배포와 함께)**
1. 페이지/라우트 준비 (`portal.pages` 또는 `@GetMapping` — 기존 방식 그대로)
2. `menu.yml` 에 항목 추가 (필요 시 breadcrumb 용 `page.home` 트리도 갱신 — 별도 체계 유지)
3. 재기동 → 시딩 로그 확인 → 헤더/드로어 노출 확인
4. 이미 운영 중인 DB 라면 배치는 자동 추가되지 않음(배치 시딩은 최초 1회) —
admin 화면에서 미배치 → 원하는 위치로 드래그 후 저장
**운영자 임시 메뉴(외부 링크 등)**: admin 포탈메뉴관리 [메뉴 추가] → 미배치 생성 → 드래그 배치 → 저장 → 캐시 Reload.
커스텀 항목은 미배치 시 삭제된다.
## 로컬 개발 주의
- `gradle bootRun` 으로 시딩까지 확인하려면 damo-manager 가 classpath 에 필요:
`JAVA_TOOL_OPTIONS="-Xbootclasspath/a:<...>/apache-tomcat-9.0.115-djb/lib/damo-manager.jar"`
(미지정 시 감사 컬럼 암호화 컨버터에서 NoClassDefFoundError).
- 템플릿/메뉴 반영 확인은 서버 재시작 후 curl 로.
- elink-portal-common 수정 후 Q클래스 duplicate 컴파일 오류 시 각 모듈 `build/generated` 삭제 후 재컴파일.
## 역할(roles.yml) 변경
- 로그인 권한 확장은 `PortalRolesProperties`(yml 바인딩)를 직접 사용 — DB 미러(PTL_ROLE*)는
admin 권한 선택 체크박스 소스 전용.
- 역할 추가 시 `roles.yml``authority-names` 에 한글 라벨을 함께 등록해야 admin 화면에 표기된다.
+112
View File
@@ -0,0 +1,112 @@
# DJB 커스텀 소스 납품본 수집
발주사 납품용으로, 기준 시점 이후 **신규로 추가된 커스텀 소스 파일**만 모아
디렉터리 구조를 유지한 채 한곳에 복사하고 `MANIFEST.csv` / `SUMMARY.md` / zip 을 만든다.
## 파일
| 파일 | 용도 |
|---|---|
| `export-custom.sh` | macOS / Linux 실행 스크립트 |
| `export-custom.ps1` | Windows PowerShell 실행 스크립트 (실제 로직) |
| `export-custom.bat` | Windows cmd 래퍼 — 인자를 `.ps1` 로 그대로 전달 |
| `modules.conf` | 대상 모듈 / 소스 경로 / 수집 확장자 |
| `export-filter.rules` | 포함·제외 규칙 (`.gitignore` 유사 문법) |
| `filelist.txt` | 마지막으로 뽑은 수집 대상 목록 (검토용, 재생성 가능) |
| `filelist-no-djb-marker.txt` | 그중 `djb` / `custom` 마커가 **없는** 파일 — 업그레이드 유입분 검토 대상 |
## 수집 방식
1. 모듈별로 **컷오프 날짜 직전의 마지막 커밋**을 base 로 잡는다.
`git rev-list -1 --before=<CUTOFF> HEAD`
컷오프 이전 커밋이 아예 없으면 저장소 전체를 신규로 본다.
2. `git diff --name-only --diff-filter=A -M <base> HEAD -- <소스경로>`
**신규 추가(A)** 파일만 뽑는다. `-M` 이라 단순 rename 은 신규로 잡히지 않는다.
3. `modules.conf` 의 확장자 allowlist 로 거른다. (확장자 없는 파일은 제외)
4. `export-filter.rules` 로 포함·제외를 최종 결정한다.
5. 남은 파일을 `<out>/src/<모듈>/<원래 경로>` 로 복사한다.
## 사용법
```bash
# macOS / Linux
./export-custom.sh --dry-run > filelist.txt # 목록만 뽑기 (로그는 stderr)
./export-custom.sh --show-excluded # 규칙에 걸려 빠진 목록 + 걸린 규칙
./export-custom.sh # 실제 수집
./export-custom.sh --zip # 수집 + zip
./export-custom.sh -c 2026-05-01 -m eapim-portal
```
```powershell
# Windows
.\export-custom.ps1 -DryRun > filelist.txt
.\export-custom.ps1 -ShowExcluded
.\export-custom.ps1 -Zip
.\export-custom.ps1 -Cutoff 2026-05-01 -Module eapim-portal,eapim-admin
# cmd 에서
export-custom.bat -DryRun > filelist.txt
```
### 주요 옵션
| sh | ps1 | 설명 |
|---|---|---|
| `-c, --cutoff` | `-Cutoff` | 기준 날짜 (기본 `2026-05-01`) |
| `-r, --root` | `-Root` | 저장소 루트. 미지정 시 스크립트 위치 기준 자동 탐지 |
| `-o, --out` | `-Out` | 출력 디렉터리 (기본 `<root>/build/djb-custom-export`) |
| `-m, --module` | `-Module` | 특정 모듈만 |
| `--zip` | `-Zip` | zip 생성 |
| `--include-untracked` | `-IncludeUntracked` | 미커밋 신규 파일도 포함 |
| `--dry-run` | `-DryRun` | 복사 없이 목록만 |
| `--show-excluded` | `-ShowExcluded` | 규칙에 걸려 빠진 목록 |
## `export-filter.rules` 문법
`.gitignore` 와 비슷하되 더 단순하다.
- 한 줄에 패턴 하나. `#` 주석, 빈 줄 무시
- 기본은 **모두 포함**. 패턴에 걸리면 제외
- `!` 로 시작하면 예외(다시 포함)
- **뒤에 오는 규칙이 앞 규칙을 덮는다** — 순서가 중요하다
- 패턴에 `/` 가 있으면 `<모듈>/<경로>` 전체와 매칭, 없으면 파일명만 매칭
- `/` 로 끝나면 그 디렉터리 하위 전체
- `*``/` 도 포함해 매칭한다 (단순 glob, `**` 없음)
매칭 대상 경로 예:
```
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/WebhookService.java
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookList.jsp
```
규칙 파일은 세 구역으로 나뉜다.
1. **업그레이드 유입분 제외**`filelist-no-djb-marker.txt` 를 검토하며 채운다
2. **`!*djb*` 예외** — djb 마커가 붙은 파일은 커스텀이므로 되살린다
3. **무조건 제외** — 빌드 산출물·서드파티. 2)보다 뒤에 있어 최종 승리한다
## 신규지만 커스텀이 아닌 파일 걸러내기
신규 파일이라도 제품 업그레이드로 딸려 온 것일 수 있다. 판별 순서:
```bash
# 1) 전체 목록
./export-custom.sh --dry-run > filelist.txt
# 2) djb / custom 마커가 없는 것 = 검토 대상
grep -vi djb filelist.txt | grep -v '/custom/' > filelist-no-djb-marker.txt
# 3) 검토 후 업그레이드 유입분을 export-filter.rules 의 1) 구역에 추가
# 4) 반영 확인
./export-custom.sh --show-excluded
```
## 주의
- 컷오프 이전 커밋이 없으면 base 가 **빈 트리**가 되어 저장소 전체가 신규로 잡힌다.
`SUMMARY.md` 의 base 컬럼에 `(컷오프 이전 커밋 없음 …)` 으로 표시된다.
- 모듈에 컷오프 직전 커밋이 드문드문 있으면 base 가 컷오프보다 훨씬 이전으로 잡힐 수 있다.
실행 로그의 `base:` 날짜를 항상 확인할 것.
- 추가된 뒤 삭제·이동된 파일은 현재 트리에 없으므로 건너뛰고, 건수를 `SUMMARY.md` 에 남긴다.
@@ -0,0 +1,29 @@
@echo off
REM ---------------------------------------------------------------------------
REM DJB 커스텀 소스 납품본 수집 (Windows cmd 래퍼)
REM
REM 실제 로직은 export-custom.ps1 에 있다. 이 배치는 PowerShell 을 띄우고
REM 전달받은 인자를 그대로 넘기기만 한다.
REM
REM 사용 예:
REM export-custom.bat
REM export-custom.bat -Cutoff 2026-05-01 -Zip
REM export-custom.bat -DryRun > filelist.txt
REM export-custom.bat -ShowExcluded
REM export-custom.bat -Module eapim-portal,eapim-admin
REM ---------------------------------------------------------------------------
setlocal
REM 한글 경로/출력이 깨지지 않도록 코드페이지를 UTF-8 로 전환
chcp 65001 >nul
where powershell >nul 2>nul
if errorlevel 1 (
echo [오류] PowerShell 을 찾을 수 없습니다. 1>&2
exit /b 1
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0export-custom.ps1" %*
set RC=%ERRORLEVEL%
endlocal & exit /b %RC%
+278
View File
@@ -0,0 +1,278 @@
<#
.SYNOPSIS
DJB 커스텀 소스 납품본 수집 스크립트 (Windows / PowerShell)
.DESCRIPTION
기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
.EXAMPLE
.\export-custom.ps1
.\export-custom.ps1 -Cutoff 2026-05-01 -Zip
.\export-custom.ps1 -DryRun
.\export-custom.ps1 -Module eapim-portal,eapim-admin
#>
[CmdletBinding()]
param(
[string] $Cutoff = '2026-05-01',
[string] $Root,
[string] $Out,
[string[]] $Module,
[switch] $Zip,
[switch] $IncludeUntracked,
[switch] $DryRun,
[switch] $ShowExcluded
)
if ($ShowExcluded) { $DryRun = $true }
$ErrorActionPreference = 'Stop'
# git 출력의 한글이 깨지지 않도록 콘솔 인코딩을 UTF-8로 고정한다.
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { }
$OutputEncoding = [Text.Encoding]::UTF8
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ConfFile = Join-Path $ScriptDir 'modules.conf'
$RulesFile = Join-Path $ScriptDir 'export-filter.rules'
# 저장소 루트: 스크립트는 <root>\eapim-portal\script\djb-custom-export\ 에 있다.
if (-not $Root) { $Root = (Resolve-Path (Join-Path $ScriptDir '..\..\..')).Path }
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
throw "저장소 루트를 찾을 수 없다: $Root"
}
if (-not $Out) { $Out = Join-Path $Root 'build\djb-custom-export' }
if (-not (Test-Path -LiteralPath $ConfFile)) { throw "설정 파일 없음: $ConfFile" }
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
$EmptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
# ---- 설정 로드 ------------------------------------------------------------
$Modules = @()
$AllowExt = @()
foreach ($line in (Get-Content -LiteralPath $ConfFile -Encoding UTF8)) {
$t = $line.Trim()
if ($t -eq '' -or $t.StartsWith('#')) { continue }
$idx = $t.IndexOf('|')
if ($idx -lt 0) { continue }
$key = $t.Substring(0, $idx)
$rest = $t.Substring($idx + 1)
switch ($key) {
'MODULE' {
$i2 = $rest.IndexOf('|')
if ($i2 -lt 0) { continue }
$Modules += [pscustomobject]@{
Name = $rest.Substring(0, $i2)
Paths = $rest.Substring($i2 + 1).Split(' ') | Where-Object { $_ -ne '' }
}
}
'EXT' { $AllowExt = $rest.Split(',') | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ -ne '' } }
}
}
# ---- 포함/제외 규칙 로드 (.gitignore 유사) --------------------------------
$Rules = @()
if (Test-Path -LiteralPath $RulesFile) {
foreach ($line in (Get-Content -LiteralPath $RulesFile -Encoding UTF8)) {
$t = $line.Trim()
if ($t -eq '' -or $t.StartsWith('#')) { continue }
$Rules += $t
}
}
# 마지막으로 매칭된 규칙이 이긴다. 기본 포함, '!' 는 예외(다시 포함).
# 반환: @{ Keep = $true/$false; Rule = '<제외시킨 규칙>' }
function Test-RuleKeep([string]$RelPath) {
$leaf = Split-Path $RelPath -Leaf
$keep = $true
$matched = ''
foreach ($raw in $Rules) {
$rule = $raw
$neg = $false
if ($rule.StartsWith('!')) { $neg = $true; $rule = $rule.Substring(1) }
if ($rule.EndsWith('/')) { $rule = $rule + '*' } # 디렉터리 규칙
$target = if ($rule.Contains('/')) { $RelPath } else { $leaf }
if ($target -like $rule) {
if ($neg) { $keep = $true; $matched = '' }
else { $keep = $false; $matched = $rule }
}
}
return @{ Keep = $keep; Rule = $matched }
}
function Test-AllowedExt([string]$RelPath) {
$leaf = Split-Path $RelPath -Leaf
if ($leaf -notmatch '\.') { return $false } # 확장자 없는 파일 제외
$ext = $leaf.Substring($leaf.LastIndexOf('.') + 1).ToLower()
return $AllowExt -contains $ext
}
function Invoke-Git([string]$Dir, [string[]]$GitArgs) {
$all = @('-C', $Dir, '-c', 'core.quotepath=false') + $GitArgs
$res = & git @all 2>$null
if ($LASTEXITCODE -ne 0) { return @() }
return @($res)
}
# ---- 준비 -----------------------------------------------------------------
$Manifest = Join-Path $Out 'MANIFEST.csv'
$Summary = Join-Path $Out 'SUMMARY.md'
if (-not $DryRun) {
if (Test-Path -LiteralPath $Out) { Remove-Item -LiteralPath $Out -Recurse -Force }
New-Item -ItemType Directory -Path (Join-Path $Out 'src') -Force | Out-Null
# Excel 한글용 UTF-8 BOM
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[IO.File]::WriteAllText($Manifest, "module,path,ext,added_commit,added_date,author`r`n", $utf8Bom)
}
Write-Host "저장소 루트 : $Root"
Write-Host "기준 날짜 : $Cutoff (이 날짜 직전 마지막 커밋이 base)"
Write-Host "출력 경로 : $Out"
if ($DryRun) { Write-Host '모드 : DRY-RUN (복사 안 함)' }
Write-Host ''
$Rows = New-Object System.Collections.Generic.List[string]
$SummaryRs = New-Object System.Collections.Generic.List[object]
$Total = 0
$Missing = 0
$Excluded = 0
# ---- 모듈 순회 ------------------------------------------------------------
foreach ($m in $Modules) {
if ($Module -and ($Module -notcontains $m.Name)) { continue }
$modDir = Join-Path $Root $m.Name
if (-not (Test-Path -LiteralPath (Join-Path $modDir '.git'))) {
Write-Host "[건너뜀] $($m.Name) — git 저장소 아님 ($modDir)"
continue
}
# base 커밋 결정
$base = (Invoke-Git $modDir @('rev-list', '-1', "--before=$Cutoff", 'HEAD') | Select-Object -First 1)
if ([string]::IsNullOrWhiteSpace($base)) {
$base = $EmptyTree
$baseDesc = '(컷오프 이전 커밋 없음 → 전체를 신규로 간주)'
} else {
$d = (Invoke-Git $modDir @('log', '-1', '--format=%h %ad %s', '--date=short', $base) | Select-Object -First 1)
$baseDesc = if ($d.Length -gt 80) { $d.Substring(0, 80) } else { $d }
}
# 실제 존재하는 소스 경로만 pathspec 으로 사용
$spec = @()
foreach ($p in $m.Paths) {
if (Test-Path -LiteralPath (Join-Path $modDir ($p -replace '/', '\')) -PathType Container) { $spec += $p }
}
if ($spec.Count -eq 0) {
Write-Host "[건너뜀] $($m.Name) — 설정된 소스 경로가 존재하지 않음"
continue
}
# 신규(Added) 파일만. -M 으로 rename 은 신규에서 제외한다.
$files = @(Invoke-Git $modDir (@('diff', '--name-only', '--diff-filter=A', '-M', $base, 'HEAD', '--') + $spec))
if ($IncludeUntracked) {
$files += @(Invoke-Git $modDir (@('ls-files', '--others', '--exclude-standard', '--') + $spec))
}
$files = $files | Where-Object { $_ -ne '' } | Sort-Object -Unique
$count = 0
$miss = 0
foreach ($f in $files) {
if (-not (Test-AllowedExt $f)) { continue }
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
$rk = Test-RuleKeep "$($m.Name)/$f"
if (-not $rk.Keep) {
$Excluded++
if ($ShowExcluded) { Write-Output "$($m.Name)/$f`t# $($rk.Rule)" }
continue
}
if ($ShowExcluded) { continue }
$src = Join-Path $modDir ($f -replace '/', '\')
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
Write-Warning " [없음] $($m.Name)/$f"
$miss++
continue
}
$count++
# 목록은 Write-Output(성공 스트림) 으로 — 리다이렉트하면 목록만 파일로 떨어진다.
if ($DryRun) { Write-Output "$($m.Name)/$f"; continue }
$dst = Join-Path (Join-Path $Out 'src') (Join-Path $m.Name ($f -replace '/', '\'))
$dstDir = Split-Path -Parent $dst
if (-not (Test-Path -LiteralPath $dstDir)) { New-Item -ItemType Directory -Path $dstDir -Force | Out-Null }
Copy-Item -LiteralPath $src -Destination $dst -Force
$meta = (Invoke-Git $modDir @('log', '-1', '--diff-filter=A', '--format=%h|%ad|%an', '--date=short', '--', $f) | Select-Object -First 1)
$cHash = ''; $cDate = ''; $cAuth = ''
if ($meta) {
$parts = $meta.Split('|')
if ($parts.Count -ge 3) { $cHash = $parts[0]; $cDate = $parts[1]; $cAuth = $parts[2] }
}
$leaf = Split-Path $f -Leaf
$ext = if ($leaf -match '\.') { $leaf.Substring($leaf.LastIndexOf('.') + 1) } else { '' }
$esc = { param($s) '"' + ($s -replace '"', '""') + '"' }
$Rows.Add((@(
(& $esc $m.Name), (& $esc $f), (& $esc $ext),
(& $esc $cHash), (& $esc $cDate), (& $esc $cAuth)
) -join ','))
}
$Total += $count
$Missing += $miss
$SummaryRs.Add([pscustomobject]@{ Module = $m.Name; Count = $count; Base = $baseDesc })
Write-Host ("[수집] {0,-22} {1,5} 개 base: {2}" -f $m.Name, $count, $baseDesc)
}
# ---- 출력 ------------------------------------------------------------------
if (-not $DryRun) {
if ($Rows.Count -gt 0) {
[IO.File]::AppendAllText($Manifest, (($Rows -join "`r`n") + "`r`n"), (New-Object System.Text.UTF8Encoding($false)))
}
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# DJB 커스텀 소스 납품본')
[void]$sb.AppendLine('')
[void]$sb.AppendLine("- 생성 일시: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
[void]$sb.AppendLine("- 기준 날짜: ``$Cutoff`` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)")
[void]$sb.AppendLine('- 수집 규칙: `git diff --diff-filter=A -M <base> HEAD` / 확장자 allowlist / `export-filter.rules` 필터')
[void]$sb.AppendLine("- 대상 확장자: ``$($AllowExt -join ',')``")
[void]$sb.AppendLine("- 규칙 파일: ``$(Split-Path $RulesFile -Leaf)`` ($($Rules.Count) 개 규칙, 제외 $Excluded 건)")
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 모듈별 수집 결과')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('| 모듈 | 파일 수 | base 커밋 |')
[void]$sb.AppendLine('|---|---:|---|')
foreach ($r in $SummaryRs) { [void]$sb.AppendLine("| $($r.Module) | $($r.Count) | $($r.Base) |") }
[void]$sb.AppendLine('')
[void]$sb.AppendLine("**합계: $Total 개**")
if ($Missing -gt 0) { [void]$sb.AppendLine("> 추가 후 삭제/이동되어 현재 트리에 없는 파일 $Missing 개는 제외됨.") }
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 파일 목록')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('`MANIFEST.csv` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자).')
[IO.File]::WriteAllText($Summary, $sb.ToString(), (New-Object System.Text.UTF8Encoding($true)))
Write-Host ''
Write-Host "MANIFEST : $Manifest"
Write-Host "SUMMARY : $Summary"
if ($Zip) {
$zipName = "djb-custom-export-$(Get-Date -Format 'yyyyMMdd').zip"
$zipPath = Join-Path (Split-Path -Parent $Out) $zipName
if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
Compress-Archive -Path (Join-Path $Out '*') -DestinationPath $zipPath
Write-Host "ZIP : $zipPath"
}
}
+352
View File
@@ -0,0 +1,352 @@
#!/usr/bin/env bash
#
# DJB 커스텀 소스 납품본 수집 스크립트 (macOS / Linux)
#
# 기준 커밋(컷오프 직전 마지막 커밋) 이후 "신규로 추가된" 소스 파일만 모아
# 모듈별 디렉터리 구조를 유지한 채 출력 디렉터리에 복사하고,
# MANIFEST.csv / SUMMARY.md / (선택) zip 아카이브를 생성한다.
#
# 사용법:
# ./export-custom.sh # 기본값으로 실행
# ./export-custom.sh -c 2026-05-01 --zip
# ./export-custom.sh --dry-run
# ./export-custom.sh -m eapim-portal -m eapim-admin
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_FILE="$SCRIPT_DIR/modules.conf"
RULES_FILE="$SCRIPT_DIR/export-filter.rules"
# ---- 기본값 ---------------------------------------------------------------
CUTOFF="2026-05-01"
REPO_ROOT=""
OUT_DIR=""
MAKE_ZIP=0
DRY_RUN=0
INCLUDE_UNTRACKED=0
ONLY_MODULES=""
SHOW_EXCLUDED=0
usage() {
cat <<'EOF'
DJB 커스텀 소스 납품본 수집
옵션:
-c, --cutoff <YYYY-MM-DD> 기준 날짜. 이 날짜 직전 마지막 커밋을 base로 삼는다.
(기본: 2026-05-01)
-r, --root <DIR> 저장소 루트(djb-eapim). 미지정 시 스크립트 위치에서 자동 탐지.
-o, --out <DIR> 출력 디렉터리. (기본: <root>/build/djb-custom-export)
-m, --module <NAME> 특정 모듈만 수집. 여러 번 지정 가능.
--zip 수집 후 zip 아카이브 생성.
--include-untracked git에 아직 커밋되지 않은 신규 파일도 포함.
--dry-run 복사하지 않고 대상 목록만 stdout 으로 출력(로그는 stderr).
--show-excluded --dry-run 과 함께. 규칙에 걸려 제외된 목록을 대신 출력.
-h, --help 도움말.
포함/제외 규칙은 export-filter.rules (.gitignore 유사 문법) 에서 관리한다.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
-c|--cutoff) CUTOFF="$2"; shift 2 ;;
-r|--root) REPO_ROOT="$2"; shift 2 ;;
-o|--out) OUT_DIR="$2"; shift 2 ;;
-m|--module) ONLY_MODULES="$ONLY_MODULES $2"; shift 2 ;;
--zip) MAKE_ZIP=1; shift ;;
--include-untracked) INCLUDE_UNTRACKED=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--show-excluded) SHOW_EXCLUDED=1; DRY_RUN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "알 수 없는 옵션: $1" >&2; usage >&2; exit 1 ;;
esac
done
# ---- 저장소 루트 탐지 -----------------------------------------------------
# 스크립트는 <root>/eapim-portal/script/djb-custom-export/ 에 있다.
if [ -z "$REPO_ROOT" ]; then
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
fi
if [ ! -d "$REPO_ROOT" ]; then
echo "저장소 루트를 찾을 수 없다: $REPO_ROOT" >&2
exit 1
fi
[ -z "$OUT_DIR" ] && OUT_DIR="$REPO_ROOT/build/djb-custom-export"
if [ ! -f "$CONF_FILE" ]; then
echo "설정 파일 없음: $CONF_FILE" >&2
exit 1
fi
# ---- 설정 로드 ------------------------------------------------------------
ALLOW_EXT=""
EXCLUDE_PAT=""
MODULE_LINES=""
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
key="${line%%|*}"
rest="${line#*|}"
case "$key" in
MODULE) MODULE_LINES="$MODULE_LINES
$rest" ;;
EXT) ALLOW_EXT="$rest" ;;
EXCLUDE) EXCLUDE_PAT="$rest" ;;
esac
done < "$CONF_FILE"
# 빈 트리 해시 — base 커밋이 없을 때(= 모든 이력이 컷오프 이후) 사용한다.
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
is_allowed_ext() {
_f="$1"
_base="${_f##*/}"
case "$_base" in
*.*) _ext="${_base##*.}" ;;
*) return 1 ;; # 확장자 없는 파일 제외
esac
# 소문자 변환 (bash 3.2 호환)
_ext="$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')"
case ",$ALLOW_EXT," in
*",$_ext,"*) return 0 ;;
*) return 1 ;;
esac
}
# ---- 포함/제외 규칙 (.gitignore 유사) --------------------------------------
RULES=()
if [ -f "$RULES_FILE" ]; then
while IFS= read -r r || [ -n "$r" ]; do
# 앞뒤 공백 제거
r="${r#"${r%%[![:space:]]*}"}"
r="${r%"${r##*[![:space:]]}"}"
case "$r" in
''|'#'*) continue ;;
esac
RULES[${#RULES[@]}]="$r"
done < "$RULES_FILE"
fi
# 경로 하나에 규칙을 순서대로 적용한다. 마지막으로 매칭된 규칙이 이긴다.
# 기본은 포함(0), '!' 규칙은 다시 포함, 일반 규칙은 제외(1).
# 반환: 0 = 포함, 1 = 제외. 제외시킨 규칙은 MATCHED_RULE 에 담긴다.
MATCHED_RULE=""
rule_keep() {
_path="$1"
_leaf="${_path##*/}"
_keep=0
MATCHED_RULE=""
_i=0
while [ $_i -lt ${#RULES[@]} ]; do
_rule="${RULES[$_i]}"
_i=$((_i + 1))
_neg=0
case "$_rule" in
'!'*) _neg=1; _rule="${_rule#!}" ;;
esac
# 디렉터리 규칙: 하위 전체
case "$_rule" in
*/) _rule="${_rule}*" ;;
esac
# '/' 가 있으면 전체 경로, 없으면 파일명과 매칭
case "$_rule" in
*/*) _target="$_path" ;;
*) _target="$_leaf" ;;
esac
# shellcheck disable=SC2254 # 의도적으로 unquoted — glob 패턴으로 쓴다
case "$_target" in
$_rule)
if [ $_neg -eq 1 ]; then
_keep=0; MATCHED_RULE=""
else
_keep=1; MATCHED_RULE="$_rule"
fi
;;
esac
done
return $_keep
}
csv_escape() {
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
}
# ---- 준비 -----------------------------------------------------------------
if [ "$DRY_RUN" -eq 0 ]; then
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR/src"
fi
MANIFEST="$OUT_DIR/MANIFEST.csv"
SUMMARY="$OUT_DIR/SUMMARY.md"
TMP_SUMMARY="$(mktemp)"
trap 'rm -f "$TMP_SUMMARY"' EXIT
if [ "$DRY_RUN" -eq 0 ]; then
printf '\357\273\277' > "$MANIFEST" # Excel 한글용 UTF-8 BOM
echo 'module,path,ext,added_commit,added_date,author' >> "$MANIFEST"
fi
# dry-run(목록 뽑기) 모드에서는 파일 목록만 stdout으로 내보내고
# 진행 로그는 전부 stderr로 보낸다. 그래야 파이프/리다이렉트로 목록을 바로 가공할 수 있다.
if [ "$DRY_RUN" -eq 1 ]; then LOG=2; else LOG=1; fi
{
echo "저장소 루트 : $REPO_ROOT"
echo "기준 날짜 : $CUTOFF (이 날짜 직전 마지막 커밋이 base)"
echo "출력 경로 : $OUT_DIR"
[ "$DRY_RUN" -eq 1 ] && echo "모드 : DRY-RUN (목록만 출력, 복사 안 함)"
echo
} >&$LOG
TOTAL=0
MISSING=0
EXCLUDED=0
# ---- 모듈 순회 ------------------------------------------------------------
# 파이프 대신 process substitution 을 써서 집계 변수가 서브셸에 갇히지 않게 한다.
while IFS= read -r mline; do
[ -z "$mline" ] && continue
MOD="${mline%%|*}"
PATHS="${mline#*|}"
if [ -n "$ONLY_MODULES" ]; then
case " $ONLY_MODULES " in
*" $MOD "*) : ;;
*) continue ;;
esac
fi
MOD_DIR="$REPO_ROOT/$MOD"
if [ ! -d "$MOD_DIR/.git" ]; then
echo "[건너뜀] $MOD — git 저장소 아님 ($MOD_DIR)" >&$LOG
continue
fi
# base 커밋 결정
BASE="$(git -C "$MOD_DIR" -c core.quotepath=false rev-list -1 --before="$CUTOFF" HEAD 2>/dev/null || true)"
if [ -z "$BASE" ]; then
BASE="$EMPTY_TREE"
BASE_DESC="(컷오프 이전 커밋 없음 → 전체를 신규로 간주)"
else
BASE_DESC="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --format='%h %ad %s' --date=short "$BASE" | cut -c1-80)"
fi
# 실제 존재하는 소스 경로만 pathspec으로 사용
SPEC=""
for p in $PATHS; do
[ -d "$MOD_DIR/$p" ] && SPEC="$SPEC $p"
done
if [ -z "$SPEC" ]; then
echo "[건너뜀] $MOD — 설정된 소스 경로가 존재하지 않음" >&$LOG
continue
fi
# 신규(Added) 파일만. -M 으로 rename은 신규에서 제외한다.
FILES="$(git -C "$MOD_DIR" -c core.quotepath=false diff --name-only --diff-filter=A -M "$BASE" HEAD -- $SPEC 2>/dev/null || true)"
if [ "$INCLUDE_UNTRACKED" -eq 1 ]; then
UNTRACKED="$(git -C "$MOD_DIR" -c core.quotepath=false ls-files --others --exclude-standard -- $SPEC 2>/dev/null || true)"
FILES="$FILES
$UNTRACKED"
fi
COUNT=0
MISS=0
while IFS= read -r f; do
[ -z "$f" ] && continue
is_allowed_ext "$f" || continue
# 규칙 매칭은 <모듈>/<경로> 전체 문자열 기준
if ! rule_keep "$MOD/$f"; then
EXCLUDED=$((EXCLUDED + 1))
[ "$SHOW_EXCLUDED" -eq 1 ] && printf '%s\t# %s\n' "$MOD/$f" "$MATCHED_RULE"
continue
fi
[ "$SHOW_EXCLUDED" -eq 1 ] && continue
SRC="$MOD_DIR/$f"
if [ ! -f "$SRC" ]; then
# 추가된 뒤 삭제/이동된 파일 — 납품 대상 아님
echo " [없음] $MOD/$f" >&2
MISS=$((MISS + 1))
continue
fi
COUNT=$((COUNT + 1))
if [ "$DRY_RUN" -eq 1 ]; then
echo "$MOD/$f" # 목록은 stdout으로 (가공하기 쉽게 접두 공백 없음)
continue
fi
DST="$OUT_DIR/src/$MOD/$f"
mkdir -p "$(dirname "$DST")"
cp -p "$SRC" "$DST"
META="$(git -C "$MOD_DIR" -c core.quotepath=false log -1 --diff-filter=A --format='%h|%ad|%an' --date=short -- "$f" 2>/dev/null || true)"
C_HASH="${META%%|*}"; _r="${META#*|}"
C_DATE="${_r%%|*}"
C_AUTH="${_r#*|}"
BASE_F="${f##*/}"
case "$BASE_F" in *.*) EXT="${BASE_F##*.}" ;; *) EXT="" ;; esac
{
csv_escape "$MOD"; printf ','
csv_escape "$f"; printf ','
csv_escape "$EXT"; printf ','
csv_escape "$C_HASH"; printf ','
csv_escape "$C_DATE"; printf ','
csv_escape "$C_AUTH"; printf '\n'
} >> "$MANIFEST"
done < <(printf '%s\n' "$FILES" | sort -u)
TOTAL=$((TOTAL + COUNT))
MISSING=$((MISSING + MISS))
printf '%s|%s|%s\n' "$MOD" "$COUNT" "$BASE_DESC" >> "$TMP_SUMMARY"
printf '[수집] %-22s %5s 개 base: %s\n' "$MOD" "$COUNT" "$BASE_DESC" >&$LOG
done < <(printf '%s\n' "$MODULE_LINES")
# ---- 요약 ------------------------------------------------------------------
if [ "$DRY_RUN" -eq 0 ]; then
{
echo "# DJB 커스텀 소스 납품본"
echo
echo "- 생성 일시: $(date '+%Y-%m-%d %H:%M:%S')"
echo "- 기준 날짜: \`$CUTOFF\` (이 날짜 직전 마지막 커밋을 base로 삼아, 이후 **신규 추가된** 파일만 수집)"
echo "- 수집 규칙: \`git diff --diff-filter=A -M <base> HEAD\` / 확장자 allowlist / \`export-filter.rules\` 필터"
echo "- 대상 확장자: \`$ALLOW_EXT\`"
echo "- 규칙 파일: \`$(basename "$RULES_FILE")\` (${#RULES[@]} 개 규칙, 제외 ${EXCLUDED} 건)"
echo
echo "## 모듈별 수집 결과"
echo
echo "| 모듈 | 파일 수 | base 커밋 |"
echo "|---|---:|---|"
while IFS='|' read -r m n d; do
[ -z "$m" ] && continue
echo "| $m | $n | $d |"
done < "$TMP_SUMMARY"
echo
echo "**합계: ${TOTAL} 개**"
[ "$MISSING" -gt 0 ] && echo "> 추가 후 삭제/이동되어 현재 트리에 없는 파일 ${MISSING} 개는 제외됨."
echo
echo "## 파일 목록"
echo
echo "\`MANIFEST.csv\` 참조 (module, path, ext, 최초 추가 커밋/일자/작성자)."
} > "$SUMMARY"
echo
echo "MANIFEST : $MANIFEST"
echo "SUMMARY : $SUMMARY"
if [ "$MAKE_ZIP" -eq 1 ]; then
ZIP_NAME="djb-custom-export-$(date '+%Y%m%d').zip"
ZIP_PATH="$(dirname "$OUT_DIR")/$ZIP_NAME"
rm -f "$ZIP_PATH"
( cd "$OUT_DIR" && zip -qr "$ZIP_PATH" . )
echo "ZIP : $ZIP_PATH"
fi
fi
@@ -0,0 +1,74 @@
# DJB 커스텀 소스 납품 - 포함/제외 규칙
#
# .gitignore 와 비슷하게 동작한다.
# - 한 줄에 패턴 하나. '#' 주석, 빈 줄 무시.
# - 기본은 "모두 포함". 패턴에 걸리면 제외된다.
# - '!' 로 시작하면 예외(다시 포함).
# - 뒤에 오는 규칙이 앞 규칙을 덮는다 (마지막 매칭 승리). → 순서가 중요하다.
# - 패턴에 '/' 가 있으면 <모듈>/<경로> 전체와 glob 매칭,
# 없으면 파일명(basename)과만 매칭한다.
# - '/' 로 끝나면 그 디렉터리 하위 전체를 뜻한다.
# - '*' 는 '/' 도 포함해 매칭한다(단순 glob).
#
# 매칭 대상 경로 예:
# eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/WebhookService.java
# eapim-admin/WebContent/jsp/onl/apim/webhook/webhookList.jsp
#
# 확인: ./export-custom.sh --dry-run (남는 목록)
# ./export-custom.sh --dry-run --show-excluded (규칙에 걸려 빠진 목록)
# ===========================================================================
# 1) 제품 업그레이드로 유입된 신규 파일 — 커스텀 아니므로 제외
# filelist-no-djb-marker.txt(djb/custom 마커 없는 신규 파일)를 검토하며 채운다.
# ===========================================================================
# 예시)
# eapim-online/src/main/java/com/eactive/eai/agent/inflow/
# eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/
eapim-portal/src/main/java/com/eactive/apim/portal/apps/
eapim-portal/src/main/java/com/eactive/apim/portal/apps/apis/
eapim-portal/src/main/java/com/eactive/apim/portal/apps/app/
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/
eapim-portal/src/main/java/com/eactive/apim/portal/config/
eapim-portal/src/main/java/com/eactive/apim/portal/common/
eapim-portal/src/main/resources/templates/views/fragment/popup/
eapim-portal/src/test/
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/
# ===========================================================================
# 2) 위 제외에 걸렸더라도 되살릴 예외
# djb 마커가 붙은 파일은 어떤 경우에도 커스텀이다.
# ※ 아래 3)의 빌드 산출물 제외보다 반드시 앞에 와야 한다.
# ===========================================================================
!*djb*
!*DJB*
!*Djb*
# ===========================================================================
# 3) 무조건 제외 (최종 승리) — 소스가 아니거나 서드파티
# ===========================================================================
*/build/
*/out/
*/bin/
*/.gradle/
*/.metadata/
*/node_modules/
*/generated/
*/design-backup/
*/WebContent/plugins/
# SafeDB(damo) 벤더 API stub — 컴파일용 껍데기라 납품 대상 아님
damo-manager/src/stub/
# 서드파티 폴더 안에 섞여 있는 우리 커스텀 파일은 되살린다
# (예: WebContent/plugins/swaggerUI/djb-swagger-i18n.js)
!*/WebContent/plugins/*djb*
@@ -0,0 +1,157 @@
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsDay.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsDayId.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsHour.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsHourId.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsMonth.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/ApiStatsMonthId.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwApiStatus.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwAuthClient.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/entity/GwSvcInfo.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsDayRepository.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsHourRepository.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/ApiStatsMonthRepository.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwApiStatusRepository.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwAuthClientRepository.java
eapim-portal/src/main/java/com/eactive/apim/gateway/data/statistics/repository/GwSvcInfoRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/apis/filter/ApiTesterAuditLogger.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/AuthNoticeProperties.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorChannel.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorInfoResponse.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorSendResponse.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/dto/TwoFactorVerifyResponse.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpAuthInterceptor.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpPasswordController.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/StepUpProtectedPaths.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorCleanupScheduler.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorContext.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorProperties.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/community/notice/dto/IncidentAffectedApiDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/community/partnership/dto/PartnershipApplicationSummaryDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/constants/LoginFailureReason.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/constants/LoginType.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/controller/DuplicateLoginController.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/service/DuplicateLoginService.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/controller/SessionApiController.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/entity/UserSession.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/filter/SessionValidationFilter.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/repository/UserSessionRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/dto/ApiStatisticsPeriodDto.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/repository/entity/JobInfo.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/statistics/repository/JobInfoRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/apps/user/service/UserRoleHistoryService.java
eapim-portal/src/main/java/com/eactive/apim/portal/common/exception/UserErrorMessageResolver.java
eapim-portal/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
eapim-portal/src/main/java/com/eactive/apim/portal/common/security/ClientGuardService.java
eapim-portal/src/main/java/com/eactive/apim/portal/common/security/LoginLockPolicy.java
eapim-portal/src/main/java/com/eactive/apim/portal/config/PasswordChangeEnforcementInterceptor.java
eapim-portal/src/main/java/com/eactive/apim/portal/config/PasswordEnforcementPolicy.java
eapim-portal/src/main/java/com/eactive/apim/portal/config/PortalPropertyDuplicateChecker.java
eapim-portal/src/main/java/com/eactive/apim/portal/config/StartupInfoPrinter.java
eapim-portal/src/main/resources/menu.yml
eapim-portal/src/main/resources/roles.yml
eapim-portal/src/main/resources/static/js/api-selector.js
eapim-portal/src/main/resources/static/js/password-policy.js
eapim-portal/src/main/resources/static/js/popup/two-factor-auth.js
eapim-portal/src/main/resources/static/sass/components/_board-common.scss
eapim-portal/src/main/resources/static/sass/components/_password-policy.scss
eapim-portal/src/main/resources/static/sass/components/_session-timer.scss
eapim-portal/src/main/resources/static/sass/components/_toast.scss
eapim-portal/src/main/resources/static/sass/components/_two-factor.scss
eapim-portal/src/main/resources/static/sass/pages/_api-status.scss
eapim-portal/src/main/resources/static/sass/pages/_webhook.scss
eapim-portal/src/main/resources/templates/views/apps/auth/stepupPassword.html
eapim-portal/src/main/resources/templates/views/apps/auth/twoFactorChallenge.html
eapim-portal/src/main/resources/templates/views/apps/community/mainFaqDetail.html
eapim-portal/src/main/resources/templates/views/apps/register/signupVerificationEmail.html
eapim-portal/src/main/resources/templates/views/apps/service/oauth2-guide.html
eapim-portal/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookEmpty.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookList.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep2.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep3.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep2.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep3.html
eapim-portal/src/main/resources/templates/views/fragment/api_selector.html
eapim-portal/src/main/resources/templates/views/fragment/popup/cancelInvitationPopup.html
eapim-portal/src/main/resources/templates/views/fragment/popup/terminateRequestPopup.html
eapim-portal/src/main/resources/templates/views/fragment/popup/twoFactorAuthPopup.html
eapim-portal/src/test/java/com/eactive/apim/portal/common/exception/UserErrorMessageResolverTest.java
eapim-portal/src/test/java/com/eactive/apim/portal/common/util/PhoneNumberUtilTest.java
eapim-portal/src/test/java/com/eactive/apim/portal/common/util/StringMaskingUtilTest.java
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapController.java
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapService.java
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/SitemapServiceImpl.java
eapim-admin/src/main/java/com/eactive/eai/rms/common/acl/sitemap/ui/SitemapNode.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestEmsRepository.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestService.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestUISearch.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentRepository.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/portalinquiry/PortalInquiryCommentService.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataService.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigDataServiceImpl.java
eapim-admin/src/main/java/com/eactive/eai/rms/data/entity/onl/security/CryptoModuleConfigRepository.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/authserver/ClientBlockApiController.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtils.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManController.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManService.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestUI.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryClosingService.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUI.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalInquiry/PortalInquiryCommentUIMapper.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuManController.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuManService.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuUI.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuUIMapper.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentAffectedApiUI.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentTimelineUI.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManController.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigManService.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUI.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/crypto/CryptoModuleConfigUiMapper.java
eapim-admin/src/main/java/com/eactive/eai/rms/onl/manage/inflow/inflow/InflowClientControlManController.java
eapim-admin/src/main/resources/apistatus-draft.yml
eapim-admin/src/main/resources/logback-rinjae_ma.xml
eapim-admin/src/test/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtilsTest.java
eapim-admin/src/test/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtilsTest.java
eapim-admin/WebContent/damo.jsp
eapim-admin/WebContent/jsp/common/acl/sitemap/sitemapMan.jsp
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowClientControlMan.jsp
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowClientControlManDetail.jsp
eapim-admin/WebContent/jsp/onl/admin/rule/transform2/transform2ManApiPopup.jsp
eapim-admin/WebContent/jsp/onl/admin/security/cryptoModuleMan.jsp
eapim-admin/WebContent/jsp/onl/admin/security/cryptoModuleManDetail.jsp
eapim-admin/WebContent/jsp/onl/apim/messagerequest/messageRequestMan.jsp
eapim-admin/WebContent/jsp/onl/apim/messagerequest/messageRequestManDetail.jsp
eapim-admin/WebContent/jsp/onl/apim/portalmenu/portalMenuMan.jsp
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp
eapim-admin/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp
eapim-admin/WebContent/jsp/onl/kjb/statistics/apiUseStatsMan.jsp
elink-portal-common/src/main/java/com/eactive/apim/portal/apprequest/entity/GwAction.java
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/IpAddressMatcher.java
elink-portal-common/src/main/java/com/eactive/apim/portal/common/util/PhoneNumberUtil.java
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/PersonalDataEncryptConverter.java
elink-portal-common/src/main/java/com/eactive/apim/portal/jpa/UuidV7Generator.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/entity/UserRoleHistory.java
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/repository/UserRoleHistoryRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/InquiryComment.java
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/VisibilityScope.java
elink-portal-common/src/test/java/com/eactive/apim/portal/common/util/IpAddressMatcherTest.java
+173
View File
@@ -0,0 +1,173 @@
eapim-portal/src/main/java/com/eactive/apim/portal/custom/config/DjbPasswordEncoder.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/controller/ApiStatusController.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ActiveIncidentDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/AffectedApiDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ApiCurrentStatusDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/ApiOptionDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/DailyStatDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/IssueDateEntryDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/MaintenanceCardDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/MyApiStatusDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/PastIssueCardDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/dto/TimelineEntryDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusCatalogService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/apistatus/service/MyApiStatusQueryService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/controller/InquiryCommentController.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/dto/InquiryCommentCreateRequest.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/dto/InquiryCommentDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/InquiryCommentRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacade.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/constant/DjbInquiryStatus.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/exception/InquiryClosedException.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/exception/InquiryCommentNotOwnedException.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/community/qna/support/InquiryCommentPermissionChecker.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSite.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/footer/RelatedSiteService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuAccessInterceptor.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuModelAdvice.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuNode.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuSeeder.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/MenuView.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/PortalMenuYmlProperties.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/menu/PortalRolesProperties.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/EmployeeIdPolicy.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingMessageWriter.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifier.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifyProperties.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/config/SwingAsyncConfig.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/swing/repository/SwingStaffRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/advice/DjbTestbedExceptionHandler.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/config/DjbTestbedGatewayProperty.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/controller/DjbTestbedAuthController.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/controller/DjbTestbedSpecController.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbCredentialOptionDto.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbCredentialSecretDto.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/enums/DjbAuthType.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/enums/DjbGatewayMode.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/exception/DjbUnsupportedAuthTypeException.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbSwaggerSpecEnricher.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedSpecServerRewriter.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookCreatedResult.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookEventTypeDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookRegistrationDTO.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/exception/WebhookAlreadyExistsException.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/exception/WebhookNotFoundException.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/mapper/WebhookMapper.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestApiRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestEventRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/WebhookRequestRepository.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequest.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestApi.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestApiId.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestEvent.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/repository/entity/WebhookRequestEventId.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookEventTypeProvider.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookSecretGenerator.java
eapim-portal/src/main/java/com/eactive/apim/portal/djb/webhook/service/WebhookService.java
eapim-portal/src/main/resources/menu.yml
eapim-portal/src/main/resources/roles.yml
eapim-portal/src/main/resources/static/favicon.png
eapim-portal/src/main/resources/static/img/avatar1.svg
eapim-portal/src/main/resources/static/img/avatar2.svg
eapim-portal/src/main/resources/static/img/favicon/apple-touch-icon.png
eapim-portal/src/main/resources/static/img/favicon/favicon-16x16.png
eapim-portal/src/main/resources/static/img/favicon/favicon-180x180.png
eapim-portal/src/main/resources/static/img/favicon/favicon-192x192.png
eapim-portal/src/main/resources/static/img/favicon/favicon-32x32.png
eapim-portal/src/main/resources/static/img/favicon/favicon-48x48.png
eapim-portal/src/main/resources/static/img/favicon/favicon-512x512.png
eapim-portal/src/main/resources/static/img/favicon/favicon.png
eapim-portal/src/main/resources/static/img/icon/icon_check_green.png
eapim-portal/src/main/resources/static/img/icon/img_icon.png
eapim-portal/src/main/resources/static/img/keyimage/api_img.png
eapim-portal/src/main/resources/static/img/keyimage/api_img.svg
eapim-portal/src/main/resources/static/img/keyimage/faq_img.png
eapim-portal/src/main/resources/static/img/keyimage/faq_img.svg
eapim-portal/src/main/resources/static/img/keyimage/feedback_img.svg
eapim-portal/src/main/resources/static/img/keyimage/notice_img.png
eapim-portal/src/main/resources/static/img/keyimage/notice_img.svg
eapim-portal/src/main/resources/static/img/keyimage/q&a_img.png
eapim-portal/src/main/resources/static/img/keyimage/q&a_img.svg
eapim-portal/src/main/resources/static/img/keyimage/sigdnUp_img.svg
eapim-portal/src/main/resources/static/img/keyimage/signUp_img.png
eapim-portal/src/main/resources/static/img/logo/logo-djb.png
eapim-portal/src/main/resources/static/img/logo/logo-jjb.png
eapim-portal/src/main/resources/static/img/logo/logo-jjb_white.png
eapim-portal/src/main/resources/static/js/api-selector.js
eapim-portal/src/main/resources/static/js/djb/api-status-issues.js
eapim-portal/src/main/resources/static/js/djb/api-status.js
eapim-portal/src/main/resources/static/js/djb/client-guard.js
eapim-portal/src/main/resources/static/js/djb/inquiry-comments.js
eapim-portal/src/main/resources/static/js/djb/inquiry-view.js
eapim-portal/src/main/resources/static/js/djb/toast.js
eapim-portal/src/main/resources/static/js/password-policy.js
eapim-portal/src/main/resources/static/js/popup/two-factor-auth.js
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-i18n.js
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-response.js
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-snippet-panel.js
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-snippets.js
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-testbed.css
eapim-portal/src/main/resources/static/plugins/swaggerUI/djb-swagger-validator.js
eapim-portal/src/main/resources/static/sass/base/_djb-font.scss
eapim-portal/src/main/resources/static/sass/components/_board-common.scss
eapim-portal/src/main/resources/static/sass/components/_djb-inquiry-comments.scss
eapim-portal/src/main/resources/static/sass/components/_password-policy.scss
eapim-portal/src/main/resources/static/sass/components/_session-timer.scss
eapim-portal/src/main/resources/static/sass/components/_toast.scss
eapim-portal/src/main/resources/static/sass/components/_two-factor.scss
eapim-portal/src/main/resources/static/sass/pages/_api-status.scss
eapim-portal/src/main/resources/static/sass/pages/_webhook.scss
eapim-portal/src/main/resources/templates/views/apps/auth/stepupPassword.html
eapim-portal/src/main/resources/templates/views/apps/auth/twoFactorChallenge.html
eapim-portal/src/main/resources/templates/views/apps/community/djb/fragments-inquiry-comments.html
eapim-portal/src/main/resources/templates/views/apps/community/mainFaqDetail.html
eapim-portal/src/main/resources/templates/views/apps/register/signupVerificationEmail.html
eapim-portal/src/main/resources/templates/views/apps/service/oauth2-guide.html
eapim-portal/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookEmpty.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookList.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep2.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookModifyStep3.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep2.html
eapim-portal/src/main/resources/templates/views/apps/webhook/webhookRegisterStep3.html
eapim-portal/src/main/resources/templates/views/djb/apistatus/index.html
eapim-portal/src/main/resources/templates/views/djb/apistatus/issues.html
eapim-portal/src/main/resources/templates/views/fragment/api_selector.html
eapim-portal/src/main/resources/templates/views/fragment/djbank/footer.html
eapim-portal/src/main/resources/templates/views/fragment/djbank/header_container.html
eapim-portal/src/main/resources/templates/views/fragment/djbank/service_sidebar.html
eapim-portal/src/main/resources/templates/views/fragment/djbank/terms_agreements.html
elink-portal-common/src/main/java/com/eactive/apim/portal/apprequest/entity/GwAction.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncident.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentApi.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentApiId.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/DjbApistatusIncidentTimeline.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/IncidentKind.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/entity/IncidentState.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentApiRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/djb/apistatus/incident/repository/DjbApistatusIncidentTimelineRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/entity/UserRoleHistory.java
elink-portal-common/src/main/java/com/eactive/apim/portal/portaluser/repository/UserRoleHistoryRepository.java
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/InquiryComment.java
elink-portal-common/src/main/java/com/eactive/apim/portal/qna/entity/VisibilityScope.java
damo-manager/src/main/java/com/eactive/ext/djb/DamoCli.java
damo-manager/src/main/java/com/eactive/ext/djb/DamoManager.java
+26
View File
@@ -0,0 +1,26 @@
# DJB 커스텀 소스 납품 수집 설정
#
# 형식:
# MODULE|<모듈 디렉터리>|<소스 경로들, 공백 구분>
# EXT|<수집 대상 확장자, 콤마 구분>
#
# - 모듈 디렉터리는 저장소 루트(djb-eapim) 기준 상대 경로.
# - 소스 경로가 실제로 없으면 조용히 건너뛴다.
# - '#'으로 시작하는 줄과 빈 줄은 무시된다.
# - 파일 단위 포함/제외는 export-filter.rules 에서 관리한다.
MODULE|eapim-portal|src/main
#MODULE|eapim-admin|src WebContent
MODULE|elink-portal-common|src/main
MODULE|damo-manager|src
# eapim-online 은 납품 범위에서 제외한다.
# Gateway 제품(elink-online) 본체라 신규 파일 중 무엇이 DJB 커스텀이고
# 무엇이 제품 업그레이드분인지 포털 쪽에서 판정할 수 없다.
# 규칙 파일이 아니라 여기서 빼는 이유: export-filter.rules 의 '!*djb*' 예외가
# 뒤에 있어 DJBEncrypt.java 같은 파일이 되살아나기 때문.
#MODULE|eapim-online|src elink-online-common/src elink-online-core/src elink-online-core-jpa/src elink-online-emsclient/src elink-online-transformer/src
# 수집 대상 확장자 (확장자 없는 파일은 제외된다)
# EXT|java,xml,yml,yaml,properties,sql,html,jsp,jspf,tag,js,ts,css,scss,json,txt,md
EXT|java,xml,yml,yaml,properties,html,jsp,jspf,tag,js,ts,css,scss,jpg,jpeg,png,gif,svg,ico
+2 -1
View File
@@ -254,7 +254,8 @@ CREATE TABLE DVPOWN.PT_MESSAGE_RECIPIENT
)
;
create table DVPOWN.PT_TOKEN
create table PT_TOKEN
(
(
TOKEN VARCHAR2(255) not null
primary key,
@@ -0,0 +1,36 @@
-- =============================================================================
-- 세션 타이머 / 유휴 자동 로그아웃 / 중복로그인 처리용 사용자 세션 추적 테이블
-- 대상: EMS 스키마 소유자(EMSAPP 등)로 접속하여 실행
-- 엔티티: com.eactive.apim.portal.apps.session.entity.UserSession
-- =============================================================================
CREATE TABLE PTL_USER_SESSION
(
SESSION_ID VARCHAR2(128) NOT NULL,
USER_ID VARCHAR2(36) NOT NULL,
LOGIN_ID VARCHAR2(200) NOT NULL,
LOGIN_TIME TIMESTAMP(6) NOT NULL,
LAST_ACCESS_TIME TIMESTAMP(6) NOT NULL,
IP_ADDRESS VARCHAR2(45),
USER_AGENT VARCHAR2(500),
FORCE_LOGOUT CHAR(1) DEFAULT 'N' NOT NULL,
CONSTRAINT PK_PTL_USER_SESSION PRIMARY KEY (SESSION_ID)
);
-- 중복로그인 강제 로그아웃 / 활성 세션 조회는 LOGIN_ID 기준
CREATE INDEX IX_PTL_USER_SESSION_LOGIN ON PTL_USER_SESSION (LOGIN_ID);
COMMENT ON TABLE PTL_USER_SESSION IS '포털 사용자 세션 추적 (타이머/유휴 로그아웃/중복로그인)';
COMMENT ON COLUMN PTL_USER_SESSION.SESSION_ID IS 'HTTP 세션 ID (PK)';
COMMENT ON COLUMN PTL_USER_SESSION.USER_ID IS '포털 사용자 ID';
COMMENT ON COLUMN PTL_USER_SESSION.LOGIN_ID IS '로그인 ID (이메일, 소문자)';
COMMENT ON COLUMN PTL_USER_SESSION.LOGIN_TIME IS '로그인 시각';
COMMENT ON COLUMN PTL_USER_SESSION.LAST_ACCESS_TIME IS '마지막 접근 시각 (만료 판단 기준)';
COMMENT ON COLUMN PTL_USER_SESSION.IP_ADDRESS IS '접속 IP';
COMMENT ON COLUMN PTL_USER_SESSION.USER_AGENT IS 'User-Agent';
COMMENT ON COLUMN PTL_USER_SESSION.FORCE_LOGOUT IS '강제 로그아웃 플래그 (Y/N)';
-- (선택) 세션 타임아웃(분) 설정값을 미리 지정. 미삽입 시 최초 접근 때 기본값 15로 자동 생성됨.
-- INSERT INTO PTL_PROPERTY (PROPERTY_GROUP_NAME, PROPERTY_NAME, PROPERTY_VALUE, PROPERTY_DESC)
-- VALUES ('Portal', 'session.timeout.minutes', '15', '세션 타임아웃 시간 (분)');
-- COMMIT;
@@ -0,0 +1,23 @@
-- =============================================================================
-- 2FA 인증코드 조회용 결정적 해시 키 컬럼 추가
-- 대상: EMS 스키마 소유자(EMSAPP 등)로 접속하여 실행
-- 엔티티: com.eactive.apim.portal.portaluser.entity.TwoFactorAuth
--
-- 배경: RECIPIENT(이메일/휴대폰) 컬럼에 PersonalDataEncryptConverter 암호화가
-- 적용되면서, 값 동등 조회(WHERE RECIPIENT = ?)와 중복정리가 깨져
-- 인증코드 검증이 "일치하지 않습니다"로 실패함.
-- 해결: RECIPIENT 는 암호화 보관(감사/표시용)을 유지하고, 평문의 결정적 해시
-- (HMAC-SHA256, 소문자 hex 64자)를 RECIPIENT_KEY 에 저장하여 조회/정리에 사용.
--
-- ※ hibernate.hbm2ddl.auto=validate 이므로, 신규 엔티티 배포(재기동) "전"에
-- 반드시 이 스크립트를 먼저 실행해야 기동 검증을 통과함.
-- =============================================================================
ALTER TABLE PTL_TWO_FACTOR_AUTH ADD (RECIPIENT_KEY VARCHAR2(64));
-- 인증코드 조회/중복정리는 RECIPIENT_KEY 기준
CREATE INDEX IX_PTL_TWO_FACTOR_AUTH_RKEY ON PTL_TWO_FACTOR_AUTH (RECIPIENT_KEY);
COMMENT ON COLUMN PTL_TWO_FACTOR_AUTH.RECIPIENT_KEY IS '수신자 결정적 해시 조회키 (HMAC-SHA256 hex). RECIPIENT 암호화로 깨지는 동등조회/중복정리용';
COMMIT;
+1 -5
View File
@@ -4,8 +4,4 @@ include 'elink-online-core-jpa'
include 'elink-portal-common'
project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-online/elink-online-core-jpa")
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
//include 'kjb-safedb'
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
+61
View File
@@ -0,0 +1,61 @@
# SonarQube 분석 설정 (SonarScanner CLI 용)
#
# Jenkinsfile.sonar 가 이 파일을 그대로 읽고, 아래 항목만 실행 시점에 -D 로 덮어쓴다.
# sonar.projectVersion : 빌드 번호
# sonar.java.libraries : ci/sonar-classpath.gradle 이 덤프한 컴파일 클래스패스
# sonar.java.test.libraries : 위 + 테스트 클래스패스
# sonar.junit.reportPaths : 테스트를 실행한 경우에만
#
# 로컬에서 수동 분석 시(스캐너는 JDK 17 필요):
# gradle clean classes testClasses
# gradle -I ci/sonar-classpath.gradle exportSonarClasspath
# sonar-scanner -Dsonar.host.url=... -Dsonar.token=... \
# -Dsonar.java.libraries="$(cat build/sonar/java-libraries.txt)"
sonar.projectKey=djb-eapim-portal
sonar.projectName=DJB eAPIM Portal
sonar.sourceEncoding=UTF-8
# --- 분석 대상 -------------------------------------------------------------
# src/main/resources 를 포함시켜 Thymeleaf 템플릿(XSS), yml/properties(하드코딩 시크릿)까지 검사한다.
sonar.sources=src/main/java,src/main/resources
sonar.tests=src/test/java
# --- Java ------------------------------------------------------------------
# 소스는 Java 8. 스캐너 실행 JRE 는 별개로 17 이상이어야 한다(Jenkinsfile.sonar 참고).
sonar.java.source=8
sonar.java.binaries=build/classes/java/main
sonar.java.test.binaries=build/classes/java/test
# --- 제외 대상 -------------------------------------------------------------
# 서드파티 번들/컴파일 산출물/바이너리. 분석해도 우리가 고칠 수 없는 코드는 뺀다.
sonar.exclusions=\
**/build/**,\
**/node_modules/**,\
**/lib/**,\
**/libs/**,\
**/local_jars/**,\
src/main/resources/static/plugins/**,\
src/main/resources/static/js/egovframework/**,\
src/main/resources/static/js/**/*.min.js,\
src/main/resources/static/js/lodash.js,\
src/main/resources/static/js/popper.js,\
src/main/resources/static/js/daterangepicker.js,\
src/main/resources/static/js/json-lint.js,\
src/main/resources/static/html/**,\
src/main/resources/static/css/**,\
src/main/resources/static/sass/**,\
src/main/resources/static/font/**,\
src/main/resources/static/webfonts/**,\
src/main/resources/static/img/**
# 자동 생성 코드(MapStruct Impl, QueryDSL Q클래스)는 build/ 아래라 이미 제외된다.
# --- SCM -------------------------------------------------------------------
# blame 기반 "새 코드" 판정을 위해 Jenkins Job 에서 shallow clone 을 쓰지 않는다.
sonar.scm.provider=git
# --- 브랜치 ----------------------------------------------------------------
# sonar.branch.name 은 SonarQube Developer Edition 이상에서만 동작한다.
# Community Edition 에서 지정하면 분석이 실패하므로 기본적으로 설정하지 않는다.
# sonar.branch.name=feats/ci-test
@@ -0,0 +1,72 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* API Gateway 일별 처리 통계 (AGWAPP.API_STATS_DAY) — 읽기 전용.
*
* 월별 조회 시 "현재 월"은 아직 API_STATS_MONTH 에 집계되지 않으므로(월 집계 잡은 익월 1일 실행)
* 현재 월분은 이 DAY 테이블에서 조회한다.
* 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
*/
@Getter
@Setter
@Entity
@Table(name = "API_STATS_DAY")
@IdClass(ApiStatsDayId.class)
public class ApiStatsDay implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "STAT_TIME", nullable = false)
private LocalDate statTime;
@Id
@Column(name = "API_NAME", nullable = false, length = 100)
private String apiName;
@Id
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
private String gwInstanceId;
@Id
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
private String bizDivCode = "NONE";
@Id
@Column(name = "CLIENT_ID", nullable = false, length = 100)
private String clientId = "NONE";
@Id
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
private String inboundAdapter = "NONE";
@Id
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
private String outboundAdapter = "NONE";
@Column(name = "TOTAL_CNT", nullable = false)
private Long totalCnt = 0L;
@Column(name = "SUCCESS_CNT", nullable = false)
private Long successCnt = 0L;
@Column(name = "TIMEOUT_CNT", nullable = false)
private Long timeoutCnt = 0L;
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
private Long systemErrCnt = 0L;
@Column(name = "BIZ_ERR_CNT", nullable = false)
private Long bizErrCnt = 0L;
}
@@ -0,0 +1,26 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 통계 일별 복합키
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiStatsDayId implements Serializable {
private static final long serialVersionUID = 1L;
private LocalDate statTime;
private String apiName;
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String inboundAdapter;
private String outboundAdapter;
}
@@ -0,0 +1,71 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* API Gateway 시간별 처리 통계 (AGWAPP.API_STATS_HOUR).
*
* 스키마명은 소스에 하드코딩하지 않는다 — gateway datasource 의 hibernate.default_schema(AGWAPP)로 해석된다.
* 포털은 읽기 전용으로만 사용한다(집계 주체는 eapim-admin 스케줄러). 사용 컬럼(카운트)만 매핑한다.
*/
@Getter
@Setter
@Entity
@Table(name = "API_STATS_HOUR")
@IdClass(ApiStatsHourId.class)
public class ApiStatsHour implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "STAT_TIME", nullable = false)
private LocalDateTime statTime;
@Id
@Column(name = "API_NAME", nullable = false, length = 100)
private String apiName;
@Id
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
private String gwInstanceId;
@Id
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
private String bizDivCode = "NONE";
@Id
@Column(name = "CLIENT_ID", nullable = false, length = 100)
private String clientId = "NONE";
@Id
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
private String inboundAdapter = "NONE";
@Id
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
private String outboundAdapter = "NONE";
@Column(name = "TOTAL_CNT", nullable = false)
private Long totalCnt = 0L;
@Column(name = "SUCCESS_CNT", nullable = false)
private Long successCnt = 0L;
@Column(name = "TIMEOUT_CNT", nullable = false)
private Long timeoutCnt = 0L;
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
private Long systemErrCnt = 0L;
@Column(name = "BIZ_ERR_CNT", nullable = false)
private Long bizErrCnt = 0L;
}
@@ -0,0 +1,26 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 통계 시간별 복합키
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiStatsHourId implements Serializable {
private static final long serialVersionUID = 1L;
private LocalDateTime statTime;
private String apiName;
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String inboundAdapter;
private String outboundAdapter;
}
@@ -0,0 +1,70 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* API Gateway 월별 처리 통계 (AGWAPP.API_STATS_MONTH).
*
* STAT_TIME 은 YYYYMM 문자열. 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
* 포털은 읽기 전용으로만 사용한다. 사용 컬럼(카운트)만 매핑한다.
*/
@Getter
@Setter
@Entity
@Table(name = "API_STATS_MONTH")
@IdClass(ApiStatsMonthId.class)
public class ApiStatsMonth implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "STAT_TIME", nullable = false, length = 6)
private String statTime; // YYYYMM format
@Id
@Column(name = "API_NAME", nullable = false, length = 100)
private String apiName;
@Id
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
private String gwInstanceId;
@Id
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
private String bizDivCode = "NONE";
@Id
@Column(name = "CLIENT_ID", nullable = false, length = 100)
private String clientId = "NONE";
@Id
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
private String inboundAdapter = "NONE";
@Id
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
private String outboundAdapter = "NONE";
@Column(name = "TOTAL_CNT", nullable = false)
private Long totalCnt = 0L;
@Column(name = "SUCCESS_CNT", nullable = false)
private Long successCnt = 0L;
@Column(name = "TIMEOUT_CNT", nullable = false)
private Long timeoutCnt = 0L;
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
private Long systemErrCnt = 0L;
@Column(name = "BIZ_ERR_CNT", nullable = false)
private Long bizErrCnt = 0L;
}
@@ -0,0 +1,25 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 통계 월별 복합키
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiStatsMonthId implements Serializable {
private static final long serialVersionUID = 1L;
private String statTime; // YYYYMM format
private String apiName;
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String inboundAdapter;
private String outboundAdapter;
}
@@ -0,0 +1,36 @@
package com.eactive.apim.gateway.data.statistics.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.LocalDateTime;
/**
* API 상태 모니터링 결과 (AGWAPP.API_STATUS).
*
* <p>eapim-admin 의 {@code ApiStatusMonitorJob} 이 상태 변화가 있을 때만 upsert 한다.
* 포털은 읽기 전용으로 "마지막 상태 변경 시각" 표시에 사용한다.</p>
*/
@Entity
@Table(name = "API_STATUS")
@Data
public class GwApiStatus {
/** EAI 서비스명 */
@Id
@Column(name = "EAISVCNAME", length = 30)
private String eaisvcname;
/** N 정상 / C 점검 / D 지연 / E 장애 */
@Column(name = "STATUS_CODE", length = 1)
private String statusCode;
@Column(name = "MODIFIED_BY", length = 20)
private String modifiedBy;
@Column(name = "MODIFIED_DATE")
private LocalDateTime modifiedDate;
}
@@ -0,0 +1,41 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* GW 인증 Client 정보 (AGWAPP.TSEAIAU01) — 읽기 전용.
*
* API 통계의 org 스코핑에 사용한다: 통계 테이블에는 org_id 가 없으므로
* TSEAIAU01.ORGID 로 org 의 CLIENTID 목록을 구해 API_STATS_*.CLIENT_ID 와 매칭한다.
* (admin ApiUseStatsService 도 API_STATS.CLIENT_ID = TSEAIAU01.CLIENTID 로 조인)
*
* 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
*/
@Getter
@Setter
@Entity
@Table(name = "TSEAIAU01")
public class GwAuthClient implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "CLIENTID", nullable = false, length = 256)
private String clientId;
@Column(name = "CLIENTNAME", length = 150)
private String clientName;
@Column(name = "ORGID", length = 38)
private String orgId;
@Column(name = "APPSTATUS", length = 1)
private String appStatus;
}
@@ -0,0 +1,33 @@
package com.eactive.apim.gateway.data.statistics.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* GW 인터페이스(서비스) 정보 (AGWAPP.TSEAIHE01) — 읽기 전용.
*
* API_STATS_*.API_NAME 은 인터페이스 ID(=EAISVCNAME)이며, 화면 표시용 실제 명칭은 EAISVCDESC 다.
* (admin ApiUseStatsService 도 API_STATS.API_NAME = TSEAIHE01.EAISVCNAME 으로 조인)
* 스키마명은 소스에 하드코딩하지 않는다(gateway default_schema=AGWAPP).
*/
@Getter
@Setter
@Entity
@Table(name = "TSEAIHE01")
public class GwSvcInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "EAISVCNAME", nullable = false, length = 30)
private String svcName; // 인터페이스 ID (= API_STATS.API_NAME)
@Column(name = "EAISVCDESC", length = 400)
private String svcDesc; // 인터페이스 표시 명칭
}
@@ -0,0 +1,66 @@
package com.eactive.apim.gateway.data.statistics.repository;
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsDay;
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsDayId;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
/**
* API_STATS_DAY(AGWAPP) 조회 리포지토리.
* 월별 조회의 "현재 월" 분(아직 API_STATS_MONTH 미집계)을 일 범위로 합산하는 데 사용한다.
*/
public interface ApiStatsDayRepository extends JpaRepository<ApiStatsDay, ApiStatsDayId> {
/**
* 전체 요약 (일자 범위, 지정 clientId 집합).
*/
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), COALESCE(SUM(s.timeoutCnt), 0), " +
"COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
"FROM ApiStatsDay s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startDate AND :endDate")
ApiStatisticsSummaryDto findSummary(
@Param("clientIds") List<String> clientIds,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
/**
* API별 통계 (API_NAME 단위 집계).
*/
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
"s.apiName, COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
"FROM ApiStatsDay s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startDate AND :endDate " +
"GROUP BY s.apiName " +
"ORDER BY SUM(s.totalCnt) DESC")
List<ApiStatisticsDetailDto> findDetail(
@Param("clientIds") List<String> clientIds,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
/**
* 일자별 누적 통계 (월별 조회 하단 표에 사용 — 월별이라도 일별로 표기).
* 생성자 표현식 안의 FUNCTION 은 Hibernate 5.6 이 타입 해석을 못 하므로 Object[] 로 받는다.
* row: [0]=일자(String yyyy-MM-dd), [1]=total, [2]=success, [3]=timeout, [4]=systemErr, [5]=biz
*/
@Query("SELECT FUNCTION('TO_CHAR', s.statTime, 'YYYY-MM-DD'), " +
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0) " +
"FROM ApiStatsDay s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startDate AND :endDate " +
"GROUP BY FUNCTION('TO_CHAR', s.statTime, 'YYYY-MM-DD') " +
"ORDER BY FUNCTION('TO_CHAR', s.statTime, 'YYYY-MM-DD')")
List<Object[]> findPeriodByDay(
@Param("clientIds") List<String> clientIds,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
}
@@ -0,0 +1,56 @@
package com.eactive.apim.gateway.data.statistics.repository;
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsHour;
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsHourId;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
/**
* API_STATS_HOUR(AGWAPP) 조회 리포지토리.
*
* 통계 수치는 보존기간이 긴 API_STATS_DAY 를 사용하되, "오늘"은 아직 DAY 로 집계되지 않았으므로
* (일 집계 잡은 익일 실행) 오늘분만 HOUR 에서 합산해 DAY 결과에 더한다. 최신 집계 시각도 HOUR 로 산출.
*/
public interface ApiStatsHourRepository extends JpaRepository<ApiStatsHour, ApiStatsHourId> {
/**
* 전체 요약 (시각 범위, 지정 clientId 집합) — 오늘분 합산용.
*/
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), COALESCE(SUM(s.timeoutCnt), 0), " +
"COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
"FROM ApiStatsHour s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startTime AND :endTime")
ApiStatisticsSummaryDto findSummary(
@Param("clientIds") List<String> clientIds,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* API별 통계 (API_NAME 단위 집계) — 오늘분 합산용.
*/
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
"s.apiName, COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
"FROM ApiStatsHour s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startTime AND :endTime " +
"GROUP BY s.apiName " +
"ORDER BY SUM(s.totalCnt) DESC")
List<ApiStatisticsDetailDto> findDetail(
@Param("clientIds") List<String> clientIds,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 최신 집계 시각 (안내 문구용). 데이터 없으면 null.
*/
@Query("SELECT MAX(s.statTime) FROM ApiStatsHour s WHERE s.clientId IN :clientIds")
LocalDateTime findLatestStatTime(@Param("clientIds") List<String> clientIds);
}
@@ -0,0 +1,54 @@
package com.eactive.apim.gateway.data.statistics.repository;
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto;
import com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto;
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsMonth;
import com.eactive.apim.gateway.data.statistics.entity.ApiStatsMonthId;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
/**
* API_STATS_MONTH(AGWAPP) 통계 조회 리포지토리 (월별 모드).
* STAT_TIME 은 YYYYMM 문자열이므로 문자열 범위 비교로 조회한다.
*/
public interface ApiStatsMonthRepository extends JpaRepository<ApiStatsMonth, ApiStatsMonthId> {
/**
* 전체 요약 (YYYYMM 범위, 지정 clientId 집합).
*/
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsSummaryDto(" +
"COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), COALESCE(SUM(s.timeoutCnt), 0), " +
"COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
"FROM ApiStatsMonth s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startMonth AND :endMonth")
ApiStatisticsSummaryDto findSummary(
@Param("clientIds") List<String> clientIds,
@Param("startMonth") String startMonth,
@Param("endMonth") String endMonth);
/**
* API별 통계 (API_NAME 단위 집계).
*/
@Query("SELECT new com.eactive.apim.portal.apps.statistics.dto.ApiStatisticsDetailDto(" +
"s.apiName, COALESCE(SUM(s.totalCnt), 0), COALESCE(SUM(s.successCnt), 0), " +
"COALESCE(SUM(s.timeoutCnt), 0), COALESCE(SUM(s.systemErrCnt), 0), COALESCE(SUM(s.bizErrCnt), 0)) " +
"FROM ApiStatsMonth s " +
"WHERE s.clientId IN :clientIds " +
"AND s.statTime BETWEEN :startMonth AND :endMonth " +
"GROUP BY s.apiName " +
"ORDER BY SUM(s.totalCnt) DESC")
List<ApiStatisticsDetailDto> findDetail(
@Param("clientIds") List<String> clientIds,
@Param("startMonth") String startMonth,
@Param("endMonth") String endMonth);
/**
* 조회 가능한 월 목록 (YYYYMM, 최신순).
*/
@Query("SELECT DISTINCT s.statTime FROM ApiStatsMonth s " +
"WHERE s.clientId IN :clientIds ORDER BY s.statTime DESC")
List<String> findAvailableMonths(@Param("clientIds") List<String> clientIds);
}
@@ -0,0 +1,17 @@
package com.eactive.apim.gateway.data.statistics.repository;
import com.eactive.apim.gateway.data.statistics.entity.GwApiStatus;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.time.LocalDateTime;
import java.util.Optional;
public interface GwApiStatusRepository extends Repository<GwApiStatus, String> {
/**
* API 상태가 마지막으로 변경된 시각. 데이터가 없으면 empty.
*/
@Query("SELECT MAX(s.modifiedDate) FROM GwApiStatus s")
Optional<LocalDateTime> findLastModifiedDate();
}
@@ -0,0 +1,25 @@
package com.eactive.apim.gateway.data.statistics.repository;
import com.eactive.apim.gateway.data.statistics.entity.GwAuthClient;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
/**
* TSEAIAU01(AGWAPP 인증 Client) 조회 — API 통계 org 스코핑용.
* PTL_ORG.ID == TSEAIAU01.ORGID (1:N), TSEAIAU01.CLIENTID == API_STATS_*.CLIENT_ID.
*/
public interface GwAuthClientRepository extends JpaRepository<GwAuthClient, String> {
/**
* org 소속 클라이언트 목록 (앱 선택 드롭다운용).
*/
List<GwAuthClient> findByOrgIdOrderByClientNameAsc(String orgId);
/**
* org 소속 CLIENTID 목록 (통계 필터용).
*/
@Query("SELECT c.clientId FROM GwAuthClient c WHERE c.orgId = :orgId")
List<String> findClientIdsByOrgId(@Param("orgId") String orgId);
}
@@ -0,0 +1,14 @@
package com.eactive.apim.gateway.data.statistics.repository;
import com.eactive.apim.gateway.data.statistics.entity.GwSvcInfo;
import java.util.Collection;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* TSEAIHE01(AGWAPP 인터페이스 정보) 조회 — API_NAME(인터페이스 ID) → 표시명(EAISVCDESC) 매핑용.
*/
public interface GwSvcInfoRepository extends JpaRepository<GwSvcInfo, String> {
List<GwSvcInfo> findBySvcNameIn(Collection<String> svcNames);
}
@@ -22,6 +22,10 @@ public class PortalApplication extends SpringBootServletInitializer {
private static final Logger portal_logger = LoggerFactory.getLogger(PortalApplication.class);
public PortalApplication() {
super();
}
public static void main(String[] args) {
portal_logger.info("##### PortalApplication Start #####");
@@ -0,0 +1,67 @@
package com.eactive.apim.portal.apps;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Readiness probe.
*
* <p>Distinct from {@link HealthCheckController} (liveness — servlet alive?).
* This endpoint validates both EMS and Gateway datasources via JDBC
* {@code Connection.isValid(timeout)} to confirm the app is ready to serve
* requests that depend on the database.
*
* <p>HTTP 200 + JSON when all checks pass.
* <p>HTTP 503 + JSON when any check fails — body still includes the per-component
* status so CI/CD logs the failing dependency.
*/
@RestController
public class ReadinessController {
private static final int VALIDATION_TIMEOUT_SECONDS = 2;
private final DataSource portalDataSource;
private final DataSource gatewayDataSource;
public ReadinessController(
@Qualifier("portalDataSource") DataSource portalDataSource,
@Qualifier("gatewayDataSource") DataSource gatewayDataSource) {
this.portalDataSource = portalDataSource;
this.gatewayDataSource = gatewayDataSource;
}
@GetMapping(value = "/health/ready", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> ready() {
String ems = check(portalDataSource);
String gw = check(gatewayDataSource);
boolean ok = "UP".equals(ems) && "UP".equals(gw);
String body = "{"
+ "\"status\":\"" + (ok ? "UP" : "DOWN") + "\","
+ "\"ems\":\"" + ems + "\","
+ "\"gateway\":\""+ gw + "\""
+ "}";
return ResponseEntity.status(ok ? 200 : 503)
.contentType(MediaType.APPLICATION_JSON)
.body(body);
}
private static String check(DataSource ds) {
if (ds == null) return "DOWN:NO_DATASOURCE";
try (Connection c = ds.getConnection()) {
return c.isValid(VALIDATION_TIMEOUT_SECONDS) ? "UP" : "DOWN:INVALID";
} catch (SQLException e) {
String msg = e.getMessage() == null ? "" : e.getMessage();
return "DOWN:" + msg.replace('"', '\'').replace('\n', ' ').replace('\r', ' ');
}
}
}
@@ -15,7 +15,11 @@ import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/agreements")
public class AgreementsController {
public static final String TERMS_AGREEMENTS = "fragment/kjbank/terms_agreements";
public static final String TERMS_AGREEMENTS = "fragment/djbank/terms_agreements";
/** 개인정보처리방침은 약관 페이지에서 분리되어 제주은행 공식 사이트 외부 링크로 대체됨 */
public static final String PRIVACY_POLICY_EXTERNAL_URL =
"https://www.jejubank.co.kr/hmpg/csct/secuCenr/ptctPlcy/procsPlcy/ctnt.do";
private final AgreementsFacade agreementsFacade;
@@ -28,28 +32,49 @@ public class AgreementsController {
public String showTerms(@RequestParam(required = false) String tab,
@RequestParam(required = false) String publishedOn,
Model model) {
String currentTab = tab != null ? tab : "terms";
// 구 개인정보처리방침 탭(tab=privacy)은 외부 링크로 이동했으므로 외부 URL로 리다이렉트(북마크 호환)
if ("privacy".equals(currentTab)) {
return "redirect:" + PRIVACY_POLICY_EXTERNAL_URL;
}
AgreementType type;
if ("privacy".equals(tab)) {
type = AgreementType.PRIVACY_POLICY;
} else {
type = AgreementType.TERMS_OF_USE;
switch (currentTab) {
case "privacy-collect":
// 개인정보수집동의서 = PRIVACY_COLLECT (신설항목)
type = AgreementType.PRIVACY_COLLECT;
break;
case "notification":
type = AgreementType.NOTIFICATION_CONSENT;
break;
case "terms":
default:
currentTab = "terms";
type = AgreementType.TERMS_OF_USE;
break;
}
List<AgreementsDTO> agreementsList = agreementsFacade.getAgreementsList(String.valueOf(type));
model.addAttribute("agreementsList", agreementsList);
AgreementsDTO selectedAgreement = publishedOn != null && !publishedOn.isEmpty() ?
findAgreementByDate(agreementsList, publishedOn) : agreementsList.get(0);
// 해당 약관 종류가 아직 시드되지 않았을 수 있으므로 빈 리스트 방어
AgreementsDTO selectedAgreement = null;
if (agreementsList != null && !agreementsList.isEmpty()) {
selectedAgreement = publishedOn != null && !publishedOn.isEmpty() ?
findAgreementByDate(agreementsList, publishedOn) : agreementsList.get(0);
}
model.addAttribute("selectedAgreement", selectedAgreement);
model.addAttribute("selectedDate", publishedOn);
model.addAttribute("isTermsOfUse", type == AgreementType.TERMS_OF_USE);
model.addAttribute("isPrivacyPolicy", type == AgreementType.PRIVACY_POLICY);
model.addAttribute("isPrivacyCollect", type == AgreementType.PRIVACY_COLLECT);
model.addAttribute("isNotification", type == AgreementType.NOTIFICATION_CONSENT);
model.addAttribute("agreementTitle", type.getDescription());
model.addAttribute("agreementType", type.getCode());
model.addAttribute("currentTab", tab != null ? tab : "terms");
model.addAttribute("currentTab", currentTab);
return TERMS_AGREEMENTS;
}
@@ -71,8 +71,9 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
throw new NotFoundException("현재 시행중인 이용약관이 없습니다.");
}
// 2. 개인정보수집동의서 처리
if (privacyCollectType != null && privacyCollectType.isPrivacyCollect()) {
// 2. 개인정보수집동의서 처리 (개인정보수집동의서 = PRIVACY_COLLECT 신설항목 포함)
if (privacyCollectType != null
&& (privacyCollectType.isPrivacyCollect() || privacyCollectType == AgreementType.PRIVACY_COLLECT)) {
Optional<Agreements> privacyCollect = agreementsService
.findLastesBeforeDate(
privacyCollectType,
@@ -199,6 +200,10 @@ public class AgreementsFacadeImpl implements AgreementsFacade {
defaultAgreement.setName("개인정보처리방침");
defaultAgreement.setContents("현재 개인정보처리방침을 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
break;
case "PRIVACY_COLLECT":
defaultAgreement.setName("개인정보수집동의서");
defaultAgreement.setContents("현재 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
break;
case "PRIVACY_COLLECT_IND":
defaultAgreement.setName("개인용 개인정보수집동의서");
defaultAgreement.setContents("현재 개인용 개인정보수집동의서를 불러올 수 없습니다. 잠시 후 다시 시도해 주세요.");
@@ -8,6 +8,8 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
import com.eactive.apim.portal.common.exception.NotFoundException;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.djb.apistatus.service.ApiStatusCatalogService;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -30,6 +32,7 @@ public class ApiController {
private final ApiService apiService;
private final ApiServiceService apiServiceService;
private final ApiSearchFacade apiSearchFacade;
private final ApiStatusCatalogService apiStatusCatalogService;
private static final String DEFAULT_TOKEN_API_ID = "default-token-api-spec";
private static final String DEFAULT_TOKEN_API_NAME = "인증";
@@ -38,7 +41,24 @@ public class ApiController {
if (id == null) {
return "redirect:/apis/common";
}
populateDetailModel(id, model);
model.addAttribute("activeTab", "api-info");
return "apps/apis/mainApiDetail";
}
// 테스트베드를 API 정보와 별도 URL로 분리(딥링크·북마크 가능). 미인증 사용자도 페이지 진입은
// 허용하되, 실제 테스트베드(Swagger)는 인증 사용자에게만 렌더하고 미인증에는 로그인 안내를 노출한다.
@GetMapping("/detail/testbed")
public String apidetailTestbed(@RequestParam(value = "id", required = false) String id, ModelMap model) {
if (id == null) {
return "redirect:/apis/common";
}
populateDetailModel(id, model);
model.addAttribute("activeTab", "testbed");
return "apps/apis/mainApiDetail";
}
private void populateDetailModel(String id, ModelMap model) {
ApiSpecInfoDto api = apiService.selectDetail(id);
if (api == null) {
throw new NotFoundException(NOT_FOUND_MESSAGE);
@@ -46,10 +66,16 @@ public class ApiController {
Map<String, Object> searchResult = apiSearchFacade.searchApis(new ApiGroupSearch());
// 상세 타이틀에 노출할 현재 API의 그룹명 세팅(selectDetail은 apiGroupName을 채우지 않음)
ApiServiceDTO apiGroup = apiServiceService.findApiServiceByApiId(id);
if (apiGroup != null) {
api.setApiGroupName(apiGroup.getGroupName());
}
model.addAttribute("apiSpecInfo", api);
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
model.addAttribute("services", searchResult.get("services"));
return "apps/apis/mainApiDetail";
model.addAttribute("authenticated", SecurityUtil.isAuthenticated());
}
@GetMapping
@@ -62,12 +88,18 @@ public class ApiController {
model.addAttribute("totalApiCount", searchResult.get("totalApiCount"));
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1");
// 카드의 현재 상태 태그 노출 여부 (PTL_PROPERTY djb.apistatus.api-list-status-badge)
model.addAttribute("apiStatusBadgeEnabled", apiStatusCatalogService.isApiListStatusBadgeEnabled());
return "apps/apis/mainApiList";
}
@GetMapping("/testbed/api")
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
if (!SecurityUtil.isAuthenticated()) {
return "redirect:/login?reason=auth";
}
String selectedApiServiceName = "API 서비스 선택";
String selectedServiceId = "";
boolean idExists = false;
@@ -93,6 +125,10 @@ public class ApiController {
@GetMapping("/testbed")
public String testbedByApiService(@RequestParam(value = "id", required = false) String id, Model model) {
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
if (!SecurityUtil.isAuthenticated()) {
return "redirect:/login?reason=auth";
}
String selectedApiServiceName = "API 서비스 선택";
boolean idExists = false;
@@ -2,15 +2,15 @@ package com.eactive.apim.portal.apps.apis.controller;
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
import com.eactive.apim.portal.apps.apis.service.ApiService;
import com.eactive.apim.portal.apps.apiservice.service.ApiSpecService;
import com.eactive.apim.portal.djb.testbed.service.DjbTestbedSpecServerRewriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
@@ -22,35 +22,46 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/apis")
@RequiredArgsConstructor
@Slf4j
public class TestbedSpecController {
@Autowired
private ApiSpecInfoService apiSpecInfoService;
private final ApiSpecInfoService apiSpecInfoService;
private final DjbTestbedSpecServerRewriter serverRewriter;
private static final String DEFAULT_TOKEN_API_ID = "default-token-api-spec";
private static final String DEFAULT_SPEC_PATH = "swagger/default-token-api-spec.json";
@GetMapping(value = "/{id}/swagger.json", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> getSwagger(@PathVariable String id) {
public ResponseEntity<String> getSwagger(@PathVariable String id, HttpServletRequest request) {
String json = buildSpecJson(id, request);
return json == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(json);
}
@GetMapping(value = "/{id}/swagger.yaml", produces = "application/x-yaml")
public ResponseEntity<String> getSwaggerYaml(@PathVariable String id, HttpServletRequest request) {
String json = buildSpecJson(id, request);
return json == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(serverRewriter.toYaml(json));
}
/** default 토큰 spec 또는 저장 spec(서버 → GW 주소 치환)을 JSON 으로 반환. 없으면 null. */
private String buildSpecJson(String id, HttpServletRequest request) {
if (DEFAULT_TOKEN_API_ID.equals(id)) {
try {
// 기본 토큰 spec 은 서버 치환 대상이 아니다. path 가 포탈 mock 토큰 경로라
// GW 호스트를 붙이면 실재하지 않는 주소가 된다(servers 없음 → 문서 origin 사용).
Resource resource = new ClassPathResource(DEFAULT_SPEC_PATH);
String content = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
return ResponseEntity.ok().body(content);
return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("Failed to read default token api spec file", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
return null;
}
}
Optional<ApiSpecInfo> spec = apiSpecInfoService.findById(id);
if (!spec.isPresent()) {
StringUtils.hasText(spec.get().getTestbedSpec());
if (!spec.isPresent() || !StringUtils.hasText(spec.get().getTestbedSpec())) {
return null;
}
return ResponseEntity.ok().body(spec.get().getTestbedSpec());
return serverRewriter.rewriteServerToGateway(spec.get().getTestbedSpec(), request);
}
}
@@ -1,5 +1,7 @@
package com.eactive.apim.portal.apps.apis.filter;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.djb.testbed.config.DjbTestbedGatewayProperty;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -10,6 +12,9 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,15 +25,85 @@ public class APISender {
private static final Logger logger = LoggerFactory.getLogger(APISender.class);
public String requestPost(String uri, String requestBody) throws IOException {
/** 응답 charset 을 Content-Type 에서 얻지 못할 때 사용할 기본값. */
private static final String DEFAULT_CHARSET = "UTF-8";
/**
* 프록시 대상(GW/mock) 응답을 상태코드까지 포함해 전달하기 위한 홀더.
*
* <p>본문만 반환하면 대상이 4xx/5xx 를 내려도 호출측이 200 으로 되돌려주게 되므로
* 상태코드와 Content-Type 을 함께 담는다.</p>
*/
public static class ApiResponse {
private final int status;
private final String body;
private final String contentType;
private final Map<String, List<String>> headers;
public ApiResponse(int status, String body, String contentType, Map<String, List<String>> headers) {
this.status = status;
this.body = body;
this.contentType = contentType;
this.headers = headers == null
? Collections.<String, List<String>>emptyMap()
: Collections.unmodifiableMap(headers);
}
public int getStatus() {
return status;
}
public String getBody() {
return body;
}
/** 대상 응답의 Content-Type 원본 (없으면 null). */
public String getContentType() {
return contentType;
}
/** 대상 응답 헤더 (상태줄 의사헤더인 null 키는 제외). 값은 헤더당 복수 가능. */
public Map<String, List<String>> getHeaders() {
return headers;
}
@Override
public String toString() {
return "ApiResponse{status=" + status + ", contentType=" + contentType
+ ", bodyLen=" + (body == null ? 0 : body.length())
+ ", headers=" + headers.keySet() + "}";
}
}
// 테스트베드 프록시 연결/응답 타임아웃 — DjbTestbedGatewayProperty(djb.gateway.timeout, 단위: 초) 단일 기준.
private final DjbTestbedGatewayProperty gatewayProperty;
public APISender(DjbTestbedGatewayProperty gatewayProperty) {
this.gatewayProperty = gatewayProperty;
}
/** connect/read 타임아웃 적용. 반드시 connect(getOutputStream/getResponseCode) 이전에 호출. */
private void applyTimeout(HttpURLConnection connection) {
int t = gatewayProperty.timeoutMillis();
connection.setConnectTimeout(t);
connection.setReadTimeout(t);
}
public ApiResponse requestPost(String uri, String requestBody) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("APISender POST(json) 요청 - uri={}, bodyLen={}, body={}",
uri, requestBody == null ? 0 : requestBody.length(), StringMaskingUtil.maskFormBody(requestBody));
}
HttpURLConnection connection = getHttpURLConnection(uri, requestBody);
String response = getResponse(connection);
ApiResponse response = getResponse(connection);
connection.disconnect();
if (logger.isDebugEnabled()) {
logger.debug(response);
logger.debug("APISender POST(json) 응답 - uri={}, status={}, response={}", uri, response.getStatus(), response.getBody());
}
return response;
}
@@ -54,10 +129,11 @@ public class APISender {
return uriBuilder.toString();
}
public String requestGet(String uri, Map<String, String> headers, Map<String, String[]> params) throws IOException {
public ApiResponse requestGet(String uri, Map<String, String> headers, Map<String, String[]> params) throws IOException {
URL endpoint = new URL(appendUriAndParams(uri, params));
HttpURLConnection connection = (HttpURLConnection) endpoint.openConnection();
applyTimeout(connection);
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
for (Map.Entry<String, String> entry : headers.entrySet()) {
@@ -69,19 +145,24 @@ public class APISender {
}
connection.setDoOutput(true);
String response = getResponse(connection);
if (logger.isDebugEnabled()) {
logger.debug("APISender GET 요청 - uri={}", appendUriAndParams(uri, params));
}
ApiResponse response = getResponse(connection);
connection.disconnect();
if (logger.isDebugEnabled()) {
logger.debug(response);
logger.debug("APISender GET 응답 - uri={}, status={}, response={}", uri, response.getStatus(), response.getBody());
}
return response;
}
public String requestPost(String uri, Map<String, String> headers, Map<String, String[]> params, String requestBody) throws IOException {
public ApiResponse requestPost(String uri, Map<String, String> headers, Map<String, String[]> params, String requestBody) throws IOException {
URL endpoint = new URL(appendUriAndParams(uri, params));
HttpURLConnection connection = (HttpURLConnection) endpoint.openConnection();
applyTimeout(connection);
connection.setRequestMethod("POST");
for (Map.Entry<String, String> entry : headers.entrySet()) {
@@ -93,42 +174,86 @@ public class APISender {
}
connection.setDoOutput(true);
if (logger.isDebugEnabled()) {
// client_secret 등 민감 파라미터는 마스킹. body 비어있으면 상위에서 본문 전송 유실.
logger.debug("APISender POST 요청 - uri={}, bodyLen={}, body={}",
uri, requestBody == null ? 0 : requestBody.length(), StringMaskingUtil.maskFormBody(requestBody));
}
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
byte[] requestBodyBytes = requestBody.getBytes(StandardCharsets.UTF_8);
outputStream.write(requestBodyBytes);
outputStream.flush();
}
String response = getResponse(connection);
ApiResponse response = getResponse(connection);
connection.disconnect();
if (logger.isDebugEnabled()) {
logger.debug(response);
logger.debug("APISender POST 응답 - uri={}, status={}, response={}", uri, response.getStatus(), response.getBody());
}
return response;
}
private static String getResponse(HttpURLConnection connection) throws IOException {
/**
* 대상 응답을 상태코드·Content-Type·본문으로 읽는다.
*
* <p>4xx/5xx 는 {@code getInputStream()} 이 IOException 을 던지므로 errorStream 으로 본문을 읽고,
* 본문이 아예 없는 응답(errorStream == null)은 빈 문자열로 처리한다.</p>
*/
private static ApiResponse getResponse(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();
String contentType = connection.getContentType();
StringBuilder response = new StringBuilder();
try (InputStream stream = (responseCode < 400) ? connection.getInputStream() : connection.getErrorStream(); InputStreamReader isr = new InputStreamReader(stream);
BufferedReader reader = new BufferedReader(isr)) {
InputStream stream = (responseCode < 400) ? connection.getInputStream() : connection.getErrorStream();
if (stream != null) {
try (InputStreamReader isr = new InputStreamReader(stream, charsetOf(contentType));
BufferedReader reader = new BufferedReader(isr)) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
}
}
return response.toString();
return new ApiResponse(responseCode, response.toString(), contentType, copyHeaders(connection));
}
private static HttpURLConnection getHttpURLConnection(String uri, String requestBody) throws IOException {
/** 대상 응답 헤더 복사. {@code getHeaderFields()} 의 null 키(상태줄)는 제외. */
private static Map<String, List<String>> copyHeaders(HttpURLConnection connection) {
Map<String, List<String>> headers = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
if (entry.getKey() != null) {
headers.put(entry.getKey(), entry.getValue());
}
}
return headers;
}
/** Content-Type 의 charset 파라미터를 파싱. 없거나 인식 불가면 UTF-8. */
private static String charsetOf(String contentType) {
if (contentType != null) {
for (String part : contentType.split(";")) {
String token = part.trim();
if (token.toLowerCase().startsWith("charset=")) {
String charset = token.substring("charset=".length()).replace("\"", "").trim();
if (!charset.isEmpty() && java.nio.charset.Charset.isSupported(charset)) {
return charset;
}
}
}
}
return DEFAULT_CHARSET;
}
private HttpURLConnection getHttpURLConnection(String uri, String requestBody) throws IOException {
URL endpoint = new URL(uri);
HttpURLConnection connection = (HttpURLConnection) endpoint.openConnection();
applyTimeout(connection);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
@@ -0,0 +1,294 @@
package com.eactive.apim.portal.apps.apis.filter;
import com.eactive.apim.portal.common.util.HttpRequestUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* API 테스트베드(/api/call-api) 감사(audit) 로그 기록기.
*
* <p>logback 의 {@code eapim.portal.apitester.audit} 로거(전용 파일, 1년 보관)로 기록한다.
* 요청 1건당 REQ/RES 두 줄을 같은 auditId 로 남긴다.</p>
*
* <p>마스킹 정책:</p>
* <ul>
* <li>secret 계열 키(client_secret, password, api_key, authorization 등)의 값은 전체 마스킹</li>
* <li>그 외 파라미터/JSON 값은 앞 일부만 남기고 마스킹</li>
* <li>JSON 이 아닌 본문은 전체 길이(byte)와 앞 {@value #NON_JSON_PREVIEW_LENGTH}글자만 남기고 마스킹</li>
* </ul>
*/
public final class ApiTesterAuditLogger {
private static final Logger auditLogger = LoggerFactory.getLogger("eapim.portal.apitester.audit");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/** 값 전체를 마스킹할 키(소문자 비교) */
private static final Set<String> SECRET_KEYS = new HashSet<>(Arrays.asList(
"client_secret", "clientsecret", "secret", "password", "passwd", "pwd",
"api_key", "apikey", "access_token", "refresh_token", "authorization"));
/** 감사 로그에 남길 주요 요청 헤더 화이트리스트 */
private static final String[] AUDIT_HEADERS = {
"content-type", "accept", "referer", "origin", "x-forwarded-for",
"original-api-id", "authorization"};
/** 마스킹된 JSON 본문 로그 최대 길이(초과분 절단) — 대용량 본문의 로그 파일 비대화 방지 */
private static final int JSON_LOG_MAX_LENGTH = 2000;
/** JSON 이 아닌 본문의 노출 프리뷰 글자 수 */
private static final int NON_JSON_PREVIEW_LENGTH = 8;
private ApiTesterAuditLogger() {
}
/** REQ/RES 두 줄을 연결하는 짧은 감사 ID */
public static String newAuditId() {
return UUID.randomUUID().toString().substring(0, 8);
}
/**
* 요청 수신 시점 기록. 감사 로그 실패가 프록시 동작을 막지 않도록 예외는 삼킨다.
*
* @param targetUrl original-url 헤더 값 (없으면 null)
* @param gatewayMode 게이트웨이 모드명 (판별 전이면 "-")
* @param tokenRequest OAuth 토큰 발급 요청 여부
* @param body 이미 읽어 둔 요청 본문 (없으면 null/빈 문자열)
*/
public static void logRequest(String auditId, HttpServletRequest request, String targetUrl,
String gatewayMode, boolean tokenRequest, String body) {
try {
StringBuilder sb = new StringBuilder(256);
sb.append("REQ [").append(auditId).append(']');
sb.append(" ip=").append(HttpRequestUtil.getClientIpAddress(request));
sb.append(" proxied=").append(HttpRequestUtil.isProxied(request));
sb.append(" user=").append(currentUser());
sb.append(" method=").append(request.getMethod());
sb.append(" mode=").append(gatewayMode);
sb.append(" token=").append(tokenRequest);
sb.append(" target=").append(targetUrl == null ? "-" : maskQueryValues(sanitize(targetUrl)));
sb.append(" ua=\"").append(sanitize(request.getHeader("User-Agent"))).append('"');
sb.append(" headers=").append(buildHeaderSummary(request));
sb.append(" body=").append(buildBodySummary(request.getContentType(), tokenRequest, body));
auditLogger.info(sb.toString());
} catch (Exception e) {
auditLogger.warn("REQ [{}] 감사 로그 기록 실패: {}", auditId, e.toString());
}
}
/** 처리 완료 시점 기록. type 은 처리 분기(TOKEN_GW/TOKEN_MOCK/SAMPLE/GW/MOCK 등). */
public static void logResult(String auditId, int status, String type, long elapsedMillis) {
auditLogger.info("RES [{}] status={} type={} elapsedMs={}", auditId, status, type, elapsedMillis);
}
// =========================================================================
// 요청 정보 구성
// =========================================================================
/** 로그인 사용자 식별자(마스킹). 미인증이면 anonymous. */
private static String currentUser() {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated() || "anonymousUser".equals(auth.getName())) {
return "anonymous";
}
String name = auth.getName();
return name.contains("@") ? StringMaskingUtil.maskEmail(name) : partialMask(name);
} catch (Exception e) {
return "unknown";
}
}
/** 화이트리스트 헤더만 {k:"v"} 형태로 요약. secret 계열 헤더 값은 마스킹. */
private static String buildHeaderSummary(HttpServletRequest request) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (String name : AUDIT_HEADERS) {
String value = request.getHeader(name);
if (value == null) {
continue;
}
if (!first) {
sb.append(", ");
}
first = false;
sb.append(name).append(":\"").append(maskHeaderValue(name, sanitize(value))).append('"');
}
return sb.append('}').toString();
}
/** Authorization 등 인증 헤더는 스킴만 남기고 토큰부 마스킹. */
private static String maskHeaderValue(String name, String value) {
if (!SECRET_KEYS.contains(name.toLowerCase())) {
return value;
}
int space = value.indexOf(' ');
if (space > 0) {
return value.substring(0, space) + " " + partialMask(value.substring(space + 1).trim());
}
return partialMask(value);
}
// =========================================================================
// 본문 마스킹
// =========================================================================
private static String buildBodySummary(String contentType, boolean tokenRequest, String body) {
if (body == null || body.isEmpty()) {
return "-";
}
// 토큰 발급: form 필드 단위 마스킹 (client_secret 전체 마스킹)
if (tokenRequest) {
return "\"" + maskFormBody(body) + "\"";
}
// 일반 요청: JSON 이면 값 단위 부분 마스킹, 그 외(비 JSON)는 길이 + 프리뷰만
if (contentType != null && contentType.toLowerCase().contains("json")) {
String maskedJson = tryMaskJson(body);
if (maskedJson != null) {
return maskedJson;
}
}
return nonJsonSummary(body);
}
/** k=v&k=v 형태 본문의 값 단위 마스킹. secret 키는 전체 마스킹. */
private static String maskFormBody(String body) {
StringBuilder sb = new StringBuilder(body.length());
String[] pairs = body.split("&");
for (int i = 0; i < pairs.length; i++) {
if (i > 0) {
sb.append('&');
}
int eq = pairs[i].indexOf('=');
if (eq < 0) {
sb.append(partialMask(pairs[i]));
continue;
}
String key = pairs[i].substring(0, eq);
String value = pairs[i].substring(eq + 1);
sb.append(key).append('=');
sb.append(SECRET_KEYS.contains(key.toLowerCase()) ? "*****" : partialMask(value));
}
return sanitize(sb.toString());
}
/** URL 쿼리스트링 값 단위 마스킹 (경로는 그대로). */
private static String maskQueryValues(String url) {
int qs = url.indexOf('?');
if (qs < 0) {
return url;
}
return url.substring(0, qs) + "?" + maskFormBody(url.substring(qs + 1));
}
/** JSON 파싱 성공 시 값 단위 마스킹 문자열, 실패 시 null. */
private static String tryMaskJson(String body) {
try {
JsonNode masked = maskJsonNode(OBJECT_MAPPER.readTree(body));
String out = OBJECT_MAPPER.writeValueAsString(masked);
if (out.length() > JSON_LOG_MAX_LENGTH) {
out = out.substring(0, JSON_LOG_MAX_LENGTH) + "...(truncated)";
}
return out;
} catch (Exception e) {
return null;
}
}
/** JSON 트리의 leaf 값을 재귀적으로 마스킹. secret 키 필드는 전체 마스킹. */
private static JsonNode maskJsonNode(JsonNode node) {
if (node.isObject()) {
ObjectNode obj = (ObjectNode) node;
Iterator<String> names = obj.fieldNames();
Set<String> fieldNames = new HashSet<>();
while (names.hasNext()) {
fieldNames.add(names.next());
}
for (String field : fieldNames) {
if (SECRET_KEYS.contains(field.toLowerCase())) {
obj.set(field, TextNode.valueOf("*****"));
} else {
obj.set(field, maskJsonNode(obj.get(field)));
}
}
return obj;
}
if (node.isArray()) {
ArrayNode arr = (ArrayNode) node;
for (int i = 0; i < arr.size(); i++) {
arr.set(i, maskJsonNode(arr.get(i)));
}
return arr;
}
if (node.isNull() || node.isMissingNode()) {
return node;
}
return TextNode.valueOf(partialMask(node.asText()));
}
/** 비 JSON 본문: 전체 길이(byte)와 앞 몇 글자만 노출. */
private static String nonJsonSummary(String body) {
int bytes = body.getBytes(StandardCharsets.UTF_8).length;
String preview = body.length() <= NON_JSON_PREVIEW_LENGTH
? body : body.substring(0, NON_JSON_PREVIEW_LENGTH);
return "(non-json,bytes=" + bytes + ",preview=\"" + sanitize(preview) + "***\")";
}
// =========================================================================
// 공통 helper
// =========================================================================
/** 앞 일부(최대 4자)만 남기고 마스킹. 2자 이하는 전체 마스킹. */
private static String partialMask(String value) {
if (value == null || value.isEmpty()) {
return "";
}
int len = value.length();
if (len <= 2) {
return stars(len);
}
int visible = Math.min(4, Math.max(1, len / 3));
return value.substring(0, visible) + "***";
}
private static String stars(int count) {
char[] arr = new char[count];
Arrays.fill(arr, '*');
return new String(arr);
}
/** 제어문자·개행·따옴표를 치환해 한 줄 로그 형식을 보존. */
private static String sanitize(String value) {
if (value == null) {
return "-";
}
StringBuilder sb = new StringBuilder(value.length());
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == '"') {
sb.append('\'');
} else if (c == '\r' || c == '\n' || c == '\t') {
sb.append(' ');
} else if (c < 0x20) {
sb.append('?');
} else {
sb.append(c);
}
}
return sb.toString();
}
}

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