34 Commits

Author SHA1 Message Date
eastargh bda9fad391 관리자포탈 로그인 2-factor 인증은 KAKAO로 발송
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-26 10:36:22 +09:00
Rinjae 363863746b - 인증 이벤트 매핑 추가: 이메일/휴대폰 인증번호 DB 변수 설정
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 초대 관련 이벤트 매핑 개선: %ORG_NAME% 변수 지원 추가
- 메시지 발송 공통 변수 자동 주입 로직 추가
2026-06-24 19:47:33 +09:00
Rinjae 1a12816643 제휴 생성 이벤트 처리 로직 추가: PartnershipCreatedEvent 구현
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-24 19:43:54 +09:00
Rinjae 6abb55a656 Merge remote-tracking branch 'origin/master'
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-24 16:56:07 +09:00
Rinjae 8cc6f775db DB 복호화 로직 개선: 디버그 로그에 홀수 글자 마스킹 추가 2026-06-24 16:55:59 +09:00
eastargh c3c10d1479 기관관리 - 리버스 프록시 경로 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-24 16:38:34 +09:00
Rinjae 13b00504ac - 모바일 약관 탭 UI 개선 - 스크롤 추가 및 스타일 조정
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 필수 입력 경고 메세지 추가 및 인증 미완료 경고 추가
2026-06-24 15:32:18 +09:00
Rinjae 208b0be097 - 초대 로직 변경: 이메일→휴대폰 기반 초대 전환 및 UI 업데이트
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 알림 수신 동의 추가: 초대/취소 시 알림 발송 옵션 탑재
- 관리자 기능 추가: 소속 제외 및 권한 조정 기능 구현
2026-06-23 20:58:27 +09:00
Rinjae 8c45451230 Merge remote-tracking branch 'origin/master'
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-23 19:27:46 +09:00
Rinjae dbdc06cf7e - 약관 유형에 개인정보수집동의서(PRIVACY_COLLECT) 항목 추가
- 관리화면 약관 유형 표시 시 '(사용안함)' 라벨 추가 처리
- 약관 유형 라벨 변환 로직 PortalTermsManService로 이동 및 적용
2026-06-23 19:27:37 +09:00
eastargh 898a6d37aa UMS 발송
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-23 14:50:52 +09:00
eastargh f67970c280 @component 제거, 다른 클래스에서 직접생성하여 사용을 권장
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-23 10:34:59 +09:00
eastargh 47b99d1858 admin에서 사용하기 위해 @component 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-23 10:20:42 +09:00
eastargh 384a3a5e47 UMS 발송
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-22 17:57:30 +09:00
Rinjae 207a2d692a 휴대폰번호 중복 사용자 조회 기능 추가
- 중복 사용자 필터링 및 목록 조회 기능 구현
- 중복 사용자 알림 배너 및 상세 조회 UI 개선
- 중복 허용 여부 설정 동기화 및 관련 서비스 추가
2026-06-22 17:03:33 +09:00
Rinjae ea6ae8c022 AgreementType 코드 업데이트: NOTIFICATION_CONSENT 동의서 항목 추가 2026-06-22 17:02:12 +09:00
Rinjae de262cec7b Damo 적용 준비 코드
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- KjbNotRnnoJpaAttributeConverter 삭제 및 PersonalDataEncryptConverter 추가
- damo-manager 지원을 위한 JSP 추가 및 레거시 암호화 마이그레이션 컨트롤러 구현
- 기존 엔티티 암호화 로직 변경 및 build.gradle에 damo-manager 의존성 추가
2026-06-22 16:29:45 +09:00
Rinjae 09013156dc 로그 레벨 수정: warn → error
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 메세지 템플릿 미존재 경고에서 에러로 변경
2026-06-19 19:40:30 +09:00
Rinjae 2e77ce373e 문의 상태값 수정: IN_PROGRESS → RESPONDED
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 문의 상태 필드 및 관련 로직/뷰 업데이트
- Validation 패턴 및 테스트 코드 반영
- 뷰 상태 배지 및 응답 섹션 표시 로직 수정
2026-06-19 10:16:16 +09:00
Rinjae-gf63 c959549b99 - PortalUser-테이블명 대소문자 수정
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- Credential-EqualsAndHashCode 어노테이션 추가
- 사업제휴신청 메시지 수정 및 파일 null 검사 추가
2026-06-18 16:28:20 +09:00
eastargh a974065fef 로그 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-18 09:18:59 +09:00
Rinjae a23f0f73f3 djb_apistatus_incident 엔티티 및 리포지토리 신설
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-16 14:54:31 +09:00
eastargh f26210b71a Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/elink-portal-common.git
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-16 11:12:08 +09:00
eastargh 284b843e30 ums관련 수정 2026-06-16 11:11:58 +09:00
Rinjae 2d5d8e4c4f Q&A 이벤트 리팩토링
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- InquiryCommentCreatedAdminEvent 삭제 및 경로 변경
- userName 정보 추가 및 description 업데이트
- inquirySubject 처리 로직 개선
2026-06-15 18:57:51 +09:00
Rinjae a14e755e6d Q&A 댓글 관련 코드 리팩토링 및 역할 기반 알림 기능 개선
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- InquiryComment 코드 구조 리팩토링 및 역할 분리
- portal-admin 알림 기능 추가 및 예외 처리 개선
- InquiryComment 관련 불필요 클래스/메서드 제거
2026-06-15 18:49:14 +09:00
Rinjae 2882ed035d Q&A 댓글 엔터티 헬퍼 및 알림 코드 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- InquiryComment 헬퍼 메서드 - 삭제/관리자 판정
- MessageCode - INQUIRY_COMMENT_CREATED_ADMIN 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 12:09:08 +09:00
Rinjae d4b9fe8ed5 Merge commit 'd0c029e954b26263492fb327a95c128f28dd4ec5'
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-06-09 20:34:33 +09:00
Rinjae f361a5adac eapim-portal CI 워크플로 및 정리
- gitea actions CI 워크플로
- 승인 관리 / 파일 / Q&A 등 잡정리
- 부팅/로깅 경로 정리 반영
2026-06-09 19:16:56 +09:00
eastargh d0c029e954 Q&A 댓글 엔터티 2026-06-09 10:04:57 +09:00
eastargh 9270167790 QnA게시판 댓글 작성 엔터티 2026-06-05 17:42:24 +09:00
eastargh 38d100543b 예상완료일 추가 2026-06-05 15:10:45 +09:00
eastargh 264bb8f729 제주은행 메세지 발송 2026-06-02 13:31:32 +09:00
eastargh ddb0ce8397 Message code 추가 2026-06-02 09:42:31 +09:00
42 changed files with 738 additions and 252 deletions
+2
View File
@@ -114,6 +114,8 @@ dependencies {
api group: 'commons-io', name: 'commons-io', version: '2.11.0' api group: 'commons-io', name: 'commons-io', version: '2.11.0'
compileOnly files('libs/damo-manager.jar')
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testImplementation 'ch.qos.logback:logback-classic:1.2.10' testImplementation 'ch.qos.logback:logback-classic:1.2.10'
Binary file not shown.
@@ -4,8 +4,10 @@ package com.eactive.apim.portal.agreements.entity;
public enum AgreementType { public enum AgreementType {
TERMS_OF_USE("TERMS_OF_USE", "이용약관"), TERMS_OF_USE("TERMS_OF_USE", "이용약관"),
PRIVACY_POLICY("PRIVACY_POLICY", "개인정보처리방침"), PRIVACY_POLICY("PRIVACY_POLICY", "개인정보처리방침"),
PRIVACY_COLLECT("PRIVACY_COLLECT", "개인정보수집동의서"),
PRIVACY_COLLECT_IND("PRIVACY_COLLECT_IND", "개인용 개인정보수집동의서"), PRIVACY_COLLECT_IND("PRIVACY_COLLECT_IND", "개인용 개인정보수집동의서"),
PRIVACY_COLLECT_ORG("PRIVACY_COLLECT_ORG", "법인용 개인정보수집동의서"); PRIVACY_COLLECT_ORG("PRIVACY_COLLECT_ORG", "법인용 개인정보수집동의서"),
NOTIFICATION_CONSENT("NOTIFICATION_CONSENT", "알림 수신 동의서");
private final String code; private final String code;
private final String description; private final String description;
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.agreements.entity.AgreementsId;
import com.eactive.apim.portal.agreements.entity.AgreementsStatus; import com.eactive.apim.portal.agreements.entity.AgreementsStatus;
import com.eactive.apim.portal.agreements.repository.AgreementsRepository; import com.eactive.apim.portal.agreements.repository.AgreementsRepository;
import com.eactive.apim.portal.common.exception.NotFoundException; import com.eactive.apim.portal.common.exception.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -16,6 +17,7 @@ import java.util.Optional;
@Service @Service
@Transactional @Transactional
@Slf4j
public class AgreementsService { public class AgreementsService {
private static final String NOT_FOUND_MESSAGE = "해당 버전의 약관이 존재하지 않습니다."; private static final String NOT_FOUND_MESSAGE = "해당 버전의 약관이 존재하지 않습니다.";
@@ -111,6 +113,7 @@ public class AgreementsService {
public List<Agreements> findAllAgreementsSortPublishedOn(AgreementType agreementType) { public List<Agreements> findAllAgreementsSortPublishedOn(AgreementType agreementType) {
List<Agreements> agreements = agreementsRepository.findAllByAgreementsTypeOrderByPublishedOnDesc(agreementType); List<Agreements> agreements = agreementsRepository.findAllByAgreementsTypeOrderByPublishedOnDesc(agreementType);
if(agreements.isEmpty()) { if(agreements.isEmpty()) {
log.error("해당 약관 종류의 약관이 존재하지 않습니다. agreementType={}", agreementType);
throw new IllegalStateException(NOT_FOUND_BY_DATE_MESSAGE); throw new IllegalStateException(NOT_FOUND_BY_DATE_MESSAGE);
} }
return agreements; return agreements;
@@ -16,6 +16,7 @@ import javax.persistence.JoinTable;
import javax.persistence.ManyToMany; import javax.persistence.ManyToMany;
import javax.persistence.Table; import javax.persistence.Table;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull; import lombok.NonNull;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollection;
@@ -23,6 +24,7 @@ import org.hibernate.annotations.LazyCollectionOption;
import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
@EqualsAndHashCode(callSuper = false)
@Entity @Entity
@Data @Data
@Table(name = "ptl_credential") @Table(name = "ptl_credential")
@@ -75,4 +75,7 @@ public class Approval implements Serializable, com.eactive.eai.data.Data {
@Column(length = 17, name = "end_date") @Column(length = 17, name = "end_date")
@Convert(converter = LocalDateTimeToStringConverter17.class) @Convert(converter = LocalDateTimeToStringConverter17.class)
private LocalDateTime approvalDate; private LocalDateTime approvalDate;
@Column(name = "expect_end_date")
private String expectEndDate; //예상완료일
} }
@@ -28,4 +28,8 @@ public interface ApprovalState extends Serializable {
} }
String getDescription(); String getDescription();
default String getCode() {
return toString();
}
} }
@@ -1,7 +1,7 @@
package com.eactive.apim.portal.common.entity; package com.eactive.apim.portal.common.entity;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14;
import kjb.safedb.KjbNotRnnoJpaAttributeConverter;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedBy;
@@ -34,7 +34,7 @@ public abstract class Auditable {
* 최초등록자ID * 최초등록자ID
*/ */
@Column(name = "created_by") @Column(name = "created_by")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
@CreatedBy @CreatedBy
private String createdBy; private String createdBy;
@@ -50,7 +50,7 @@ public abstract class Auditable {
* 최종수정자ID * 최종수정자ID
*/ */
@Column(name = "LAST_MODIFIED_BY") @Column(name = "LAST_MODIFIED_BY")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
@LastModifiedBy @LastModifiedBy
private String lastModifiedBy; private String lastModifiedBy;
@@ -0,0 +1,71 @@
package com.eactive.apim.portal.djb.apistatus.incident.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.time.LocalDateTime;
@Entity
@Table(name = "DJB_APISTATUS_INCIDENT")
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class DjbApistatusIncident extends Auditable {
@Id
@SequenceGenerator(name = "seqDjbApistatusIncident",
sequenceName = "SEQ_DJB_APISTATUS_INCIDENT", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqDjbApistatusIncident")
@Column(name = "INCIDENT_ID", nullable = false)
private Long incidentId;
@Enumerated(EnumType.STRING)
@Column(name = "KIND", length = 20, nullable = false)
private IncidentKind kind;
@Enumerated(EnumType.STRING)
@Column(name = "STATE", length = 30)
private IncidentState state;
@Enumerated(EnumType.STRING)
@Column(name = "PREVIOUS_STATE", length = 30)
private IncidentState previousState;
@Column(name = "TITLE", length = 500, nullable = false)
private String title;
@Column(name = "SUMMARY", length = 2000)
private String summary;
@Column(name = "STARTED_AT")
private LocalDateTime startedAt;
@Column(name = "END_AT")
private LocalDateTime endAt;
@Column(name = "DETECTED_BY", length = 30, nullable = false)
private String detectedBy = "MANUAL";
@Column(name = "INTERFACE_ID", length = 200)
private String interfaceId;
@Column(name = "NOTICE_ID", length = 36)
private String noticeId;
@Column(name = "DRAFT_YN", length = 1, nullable = false)
private String draftYn = "N";
@Column(name = "FIX_YN", length = 1, nullable = false)
private String fixYn = "N";
}
@@ -0,0 +1,38 @@
package com.eactive.apim.portal.djb.apistatus.incident.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import java.time.LocalDateTime;
@Entity
@Table(name = "DJB_APISTATUS_INCIDENT_API")
@IdClass(DjbApistatusIncidentApiId.class)
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class DjbApistatusIncidentApi extends Auditable {
@Id
@EqualsAndHashCode.Include
@Column(name = "INCIDENT_ID", nullable = false)
private Long incidentId;
@Id
@EqualsAndHashCode.Include
@Column(name = "API_ID", length = 30, nullable = false)
private String apiId;
@Column(name = "API_NAME", length = 200)
private String apiName;
@Column(name = "RECOVERED_AT")
private LocalDateTime recoveredAt;
}
@@ -0,0 +1,18 @@
package com.eactive.apim.portal.djb.apistatus.incident.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DjbApistatusIncidentApiId implements Serializable {
private static final long serialVersionUID = 1L;
private Long incidentId;
private String apiId;
}
@@ -0,0 +1,51 @@
package com.eactive.apim.portal.djb.apistatus.incident.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.time.LocalDateTime;
@Entity
@Table(name = "DJB_APISTATUS_INCIDENT_TIMELINE")
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class DjbApistatusIncidentTimeline extends Auditable {
@Id
@SequenceGenerator(name = "seqDjbApistatusIncidentTimeline",
sequenceName = "SEQ_DJB_APISTATUS_INCIDENT_TIMELINE", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqDjbApistatusIncidentTimeline")
@Column(name = "TIMELINE_ID", nullable = false)
private Long timelineId;
@Column(name = "INCIDENT_ID", nullable = false)
private Long incidentId;
@Column(name = "EVENT_AT", nullable = false)
private LocalDateTime eventAt;
@Enumerated(EnumType.STRING)
@Column(name = "STATE_AFTER", length = 30)
private IncidentState stateAfter;
@Column(name = "BODY", length = 4000, nullable = false)
private String body;
@Column(name = "AUTHOR_TYPE", length = 20, nullable = false)
private String authorType = "OPERATOR";
@Column(name = "VISIBLE_YN", length = 1, nullable = false)
private String visibleYn = "Y";
}
@@ -0,0 +1,6 @@
package com.eactive.apim.portal.djb.apistatus.incident.entity;
public enum IncidentKind {
INCIDENT,
MAINTENANCE
}
@@ -0,0 +1,9 @@
package com.eactive.apim.portal.djb.apistatus.incident.entity;
public enum IncidentState {
INVESTIGATING,
IDENTIFIED,
MONITORING,
RESOLVED,
CANCELED
}
@@ -0,0 +1,19 @@
package com.eactive.apim.portal.djb.apistatus.incident.repository;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApiId;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@EMSDataSource
public interface DjbApistatusIncidentApiRepository
extends JpaRepository<DjbApistatusIncidentApi, DjbApistatusIncidentApiId> {
List<DjbApistatusIncidentApi> findByIncidentIdOrderByApiId(Long incidentId);
void deleteByIncidentId(Long incidentId);
}
@@ -0,0 +1,17 @@
package com.eactive.apim.portal.djb.apistatus.incident.repository;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
@EMSDataSource
public interface DjbApistatusIncidentRepository
extends JpaRepository<DjbApistatusIncident, Long>, JpaSpecificationExecutor<DjbApistatusIncident> {
Optional<DjbApistatusIncident> findByNoticeId(String noticeId);
}
@@ -0,0 +1,18 @@
package com.eactive.apim.portal.djb.apistatus.incident.repository;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentTimeline;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@EMSDataSource
public interface DjbApistatusIncidentTimelineRepository
extends JpaRepository<DjbApistatusIncidentTimeline, Long> {
List<DjbApistatusIncidentTimeline> findByIncidentIdOrderByEventAtAsc(Long incidentId);
void deleteByIncidentId(Long incidentId);
}
@@ -1,6 +1,7 @@
package com.eactive.apim.portal.invitation.entity; package com.eactive.apim.portal.invitation.entity;
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus; import com.eactive.apim.portal.invitation.entity.UserInvitationEnums.InvitationStatus;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14;
import lombok.Data; import lombok.Data;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
@@ -29,10 +30,15 @@ public class UserInvitation {
@Comment("기관ID") @Comment("기관ID")
private String orgId; private String orgId;
@Column(name = "INVITATION_EMAIL", length = 30, nullable = false) @Column(name = "INVITATION_EMAIL", length = 256)
@Comment("초대이메일") @Comment("초대이메일")
private String invitationEmail; private String invitationEmail;
@Column(name = "INVITATION_MOBILE", length = 256)
@Convert(converter = PersonalDataEncryptConverter.class)
@Comment("초대휴대폰")
private String invitationMobile;
@Column(name = "INVITATION_DATE", length = 14, nullable = false) @Column(name = "INVITATION_DATE", length = 14, nullable = false)
@Convert(converter = LocalDateTimeToStringConverter14.class) @Convert(converter = LocalDateTimeToStringConverter14.class)
@Comment("초대일시") @Comment("초대일시")
@@ -25,6 +25,8 @@ public class UserInvitationCancelEvent implements MessageEventHandler {
Map<String, String> requestParams = new HashMap<>(); Map<String, String> requestParams = new HashMap<>();
requestParams.put("corpName", params.get("corpName").toString()); requestParams.put("corpName", params.get("corpName").toString());
requestParams.put("managerName", params.get("managerName").toString()); requestParams.put("managerName", params.get("managerName").toString());
// DB 템플릿 USER_INVITATION_CANCELED SMS 변수(%ORG_NAME%)에 매핑
requestParams.put("ORG_NAME", params.get("corpName").toString());
return new MessageSendEvent(source, KEY, recipient, requestParams); return new MessageSendEvent(source, KEY, recipient, requestParams);
} }
@@ -26,6 +26,8 @@ public class UserInvitationEvent implements MessageEventHandler {
requestParams.put("url", params.get("url").toString()); requestParams.put("url", params.get("url").toString());
requestParams.put("loginId",params.get("corpName").toString() + " 이용자"); requestParams.put("loginId",params.get("corpName").toString() + " 이용자");
requestParams.put("authNumber", params.get("authNumber").toString()); requestParams.put("authNumber", params.get("authNumber").toString());
// DB 템플릿 USER_INVITATION SMS 변수(%ORG_NAME%)에 매핑 (USER_NAME 은 공통 자동주입)
requestParams.put("ORG_NAME", params.get("corpName").toString());
return new MessageSendEvent(source, KEY, recipient, requestParams); return new MessageSendEvent(source, KEY, recipient, requestParams);
} }
@@ -22,4 +22,11 @@ public interface UserInvitationRepository extends JpaRepository<UserInvitation,
Optional<UserInvitation> findFirstByInvitationEmailAndOrgIdAndStatus(String email, String orgId, InvitationStatus status); Optional<UserInvitation> findFirstByInvitationEmailAndOrgIdAndStatus(String email, String orgId, InvitationStatus status);
Optional<UserInvitation> findFirstByInvitationEmailAndOrgId(String email, String orgId); Optional<UserInvitation> findFirstByInvitationEmailAndOrgId(String email, String orgId);
// 휴대폰 번호 기반 초대 조회 (INVITATION_MOBILE 은 결정적 암호화로 저장되어 equality 조회 가능)
Optional<UserInvitation> findFirstByInvitationMobileAndStatus(String mobile, InvitationStatus status);
Optional<UserInvitation> findFirstByInvitationMobileAndOrgIdAndStatus(String mobile, String orgId, InvitationStatus status);
Optional<UserInvitation> findFirstByInvitationMobileAndOrgId(String mobile, String orgId);
} }
@@ -0,0 +1,130 @@
package com.eactive.apim.portal.jpa;
import com.eactive.ext.djb.DamoManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Slf4j
@Converter
public class PersonalDataEncryptConverter implements AttributeConverter<String, String> {
/**
* 암호화
* @param attribute the entity attribute value to be converted
* @return
*/
@Override
public String convertToDatabaseColumn(String attribute) {
String originalAttribute = attribute;
if (StringUtils.isNotEmpty(attribute)) {
attribute = DamoManager.getInstance().encrypt(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 암호화 : {} -> {}", originalAttribute, attribute);
}
}
return attribute;
}
/**
* 복호화
* @param dbData the data from the database column to be
* converted
* @return
*/
@Override
public String convertToEntityAttribute(String dbData) {
String dbDataOriginal = dbData;
if (StringUtils.isNotEmpty(dbData)) {
if (this.isEncrypted(dbData)) {
try {
dbData = DamoManager.getInstance().decrypt(dbData);
} catch (Exception e) {
// 복호화 실패시 원본 반환
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
log.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
return dbData;
}
} else {
log.debug("DB 복호화 경고: Base64 형식이 아님. 복호화하지 않고 원본 반환. dbData={}", dbData);
}
}
if (log.isDebugEnabled()) {
// 복호화 결과(평문) 홀수 글자 마스킹
String maskedDbData = dbData;
if (StringUtils.isNotEmpty(dbData)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < dbData.length(); i++) {
if (i % 2 == 0) {
sb.append(dbData.charAt(i));
} else {
sb.append("*");
}
}
maskedDbData = sb.toString();
}
log.debug("DB 복호화 : {} -> {}", dbDataOriginal, maskedDbData);
}
return dbData;
}
/**
* 현재 damo-manager 가 bypass 모드(무변환)인지 여부.
* bypass 면 {@code encrypt}/{@code decrypt} 가 원문을 그대로 반환하므로 암호화/정규화가 no-op 이 된다.
* 레거시 평문 일괄 암호화 같은 마이그레이션 작업은 이 경우 의미가 없으므로 호출 측에서 차단해야 한다.
*/
public boolean isBypassMode() {
return DamoManager.getInstance().isBypassMode();
}
/**
* 암호화 여부 확인(base64 인코딩 여부로 판단, 또는 0-9, - 로 구성된 경우 암호화 되지 않은 걸로 판단(연락처))
* @param data
* @return
*/
private boolean isEncrypted(String data) {
if (StringUtils.isEmpty(data)) {
return false;
}
// 숫자, - 로만 구성된 경우 암호화 되지 않은 걸로 판단 (ex: 연락처)
if (data.matches("^[0-9-]+$")) {
return false;
}
data = data.trim();
// Base64 형식 체크
if (!data.matches("^[A-Za-z0-9+/]+={0,2}$")) {
return false;
}
// Base64 길이 체크 (4의 배수)
if (data.length() % 4 != 0) {
return false;
}
return true;
}
}
@@ -0,0 +1,60 @@
package com.eactive.apim.portal.partnership.event;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class PartnershipCreatedEvent implements MessageEventHandler {
public static final MessageCode KEY = MessageCode.PARTNERSHIP_CREATED;
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> requestParams = new HashMap<>();
String subject = toStr(params.get("bizSubject"));
if (subject.isEmpty()) {
subject = toStr(params.get("subject"));
}
requestParams.put("partnershipId", toStr(params.get("partnershipId")));
requestParams.put("bizSubject", subject);
requestParams.put("subject", subject);
requestParams.put("writerName", toStr(params.get("writerName")));
return new MessageSendEvent(source, KEY, recipient, requestParams);
}
private String toStr(Object value) {
return value == null ? "" : value.toString();
}
@Override
public MessageCode getKey() {
return KEY;
}
@Override
public String getDisplayName() {
return "개선요청/제휴 게시물 등록";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - userName: 수신자 이름 </div><br/>"
+ "<div> - userId: 수신자 ID (이메일) </div><br/>"
+ "<div> - partnershipId: 신청 ID </div><br/>"
+ "<div> - bizSubject: 신청 제목 </div><br/>"
+ "<div> - subject: 신청 제목 (별칭) </div><br/>"
+ "<div> - writerName: 작성자 이름 </div><br/>";
}
}
@@ -91,4 +91,8 @@ public class PortalOrg extends Auditable implements Serializable, com.eactive.ea
@Column(name = "sc_phone_number", length = 20) @Column(name = "sc_phone_number", length = 20)
@Comment("고객센터연락처") @Comment("고객센터연락처")
private String scPhoneNumber; private String scPhoneNumber;
@Column(name = "reverse_proxy_path", length = 255)
@Comment("리버스 프록시 경로")
private String reverseProxyPath;
} }
@@ -1,13 +1,12 @@
package com.eactive.apim.portal.portaluser.entity; package com.eactive.apim.portal.portaluser.entity;
import com.eactive.apim.portal.common.entity.Auditable; import com.eactive.apim.portal.common.entity.Auditable;
import com.eactive.apim.portal.portalorg.entity.PortalOrg; import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.ApprovalStatus; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.ApprovalStatus;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus;
import java.io.Serializable; import java.io.Serializable;
import kjb.safedb.KjbNotRnnoJpaAttributeConverter;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
@@ -22,7 +21,7 @@ import java.time.LocalDateTime;
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity @Entity
@Table(name = "ptl_user") @Table(name = "PTL_USER")
public class PortalUser extends Auditable implements Serializable, com.eactive.eai.data.Data { public class PortalUser extends Auditable implements Serializable, com.eactive.eai.data.Data {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -34,7 +33,7 @@ public class PortalUser extends Auditable implements Serializable, com.eactive.e
@Column(name = "login_id", length = 200, nullable = false) @Column(name = "login_id", length = 200, nullable = false)
@Comment("로그인ID") @Comment("로그인ID")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String loginId; private String loginId;
@Column(name = "password_hash", length = 60) @Column(name = "password_hash", length = 60)
@@ -47,17 +46,17 @@ public class PortalUser extends Auditable implements Serializable, com.eactive.e
@Column(name = "phone_number", length = 50) @Column(name = "phone_number", length = 50)
@Comment("전화번호") @Comment("전화번호")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String phoneNumber; private String phoneNumber;
@Column(name = "mobile_number", length = 50) @Column(name = "mobile_number", length = 50)
@Comment("휴대폰 번호") @Comment("휴대폰 번호")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String mobileNumber; private String mobileNumber;
@Column(name = "email_addr", length = 200) @Column(name = "email_addr", length = 200)
@Comment("이메일주소") @Comment("이메일주소")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String emailAddr; private String emailAddr;
@ManyToOne(fetch = FetchType.EAGER) @ManyToOne(fetch = FetchType.EAGER)
@@ -1,7 +1,7 @@
package com.eactive.apim.portal.portaluser.entity; package com.eactive.apim.portal.portaluser.entity;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14;
import kjb.safedb.KjbNotRnnoJpaAttributeConverter;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*; import javax.persistence.*;
@@ -25,9 +25,14 @@ public class TwoFactorAuth {
private String id; private String id;
@Column(name = "recipient", length = 255) @Column(name = "recipient", length = 255)
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String recipient; private String recipient;
// recipient 가 암호화 컬럼이라 값으로 동등 조회/중복정리가 불가능하므로,
// recipient 평문의 결정적 해시(HMAC-SHA256, hex)를 별도 조회 키로 둔다. (비암호화)
@Column(name = "recipient_key", length = 64)
private String recipientKey;
@Column(name = "auth_number", length = 255) @Column(name = "auth_number", length = 255)
private String authNumber; private String authNumber;
@@ -49,6 +54,14 @@ public class TwoFactorAuth {
this.recipient = recipient; this.recipient = recipient;
} }
public String getRecipientKey() {
return recipientKey;
}
public void setRecipientKey(String recipientKey) {
this.recipientKey = recipientKey;
}
public String getAuthNumber() { public String getAuthNumber() {
return authNumber; return authNumber;
} }
@@ -24,7 +24,10 @@ public class UserPasswordResetEvent implements MessageEventHandler {
@Override @Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) { public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> requestParams = new HashMap<>(); Map<String, String> requestParams = new HashMap<>();
requestParams.put("tempPassword", (String) params.get("tempPassword")); String tempPassword = (String) params.get("tempPassword");
requestParams.put("tempPassword", tempPassword);
// DB 템플릿 USER_PASSWORD_RESET SMS 변수(%TEMP_PASSWORD%)에 매핑
requestParams.put("TEMP_PASSWORD", tempPassword);
return new MessageSendEvent(source, KEY, recipient, requestParams); return new MessageSendEvent(source, KEY, recipient, requestParams);
} }
@@ -23,7 +23,10 @@ public class UserVerificationEmailEvent implements MessageEventHandler {
@Override @Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) { public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> requestParams = new HashMap<>(); Map<String, String> requestParams = new HashMap<>();
requestParams.put("authNumber", (String) params.get("authNumber")); String authNumber = (String) params.get("authNumber");
requestParams.put("authNumber", authNumber);
// DB 템플릿 USER_VERIFICATION_EMAIL 변수(%AUTH_NO%)에 매핑
requestParams.put("AUTH_NO", authNumber);
return new MessageSendEvent(source, KEY, recipient, requestParams); return new MessageSendEvent(source, KEY, recipient, requestParams);
} }
@@ -24,7 +24,10 @@ public class UserVerificationMobilePhoneEvent implements MessageEventHandler {
@Override @Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) { public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> requestParams = new HashMap<>(); Map<String, String> requestParams = new HashMap<>();
requestParams.put("authNumber", (String) params.get("authNumber")); String authNumber = (String) params.get("authNumber");
requestParams.put("authNumber", authNumber);
// DB 템플릿 USER_VERIFICATION_MOBILEPHONE SMS 변수(%AUTH_CODE%)에 매핑
requestParams.put("AUTH_CODE", authNumber);
return new MessageSendEvent(source, KEY, recipient, requestParams); return new MessageSendEvent(source, KEY, recipient, requestParams);
} }
@@ -28,6 +28,11 @@ public interface PortalUserRepository extends BaseRepository<PortalUser, String>
boolean existsByLoginId(String loginId); boolean existsByLoginId(String loginId);
boolean existsByMobileNumber(String mobileNumber);
// 휴대폰 번호 기반 기존 회원 조회 (mobileNumber 는 유니크 제약이 없어 다건 가능)
List<PortalUser> findAllByMobileNumber(String mobileNumber);
@Query("SELECT u FROM PortalUser u WHERE u.portalOrg = :org AND u.userStatus != :status") @Query("SELECT u FROM PortalUser u WHERE u.portalOrg = :org AND u.userStatus != :status")
Page<PortalUser> findActiveUsers(@Param("org") PortalOrg org, @Param("status") PortalUserEnums.UserStatus status, Pageable pageable); Page<PortalUser> findActiveUsers(@Param("org") PortalOrg org, @Param("status") PortalUserEnums.UserStatus status, Pageable pageable);
@@ -36,5 +41,12 @@ public interface PortalUserRepository extends BaseRepository<PortalUser, String>
long countByPortalOrg(PortalOrg portalOrg); long countByPortalOrg(PortalOrg portalOrg);
Optional<PortalUser> findByUserName(String userName); Optional<PortalUser> findByUserName(String userName);
// 암복
@Query(
value = "select * from ${ems.datasource.schema}.ptl_user where email_addr = :emailAddr",
nativeQuery = true
)
Optional<PortalUser> findByEmailAddrRaw(@Param("emailAddr") String emailAddr);
} }
@@ -6,7 +6,16 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional; import java.util.Optional;
public interface TwoFactorAuthRepository extends JpaRepository<TwoFactorAuth, String> { public interface TwoFactorAuthRepository extends JpaRepository<TwoFactorAuth, String> {
// recipientKey(결정적 해시) 기반 조회/정리. recipient 가 암호화 컬럼이라 값 동등 조회가 깨지는 문제를 우회한다.
Optional<TwoFactorAuth> findByRecipientKey(String recipientKey);
void deleteAllByRecipientKey(String recipientKey);
// 아래 두 메서드는 recipient(암호화 컬럼) 기반이라 신뢰할 수 없음 — 신규 코드에서는 RecipientKey 버전을 사용할 것.
@Deprecated
Optional<TwoFactorAuth> findByRecipient(String recipientKey); Optional<TwoFactorAuth> findByRecipient(String recipientKey);
@Deprecated
void deleteAllByRecipient(String recipient); void deleteAllByRecipient(String recipient);
} }
@@ -49,6 +49,7 @@ public class Inquiry extends Auditable {
/** /**
* 문의 상태 (예: 대기중, 답변 완료 등) * 문의 상태 (예: 대기중, 답변 완료 등)
* @DjbInquiryStatus 참조
*/ */
@Column(name = "INQUIRY_STATUS", length = 32) @Column(name = "INQUIRY_STATUS", length = 32)
private String inquiryStatus; private String inquiryStatus;
@@ -0,0 +1,50 @@
package com.eactive.apim.portal.qna.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "PTL_INQUIRY_COMMENT")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(exclude = "inquiry")
public class InquiryComment extends Auditable implements Serializable {
@Id
@Column(name = "id", length = 36, nullable = false)
@GenericGenerator(name = "uuid-gen", strategy = "uuid2")
@GeneratedValue(generator = "uuid-gen", strategy = GenerationType.IDENTITY)
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "INQUIRY_ID", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Inquiry inquiry;
@Lob
@Column(name = "COMMENT_DETAIL")
private String commentDetail;
@Column(name = "DEL_YN", length = 1)
private String delYn = "N";
@Column(name = "ADMIN_YN", length = 1)
private String adminYn = "N";
public void markDeleted() {
this.delYn = "Y";
}
public boolean isDeleted() {
return "Y".equals(this.delYn);
}
public boolean isAdmin() {
return "Y".equals(this.adminYn);
}
}
@@ -0,0 +1,56 @@
package com.eactive.apim.portal.qna.event;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class InquiryCommentCreatedEvent implements MessageEventHandler {
public static final MessageCode KEY = MessageCode.INQUIRY_COMMENT_CREATED;
@Override
public MessageCode getKey() {
return KEY;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "Q&A 댓글 등록 알림";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - userName: 수신자 이름 </div><br/>"
+ "<div> - userId: 수신자 ID </div><br/>"
+ "<div> - inquiryId: 문의 ID </div><br/>"
+ "<div> - inquirySubject: 문의 제목 </div><br/>"
+ "<div> - commentContent: 댓글 본문 </div><br/>"
+ "<div> - writerName: 작성자 이름 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> requestParams = new HashMap<>();
requestParams.put("inquiryId", toStr(params.get("inquiryId")));
requestParams.put("inquirySubject", toStr(params.get("inquirySubject")));
requestParams.put("commentContent", toStr(params.get("commentContent")));
requestParams.put("writerName", toStr(params.get("writerName")));
return new MessageSendEvent(source, KEY, recipient, requestParams);
}
private String toStr(Object value) {
return value == null ? "" : value.toString();
}
}
@@ -23,11 +23,21 @@ public class InquiryCreatedEvent implements MessageEventHandler {
@Override @Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) { public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> requestParams = new HashMap<>(); Map<String, String> requestParams = new HashMap<>();
requestParams.put("inquiryId", (String) params.get("inquiryId")); String subject = toStr(params.get("inquirySubject"));
requestParams.put("subject", (String) params.get("subject")); if (subject.isEmpty()) {
subject = toStr(params.get("subject"));
}
requestParams.put("inquiryId", toStr(params.get("inquiryId")));
requestParams.put("inquirySubject", subject);
requestParams.put("subject", subject);
requestParams.put("writerName", toStr(params.get("writerName")));
return new MessageSendEvent(source, KEY, recipient, requestParams); return new MessageSendEvent(source, KEY, recipient, requestParams);
} }
private String toStr(Object value) {
return value == null ? "" : value.toString();
}
@Override @Override
public MessageCode getKey() { public MessageCode getKey() {
return KEY; return KEY;
@@ -44,7 +54,9 @@ public class InquiryCreatedEvent implements MessageEventHandler {
+ "<div> - userName: 수신자 이름 </div><br/>" + "<div> - userName: 수신자 이름 </div><br/>"
+ "<div> - userId: 수신자 ID (이메일) </div><br/>" + "<div> - userId: 수신자 ID (이메일) </div><br/>"
+ "<div> - inquiryId: 질문 ID </div><br/>" + "<div> - inquiryId: 질문 ID </div><br/>"
+ "<div> - subject: 제목 </div><br/>"; + "<div> - inquirySubject: 질문 제목 </div><br/>"
+ "<div> - subject: 질문 제목 (별칭) </div><br/>"
+ "<div> - writerName: 작성자 이름 </div><br/>";
} }
} }
@@ -10,78 +10,47 @@ public enum MessageCode {
// 사용자 이벤트 영역 // 사용자 이벤트 영역
USER_VERIFICATION_EMAIL("이메일 주소 인증", "FM_APIM001", Channels.EMAIL), USER_VERIFICATION_EMAIL("이메일 주소 인증", null, Channels.EMAIL),
USER_VERIFICATION_MOBILEPHONE("휴대폰 번호 인증", "S55_ICT_0006", Channels.KAKAO_ALIMTALK), USER_VERIFICATION_MOBILEPHONE("휴대폰 번호 인증", null, Channels.KAKAO_ALIMTALK),
USER_PASSWORD_RESET("비밀번호 재설정", "FM_APIM002", Channels.EMAIL), USER_PASSWORD_RESET("비밀번호 재설정", null, Channels.KAKAO_ALIMTALK),
USER_PASSWORD_CHANGED("사용자 비밀번호 변경", "FM_APIM003", Channels.EMAIL), USER_PASSWORD_CHANGED("사용자 비밀번호 변경", null, Channels.KAKAO_ALIMTALK),
USER_ACCOUNT_LOCKED("계정 잠금 알림", "FM_APIM004", Channels.EMAIL), USER_ACCOUNT_LOCKED("계정 잠금 알림", null, Channels.KAKAO_ALIMTALK),
// 법인관리자 이벤트 영역 // 법인관리자 이벤트 영역
USER_INVITATION("법인 이용자 이메일 초대", "FM_APIM005", Channels.EMAIL), USER_INVITATION("법인 이용자 이메일 초대", null, Channels.KAKAO_ALIMTALK),
USER_INVITATION_CANCELED("법인 이용자 이메일 초대 취소", "FM_APIM006", Channels.EMAIL), USER_INVITATION_CANCELED("법인 이용자 이메일 초대 취소", null, Channels.KAKAO_ALIMTALK),
MANAGER_WITH_ORG_REGISTER_APPROVED("법인관리자 및 법인 등록 승인", "FM_APIM007", Channels.EMAIL),
MANAGER_WITH_ORG_REGISTER_REJECTED("법인관리자 및 법인 등록 거절", "FM_APIM008", Channels.EMAIL),
MANAGER_WITH_ORG_REGISTER_APPROVED("법인관리자 및 법인 등록 승인", null, Channels.KAKAO_ALIMTALK),
MANAGER_WITH_ORG_REGISTER_REJECTED("법인관리자 및 법인 등록 거절", null, Channels.KAKAO_ALIMTALK),
// 관리자 영역 // 관리자 영역
APP_REGISTER_APPROVED("등록 승인", "FM_APIM009", Channels.EMAIL), APP_APPROVE("사용 승인", null, Channels.KAKAO_ALIMTALK),
APP_REGISTER_REJECTED("등록 거절", "FM_APIM010", Channels.EMAIL), APP_REJECTED("사용 거절", null, Channels.KAKAO_ALIMTALK),
// Q&A 등록/댓글 등록 알림 (DJBank 커스텀 — portal-admin 수신)
INQUIRY_CREATED("Q&A 등록 알림", null, Channels.SWING),
INQUIRY_COMMENT_CREATED("Q&A 댓글 등록 알림", null, Channels.SWING),
PARTNERSHIP_CREATED("개선요청/제휴 게시물 등록 알림", null, Channels.SWING),
// 미 사용, 미 분류 // 제주은행
@Deprecated API_STATUS_CHANGED("API 상태 변화", null, Channels.SWING),
INFLOW_TOKEN_FAILED("유량제어 토큰 획득 실패", null, Channels.SWING),
ADMIN_VERIFICATION_MOBILEPHONE("관리자포탈 휴대폰 번호 인증", null, Channels.KAKAO_ALIMTALK),
// 레거시/미사용 — Event 핸들러 클래스 호환을 위해 유지 (실제 발송 없음)
API_APPROVED("API 승인 알림", null, Channels.NONE), API_APPROVED("API 승인 알림", null, Channels.NONE),
@Deprecated
API_REJECTED("API 거절 알림", null, Channels.NONE),
@Deprecated
APP_REGISTER("앱 등록 완료", null, Channels.NONE),
@Deprecated
USER_MANAGER_ASSIGNED("관리자 권한 부여", null, Channels.NONE),
@Deprecated
REQUEST_AUTH_NUMBER("인증번호 요청", null, Channels.NONE),
@Deprecated
USER_WITHDRAWAL_APPROVED("계정 비활성화", null, Channels.NONE),
@Deprecated
USER_ACTIVATION("사용자 활성화 완료", null, Channels.NONE), // 이게 왜...이메일 인증인건데........
@Deprecated
APP_REGISTER_REQUEST("앱 등록 요청", null, Channels.NONE), APP_REGISTER_REQUEST("앱 등록 요청", null, Channels.NONE),
APP_REGISTER_REJECTED("앱 등록 거절", null, Channels.NONE),
@Deprecated
UNKNOWN_ERROR("알수 없는 에러 발생", null, Channels.NONE), UNKNOWN_ERROR("알수 없는 에러 발생", null, Channels.NONE),
@Deprecated
PASSWORD_RESET_COMPLETE("비밀번호 변경 완료", null, Channels.NONE),
@Deprecated
REQUEST_PASSWORD_RESET("비밀번호 초기화 요청", null, Channels.NONE),
@Deprecated // 정확한 용도 모름
USER_REGISTRATION_APPROVED("사용자 등록 승인 완료", null, Channels.NONE),
@Deprecated // 정확한 용도 모름
USER_REGISTRATION_DENIED("사용자 등록 거절", null, Channels.NONE),
@Deprecated // 정확한 용도 모름
USER_REGISTRATION_REQUEST_ADMIN("사용자 등록 승인 요청", null, Channels.NONE), USER_REGISTRATION_REQUEST_ADMIN("사용자 등록 승인 요청", null, Channels.NONE),
USER_REGISTRATION_DENIED("사용자 등록 거절", null, Channels.NONE),
@Deprecated // 정확한 용도 모름
USER_REGISTRATION_REQUEST_USER("사용자 등록 요청 완료", null, Channels.NONE), USER_REGISTRATION_REQUEST_USER("사용자 등록 요청 완료", null, Channels.NONE),
REQUEST_PASSWORD_RESET("비밀번호 초기화 요청", null, Channels.NONE),
@Deprecated // 정확한 용도 모름 USER_MANAGER_ASSIGNED("관리자 권한 부여", null, Channels.NONE),
INQUIRY_CREATED("QnA 질문 등록", null, Channels.NONE), USER_REGISTRATION_APPROVED("사용자 등록 승인 완료", null, Channels.NONE),
PASSWORD_RESET_COMPLETE("비밀번호 변경 완료", null, Channels.NONE),
@Deprecated
INQUIRY_RESPONSE("QnA 답변 작성", null, Channels.NONE) INQUIRY_RESPONSE("QnA 답변 작성", null, Channels.NONE)
; ;
@@ -115,6 +84,7 @@ public enum MessageCode {
NONE (0b0000), NONE (0b0000),
EMAIL (0b0001), EMAIL (0b0001),
KAKAO_ALIMTALK (0b0010), KAKAO_ALIMTALK (0b0010),
SWING (0b0100),
; ;
@Getter @Getter
@@ -1,9 +1,9 @@
package com.eactive.apim.portal.template.entity; package com.eactive.apim.portal.template.entity;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14;
import kjb.safedb.KjbNotRnnoJpaAttributeConverter;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@@ -53,10 +53,10 @@ public class MessageRequest implements Serializable {
private String username; private String username;
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String email; private String email;
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String phone; private String phone;
@ManyToOne(fetch = FetchType.EAGER) @ManyToOne(fetch = FetchType.EAGER)
@@ -66,7 +66,7 @@ public class MessageRequest implements Serializable {
private PortalUser user; private PortalUser user;
@Comment("UMS 연계용 UID") @Comment("UMS 연계용 UID")
@Column(name="ums_uid", length = 16) @Column(name="ums_uid", length = 36)
private String umsUid; private String umsUid;
@Column(name = "messenger_id") @Column(name = "messenger_id")
@@ -11,7 +11,8 @@ import lombok.*;
@Builder @Builder
public class MessageRecipient { public class MessageRecipient {
private String username; private String username;
private String userId; //email private String userId;
private String email;
private String phone; private String phone;
private String messengerId; private String messengerId;
@@ -19,7 +20,8 @@ public class MessageRecipient {
public static MessageRecipient of(PortalUser user) { public static MessageRecipient of(PortalUser user) {
MessageRecipient recipient = new MessageRecipient(); MessageRecipient recipient = new MessageRecipient();
recipient.setUsername(user.getUserName()); recipient.setUsername(user.getUserName());
recipient.setUserId(user.getEmailAddr()); recipient.setUserId(user.getLoginId());
recipient.setEmail(user.getEmailAddr());
recipient.setPhone(user.getPhoneNumber()); recipient.setPhone(user.getPhoneNumber());
return recipient; return recipient;
} }
@@ -59,7 +59,7 @@ public class MessageSendService {
List<MessageRecipient> recipients = new ArrayList<>(); List<MessageRecipient> recipients = new ArrayList<>();
for (UserInfo user : template.getAdditionalRecipients()) { for (UserInfo user : template.getAdditionalRecipients()) {
MessageRecipient messageRecipient = new MessageRecipient(user.getUsername(), user.getUserid(), user.getCphnno(), user.getUserid()); MessageRecipient messageRecipient = new MessageRecipient(user.getUsername(), user.getUserid(), user.getEmad(), user.getCphnno(), user.getUserid());
recipients.add(messageRecipient); recipients.add(messageRecipient);
} }
@@ -78,7 +78,7 @@ public class MessageSendService {
String subject = buildMessage(template.getSubjectTemplate(), messageParams); String subject = buildMessage(template.getSubjectTemplate(), messageParams);
if (template.getEnableSms().equalsIgnoreCase("Y")) { if (template.getEnableSms().equalsIgnoreCase("Y")) {
String smsEaiInterfaceId = portalProperties.get("message.integration.eaiId.sms"); String smsEaiInterfaceId = portalProperties.get("ums.sms.if_id");
MessageRequest smsRequest = new MessageRequest(); MessageRequest smsRequest = new MessageRequest();
smsRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode().toUpperCase())); smsRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode().toUpperCase()));
smsRequest.setSubject(subject); smsRequest.setSubject(subject);
@@ -88,14 +88,18 @@ public class MessageSendService {
smsRequest.setUsername(user.getUsername()); smsRequest.setUsername(user.getUsername());
smsRequest.setPhone(user.getPhone()); smsRequest.setPhone(user.getPhone());
smsRequest.setEaiInterfaceId(smsEaiInterfaceId); smsRequest.setEaiInterfaceId(smsEaiInterfaceId);
smsRequest.setServiceId(portalProperties.get("message.integration.template.sms." + template.getMessageCode())); smsRequest.setServiceId(portalProperties.get("ums.sms.tx_id"));
smsRequest.setMessageType("KAKAO"); if ("ADMIN_VERIFICATION_MOBILEPHONE".equals(template.getMessageCode().toUpperCase())) {
smsRequest.setMessageType("KAKAO");
} else {
smsRequest.setMessageType("SMS");
}
messageRequestRepository.save(smsRequest); messageRequestRepository.save(smsRequest);
logger.debug(smsRequest.toString()); logger.debug(smsRequest.toString());
} }
if (template.getEnableEmail().equalsIgnoreCase("Y")) { if (template.getEnableEmail().equalsIgnoreCase("Y")) {
String mailEaiInterfaceId = portalProperties.get("message.integration.eaiId.email"); String mailEaiInterfaceId = portalProperties.get("ums.email.if_id");
MessageRequest emailRequest = new MessageRequest(); MessageRequest emailRequest = new MessageRequest();
emailRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode().toUpperCase())); emailRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode().toUpperCase()));
emailRequest.setSubject(subject); emailRequest.setSubject(subject);
@@ -105,33 +109,39 @@ public class MessageSendService {
emailRequest.setUsername(user.getUsername()); emailRequest.setUsername(user.getUsername());
emailRequest.setEmail(user.getUserId()); emailRequest.setEmail(user.getUserId());
emailRequest.setEaiInterfaceId(mailEaiInterfaceId); emailRequest.setEaiInterfaceId(mailEaiInterfaceId);
emailRequest.setServiceId(portalProperties.get("message.integration.template.email." + template.getMessageCode())); emailRequest.setServiceId(portalProperties.get("ums.email.tx_id"));
emailRequest.setMessageType("EMAIL"); emailRequest.setMessageType("EMAIL");
messageRequestRepository.save(emailRequest); messageRequestRepository.save(emailRequest);
logger.debug(emailRequest.toString()); logger.debug(emailRequest.toString());
} }
//메신저는 사용하지 않아서 주석 처리함. if (template.getEnableMessenger().equalsIgnoreCase("Y")) {
// if (template.getEnableMessenger().equalsIgnoreCase("Y")) { String swingEaiInterfaceId = portalProperties.get("ums.messenger.if_id");
// MessageRequest messengerRequest = new MessageRequest(); MessageRequest messengerRequest = new MessageRequest();
// messengerRequest.setMessageCode(MessageRequestEnums.MessageCode.valueOf(template.getMessageCode())); messengerRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode()));
// messengerRequest.setSubject(subject); messengerRequest.setSubject(subject);
// String messengerTemplate = buildMessage(template.getMessengerTemplate(), messageParams); String messengerTemplate = buildMessage(template.getMessengerTemplate(), messageParams);
// messengerRequest.setMessage(messengerTemplate); messengerRequest.setMessage(messengerTemplate);
// messengerRequest.setRequestDate(LocalDateTime.now()); messengerRequest.setRequestDate(LocalDateTime.now());
// messengerRequest.setUsername(user.getUsername()); messengerRequest.setUsername(user.getUsername());
// messengerRequest.setMessengerId(user.getMessengerId()); messengerRequest.setMessengerId(user.getUserId());
// messengerRequest.setEaiInterfaceId(); messengerRequest.setEaiInterfaceId(swingEaiInterfaceId);
// messengerRequest.setServiceId(); messengerRequest.setServiceId(portalProperties.get("ums.messenger.tx_id"));
// messageRequestRepository.save(messengerRequest); messengerRequest.setMessageType("MESSENGER");
// } messageRequestRepository.save(messengerRequest);
logger.debug(messengerRequest.toString());
}
} }
private void updateMessageParams(MessageRecipient user, Map<String, String> messageParams) { private void updateMessageParams(MessageRecipient user, Map<String, String> messageParams) {
// 공통 자동주입. 대문자 별칭(USER_ID/USER_NAME)은 DB 템플릿 변수 표기와 맞춘다.
// putIfAbsent 이므로 개별 이벤트(createEvent)가 먼저 넣은 값이 있으면 그대로 둔다(오버라이드).
messageParams.put("userId", user.getUserId()); messageParams.put("userId", user.getUserId());
messageParams.putIfAbsent("USER_ID", user.getUserId());
if (StringUtils.hasText(user.getUsername())) { if (StringUtils.hasText(user.getUsername())) {
messageParams.put("userName", user.getUsername()); messageParams.put("userName", user.getUsername());
messageParams.putIfAbsent("USER_NAME", user.getUsername());
} }
} }
@@ -1,28 +1,20 @@
package com.eactive.apim.portal.user.entity; package com.eactive.apim.portal.user.entity;
import java.io.Serializable; import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import java.time.LocalDateTime; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14;
import com.eactive.eai.data.converter.StringTrimConverter;
import javax.persistence.Cacheable; import com.eactive.eai.data.entity.AbstractEntity;
import javax.persistence.Column; import lombok.Data;
import javax.persistence.Convert; import lombok.EqualsAndHashCode;
import javax.persistence.Entity; import lombok.NonNull;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import javax.persistence.*;
import com.eactive.eai.data.converter.StringTrimConverter; import java.io.Serializable;
import com.eactive.eai.data.entity.AbstractEntity; import java.time.LocalDateTime;
import kjb.safedb.KjbNotRnnoJpaAttributeConverter;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data @Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@@ -54,7 +46,7 @@ public class UserInfo extends AbstractEntity<String> implements Serializable {
@Column(length = 40) @Column(length = 40)
@Comment("휴대폰 번호") @Comment("휴대폰 번호")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String cphnno; private String cphnno;
@Column(length = 40) @Column(length = 40)
@@ -63,7 +55,7 @@ public class UserInfo extends AbstractEntity<String> implements Serializable {
@Column(length = 100) @Column(length = 100)
@Comment("이메일 주소") @Comment("이메일 주소")
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String emad; private String emad;
@Column(length = 3) @Column(length = 3)
@@ -1,19 +1,12 @@
package com.eactive.apim.portal.user.entity; package com.eactive.apim.portal.user.entity;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14; import com.eactive.eai.data.converter.LocalDateTimeToStringConverter14;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.TableGenerator;
import kjb.safedb.KjbNotRnnoJpaAttributeConverter;
import lombok.Data; import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@TableGenerator( @TableGenerator(
name = "USER_LOGIN_LOG_GEN", name = "USER_LOGIN_LOG_GEN",
table = "PTL_ID", table = "PTL_ID",
@@ -32,7 +25,7 @@ public class UserLog {
private Long logId; private Long logId;
@Column(name = "login_id", nullable = false) @Column(name = "login_id", nullable = false)
@Convert(converter = KjbNotRnnoJpaAttributeConverter.class) @Convert(converter = PersonalDataEncryptConverter.class)
private String loginId; private String loginId;
@Column(name = "login_time") @Column(name = "login_time")
@@ -1,116 +0,0 @@
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Converter
@Slf4j
public class KjbNotRnnoJpaAttributeConverter implements AttributeConverter<String, String> {
private static final Logger needFixLogger = LoggerFactory.getLogger("eapim.portal.needfix");
/**
* 암호화
* @param attribute the entity attribute value to be converted
* @return
*/
@Override
public String convertToDatabaseColumn(String attribute) {
// log.debug("DB 암호화 #1 - {} / {}", attribute, SafeDBColumns.NOT_RNNO);
// 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;
}
/**
* 복호화
* @param dbData the data from the database column to be
* converted
* @return
*/
@Override
public String convertToEntityAttribute(String dbData) {
// log.debug("DB 복호화 #1 - {} / {}", dbData, SafeDBColumns.NOT_RNNO);
// String dbDataOriginal = dbData;
// if (StringUtils.isNotEmpty(dbData)) {
// if (this.isEncrypted(dbData)) {
// KjbSafedbWrapper safeDBWrapper = KjbSafedbWrapper.getInstance();
// try {
// dbData = safeDBWrapper.decryptNotRnno(dbData);
// } catch (Exception e) {
// // 복호화 실패시 원본 반환
// String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
// log.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
// needFixLogger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length(), e);
// return dbData;
// }
// } else {
// log.debug("DB 복호화 경고: Base64 형식이 아님. 복호화하지 않고 원본 반환. dbData={}", dbData);
// }
// }
// log.debug("DB 복호화 #2 : {} -> {}", dbDataOriginal, dbData);
return dbData;
}
/**
* 암호화 여부 확인(base64 인코딩 여부로 판단, 또는 0-9, - 로 구성된 경우 암호화 되지 않은 걸로 판단(연락처))
* @param data
* @return
*/
private boolean isEncrypted(String data) {
// if (StringUtils.isEmpty(data)) {
// return false;
// }
//
// // 숫자, - 로만 구성된 경우 암호화 되지 않은 걸로 판단 (ex: 연락처)
// if (data.matches("^[0-9-]+$")) {
// return false;
// }
//
// data = data.trim();
//
// // Base64 형식 체크
// if (!Utils.isBase64(data)) {
// return false;
// }
//
// // Base64 길이 체크 (4의 배수)
// if (data.length() % 4 != 0) {
// return false;
// }
//
//
// return true;
return false;
}
}