Swing 알림 체계 도입 - 기존 Q&A 관리자 알림 제거 - Swing Notifier 구현

This commit is contained in:
Rinjae
2026-07-30 19:33:11 +09:00
parent 6c231c4189
commit 6fa2167378
12 changed files with 431 additions and 88 deletions
@@ -6,7 +6,7 @@ import com.eactive.apim.portal.apps.community.partnership.mapper.PartnershipAppl
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.SecurityUtil; import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.common.util.UserTypeUtil; import com.eactive.apim.portal.common.util.UserTypeUtil;
import com.eactive.apim.portal.djb.community.qna.comment.service.CommunityAdminNotifier; import com.eactive.apim.portal.djb.swing.SwingNotifier;
import com.eactive.apim.portal.file.entity.FileInfo; import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.service.FileService; import com.eactive.apim.portal.file.service.FileService;
import com.eactive.apim.portal.file.service.FileTypeContext; import com.eactive.apim.portal.file.service.FileTypeContext;
@@ -29,7 +29,7 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF
private final PartnershipApplicationMapper partnershipApplicationMapper; private final PartnershipApplicationMapper partnershipApplicationMapper;
private final FileService fileService; private final FileService fileService;
// portal-admin 알림 발행기(범용). Q&A 등록 알림과 동일 컴포넌트를 재사용한다. // portal-admin 알림 발행기(범용). Q&A 등록 알림과 동일 컴포넌트를 재사용한다.
private final CommunityAdminNotifier portalAdminNotifier; private final SwingNotifier swingNotifier;
@Override @Override
@@ -61,7 +61,7 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF
if (writer != null) { if (writer != null) {
params.put("writerName", writer.getUserName()); params.put("writerName", writer.getUserName());
} }
portalAdminNotifier.notifyPortalAdmins(MessageCode.PARTNERSHIP_CREATED, params); swingNotifier.notifyPortalAdmins(MessageCode.PARTNERSHIP_CREATED, params);
} }
@Override @Override
@@ -6,7 +6,7 @@ import com.eactive.apim.portal.apps.community.qna.mapper.InquiryMapper;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser; import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.SecurityUtil; import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.djb.community.qna.comment.service.CommunityAdminNotifier; import com.eactive.apim.portal.djb.swing.SwingNotifier;
import com.eactive.apim.portal.file.entity.FileInfo; import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.exception.InvalidFileException; import com.eactive.apim.portal.file.exception.InvalidFileException;
import com.eactive.apim.portal.file.service.FileService; import com.eactive.apim.portal.file.service.FileService;
@@ -40,7 +40,7 @@ public class InquiryFacadeImpl implements InquiryFacade {
private final InquiryService inquiryService; private final InquiryService inquiryService;
private final InquiryMapper inquiryMapper; private final InquiryMapper inquiryMapper;
private final CommunityAdminNotifier inquiryAdminNotifier; private final SwingNotifier swingNotifier;
private final FileService fileService; private final FileService fileService;
@Override @Override
@@ -142,7 +142,7 @@ public class InquiryFacadeImpl implements InquiryFacade {
params.put("inquiryId", inquiry.getId()); params.put("inquiryId", inquiry.getId());
params.put("inquirySubject", inquiry.getInquirySubject()); params.put("inquirySubject", inquiry.getInquirySubject());
params.put("writerName", current.getUserName()); params.put("writerName", current.getUserName());
inquiryAdminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_CREATED, params); swingNotifier.notifyPortalAdmins(MessageCode.INQUIRY_CREATED, params);
} }
@Override @Override
@@ -3,20 +3,13 @@ package com.eactive.apim.portal.djb.community.qna.comment.repository;
import com.eactive.apim.portal.user.entity.UserInfo; import com.eactive.apim.portal.user.entity.UserInfo;
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.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
/**
* 댓글 작성자(내부 직원) 표시용 TSEAIRM02 조회.
*
* <p>역할 기반 알림 대상 조회는
* {@code com.eactive.apim.portal.djb.swing.repository.SwingStaffRepository} 로 분리했다.</p>
*/
@EMSDataSource @EMSDataSource
public interface UserInfoRepository extends BaseRepository<UserInfo, String> { public interface UserInfoRepository extends BaseRepository<UserInfo, String> {
/**
* TSEAIRM02.roleidnfiname 컬럼은 콤마로 구분된 복수 역할을 저장한다.
* (예: {@code "admin,portal-admin"}). Oracle native query로 콤마 토큰 매치.
*/
@Query(value = "SELECT * FROM TSEAIRM02 t"
+ " WHERE ',' || t.ROLEIDNFINAME || ',' LIKE '%,' || :role || ',%'",
nativeQuery = true)
List<UserInfo> findByRoleContaining(@Param("role") String role);
} }
@@ -1,57 +0,0 @@
package com.eactive.apim.portal.djb.community.qna.comment.service;
import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository;
import com.eactive.apim.portal.djb.community.qna.constant.DjbAdminRole;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.user.entity.UserInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* Q&A 등록/댓글 등록 시 portal-admin 역할(TSEAIRM02)을 가진 관리자 전원에게
* 알림 메시지를 발행한다. 발송 실패는 트랜잭션 롤백을 유발하지 않는다.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CommunityAdminNotifier {
private final UserInfoRepository userInfoRepository;
private final MessageHandlerService messageHandlerService;
public void notifyPortalAdmins(MessageCode code, Map<String, Object> params) {
try {
List<UserInfo> admins = userInfoRepository.findByRoleContaining(DjbAdminRole.PORTAL_ADMIN);
if (admins == null || admins.isEmpty()) {
log.warn("portal-admin 역할 관리자가 없습니다 — 알림 미발송 code={}", code.name());
return;
}
for (UserInfo admin : admins) {
try {
MessageRecipient recipient = toRecipient(admin);
messageHandlerService.publishEvent(code, recipient, params);
} catch (Exception e) {
log.warn("portal-admin 개별 알림 발행 실패 — userid={}, code={}",
admin.getUserid(), code.name(), e);
}
}
} catch (Exception e) {
log.warn("portal-admin 알림 발행 실패 — code={}", code.name(), e);
}
}
private MessageRecipient toRecipient(UserInfo admin) {
MessageRecipient r = new MessageRecipient();
r.setUsername(admin.getUsername());
r.setUserId(admin.getEmad());
r.setPhone(admin.getCphnno());
r.setMessengerId(admin.getUserid());
return r;
}
}
@@ -8,6 +8,7 @@ import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository; import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository; import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository;
import com.eactive.apim.portal.djb.community.qna.constant.DjbInquiryStatus; import com.eactive.apim.portal.djb.community.qna.constant.DjbInquiryStatus;
import com.eactive.apim.portal.djb.swing.SwingNotifier;
import com.eactive.apim.portal.djb.community.qna.support.InquiryCommentPermissionChecker; import com.eactive.apim.portal.djb.community.qna.support.InquiryCommentPermissionChecker;
import com.eactive.apim.portal.portalorg.entity.PortalOrg; import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.PortalUser;
@@ -47,7 +48,7 @@ public class InquiryCommentFacadeImpl implements InquiryCommentFacade {
private final InquiryCommentService commentService; private final InquiryCommentService commentService;
private final InquiryCommentRepository commentRepository; private final InquiryCommentRepository commentRepository;
private final InquiryCommentPermissionChecker permissionChecker; private final InquiryCommentPermissionChecker permissionChecker;
private final CommunityAdminNotifier adminNotifier; private final SwingNotifier swingNotifier;
private final PortalUserRepository portalUserRepository; private final PortalUserRepository portalUserRepository;
private final UserInfoRepository userInfoRepository; private final UserInfoRepository userInfoRepository;
@@ -107,7 +108,7 @@ public class InquiryCommentFacadeImpl implements InquiryCommentFacade {
params.put("inquirySubject", inquiry.getInquirySubject()); params.put("inquirySubject", inquiry.getInquirySubject());
params.put("commentContent", comment.getCommentDetail()); params.put("commentContent", comment.getCommentDetail());
params.put("writerName", current.getUserName()); params.put("writerName", current.getUserName());
adminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_COMMENT_CREATED, params); swingNotifier.notifyPortalAdmins(MessageCode.INQUIRY_COMMENT_CREATED, params);
} }
private Map<String, Writer> resolveWriters(List<InquiryComment> comments) { private Map<String, Writer> resolveWriters(List<InquiryComment> comments) {
@@ -1,10 +0,0 @@
package com.eactive.apim.portal.djb.community.qna.constant;
public final class DjbAdminRole {
/** TSEAIRM02.roleidnfiname 컬럼에서 포털 관리자를 식별하는 값. */
public static final String PORTAL_ADMIN = "portal-admin";
private DjbAdminRole() {
}
}
@@ -0,0 +1,40 @@
package com.eactive.apim.portal.djb.swing;
/**
* Swing 메신저 수신자 ID 가 행번(직원번호)이 아닐 때의 처리 정책.
*
* <p>Swing 메신저는 {@code TSEAIRM02.USERID} 를 수신자 코드로 사용하며 운영 직원은 숫자 행번을 쓴다.
* portal-admin 역할이 부여된 개발용 계정(영문 ID 등)은 Swing 에 존재하지 않아 발송이 무의미하므로
* 이 정책으로 처리 방식을 선택한다. PTL_PROPERTY
* {@code Portal / djb.swing.notify.non-employee.policy} 로 지정한다.</p>
*/
public enum EmployeeIdPolicy {
/** 정상 발송 (PTL_MESSAGE_REQUEST 에 PENDING 적재) */
SEND,
/** 발송 + WARN 로그 */
SEND_WARN,
/** 무시 처리 — REQUEST_STATUS='SKIPPED' 로 적재해 이력만 남기고 배치는 수집하지 않음 */
SKIP,
/** 거부 — PTL_MESSAGE_REQUEST 에 적재하지 않음 + INFO 로그 */
REJECT_LOG,
/** 완전 거부 — 적재하지 않고 로그도 남기지 않음 */
REJECT;
/** 프로퍼티 문자열 → enum. 값이 없거나 알 수 없으면 기본값 {@link #SKIP}. */
public static EmployeeIdPolicy from(String value) {
if (value != null) {
String trimmed = value.trim();
for (EmployeeIdPolicy policy : values()) {
if (policy.name().equalsIgnoreCase(trimmed)) {
return policy;
}
}
}
return SKIP;
}
}
@@ -0,0 +1,160 @@
package com.eactive.apim.portal.djb.swing;
import com.eactive.apim.portal.djb.swing.repository.SwingStaffRepository;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.entity.MessageTemplate;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.apim.portal.template.repository.MessageTemplateRepository;
import com.eactive.apim.portal.user.entity.UserInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Swing 메신저 발송 요청을 PTL_MESSAGE_REQUEST 에 직접 적재한다.
*
* <p>TSEAIRM02(직원) → PTL_MESSAGE_TEMPLATE(본문) → PTL_MESSAGE_REQUEST(발송 요청) 순으로
* 처리하며, 실제 발송은 기존과 동일하게 eapim-admin 의 {@code UmsDispatchJob}(5초 주기)이
* {@code REQUEST_STATUS='PENDING'} 행을 수집해 수행한다.</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SwingMessageWriter {
private static final String MESSAGE_TYPE_MESSENGER = "MESSENGER";
private static final String STATUS_PENDING = "PENDING";
/** 행번 아닌 ID 를 무시 처리할 때 쓰는 신설 상태값. 배치(PENDING 수집)가 건드리지 않는다. */
private static final String STATUS_SKIPPED = "SKIPPED";
private static final String ENABLED = "Y";
private final SwingStaffRepository swingStaffRepository;
private final MessageTemplateRepository messageTemplateRepository;
private final MessageRequestRepository messageRequestRepository;
private final SwingNotifyProperties properties;
@Transactional
public void write(MessageCode code, Map<String, Object> params) {
String role = properties.getTargetRole();
List<UserInfo> staffs = swingStaffRepository.findByRole(role);
if (staffs == null || staffs.isEmpty()) {
log.warn("Swing 알림 대상 직원이 없습니다 — role={}, code={}", role, code.name());
return;
}
MessageTemplate template = messageTemplateRepository.findById(code.name()).orElse(null);
if (template == null) {
log.warn("메세지 템플릿이 존재하지 않습니다 — code={}", code.name());
return;
}
if (!ENABLED.equalsIgnoreCase(template.getEnableMessenger())) {
log.warn("메신저 발송이 비활성화된 템플릿입니다 — code={}, enableMessenger={}",
code.name(), template.getEnableMessenger());
return;
}
SwingNotifyProperties.UmsMessengerIds umsIds = properties.getUmsMessengerIds();
EmployeeIdPolicy policy = properties.getNonEmployeePolicy();
for (UserInfo staff : staffs) {
try {
writeOne(code, template, params, staff, umsIds, policy);
} catch (Exception e) {
log.warn("Swing 알림 적재 실패 — userid={}, code={}", staff.getUserid(), code.name(), e);
}
}
}
private void writeOne(MessageCode code, MessageTemplate template, Map<String, Object> params,
UserInfo staff, SwingNotifyProperties.UmsMessengerIds umsIds,
EmployeeIdPolicy policy) {
String messengerId = staff.getUserid();
String requestStatus = STATUS_PENDING;
if (!properties.isEmployeeId(messengerId)) {
switch (policy) {
case REJECT:
return;
case REJECT_LOG:
log.info("행번이 아닌 ID — 발송 거부(미적재). userid={}, code={}", messengerId, code.name());
return;
case SKIP:
log.info("행번이 아닌 ID — 무시 처리({}). userid={}, code={}",
STATUS_SKIPPED, messengerId, code.name());
requestStatus = STATUS_SKIPPED;
break;
case SEND_WARN:
log.warn("행번이 아닌 ID — 발송 진행. Swing 미등록 계정이면 발송 실패한다. userid={}, code={}",
messengerId, code.name());
break;
case SEND:
default:
break;
}
}
Map<String, String> messageParams = toStringParams(params);
messageParams.put("userId", messengerId);
messageParams.putIfAbsent("USER_ID", messengerId);
if (StringUtils.hasText(staff.getUsername())) {
messageParams.put("userName", staff.getUsername());
messageParams.putIfAbsent("USER_NAME", staff.getUsername());
}
MessageRequest request = new MessageRequest();
request.setMessageCode(code);
request.setMessageType(MESSAGE_TYPE_MESSENGER);
request.setSubject(buildMessage(template.getSubjectTemplate(), messageParams));
request.setMessage(buildMessage(template.getMessengerTemplate(), messageParams));
request.setUsername(staff.getUsername());
request.setUserId(messengerId);
request.setMessengerId(messengerId);
request.setEaiInterfaceId(umsIds.getInterfaceId());
request.setServiceId(umsIds.getServiceId());
request.setRequestDate(LocalDateTime.now());
request.setRequestStatus(requestStatus);
// email/phone 은 설정하지 않는다 — 메신저 전용 경로라 개인정보를 적재할 이유가 없다.
messageRequestRepository.save(request);
log.debug("Swing 알림 적재 — code={}, userid={}, status={}", code.name(), messengerId, requestStatus);
}
/** 수신자별로 파라미터를 복사한다(공유 맵을 오염시키지 않기 위함). */
private static Map<String, String> toStringParams(Map<String, Object> params) {
Map<String, String> result = new HashMap<>();
if (params != null) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
continue;
}
result.put(entry.getKey(), String.valueOf(entry.getValue()));
}
}
return result;
}
/** 템플릿 본문의 {@code %KEY%} 플레이스홀더를 치환한다(정규식 아닌 리터럴 치환). */
private static String buildMessage(String contents, Map<String, String> params) {
if (!StringUtils.hasText(contents)) {
return "";
}
String result = contents;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
continue;
}
result = result.replace("%" + entry.getKey() + "%", entry.getValue());
}
return result;
}
}
@@ -0,0 +1,43 @@
package com.eactive.apim.portal.djb.swing;
import com.eactive.apim.portal.djb.swing.config.SwingAsyncConfig;
import com.eactive.apim.portal.template.entity.MessageCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* DjBankSwing(사내 메신저) 전용 알림 발행자.
*
* <p>포털 사용자용 범용 발송 경로({@code MessageHandlerService} → {@code MessageSendService})를
* 타지 않고 TSEAIRM02 / PTL_MESSAGE_TEMPLATE / PTL_MESSAGE_REQUEST 를 직접 다룬다.
* 내부 직원은 수신자 ID 체계(행번)와 채널(메신저 단일)이 포털 사용자와 완전히 달라 분리했다.</p>
*
* <p>비동기 실행이므로 게시물 등록 트랜잭션과 분리된다. 발송 적재 실패가 등록을 되돌리지 않는 대신,
* 등록이 롤백돼도 알림은 남을 수 있다.</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SwingNotifier {
private final SwingMessageWriter swingMessageWriter;
/**
* 대상 역할(PTL_PROPERTY {@code djb.swing.notify.role}) 직원 전원에게 메신저 알림을 적재한다.
*
* @param code 메세지 코드 (PTL_MESSAGE_TEMPLATE.MESSAGE_CODE 와 동일)
* @param params 템플릿 {@code %KEY%} 치환 파라미터
*/
@Async(SwingAsyncConfig.EXECUTOR)
public void notifyPortalAdmins(MessageCode code, Map<String, Object> params) {
try {
swingMessageWriter.write(code, params);
} catch (Exception e) {
log.warn("Swing 알림 발행 실패 — code={}", code == null ? null : code.name(), e);
}
}
}
@@ -0,0 +1,112 @@
package com.eactive.apim.portal.djb.swing;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Swing 알림 관련 PTL_PROPERTY 접근 래퍼.
*
* <p>그룹 {@code Portal}, 점 구분 소문자 키 관례를 따른다.
* {@link PortalPropertyService#getOrCreateProperty} 는 최초 접근 시 기본값으로 DB row 를
* 생성하므로 별도 초기 데이터 없이도 동작한다.</p>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SwingNotifyProperties {
public static final String GROUP = "Portal";
public static final String KEY_TARGET_ROLE = "djb.swing.notify.role";
public static final String KEY_EMPLOYEE_ID_PATTERN = "djb.swing.notify.employee-id.pattern";
public static final String KEY_NON_EMPLOYEE_POLICY = "djb.swing.notify.non-employee.policy";
/** UMS 메신저 연계 식별자. 기존 발송 경로(MessageSendService)와 같은 키를 그대로 읽는다. */
public static final String KEY_MESSENGER_IF_ID = "djb.ums.messenger.if_id";
public static final String KEY_MESSENGER_TX_ID = "djb.ums.messenger.tx_id";
private static final String DEFAULT_TARGET_ROLE = "portal-admin";
private static final String DEFAULT_EMPLOYEE_ID_PATTERN = "^[0-9]+$";
private final PortalPropertyService portalPropertyService;
/** 컴파일된 행번 정규식 캐시. 프로퍼티 값이 바뀌면 다시 컴파일한다. */
private volatile String cachedPatternValue;
private volatile Pattern cachedPattern;
/** 알림 대상 직원을 식별하는 TSEAIRM02.ROLEIDNFINAME 역할명 */
public String getTargetRole() {
return resolve(KEY_TARGET_ROLE, DEFAULT_TARGET_ROLE,
"Swing 알림 대상 직원 역할명 (TSEAIRM02.ROLEIDNFINAME 의 콤마 구분 토큰)");
}
/** 행번 아닌 ID 처리 정책 */
public EmployeeIdPolicy getNonEmployeePolicy() {
return EmployeeIdPolicy.from(resolve(KEY_NON_EMPLOYEE_POLICY, EmployeeIdPolicy.SKIP.name(),
"행번 아닌 ID 처리 정책 (SEND/SEND_WARN/SKIP/REJECT_LOG/REJECT)"));
}
/**
* 행번(직원번호) 형식인지 여부. 정규식이 잘못 지정된 경우 기본 정규식으로 폴백한다.
*
* @param userId TSEAIRM02.USERID
*/
public boolean isEmployeeId(String userId) {
if (userId == null || userId.trim().isEmpty()) {
return false;
}
return employeeIdPattern().matcher(userId.trim()).matches();
}
/** UMS 메신저 연계 식별자(EAI 인터페이스 ID / 서비스 ID) */
public UmsMessengerIds getUmsMessengerIds() {
// 환경별 실제 연계값이라 임의 기본값을 만들면 위험 — getOrCreateProperty 대신 조회만 한다.
Map<String, String> portalProperties = portalPropertyService.getPortalPropertiesAsMap(GROUP);
return new UmsMessengerIds(
portalProperties.get(KEY_MESSENGER_IF_ID),
portalProperties.get(KEY_MESSENGER_TX_ID));
}
private Pattern employeeIdPattern() {
String value = resolve(KEY_EMPLOYEE_ID_PATTERN, DEFAULT_EMPLOYEE_ID_PATTERN,
"행번(직원번호) 판별 정규식. 미매치 시 개발용 ID 로 간주");
Pattern cached = this.cachedPattern;
if (cached != null && value != null && value.equals(this.cachedPatternValue)) {
return cached;
}
Pattern compiled;
try {
compiled = Pattern.compile(value);
} catch (Exception e) {
log.warn("행번 판별 정규식이 잘못되어 기본값으로 대체합니다 — pattern={}", value, e);
compiled = Pattern.compile(DEFAULT_EMPLOYEE_ID_PATTERN);
}
this.cachedPatternValue = value;
this.cachedPattern = compiled;
return compiled;
}
private String resolve(String key, String defaultValue, String description) {
return portalPropertyService.getOrCreateProperty(GROUP, key, defaultValue, description);
}
/** UMS 메신저 연계 식별자 묶음 */
@Getter
public static class UmsMessengerIds {
private final String interfaceId;
private final String serviceId;
public UmsMessengerIds(String interfaceId, String serviceId) {
this.interfaceId = interfaceId;
this.serviceId = serviceId;
}
}
}
@@ -0,0 +1,34 @@
package com.eactive.apim.portal.djb.swing.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* Swing 알림 전용 비동기 실행기.
*
* <p>게시물 등록 트랜잭션이 메신저 알림 적재를 기다리지 않도록 분리한다.
* eapim-admin 의 {@code WebhookAsyncConfig} 와 동일한 구성.</p>
*/
@Configuration
@EnableAsync
public class SwingAsyncConfig {
public static final String EXECUTOR = "swingNotifyExecutor";
@Bean(name = EXECUTOR)
public Executor swingNotifyExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("swing-notify-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
@@ -0,0 +1,27 @@
package com.eactive.apim.portal.djb.swing.repository;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Swing 알림 대상 직원(TSEAIRM02) 조회 전용 리포지토리.
*/
@Repository
@EMSDataSource
public interface SwingStaffRepository extends JpaRepository<UserInfo, String> {
/**
* TSEAIRM02.roleidnfiname 컬럼은 콤마로 구분된 복수 역할을 저장한다.
* (예: {@code "admin,portal-admin"}). Oracle native query로 콤마 토큰 매치.
*/
@Query(value = "SELECT * FROM TSEAIRM02 t"
+ " WHERE ',' || t.ROLEIDNFINAME || ',' LIKE '%,' || :role || ',%'",
nativeQuery = true)
List<UserInfo> findByRole(@Param("role") String role);
}