16 Commits

Author SHA1 Message Date
Rinjae(gf63) 2cdb08a26f - 약관 동의서 노출 및 동작 방식 관리 기능 추가
eapim-portal CI (from elink-portal-common) / build (push) Waiting to run
- '전체 동의' 및 약관 항목별 활성화 설정 반영
- 사용자 타입 및 약관 페이지 구성에 따른 렌더링 로직 개선
2026-09-15 13:42:27 +09:00
Rinjae(gf63) a1be6d2b9c 알림톡 템플릿 수정 - 템플릿 코드 변경
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-09-11 10:05:57 +09:00
Rinjae 3c9c757962 Merge Gitea master with GitLab UMS changes, retaining GitLab verification template ID
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-09-11 10:03:52 +09:00
Rinjae(gf63) 4749a570e7 알림톡 템플릿 적용 2026-09-10 15:04:11 +09:00
eastargh a4976fdfa5 UMS 카톡 템플릿 적용 2026-09-10 13:20:13 +09:00
eastargh d86b169a6c UMS 응답 저장
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-09-10 10:08:22 +09:00
Rinjae 665aa5d44c "X-API-KEY 방식 제거 및 내부 API 토큰 검증으로 변경
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- ApiKeyInterceptor 삭제, InternalApiTokenInterceptor 도입
- 관련 config 및 테스트 코드 X-Internal-Token 방식으로 수정
- PortalPropertyService 연관 코드 정리 및 불필요 메서드 제거"
2026-09-03 15:46:42 +09:00
Rinjae 61086f419f 알림 테스트 도구 추가:
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- SCSS 스타일 및 HTML 템플릿 구현
- 알림 발송/상태 처리 자바 로직 추가
- 클라이언트 JS: 동적 데이터 바인딩, 발송/이력 관리
2026-08-26 12:57:03 +09:00
Rinjae b14b139abd "신규 신청 쿨다운 로직 제거 - Repository/Service/Controller 코드 정리 - 불필요한 HTML 스크립트 제거"
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-08-26 09:43:00 +09:00
Rinjae f9ad183d03 계정 보안 구현 - 연속 실패 시 계정 잠금 서비스 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 비밀번호 본인확인 연속 실패 정책 및 처리 로직 추가
- 작성 요청 빈도 제한 서비스 및 메시지 구현
- 키보드 연속 문자 검증 규칙 및 테스트 추가
2026-08-25 20:24:46 +09:00
eastargh dc9c4d5977 UMS messenger id 설정값 수정
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-08-25 09:52:09 +09:00
Rinjae ae35b2e75b - AppRequestRepository에 findAllByOrgAndClientName 메서드 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- 클라이언트 신청 해지 프로세스 및 테스트 클린업 로직 구현
- Playwright API 키 관리/클라이언트 관리 경로 및 셀렉터 업데이트
2026-08-24 15:25:18 +09:00
Rinjae 975533a790 PasswordServiceTest 추가 - 최근 비밀번호 중복 검사 로직 수정
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
TestCleanupService 확장 - 초대/회원 소속 관리 메서드 구현
API 추가 - 초대 삭제 및 소속 제외, 사용자 존재 여부 확인
2026-08-24 14:13:26 +09:00
Rinjae b92639bc04 - 테스트 정리 API용 리포지토리 메서드 추가
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
- PortalUser org 조회, 각종 이력 deleteBy 메서드
- Credential org 삭제, 개인정보동의 deleteBy 메서드
2026-08-21 17:58:05 +09:00
Rinjae ebdeab8204 활성 프로파일 뱃지 추가 - HTTP 세션 로그 설정 제거 - 개인정보 암호화 처리
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled
2026-08-21 17:25:41 +09:00
Rinjae 99dac967bc PortalPropertyService: PropertyValue null 필터 추가 2026-08-21 11:06:11 +09:00
15 changed files with 249 additions and 35 deletions
@@ -0,0 +1,146 @@
package com.eactive.apim.portal.agreements.service;
import com.eactive.apim.portal.agreements.entity.AgreementType;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 약관 종류 운영 설정 조회. PTL_PROPERTY(그룹 {@code Portal})로 관리하며 admin·portal 이 같은 값을 본다.
*
* <table>
* <caption>PTL_PROPERTY (group = Portal)</caption>
* <tr><th>{@code portal.terms.enabled-types}</th>
* <td>시스템에서 사용할 약관 종류. 쉼표 구분, 순서 무의미</td></tr>
* <tr><th>{@code portal.terms.display-types}</th>
* <td>약관 페이지에 노출할 약관 종류. 쉼표 구분, <b>기재 순서가 노출 순서</b></td></tr>
* </table>
*
* <p>값은 관리 콘솔의 '약관 목록 &gt; 약관 종류 관리' 화면에서 설정한다. 프로퍼티가 없으면
* {@link #DEFAULT_TYPES} 를 사용하며 <b>조회만으로 행을 만들지는 않는다</b>
* (PTL_PROPERTY 는 (그룹, 이름) 유니크 제약이 없어 동시 요청이 중복 행을 만들 수 있다).
* 알 수 없는 코드는 무시하므로 enum 항목이 빠져도 화면이 깨지지 않는다.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AgreementTypeConfigService {
public static final String PROPERTY_GROUP = "Portal";
public static final String ENABLED_TYPES_KEY = "portal.terms.enabled-types";
public static final String DISPLAY_TYPES_KEY = "portal.terms.display-types";
public static final String AGREE_ALL_ENABLED_KEY = "portal.terms.agree-all-enabled";
/** '전체 동의' 기본값 — 미설정 시 항목별로 끝까지 읽고 개별 동의하는 방식 */
public static final boolean DEFAULT_AGREE_ALL_ENABLED = false;
/** 프로퍼티 미설정 시 기본값 — 현재 운영중인 약관 종류 */
public static final List<AgreementType> DEFAULT_TYPES = Collections.unmodifiableList(Arrays.asList(
AgreementType.TERMS_OF_USE,
AgreementType.PRIVACY_COLLECT,
AgreementType.NOTIFICATION_CONSENT));
private final PortalPropertyService portalPropertyService;
/** 시스템에서 사용하는 약관 종류 */
public Set<AgreementType> getEnabledTypes() {
Set<AgreementType> enabled = EnumSet.noneOf(AgreementType.class);
enabled.addAll(readTypes(readGroupProperties(), ENABLED_TYPES_KEY));
return enabled;
}
/** 약관 페이지에 노출할 약관 종류(노출 순서대로). 사용하지 않는 종류는 제외된다. */
public List<AgreementType> getDisplayTypes() {
Map<String, String> properties = readGroupProperties();
List<AgreementType> enabled = readTypes(properties, ENABLED_TYPES_KEY);
List<AgreementType> display = new ArrayList<>();
for (AgreementType type : readTypes(properties, DISPLAY_TYPES_KEY)) {
if (enabled.contains(type)) {
display.add(type);
}
}
return display;
}
/** 시스템에서 사용하는 약관 종류인지 여부 */
public boolean isEnabled(AgreementType type) {
return getEnabledTypes().contains(type);
}
/** 약관 페이지에 노출(배치)된 약관 종류인지 여부. 사용하지 않는 종류는 항상 {@code false}. */
public boolean isDisplayed(AgreementType type) {
return getDisplayTypes().contains(type);
}
/**
* 회원가입 약관 동의 화면에서 '전체 동의'를 허용할지 여부.
*
* <ul>
* <li>{@code false} — 항목을 펼쳐 끝까지 읽어야 개별 동의할 수 있고, '전체 동의' 체크박스는 노출하지 않는다</li>
* <li>{@code true} — '전체 동의' 체크박스를 노출하고, 항목별 스크롤 없이 바로 동의할 수 있다</li>
* </ul>
*/
public boolean isAgreeAllEnabled() {
return parseBoolean(readGroupProperties().get(AGREE_ALL_ENABLED_KEY));
}
/** {@code true/false} 외에 레거시 {@code Y/N}, {@code 1/0} 표기도 허용한다. */
private boolean parseBoolean(String value) {
if (value == null || value.trim().isEmpty()) {
return DEFAULT_AGREE_ALL_ENABLED;
}
String trimmed = value.trim();
if ("true".equalsIgnoreCase(trimmed) || "Y".equalsIgnoreCase(trimmed) || "1".equals(trimmed)) {
return true;
}
if ("false".equalsIgnoreCase(trimmed) || "N".equalsIgnoreCase(trimmed) || "0".equals(trimmed)) {
return false;
}
log.warn("PTL_PROPERTY {}/{} 값을 해석할 수 없어 기본값({})을 사용합니다. : {}",
PROPERTY_GROUP, AGREE_ALL_ENABLED_KEY, DEFAULT_AGREE_ALL_ENABLED, trimmed);
return DEFAULT_AGREE_ALL_ENABLED;
}
/**
* {@code Portal} 그룹 프로퍼티 전체를 읽는다.
*
* <p>단건 조회({@code findById})는 중복 행에서 예외가 나므로 그룹 단위로 읽는다.
*/
private Map<String, String> readGroupProperties() {
return portalPropertyService.getPortalPropertiesAsMap(PROPERTY_GROUP);
}
/** 프로퍼티 값을 약관 종류 목록으로 변환. 미설정이면 기본값, 알 수 없는 코드는 무시한다. */
private List<AgreementType> readTypes(Map<String, String> properties, String propertyName) {
String value = properties.get(propertyName);
if (value == null) {
return new ArrayList<>(DEFAULT_TYPES);
}
List<AgreementType> types = new ArrayList<>();
for (String code : value.split(",")) {
String trimmed = code.trim();
if (trimmed.isEmpty()) {
continue;
}
try {
types.add(AgreementType.valueOf(trimmed));
} catch (IllegalArgumentException e) {
log.warn("PTL_PROPERTY {}/{} 에 알 수 없는 약관 종류 코드가 있어 무시합니다. : {}",
PROPERTY_GROUP, propertyName, trimmed);
}
}
return new ArrayList<>(new LinkedHashSet<>(types));
}
}
@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
@EMSDataSource @EMSDataSource
public interface CredentialRepository extends BaseRepository<Credential, String> { public interface CredentialRepository extends BaseRepository<Credential, String> {
@@ -19,6 +20,9 @@ public interface CredentialRepository extends BaseRepository<Credential, String>
List<Credential> findAllByOrgid(String orgid); List<Credential> findAllByOrgid(String orgid);
@Transactional
long deleteByOrgid(String orgid);
Optional<Credential> findByClientidAndOrgid(String clientid, String orgid); Optional<Credential> findByClientidAndOrgid(String clientid, String orgid);
/** /**
@@ -31,6 +31,9 @@ public interface AppRequestRepository extends BaseRepository<AppRequest, String>
Optional<AppRequest> findByIdAndOrgAndTypeIsIn(String id, PortalOrg org, List<AppRequestType> types); Optional<AppRequest> findByIdAndOrgAndTypeIsIn(String id, PortalOrg org, List<AppRequestType> types);
/** 테스트 재실행 정리처럼 법인 내 동일 클라이언트명의 신청을 좁게 조회한다. */
List<AppRequest> findAllByOrgAndClientName(PortalOrg org, String clientName);
List<AppRequest> findAllByClientIdsContainsAndTypeIsIn(String clientId, List<AppRequestType> types); List<AppRequest> findAllByClientIdsContainsAndTypeIsIn(String clientId, List<AppRequestType> types);
List<AppRequest> findAllByOrgAndStatus(PortalOrg org, ApprovalStatus status); List<AppRequest> findAllByOrgAndStatus(PortalOrg org, ApprovalStatus status);
@@ -95,16 +95,15 @@ public class InternalApiTokenService {
*/ */
public boolean matches(String presented) { public boolean matches(String presented) {
String expected = ensureToken(); String expected = ensureToken();
if (expected == null) { return tokenMatches(presented, expected);
log.error("내부 API 토큰({}/{})을 확보하지 못해 요청을 거부합니다.", PROP_GROUP, PROP_TOKEN);
return false;
} }
if (isBlank(presented)) {
return false; /**
} * admin 등 토큰을 생성하면 안 되는 애플리케이션용 검증.
return MessageDigest.isEqual( * 프로퍼티가 없으면 생성하지 않고 거부한다.
presented.trim().getBytes(StandardCharsets.UTF_8), */
expected.getBytes(StandardCharsets.UTF_8)); public boolean matchesReadOnly(String presented) {
return tokenMatches(presented, findToken());
} }
/** 기동 시 1회 호출 — 헤더명/토큰 프로퍼티를 미리 만들어 둔다. (포탈 전용) */ /** 기동 시 1회 호출 — 헤더명/토큰 프로퍼티를 미리 만들어 둔다. (포탈 전용) */
@@ -138,6 +137,19 @@ public class InternalApiTokenService {
} }
} }
private boolean tokenMatches(String presented, String expected) {
if (expected == null) {
log.error("내부 API 토큰({}/{})을 확보하지 못해 요청을 거부합니다.", PROP_GROUP, PROP_TOKEN);
return false;
}
if (isBlank(presented)) {
return false;
}
return MessageDigest.isEqual(
presented.trim().getBytes(StandardCharsets.UTF_8),
expected.getBytes(StandardCharsets.UTF_8));
}
/** JVM 당 1회만 생성되는 난수 토큰 (DB 에 값이 없을 때의 기본값). */ /** JVM 당 1회만 생성되는 난수 토큰 (DB 에 값이 없을 때의 기본값). */
private String localToken() { private String localToken() {
String token = generatedToken.get(); String token = generatedToken.get();
@@ -17,6 +17,10 @@ public interface UserInvitationRepository extends JpaRepository<UserInvitation,
List<UserInvitation> findByStatus(InvitationStatus status); List<UserInvitation> findByStatus(InvitationStatus status);
long deleteByOrgId(String orgId);
long deleteByInvitationMobile(String invitationMobile);
Optional<UserInvitation> findFirstByInvitationEmailAndStatus(String email, InvitationStatus status); Optional<UserInvitation> findFirstByInvitationEmailAndStatus(String email, InvitationStatus status);
Optional<UserInvitation> findFirstByInvitationEmailAndOrgIdAndStatus(String email, String orgId, InvitationStatus status); Optional<UserInvitation> findFirstByInvitationEmailAndOrgIdAndStatus(String email, String orgId, InvitationStatus status);
@@ -30,6 +30,7 @@ public class PortalPropertyService extends AbstractDataService<PortalPropertyGro
.orElse(new PortalPropertyGroup()); .orElse(new PortalPropertyGroup());
return portalPropertyGroup.getPortalProperties().stream() return portalPropertyGroup.getPortalProperties().stream()
.filter(portalProperty -> portalProperty.getPropertyValue() != null)
.collect(Collectors.toMap( .collect(Collectors.toMap(
portalProperty -> portalProperty.getId().getPropertyName(), portalProperty -> portalProperty.getId().getPropertyName(),
PortalProperty::getPropertyValue, PortalProperty::getPropertyValue,
@@ -1,5 +1,6 @@
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 lombok.Data; import lombok.Data;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
@@ -40,6 +41,7 @@ public class UserRoleHistory implements Serializable, com.eactive.eai.data.Data
@Column(name = "id", columnDefinition = "number(19)") @Column(name = "id", columnDefinition = "number(19)")
private Long id; private Long id;
@Convert(converter = PersonalDataEncryptConverter.class)
@Column(name = "user_id", length = 255, nullable = false) @Column(name = "user_id", length = 255, nullable = false)
@Comment("대상 사용자 ID(loginId)") @Comment("대상 사용자 ID(loginId)")
private String userId; private String userId;
@@ -56,6 +58,7 @@ public class UserRoleHistory implements Serializable, com.eactive.eai.data.Data
@Comment("변경 유형(MANAGER_ASSIGN/MANAGER_REVOKE/ORG_REMOVE 등)") @Comment("변경 유형(MANAGER_ASSIGN/MANAGER_REVOKE/ORG_REMOVE 등)")
private String changeType; private String changeType;
@Convert(converter = PersonalDataEncryptConverter.class)
@Column(name = "changed_by", length = 255, nullable = false) @Column(name = "changed_by", length = 255, nullable = false)
@Comment("변경 수행자(loginId)") @Comment("변경 수행자(loginId)")
private String changedBy; private String changedBy;
@@ -65,6 +68,7 @@ public class UserRoleHistory implements Serializable, com.eactive.eai.data.Data
@Convert(converter = LocalDateTimeToStringConverter14.class) @Convert(converter = LocalDateTimeToStringConverter14.class)
private LocalDateTime changeDate; private LocalDateTime changeDate;
@Convert(converter = PersonalDataEncryptConverter.class)
@Column(name = "created_by", length = 255, nullable = false) @Column(name = "created_by", length = 255, nullable = false)
@Comment("생성자") @Comment("생성자")
private String createdBy; private String createdBy;
@@ -3,6 +3,7 @@ package com.eactive.apim.portal.portaluser.repository;
import com.eactive.apim.portal.portaluser.entity.PortalUserPrivacyAgreement; import com.eactive.apim.portal.portaluser.entity.PortalUserPrivacyAgreement;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -15,4 +16,8 @@ public interface PortalUserPrivacyAgreementRepository extends BaseRepository<Por
List<PortalUserPrivacyAgreement> findAllByCreatedBy(String createdBy); List<PortalUserPrivacyAgreement> findAllByCreatedBy(String createdBy);
// boolean existsByCreatedByAndTermsTypeAndTermsVersionGreaterThanEqual(String userId, String termsType, int latestVersion); // boolean existsByCreatedByAndTermsTypeAndTermsVersionGreaterThanEqual(String userId, String termsType, int latestVersion);
/** createdBy = PortalUser.id. */
@Transactional
long deleteByCreatedBy(String createdBy);
} }
@@ -40,6 +40,8 @@ public interface PortalUserRepository extends BaseRepository<PortalUser, String>
long countByPortalOrg(PortalOrg portalOrg); long countByPortalOrg(PortalOrg portalOrg);
List<PortalUser> findAllByPortalOrg_Id(String orgId);
Optional<PortalUser> findByUserName(String userName); Optional<PortalUser> findByUserName(String userName);
// 암복 // 암복
@@ -5,6 +5,7 @@ import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -15,4 +16,8 @@ public interface UserPasswordHistoryRepository extends JpaRepository<UserPasswor
List<UserPasswordHistory> findRecentPasswordsByUserId(@Param("loginId") String loginId); List<UserPasswordHistory> findRecentPasswordsByUserId(@Param("loginId") String loginId);
Optional<UserPasswordHistory> findTopByUserIdOrderByChangeDateDesc(String loginId); Optional<UserPasswordHistory> findTopByUserIdOrderByChangeDateDesc(String loginId);
/** userId = PortalUser.id (loginId 아님 — UserRoleHistory.userId 와 반대 시맨틱). */
@Transactional
long deleteByUserId(String userId);
} }
@@ -3,6 +3,7 @@ package com.eactive.apim.portal.portaluser.repository;
import com.eactive.apim.portal.portaluser.entity.UserRoleHistory; import com.eactive.apim.portal.portaluser.entity.UserRoleHistory;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
@@ -10,4 +11,8 @@ import java.util.List;
public interface UserRoleHistoryRepository extends JpaRepository<UserRoleHistory, Long> { public interface UserRoleHistoryRepository extends JpaRepository<UserRoleHistory, Long> {
List<UserRoleHistory> findByUserIdOrderByChangeDateDesc(String userId); List<UserRoleHistory> findByUserIdOrderByChangeDateDesc(String userId);
/** userId = PortalUser.loginId (id 아님 — UserPasswordHistory.userId 와 반대 시맨틱). */
@Transactional
long deleteByUserId(String userId);
} }
@@ -11,23 +11,22 @@ public enum MessageCode {
// 사용자 이벤트 영역 // 사용자 이벤트 영역
USER_VERIFICATION_EMAIL("이메일 주소 인증", null, Channels.EMAIL), USER_VERIFICATION_EMAIL("이메일 주소 인증", null, Channels.EMAIL),
USER_VERIFICATION_MOBILEPHONE("휴대폰 번호 인증", null, Channels.SMS), USER_VERIFICATION_MOBILEPHONE("휴대폰 번호 인증", "JJB_85959019001", Channels.SMS),
USER_PASSWORD_RESET("비밀번호 재설정", null, Channels.SMS), USER_PASSWORD_CHANGED("사용자 비밀번호 변경", "JJB_85959019003", Channels.SMS),
USER_PASSWORD_CHANGED("사용자 비밀번호 변경", null, Channels.SMS),
USER_ACCOUNT_LOCKED("계정 잠금 알림", null, Channels.SMS), USER_ACCOUNT_LOCKED("계정 잠금 알림", "JJB_85959019002", Channels.SMS),
// 법인관리자 이벤트 영역 // 법인관리자 이벤트 영역
USER_INVITATION("법인 이용자 이메일 초대", null, Channels.SMS), USER_INVITATION("법인 이용자 이메일 초대", "JJB_85959019006", Channels.SMS),
USER_INVITATION_CANCELED("법인 이용자 이메일 초대 취소", null, Channels.SMS), USER_INVITATION_CANCELED("법인 이용자 이메일 초대 취소", "JJB_85959019007", Channels.SMS),
MANAGER_WITH_ORG_REGISTER_APPROVED("법인관리자 및 법인 등록 승인", null, Channels.SMS), MANAGER_WITH_ORG_REGISTER_APPROVED("법인관리자 및 법인 등록 승인", "JJB_85959019004", Channels.SMS),
MANAGER_WITH_ORG_REGISTER_REJECTED("법인관리자 및 법인 등록 거절", null, Channels.SMS), MANAGER_WITH_ORG_REGISTER_REJECTED("법인관리자 및 법인 등록 거절", "JJB_85959019005", Channels.SMS),
// 관리자 영역 // 관리자 영역
APP_APPROVE("앱 사용 승인", null, Channels.SMS), APP_APPROVE("앱 사용 승인", "JJB_85959019008", Channels.SMS),
APP_REJECTED("앱 사용 거절", null, Channels.SMS), APP_REJECTED("앱 사용 거절", "JJB_85959019009", Channels.SMS),
// Q&A 등록/댓글 등록 알림 (DJBank 커스텀 — portal-admin 수신) // Q&A 등록/댓글 등록 알림 (DJBank 커스텀 — portal-admin 수신)
INQUIRY_CREATED("Q&A 등록 알림", null, Channels.SWING), INQUIRY_CREATED("Q&A 등록 알림", null, Channels.SWING),
@@ -37,9 +36,10 @@ public enum MessageCode {
// 제주은행 // 제주은행
API_STATUS_CHANGED("API 상태 변화", null, Channels.SWING), API_STATUS_CHANGED("API 상태 변화", null, Channels.SWING),
INFLOW_TOKEN_FAILED("유량제어 토큰 획득 실패", null, Channels.SWING), INFLOW_TOKEN_FAILED("유량제어 토큰 획득 실패", null, Channels.SWING),
ADMIN_VERIFICATION_MOBILEPHONE("관리자포탈 휴대폰 번호 인증", null, Channels.SMS), ADMIN_VERIFICATION_MOBILEPHONE("관리자포탈 휴대폰 번호 인증", "JJB_85999019001", Channels.SMS),
// 레거시/미사용 — Event 핸들러 클래스 호환을 위해 유지 (실제 발송 없음) // 레거시/미사용 — Event 핸들러 클래스 호환을 위해 유지 (실제 발송 없음)
USER_PASSWORD_RESET("비밀번호 재설정", null, Channels.SMS),
API_APPROVED("API 승인 알림", null, Channels.NONE), API_APPROVED("API 승인 알림", null, Channels.NONE),
APP_REGISTER_REQUEST("앱 등록 요청", null, Channels.NONE), APP_REGISTER_REQUEST("앱 등록 요청", null, Channels.NONE),
APP_REGISTER_REJECTED("앱 등록 거절", null, Channels.NONE), APP_REGISTER_REJECTED("앱 등록 거절", null, Channels.NONE),
@@ -79,9 +79,16 @@ public class MessageRequest implements Serializable {
@Column(name = "request_status") @Column(name = "request_status")
private String requestStatus = "PENDING"; // PENDING, SENT, FAILED private String requestStatus = "PENDING"; // PENDING, SENT, FAILED
@Lob
@Column(name = "response_data")
private String responseData;
@Column(name = "eai_interface_id") @Column(name = "eai_interface_id")
private String eaiInterfaceId; private String eaiInterfaceId;
@Column(name = "eai_tx_id")
private String eaiTxId;
@Column(name = "service_id") @Column(name = "service_id")
private String serviceId; private String serviceId;
@@ -37,22 +37,24 @@ public class MessageSendService {
private final PortalPropertyService portalPropertyService; private final PortalPropertyService portalPropertyService;
@Transactional @Transactional
public void sendMessage(MessageCode messageCode, MessageRecipient recipient, Map<String, String> messageParams) { public List<MessageRequest> sendMessage(MessageCode messageCode, MessageRecipient recipient, Map<String, String> messageParams) {
processMessage(messageCode.name(), recipient, messageParams); return processMessage(messageCode, recipient, messageParams);
} }
private void processMessage(String messageCode, MessageRecipient recipient, Map<String, String> messageParams) { private List<MessageRequest> processMessage(MessageCode messageCode, MessageRecipient recipient, Map<String, String> messageParams) {
Optional<MessageTemplate> template = messageTemplateRepository.findById(messageCode.toUpperCase()); Optional<MessageTemplate> template = messageTemplateRepository.findById(messageCode.name().toUpperCase());
List<MessageRequest> created = new ArrayList<>();
if (template.isPresent()) { if (template.isPresent()) {
List<MessageRecipient> recipients = prepareRecipients(template.get(), recipient); List<MessageRecipient> recipients = prepareRecipients(template.get(), recipient);
for (MessageRecipient user : recipients) { for (MessageRecipient user : recipients) {
processSingleRecipient(user, template.get(), messageParams); created.addAll(processSingleRecipient(user, template.get(), messageParams, messageCode));
} }
} else { } else {
log.warn("메세지 템플릿이 존재 하지 않음 - {}", messageCode); log.warn("메세지 템플릿이 존재 하지 않음 - {}", messageCode.name());
} }
return created;
} }
private List<MessageRecipient> prepareRecipients(MessageTemplate template, MessageRecipient recipient) { private List<MessageRecipient> prepareRecipients(MessageTemplate template, MessageRecipient recipient) {
@@ -70,15 +72,18 @@ public class MessageSendService {
} }
public void processSingleRecipient(MessageRecipient user, MessageTemplate template, Map<String, String> messageParams) { public List<MessageRequest> processSingleRecipient(MessageRecipient user, MessageTemplate template, Map<String, String> messageParams, MessageCode messageCode) {
Map<String, String> portalProperties = portalPropertyService.getPortalPropertiesAsMap("Portal"); Map<String, String> portalProperties = portalPropertyService.getPortalPropertiesAsMap("Portal");
updateMessageParams(user, messageParams); updateMessageParams(user, messageParams);
String subject = buildMessage(template.getSubjectTemplate(), messageParams); String subject = buildMessage(template.getSubjectTemplate(), messageParams);
List<MessageRequest> created = new ArrayList<>();
if (template.getEnableSms().equalsIgnoreCase("Y")) { if (template.getEnableSms().equalsIgnoreCase("Y")) {
String smsEaiInterfaceId = portalProperties.get("djb.ums.sms.if_id"); String smsEaiInterfaceId = portalProperties.get("djb.ums.sms.if_id");
String smsEaiTxId = portalProperties.get("djb.ums.sms.tx_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,15 +93,17 @@ 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("djb.ums.sms.tx_id")); smsRequest.setEaiTxId(smsEaiTxId);
smsRequest.setServiceId(messageCode.getServiceId());
smsRequest.setUserId(user.getUserId()); smsRequest.setUserId(user.getUserId());
smsRequest.setMessageType("SMS"); smsRequest.setMessageType("SMS");
messageRequestRepository.save(smsRequest); created.add(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("djb.ums.email.if_id"); String mailEaiInterfaceId = portalProperties.get("djb.ums.email.if_id");
String mailEaiTxId = portalProperties.get("djb.ums.email.tx_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);
@@ -106,15 +113,17 @@ public class MessageSendService {
emailRequest.setUsername(resolveEmailUsername(user)); emailRequest.setUsername(resolveEmailUsername(user));
emailRequest.setEmail(user.getUserId()); emailRequest.setEmail(user.getUserId());
emailRequest.setEaiInterfaceId(mailEaiInterfaceId); emailRequest.setEaiInterfaceId(mailEaiInterfaceId);
emailRequest.setServiceId(portalProperties.get("djb.ums.email.tx_id")); emailRequest.setEaiTxId(mailEaiTxId);
emailRequest.setServiceId(messageCode.getServiceId());
emailRequest.setMessageType("EMAIL"); emailRequest.setMessageType("EMAIL");
emailRequest.setUserId(user.getUserId()); emailRequest.setUserId(user.getUserId());
messageRequestRepository.save(emailRequest); created.add(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("djb.ums.messenger.if_id"); String swingEaiInterfaceId = portalProperties.get("djb.ums.messenger.if_id");
String swingEaiTxId = portalProperties.get("djb.ums.messenger.tx_id");
MessageRequest messengerRequest = new MessageRequest(); MessageRequest messengerRequest = new MessageRequest();
messengerRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode())); messengerRequest.setMessageCode(MessageCode.valueOf(template.getMessageCode()));
messengerRequest.setSubject(subject); messengerRequest.setSubject(subject);
@@ -122,14 +131,17 @@ public class MessageSendService {
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.getUserId()); messengerRequest.setMessengerId(user.getMessengerId());
messengerRequest.setEaiInterfaceId(swingEaiInterfaceId); messengerRequest.setEaiInterfaceId(swingEaiInterfaceId);
messengerRequest.setServiceId(portalProperties.get("djb.ums.messenger.tx_id")); messengerRequest.setEaiTxId(swingEaiTxId);
messengerRequest.setServiceId(messageCode.getServiceId());
messengerRequest.setMessageType("MESSENGER"); messengerRequest.setMessageType("MESSENGER");
messengerRequest.setUserId(user.getUserId()); messengerRequest.setUserId(user.getUserId());
messageRequestRepository.save(messengerRequest); created.add(messageRequestRepository.save(messengerRequest));
logger.debug(messengerRequest.toString()); logger.debug(messengerRequest.toString());
} }
return created;
} }
/** /**
@@ -4,9 +4,13 @@ import com.eactive.apim.portal.user.entity.UserLog;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
public interface UserLogRepository extends JpaRepository<UserLog, Long> { public interface UserLogRepository extends JpaRepository<UserLog, Long> {
Optional<UserLog> findFirstByLoginIdAndSuccessOrderByLoginTimeDesc(@Param("loginId") String loginId,@Param("success") boolean success); Optional<UserLog> findFirstByLoginIdAndSuccessOrderByLoginTimeDesc(@Param("loginId") String loginId,@Param("success") boolean success);
@Transactional
long deleteByLoginId(String loginId);
} }