diff --git a/build.gradle b/build.gradle index b1e8352..6df5fe6 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,15 @@ idea { def nexusUrl = "https://nexus.eactive.synology.me:8090" +allprojects { + repositories { + maven { + url "${nexusUrl}/repository/maven-public/" + allowInsecureProtocol = true + } + } +} + def springVersion = "5.3.27" //def quartzVersion = "2.2.1" def queryDslVersion = "5.0.0" @@ -72,7 +81,7 @@ tasks.withType(JavaCompile) { } dependencies { - api project(':kjb-safedb') +// api project(':kjb-safedb') api "com.querydsl:querydsl-jpa:${queryDslVersion}" api "com.querydsl:querydsl-apt:${queryDslVersion}" diff --git a/db_migration_add_app_fields.sql b/db_migration_add_app_fields.sql new file mode 100644 index 0000000..8a2db86 --- /dev/null +++ b/db_migration_add_app_fields.sql @@ -0,0 +1,19 @@ +-- 1. Add fields to ptl_app_request table +ALTER TABLE ptl_app_request ADD app_description VARCHAR2(500); +COMMENT ON COLUMN ptl_app_request.app_description IS '앱 설명'; + +ALTER TABLE ptl_app_request ADD callback_url VARCHAR2(255); +COMMENT ON COLUMN ptl_app_request.callback_url IS '콜백 URL'; + +ALTER TABLE ptl_app_request ADD ip_whitelist VARCHAR2(1000); +COMMENT ON COLUMN ptl_app_request.ip_whitelist IS 'IP 화이트리스트 (comma-separated)'; + +ALTER TABLE ptl_app_request ADD app_icon_file_id VARCHAR2(38); +COMMENT ON COLUMN ptl_app_request.app_icon_file_id IS '앱 아이콘 파일 ID (ptl_file 참조)'; + +-- 2. Add fields to ptl_credential table +ALTER TABLE ptl_credential ADD app_description VARCHAR2(500); +COMMENT ON COLUMN ptl_credential.app_description IS '앱 설명'; + +ALTER TABLE ptl_credential ADD app_icon_file_id VARCHAR2(38); +COMMENT ON COLUMN ptl_credential.app_icon_file_id IS '앱 아이콘 파일 ID (ptl_file 참조)'; diff --git a/src/main/java/com/eactive/apim/portal/agreements/entity/Agreements.java b/src/main/java/com/eactive/apim/portal/agreements/entity/Agreements.java index 15c8eed..0b9efc7 100644 --- a/src/main/java/com/eactive/apim/portal/agreements/entity/Agreements.java +++ b/src/main/java/com/eactive/apim/portal/agreements/entity/Agreements.java @@ -61,4 +61,8 @@ public class Agreements extends Auditable { @Column(name = "SCHEDULED_ON") @Convert(converter = LocalDateTimeToStringConverter14.class) private LocalDateTime scheduledOn; + + @Column(name = "ATTACHED_FILE_ID", length = 36) + @Comment("첨부파일 ID (PTL_FILE.FILE_ID)") + private String attachedFileId; } \ No newline at end of file diff --git a/src/main/java/com/eactive/apim/portal/app/entity/Credential.java b/src/main/java/com/eactive/apim/portal/app/entity/Credential.java index 6c3288e..70cc0b7 100644 --- a/src/main/java/com/eactive/apim/portal/app/entity/Credential.java +++ b/src/main/java/com/eactive/apim/portal/app/entity/Credential.java @@ -111,6 +111,14 @@ public class Credential extends AbstractEntity implements Serializable { @Comment("기관이름") private String orgname; + @Column(name = "app_description", length = 500) + @Comment("앱 설명") + private String appDescription; + + @Column(name = "app_icon_file_id", length = 38) + @Comment("앱 아이콘 파일 ID") + private String appIconFileId; + @Comment("일별 토큰 제한 수 (0:무제한)") private int dailytokenlimit; diff --git a/src/main/java/com/eactive/apim/portal/app/entity/ProdClient.java b/src/main/java/com/eactive/apim/portal/app/entity/ProdClient.java deleted file mode 100644 index 76345ef..0000000 --- a/src/main/java/com/eactive/apim/portal/app/entity/ProdClient.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.eactive.apim.portal.app.entity; - -import com.eactive.apim.portal.apispec.entity.ApiSpecInfo; -import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; -import com.eactive.eai.data.entity.AbstractEntity; -import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.Table; -import lombok.Data; -import lombok.NonNull; -import org.hibernate.annotations.Comment; -import org.hibernate.annotations.LazyCollection; -import org.hibernate.annotations.LazyCollectionOption; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; - -@Entity -@Data -@Table(name = "ptl_prod_client") -@org.hibernate.annotations.Table(appliesTo = "ptl_prod_client", comment = "사용자 정보") -public class ProdClient extends AbstractEntity implements Serializable { - - private static final long serialVersionUID = 1L; - - @Id - @Column(unique = true, nullable = false, length = 256) - @Comment("client id") - private String clientid; - - @Column(length = 11) - @Comment("Access Token 유효 기간(초)") - private String accesstokenvalidityseconds; - - @Column(length = 256) - @Comment("허용된 IP") - private String allowedips; - - @Column(length = 256) - @Comment("권한") - private String authorities; - - @Column(length = 256) - @Comment("자동 승인 스코프") - private String autoapprove; - - @Column(length = 40) - @Comment("client명") - private String clientname; - - @Column(length = 512) - @Comment("Client 비밀번호") - private String clientsecret; - - @Column(length = 256) - @Comment("권한타입") - private String granttypes; - - @Column(length = 8) - @Comment("수정인") - @LastModifiedBy - private String modifiedby; - - @Column(length = 14) - @Comment("수정일") - @Convert(converter = LocalDateTimeToStringConverter14.class) - @LastModifiedDate - private LocalDateTime modifiedon; - - @Column(length = 256) - @Comment("Redirect URI") - private String redirecturi; - - @Column(length = 11) - @Comment("Refresh Token 유효 기간(초)") - private String refreshtokenvalidityseconds; - - @Column(length = 4000) - @Comment("리소스 id") - private String resourceids; - - @Column(length = 1024) - @Comment("scope") - private String scope; - - @Column(length = 4000) - @Comment("암호key") - private String securitykey; - - @Column(length = 38) - @Comment("기관ID") - private String orgid; - - @Comment("APP 상태") - private String appstatus; //0: 차단, 1: 정상 - - @Column(length = 255) - @Comment("기관이름") - private String orgname; - - @Comment("일별 토큰 제한 수 (0:무제한)") - private int dailytokenlimit; - - @Override - public @NonNull String getId() { - return clientid; - } - - @ManyToMany - @LazyCollection(LazyCollectionOption.FALSE) - @JoinTable(name = "ptl_prod_client_api", joinColumns = @JoinColumn(name = "client_id"), inverseJoinColumns = @JoinColumn(name = "eaisvcname")) - private List apiList = new ArrayList<>(); -} diff --git a/src/main/java/com/eactive/apim/portal/app/repository/CredentialRepository.java b/src/main/java/com/eactive/apim/portal/app/repository/CredentialRepository.java index 1710070..82fc2fb 100644 --- a/src/main/java/com/eactive/apim/portal/app/repository/CredentialRepository.java +++ b/src/main/java/com/eactive/apim/portal/app/repository/CredentialRepository.java @@ -7,15 +7,39 @@ import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; @EMSDataSource public interface CredentialRepository extends BaseRepository { - Page findAllByOrgidAndServer(Pageable pageable, String id, String server); + Page findAllByOrgid(Pageable pageable, String id); - int countAllByOrgidAndServer(String orgid, String server); + int countAllByOrgid(String orgid); - List findAllByOrgidAndServer(String orgid, String server); + List findAllByOrgid(String orgid); - Optional findByClientidAndOrgidAndServer(String clientid, String orgid, String server); + Optional findByClientidAndOrgid(String clientid, String orgid); + + /** + * 특정 기관 ID 목록에 속하고 이용 가능 상태(appstatus='1')인 앱의 수를 조회 + */ + @Query("SELECT COUNT(c) FROM Credential c WHERE c.appstatus = '1' AND c.orgid IN :orgIds") + long countActiveAppsByOrgIds(@Param("orgIds") List orgIds); + + /** + * 특정 기관 ID 목록에 속하고 이용 가능 상태(appstatus='1')인 앱 목록 조회 + */ + @Query("SELECT c FROM Credential c WHERE c.appstatus = '1' AND c.orgid IN :orgIds") + List findActiveAppsByOrgIds(@Param("orgIds") List orgIds); + + /** + * 이용 가능 상태인 모든 앱의 수를 조회 + */ + long countByAppstatus(String appstatus); + + /** + * 이용 가능 상태인 모든 앱 목록 조회 + */ + List findByAppstatus(String appstatus); } diff --git a/src/main/java/com/eactive/apim/portal/app/repository/ProdClientRepository.java b/src/main/java/com/eactive/apim/portal/app/repository/ProdClientRepository.java deleted file mode 100644 index c1adafa..0000000 --- a/src/main/java/com/eactive/apim/portal/app/repository/ProdClientRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.eactive.apim.portal.app.repository; - -import com.eactive.apim.portal.app.entity.ProdClient; -import com.eactive.apim.portal.approval.entity.Approval; -import com.eactive.apim.portal.approval.entity.ApprovalType; -import com.eactive.eai.data.jpa.BaseRepository; -import com.eactive.eai.rms.data.EMSDataSource; -import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -@EMSDataSource -public interface ProdClientRepository extends BaseRepository { - - Page findAllByOrgid(Pageable pageable, String id); - - Optional findByClientidAndOrgid(String clientid, String orgid); -} diff --git a/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequest.java b/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequest.java index 3d74331..0150efe 100644 --- a/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequest.java +++ b/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequest.java @@ -12,6 +12,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; @Data @EqualsAndHashCode @@ -27,7 +29,8 @@ public class AppRequest implements com.eactive.eai.data.Data { private String id; @ManyToOne - @JoinColumn(name = "org_id") + @JoinColumn(name = "org_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @NotFound(action = NotFoundAction.IGNORE) private PortalOrg org; @Enumerated(EnumType.STRING) @@ -37,7 +40,8 @@ public class AppRequest implements com.eactive.eai.data.Data { private AppRequestType type; @OneToOne - @JoinColumn(name = "approval_id") + @JoinColumn(name = "approval_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @NotFound(action = NotFoundAction.IGNORE) private Approval approval; @Column(name = "api_list") @@ -64,6 +68,18 @@ public class AppRequest implements com.eactive.eai.data.Data { @Column(name = "reason") private String reason; + @Column(name = "app_description", length = 500) + private String appDescription; + + @Column(name = "callback_url", length = 255) + private String callbackUrl; + + @Column(name = "ip_whitelist", length = 1000) + private String ipWhitelist; + + @Column(name = "app_icon_file_id", length = 38) + private String appIconFileId; + @Column(length = 17, name = "created_date") @Convert(converter = LocalDateTimeToStringConverter17.class) @CreationTimestamp diff --git a/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequestType.java b/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequestType.java index 3178ca0..61c340b 100644 --- a/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequestType.java +++ b/src/main/java/com/eactive/apim/portal/apprequest/entity/AppRequestType.java @@ -3,10 +3,7 @@ package com.eactive.apim.portal.apprequest.entity; public enum AppRequestType { NEW("API키 신규"), MODIFY("API 변경"), - DELETE("API키 삭제"), - PROD_NEW("운영 API키 신규"), - PROD_MODIFY("운영 API 변경"), - PROD_DELETE("운영 API키 삭제"); + DELETE("API키 삭제"); private final String description; diff --git a/src/main/java/com/eactive/apim/portal/apprequest/repository/AppRequestRepository.java b/src/main/java/com/eactive/apim/portal/apprequest/repository/AppRequestRepository.java index 30e1f4e..5e4bb04 100644 --- a/src/main/java/com/eactive/apim/portal/apprequest/repository/AppRequestRepository.java +++ b/src/main/java/com/eactive/apim/portal/apprequest/repository/AppRequestRepository.java @@ -8,6 +8,7 @@ import com.eactive.apim.portal.approval.statemachine.ApprovalState; import com.eactive.apim.portal.portalorg.entity.PortalOrg; import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.rms.data.EMSDataSource; +import java.util.Collection; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -20,6 +21,8 @@ public interface AppRequestRepository extends BaseRepository Optional findAppRequestByApproval(Approval approval); + List findAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(PortalOrg org, List types, List approvalStates); + int countAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(PortalOrg org, List types, List approvalStates); Optional findByIdAndOrg(String id, PortalOrg org); @@ -29,4 +32,12 @@ public interface AppRequestRepository extends BaseRepository List findAllByClientIdsContainsAndTypeIsIn(String clientId, List types); List findAllByOrgAndStatus(PortalOrg org, ApprovalStatus status); + + /** + * org.id 목록으로 AppRequest 조회 (ObpGwMetric 집계용) + * + * @param orgIds 기관 ID 목록 + * @return AppRequest 목록 + */ + List findByOrg_IdIn(Collection orgIds); } diff --git a/src/main/java/com/eactive/apim/portal/approval/entity/Approval.java b/src/main/java/com/eactive/apim/portal/approval/entity/Approval.java index 2a2bb72..0ddfc7d 100644 --- a/src/main/java/com/eactive/apim/portal/approval/entity/Approval.java +++ b/src/main/java/com/eactive/apim/portal/approval/entity/Approval.java @@ -12,6 +12,7 @@ import org.hibernate.annotations.*; import javax.persistence.CascadeType; import javax.persistence.Entity; +import javax.persistence.ForeignKey; import javax.persistence.Table; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -50,9 +51,9 @@ public class Approval implements Serializable, com.eactive.eai.data.Data { @Column(name = "target_id", nullable = false) private String targetId; //승인하는 대상 객체의 ID - @NotNull @ManyToOne - @JoinColumn(name = "REQUESTER_ID", nullable = false) + @JoinColumn(name = "REQUESTER_ID", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @NotFound(action = NotFoundAction.IGNORE) private PortalUser requester; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "approval") diff --git a/src/main/java/com/eactive/apim/portal/file/service/FileService.java b/src/main/java/com/eactive/apim/portal/file/service/FileService.java index 2731326..d3438fb 100644 --- a/src/main/java/com/eactive/apim/portal/file/service/FileService.java +++ b/src/main/java/com/eactive/apim/portal/file/service/FileService.java @@ -29,6 +29,30 @@ public class FileService { private final BinaryStorageService binaryStorageService; private final FileTypeDetector fileTypeDetector; + private static final ThreadLocal internalUserContext = new ThreadLocal<>(); + + /** + * 현재 스레드에 내부 사용자 컨텍스트 설정 + */ + public static void setInternalUserContext(boolean isInternal) { + internalUserContext.set(isInternal); + } + + /** + * 내부 사용자 컨텍스트 조회 + */ + public static boolean isInternalUserContext() { + Boolean isInternal = internalUserContext.get(); + return isInternal != null && isInternal; + } + + /** + * 내부 사용자 컨텍스트 정리 + */ + public static void clearInternalUserContext() { + internalUserContext.remove(); + } + /** * 특정 파일 ID를 기반으로 파일 정보를 검색합니다. * @@ -71,8 +95,15 @@ public class FileService { String type = fileTypeDetector.detectFileType(file.getBytes()); log.debug("upload file type:{}", type); - if(type.equalsIgnoreCase("unknown")) { - throw new InvalidFileException("허용된 파일 형식이 아닙니다.
문서파일과 이미지 파일만 등록 가능합니다.
(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)"); + boolean isValidType = !type.equalsIgnoreCase("unknown"); + + if (!isValidType) { + if (isInternalUserContext()) { + log.warn("Internal user bypassed file validation - File: {}, Type: {}", + file.getOriginalFilename(), type); + } else { + throw new InvalidFileException("허용된 파일 형식이 아닙니다.
문서파일과 이미지 파일만 등록 가능합니다.
(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)"); + } } if (StringUtils.isNotBlank(file.getOriginalFilename()) && file.getSize() > 0) { @@ -82,7 +113,13 @@ public class FileService { fileDetail.setFileSn(fileSn); fileDetail.setFileSize((int) file.getSize()); fileDetail.setOriginalFileName(FilenameUtils.getBaseName(file.getOriginalFilename())); - fileDetail.setFileExtension(type); + + if (isValidType) { + fileDetail.setFileExtension(type); + } else { + fileDetail.setFileExtension(FilenameUtils.getExtension(file.getOriginalFilename())); + } + String contentReference = binaryStorageService.store(file.getBytes()); fileDetail.setFileContents(contentReference); fileInfo.getFileDetails().add(fileDetail); @@ -232,12 +269,17 @@ public class FileService { String type = fileTypeDetector.detectFileType(file.getBytes()); log.debug("upload file type:{}", type); - if (isCheckMime) { - if(type.equalsIgnoreCase("unknown")) { - throw new InvalidFileException("허용된 파일 형식이 아닙니다.
문서파일과 이미지 파일만 등록 가능합니다.
(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)"); - } - } else { - log.debug("파일 체크 옵션 해제 - FileType : {}", type); + boolean isValidType = !type.equalsIgnoreCase("unknown"); + + if (isCheckMime && !isValidType) { + if (isInternalUserContext()) { + log.warn("Internal user bypassed file validation - File: {}, Type: {}", + fileName, type); + } else { + throw new InvalidFileException("허용된 파일 형식이 아닙니다.
문서파일과 이미지 파일만 등록 가능합니다.
(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)"); + } + } else if (!isCheckMime) { + log.debug("파일 체크 옵션 해제 - FileType : {}", type); } FileDetail fileDetail = new FileDetail(); @@ -252,6 +294,100 @@ public class FileService { return fileDetail; } + + /** + * byte[] 데이터로 단일 파일을 생성합니다. + * + * @param fileName 파일 이름 (확장자 포함) + * @param contentType 파일 MIME 타입 + * @param fileData 파일 바이트 데이터 + * @return 파일 정보 + * @throws IOException 파일 처리 중 오류 발생 시 + */ + public FileInfo createSingleFileFromBytes(String fileName, String contentType, byte[] fileData) throws IOException { + // 파일 타입 검증 + String type = fileTypeDetector.detectFileType(fileData); + log.debug("upload file type from bytes: {}", type); + + if (type.equalsIgnoreCase("unknown")) { + throw new InvalidFileException("허용된 파일 형식이 아닙니다.
문서파일과 이미지 파일만 등록 가능합니다.
(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)"); + } + + // FileInfo 생성 + FileInfo fileInfo = new FileInfo(); + String fileId = UUID.randomUUID().toString(); + fileInfo.setFileId(fileId); + fileInfo.setCreatedDate(LocalDateTime.now()); + fileInfo.setUseYn("Y"); + + // FileDetail 생성 + FileDetail fileDetail = new FileDetail(); + fileDetail.setFileId(fileId); + fileDetail.setFileSn(1); + fileDetail.setFileSize(fileData.length); + fileDetail.setOriginalFileName(FilenameUtils.getBaseName(fileName)); + fileDetail.setFileExtension(FilenameUtils.getExtension(fileName)); + + // 파일 내용 저장 + String contentReference = binaryStorageService.store(fileData); + fileDetail.setFileContents(contentReference); + + // FileInfo에 FileDetail 추가 + fileInfo.getFileDetails().add(fileDetail); + + // 저장 및 반환 + return fileRepository.save(fileInfo); + } + + /** + * byte[] 데이터로 기존 파일을 업데이트하거나 새로 생성합니다. + * + * @param existingFileId 기존 파일 ID (null이면 새로 생성) + * @param fileName 파일 이름 (확장자 포함) + * @param contentType 파일 MIME 타입 + * @param fileData 파일 바이트 데이터 + * @return 파일 정보 + * @throws IOException 파일 처리 중 오류 발생 시 + */ + public FileInfo createOrUpdateSingleFileFromBytes(String existingFileId, String fileName, String contentType, byte[] fileData) throws IOException { + // 신규 등록 또는 기존 파일 정보 조회 + FileInfo fileInfo = Optional.ofNullable(existingFileId) + .filter(StringUtils::isNotBlank) + .map(this::findById) + .orElseGet(() -> { + FileInfo newFile = new FileInfo(); + newFile.setFileId(UUID.randomUUID().toString()); + newFile.setCreatedDate(LocalDateTime.now()); + newFile.setUseYn("Y"); + return newFile; + }); + + // 파일 타입 검증 + String type = fileTypeDetector.detectFileType(fileData); + log.debug("upload file type from bytes: {}", type); + + if (type.equalsIgnoreCase("unknown")) { + throw new InvalidFileException("허용된 파일 형식이 아닙니다.
문서파일과 이미지 파일만 등록 가능합니다.
(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)"); + } + + // FileDetail 생성 + FileDetail fileDetail = new FileDetail(); + fileDetail.setFileId(fileInfo.getFileId()); + fileDetail.setFileSn(1); + fileDetail.setFileSize(fileData.length); + fileDetail.setOriginalFileName(FilenameUtils.getBaseName(fileName)); + fileDetail.setFileExtension(FilenameUtils.getExtension(fileName)); + + // 파일 내용 저장 + String contentReference = binaryStorageService.store(fileData); + fileDetail.setFileContents(contentReference); + + // 기존 파일 상세 정보 제거 후 새로운 정보 추가 + fileInfo.getFileDetails().clear(); + fileInfo.getFileDetails().add(fileDetail); + + return fileRepository.save(fileInfo); + } } diff --git a/src/main/java/com/eactive/apim/portal/invitation/event/UserInvitationEvent.java b/src/main/java/com/eactive/apim/portal/invitation/event/UserInvitationEvent.java index ab59632..ecf3455 100644 --- a/src/main/java/com/eactive/apim/portal/invitation/event/UserInvitationEvent.java +++ b/src/main/java/com/eactive/apim/portal/invitation/event/UserInvitationEvent.java @@ -21,10 +21,11 @@ public class UserInvitationEvent implements MessageEventHandler { @Override public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map params) { Map requestParams = new HashMap<>(); - requestParams.put("company", params.get("company").toString()); - requestParams.put("manager", params.get("manager").toString()); + requestParams.put("corpName", params.get("corpName").toString()); + requestParams.put("managerName", params.get("managerName").toString()); requestParams.put("url", params.get("url").toString()); - requestParams.put("userName",params.get("company").toString() + " 이용자"); + requestParams.put("loginId",params.get("corpName").toString() + " 이용자"); + requestParams.put("authNumber", params.get("authNumber").toString()); return new MessageSendEvent(source, KEY, recipient, requestParams); } diff --git a/src/main/java/com/eactive/apim/portal/invitation/repository/UserInvitationRepository.java b/src/main/java/com/eactive/apim/portal/invitation/repository/UserInvitationRepository.java index e9c0f4b..f79fe17 100644 --- a/src/main/java/com/eactive/apim/portal/invitation/repository/UserInvitationRepository.java +++ b/src/main/java/com/eactive/apim/portal/invitation/repository/UserInvitationRepository.java @@ -18,4 +18,8 @@ public interface UserInvitationRepository extends JpaRepository findByStatus(InvitationStatus status); Optional findFirstByInvitationEmailAndStatus(String email, InvitationStatus status); + + Optional findFirstByInvitationEmailAndOrgIdAndStatus(String email, String orgId, InvitationStatus status); + + Optional findFirstByInvitationEmailAndOrgId(String email, String orgId); } diff --git a/src/main/java/com/eactive/apim/portal/obp/entity/ObpGwMetric.java b/src/main/java/com/eactive/apim/portal/obp/entity/ObpGwMetric.java new file mode 100644 index 0000000..29da51d --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/obp/entity/ObpGwMetric.java @@ -0,0 +1,151 @@ +package com.eactive.apim.portal.obp.entity; + +import com.eactive.eai.data.converter.LocalDateTimeToStringConverter17; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(name = "ptl_obp_gw_metric") +public class ObpGwMetric { + @Id + @GeneratedValue(generator = "uuid-v1") + @GenericGenerator( + name = "uuid-v1", + strategy = "org.hibernate.id.UUIDGenerator", + parameters = { + @Parameter( + name = "uuid_gen_strategy_class", + value = "org.hibernate.id.uuid.CustomVersionOneStrategy" + ) + } + ) + @Column(length = 36) + @Comment("id") + private String id; + + + // json : appKey + @Column(length = 256, name = "client_id") + @Comment("클라이언트 ID") + private String clientId; + + // json : clusterHostname + @Column(length = 50, name = "hostname") + @Comment("호스트 이름") + private String hostname; + + // json : timeslice + @Column(length = 17, name = "timeslice") + @Convert(converter = LocalDateTimeToStringConverter17.class) + @Comment("타임슬라이스 (yyyyMMddHHmmssSSS)") + private LocalDateTime timeslice; + + // json : apiName + @Column(name = "api_name", length = 255) + @Comment("API 이름") + private String apiName; + + // json : apiRoutingUri + @Column(length = 255, name = "uri") + @Comment("URI") + private String uri; + + // HTTP 메서드 (GET, POST, PUT, DELETE 등) + @Column(length = 10, name = "method") + @Comment("HTTP 메서드 (GET, POST, PUT, DELETE 등)") + private String method; + + // json : attemptedCount + @Column(name = "attempted_count") + @Comment("시도 횟수") + private Long attemptedCount; + + // json : completedCount + @Column(name = "completed_count") + @Comment("완료 횟수") + private Long completedCount; + + // registeredOn + @Column(name = "created_at", nullable = false) + @Comment("생성 날짜와 시간. 레코드가 생성된 시간") + private LocalDateTime createdAt; + + // registeredBy + @Column(name = "created_by", nullable = false, length = 50) + @Comment("생성한 사용자") + private String createdBy = "GwMetricHourJob"; + + // modifiedOn + @Column(name = "updated_at") + @Comment("최종 업데이트 날짜와 시간. 레코드가 마지막으로 업데이트된 시간") + private LocalDateTime updatedAt; + + // modifiedBy + @Column(name = "updated_by", length = 50) + @Comment("최종 업데이트한 사용자") + private String updatedBy; + + + + // eapim ID + // json : apiIdByEapim + @Column(length = 40, name = "api_id") + @Comment("API ID(EAPIM) - EAISVCNAME 최대 40자") + private String apiId; + + // json : appIdByEapim + @Column(length = 36, name = "app_id") + private String appId; + + // json : orgIdByEapim + @Column(length = 36, name = "org_id") + private String orgId; + + + // AS-IS 호환용 필드 + + // json : appId + @Column(length = 50, name = "app_id_by_ca_portal") + @Comment("애플리케이션 ID(CA 포탈)") + private String appIdByCaPortal; + + // json : portalOrgId + @Column(length = 50, name = "org_id_by_ca_portal") + @Comment("기관 ID(CA 포탈)") + private String orgIdByCaPortal; + + // json : portalApiId + @Column(length = 50, name = "api_id_by_ca_portal") + @Comment("API ID(CA 포탈)") + private String apiIdByCaPortal; + + // json : partnerCode + @Column(length = 20, name = "partner_code") + @Comment("파트너 코드(OBP)") + private String partnerCode; + + // json : apiId + @Column(name = "app_id_by_ca", length = 36) + @Comment("API ID(CA)") + private String apiIdByCa; + + + @Column(name = "update_count") + @Comment("업데이트(보정) 횟수") + private int updateCount; + + + // 더미 + + // requestUri + @Transient + private String requestUri = "-1"; +} diff --git a/src/main/java/com/eactive/apim/portal/obp/repository/ObpGwMetricRepository.java b/src/main/java/com/eactive/apim/portal/obp/repository/ObpGwMetricRepository.java new file mode 100644 index 0000000..26d3c6c --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/obp/repository/ObpGwMetricRepository.java @@ -0,0 +1,82 @@ +package com.eactive.apim.portal.obp.repository; + +import com.eactive.apim.portal.obp.entity.ObpGwMetric; +import com.eactive.eai.data.jpa.BaseRepository; +import com.eactive.eai.rms.data.EMSDataSource; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * ObpGwMetric Repository + * API Gateway 시간별 거래량 지표 조회/저장 + */ +@EMSDataSource +public interface ObpGwMetricRepository extends BaseRepository { + + /** + * UPSERT 판단을 위한 기존 데이터 조회 (NULL 안전) + * Unique Key: (timeslice, api_id, client_id, hostname, org_id) + * + *

Spring Data JPA의 메서드 이름 기반 쿼리는 NULL 비교를 제대로 처리하지 못함. + * SQL에서 {@code column = NULL}은 항상 false이므로, NULL 값이 있는 레코드를 찾지 못함. + * 이 쿼리는 {@code (column = :param OR (column IS NULL AND :param IS NULL))} 패턴으로 NULL을 안전하게 처리함.

+ */ + @Query("SELECT m FROM ObpGwMetric m WHERE m.timeslice = :timeslice " + + "AND (m.apiId = :apiId OR (m.apiId IS NULL AND :apiId IS NULL)) " + + "AND (m.clientId = :clientId OR (m.clientId IS NULL AND :clientId IS NULL)) " + + "AND (m.hostname = :hostname OR (m.hostname IS NULL AND :hostname IS NULL)) " + + "AND (m.orgId = :orgId OR (m.orgId IS NULL AND :orgId IS NULL))") + Optional findByUniqueKeyNullSafe( + @Param("timeslice") LocalDateTime timeslice, + @Param("apiId") String apiId, + @Param("clientId") String clientId, + @Param("hostname") String hostname, + @Param("orgId") String orgId + ); + + /** + * @deprecated NULL 비교 문제로 {@link #findByUniqueKeyNullSafe} 사용 권장 + */ + @Deprecated + Optional findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + LocalDateTime timeslice, + String apiId, + String clientId, + String hostname, + String orgId + ); + + /** + * 특정 시간대의 모든 지표 조회 (REST API 용) + */ + List findByTimeslice(LocalDateTime timeslice); + + /** + * 특정 시간대 범위의 지표 조회 + */ + List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime); + + /** + * API ID로 지표 조회 + */ + List findByApiIdAndTimeslice(String apiId, LocalDateTime timeslice); + + /** + * 특정 시간대 이후의 모든 지표 조회 (REST API 용) + */ + List findByTimesliceGreaterThanEqual(LocalDateTime timeslice); + + /** + * 특정 시간대 이후의 지표 조회 (페이징 지원) + * + * @param timeslice 조회 시작 시간대 + * @param pageable 페이징 및 정렬 정보 + * @return 조회된 지표 목록 + */ + List findByTimesliceGreaterThanEqual(LocalDateTime timeslice, Pageable pageable); +} diff --git a/src/main/java/com/eactive/apim/portal/partnershipapplication/entity/PartnershipApplication.java b/src/main/java/com/eactive/apim/portal/partnershipapplication/entity/PartnershipApplication.java index f8fcfe4..6755ead 100644 --- a/src/main/java/com/eactive/apim/portal/partnershipapplication/entity/PartnershipApplication.java +++ b/src/main/java/com/eactive/apim/portal/partnershipapplication/entity/PartnershipApplication.java @@ -26,7 +26,6 @@ public class PartnershipApplication extends Auditable { @Comment("비즈니스제목") private String bizSubject; - @Lob @Column(name = "biz_detail", nullable = false) @Comment("비즈니스내용") private String bizDetail; diff --git a/src/main/java/com/eactive/apim/portal/portalorg/entity/OrgCodeGenerator.java b/src/main/java/com/eactive/apim/portal/portalorg/entity/OrgCodeGenerator.java index 526cd11..98e4b8c 100644 --- a/src/main/java/com/eactive/apim/portal/portalorg/entity/OrgCodeGenerator.java +++ b/src/main/java/com/eactive/apim/portal/portalorg/entity/OrgCodeGenerator.java @@ -12,6 +12,10 @@ import java.sql.ResultSet; import java.sql.SQLException; +/** + * 25.11.26 - 광주은행에서는 기관코드를 OBP 파트너코드와 매칭 시킴. 즉, 자동 생성 사용안함 + */ +@Deprecated @Slf4j public class OrgCodeGenerator implements ValueGenerator { private static final String TABLE_NAME = "PTL_ID"; diff --git a/src/main/java/com/eactive/apim/portal/portalorg/entity/PortalOrg.java b/src/main/java/com/eactive/apim/portal/portalorg/entity/PortalOrg.java index 1e42fbc..11fbae4 100644 --- a/src/main/java/com/eactive/apim/portal/portalorg/entity/PortalOrg.java +++ b/src/main/java/com/eactive/apim/portal/portalorg/entity/PortalOrg.java @@ -3,18 +3,12 @@ package com.eactive.apim.portal.portalorg.entity; import com.eactive.apim.portal.common.entity.Auditable; import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.ApprovalStatus; import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus; -import java.io.Serializable; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.Id; -import javax.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Comment; -import org.hibernate.annotations.GenerationTime; -import org.hibernate.annotations.GeneratorType; + +import javax.persistence.*; +import java.io.Serializable; @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true) @@ -40,8 +34,8 @@ public class PortalOrg extends Auditable implements Serializable, com.eactive.ea private String orgName; @Column(name = "org_code") - @Comment("제휴기관코드") - @GeneratorType(type = OrgCodeGenerator.class, when = GenerationTime.INSERT) + @Comment("제휴기관코드 - OBP 파트너코드") +// @GeneratorType(type = OrgCodeGenerator.class, when = GenerationTime.INSERT) private String orgCode; @Column(name = "org_desc", length = 255) diff --git a/src/main/java/com/eactive/apim/portal/portalproperty/service/PortalPropertyService.java b/src/main/java/com/eactive/apim/portal/portalproperty/service/PortalPropertyService.java index eae3bf0..e56af6b 100644 --- a/src/main/java/com/eactive/apim/portal/portalproperty/service/PortalPropertyService.java +++ b/src/main/java/com/eactive/apim/portal/portalproperty/service/PortalPropertyService.java @@ -2,21 +2,27 @@ package com.eactive.apim.portal.portalproperty.service; import com.eactive.apim.portal.portalproperty.entity.PortalProperty; import com.eactive.apim.portal.portalproperty.entity.PortalPropertyGroup; +import com.eactive.apim.portal.portalproperty.entity.PortalPropertyId; import com.eactive.apim.portal.portalproperty.repository.PortalPropertyGroupRepository; +import com.eactive.apim.portal.portalproperty.repository.PortalPropertyRepository; import com.eactive.eai.data.jpa.AbstractDataService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor @Transactional public class PortalPropertyService extends AbstractDataService { private final PortalPropertyGroupRepository portalPropertyGroupRepository; + private final PortalPropertyRepository portalPropertyRepository; @Transactional(readOnly = true) public Map getPortalPropertiesAsMap(String propertyGroupName) { @@ -31,5 +37,54 @@ public class PortalPropertyService extends AbstractDataService existingProperty = portalPropertyRepository.findById(propertyId); + if (existingProperty.isPresent()) { + return existingProperty.get().getPropertyValue(); + } + + // 프로퍼티가 없으면 기본값으로 생성 + return createDefaultProperty(groupName, propertyName, defaultValue, description); + } + + /** + * 기본 프로퍼티를 DB에 생성 + */ + private String createDefaultProperty(String groupName, String propertyName, String defaultValue, String description) { + try { + // 그룹이 존재하는지 확인 + PortalPropertyGroup group = portalPropertyGroupRepository.findById(groupName) + .orElse(null); + + if (group == null) { + log.warn("프로퍼티 그룹 '{}'이 존재하지 않아 기본값 저장을 건너뜁니다.", groupName); + return defaultValue; + } + + // 새 프로퍼티 생성 + PortalProperty newProperty = new PortalProperty(); + newProperty.setId(new PortalPropertyId(groupName, propertyName)); + newProperty.setPropertyValue(defaultValue); + newProperty.setPropertyDesc(description); + + portalPropertyRepository.save(newProperty); + log.info("기본 프로퍼티 생성 완료 - 그룹: {}, 키: {}, 값: {}", groupName, propertyName, defaultValue); + + return defaultValue; + } catch (Exception e) { + log.error("기본 프로퍼티 생성 실패 - 그룹: {}, 키: {}", groupName, propertyName, e); + return defaultValue; + } + } } diff --git a/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java b/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java index 74b8715..7982593 100644 --- a/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java +++ b/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java @@ -12,6 +12,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Comment; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; import javax.persistence.*; import java.time.LocalDateTime; @@ -60,6 +62,7 @@ public class PortalUser extends Auditable implements Serializable, com.eactive.e @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "org_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @NotFound(action = NotFoundAction.IGNORE) private PortalOrg portalOrg; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUserEnums.java b/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUserEnums.java index 00e4814..4b18771 100644 --- a/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUserEnums.java +++ b/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUserEnums.java @@ -3,6 +3,7 @@ package com.eactive.apim.portal.portaluser.entity; public class PortalUserEnums { public enum UserStatus { + PENDING("대기"), READY("준비"), ACTIVE("정상"), PW_FAIL("비밀번호 5회 실패"), diff --git a/src/main/java/com/eactive/apim/portal/portaluser/repository/PortalUserRepository.java b/src/main/java/com/eactive/apim/portal/portaluser/repository/PortalUserRepository.java index 154868e..fbe2813 100644 --- a/src/main/java/com/eactive/apim/portal/portaluser/repository/PortalUserRepository.java +++ b/src/main/java/com/eactive/apim/portal/portaluser/repository/PortalUserRepository.java @@ -10,17 +10,20 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; @EMSDataSource public interface PortalUserRepository extends BaseRepository { - Optional findByLoginId(String userName); + Optional findByLoginId(String String); Optional findPortalUserByEmailAddr(String loginId); PortalUser findByUserNameAndMobileNumber(String userName, String mobileNumber); + List findAllByUserNameAndMobileNumber(String userName, String mobileNumber); + Optional findByLoginIdAndUserNameAndMobileNumber(String loginId, String userName, String mobileNumber); boolean existsByLoginId(String loginId); diff --git a/src/main/java/com/eactive/apim/portal/qna/entity/Inquiry.java b/src/main/java/com/eactive/apim/portal/qna/entity/Inquiry.java index 69111b6..e2265b5 100644 --- a/src/main/java/com/eactive/apim/portal/qna/entity/Inquiry.java +++ b/src/main/java/com/eactive/apim/portal/qna/entity/Inquiry.java @@ -8,6 +8,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; import javax.persistence.*; import java.time.LocalDateTime; @@ -55,7 +57,8 @@ public class Inquiry extends Auditable { * 문의자 */ @ManyToOne - @JoinColumn(name = "INQUIRER_ID") + @JoinColumn(name = "INQUIRER_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @NotFound(action = NotFoundAction.IGNORE) private PortalUser inquirer; /** diff --git a/src/main/java/com/eactive/apim/portal/template/entity/MessageRequest.java b/src/main/java/com/eactive/apim/portal/template/entity/MessageRequest.java index b4529b5..031f0e9 100644 --- a/src/main/java/com/eactive/apim/portal/template/entity/MessageRequest.java +++ b/src/main/java/com/eactive/apim/portal/template/entity/MessageRequest.java @@ -9,6 +9,8 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import org.hibernate.annotations.Comment; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; import javax.persistence.*; import java.io.Serializable; @@ -59,6 +61,7 @@ public class MessageRequest implements Serializable { @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @NotFound(action = NotFoundAction.IGNORE) @Comment("발송 대상 사용자") private PortalUser user; diff --git a/src/main/java/com/eactive/apim/portal/user/entity/UserInfo.java b/src/main/java/com/eactive/apim/portal/user/entity/UserInfo.java index a9a9b4f..fdd9058 100644 --- a/src/main/java/com/eactive/apim/portal/user/entity/UserInfo.java +++ b/src/main/java/com/eactive/apim/portal/user/entity/UserInfo.java @@ -18,6 +18,7 @@ import org.springframework.data.annotation.LastModifiedDate; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import com.eactive.eai.data.converter.StringTrimConverter; import com.eactive.eai.data.entity.AbstractEntity; +import kjb.safedb.KjbNotRnnoJpaAttributeConverter; import lombok.Data; import lombok.EqualsAndHashCode; @@ -53,6 +54,7 @@ public class UserInfo extends AbstractEntity implements Serializable { @Column(length = 40) @Comment("휴대폰 번호") + @Convert(converter = KjbNotRnnoJpaAttributeConverter.class) private String cphnno; @Column(length = 40) @@ -61,6 +63,7 @@ public class UserInfo extends AbstractEntity implements Serializable { @Column(length = 100) @Comment("이메일 주소") + @Convert(converter = KjbNotRnnoJpaAttributeConverter.class) private String emad; @Column(length = 3) diff --git a/src/main/java/kjb/safedb/KjbNotRnnoJpaAttributeConverter.java b/src/main/java/kjb/safedb/KjbNotRnnoJpaAttributeConverter.java index 43f3772..20a0ec3 100644 --- a/src/main/java/kjb/safedb/KjbNotRnnoJpaAttributeConverter.java +++ b/src/main/java/kjb/safedb/KjbNotRnnoJpaAttributeConverter.java @@ -1,8 +1,8 @@ package kjb.safedb; -import com.eactive.ext.kjb.safedb.KjbSafedbWrapper; -import com.eactive.ext.kjb.safedb.SafeDBColumns; -import com.eactive.ext.kjb.safedb.Utils; +//import com.eactive.ext.kjb.safedb.KjbSafedbWrapper; +//import com.eactive.ext.kjb.safedb.SafeDBColumns; +//import com.eactive.ext.kjb.safedb.Utils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -25,26 +25,26 @@ public class KjbNotRnnoJpaAttributeConverter implements AttributeConverter {}", originalAttribute, attribute); - } - } +// String originalAttribute = attribute; +// if (StringUtils.isNotEmpty(attribute)) { +// KjbSafedbWrapper wrapper = KjbSafedbWrapper.getInstance(); +// attribute = wrapper.encryptNotRnnoString(attribute); +// +// if (log.isDebugEnabled()) { +// // originalAttribute 홀수 글자 마스킹 +// StringBuilder sb = new StringBuilder(); +// for (int i = 0; i < originalAttribute.length(); i++) { +// if (i % 2 == 0) { +// sb.append(originalAttribute.charAt(i)); +// } else { +// sb.append("*"); +// } +// } +// +// originalAttribute = sb.toString(); +// log.debug("DB 암호화 #2 : {} -> {}", originalAttribute, attribute); +// } +// } return attribute; } @@ -59,23 +59,23 @@ public class KjbNotRnnoJpaAttributeConverter implements AttributeConverter {}", dbDataOriginal, dbData); @@ -88,28 +88,29 @@ public class KjbNotRnnoJpaAttributeConverter implements AttributeConverter