diff --git a/src/main/java/com/eactive/apim/portal/apps/community/partnership/service/PartnershipApplicationFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/community/partnership/service/PartnershipApplicationFacadeImpl.java index 22bbcbd..157b345 100644 --- a/src/main/java/com/eactive/apim/portal/apps/community/partnership/service/PartnershipApplicationFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/community/partnership/service/PartnershipApplicationFacadeImpl.java @@ -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.util.SecurityUtil; 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.service.FileService; import com.eactive.apim.portal.file.service.FileTypeContext; @@ -29,7 +29,7 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF private final PartnershipApplicationMapper partnershipApplicationMapper; private final FileService fileService; // portal-admin 알림 발행기(범용). Q&A 등록 알림과 동일 컴포넌트를 재사용한다. - private final CommunityAdminNotifier portalAdminNotifier; + private final SwingNotifier swingNotifier; @Override @@ -61,7 +61,7 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF if (writer != null) { params.put("writerName", writer.getUserName()); } - portalAdminNotifier.notifyPortalAdmins(MessageCode.PARTNERSHIP_CREATED, params); + swingNotifier.notifyPortalAdmins(MessageCode.PARTNERSHIP_CREATED, params); } @Override diff --git a/src/main/java/com/eactive/apim/portal/apps/community/qna/service/InquiryFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/community/qna/service/InquiryFacadeImpl.java index 9b2d755..e675f1e 100644 --- a/src/main/java/com/eactive/apim/portal/apps/community/qna/service/InquiryFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/community/qna/service/InquiryFacadeImpl.java @@ -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.util.SecurityUtil; 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.exception.InvalidFileException; import com.eactive.apim.portal.file.service.FileService; @@ -40,7 +40,7 @@ public class InquiryFacadeImpl implements InquiryFacade { private final InquiryService inquiryService; private final InquiryMapper inquiryMapper; - private final CommunityAdminNotifier inquiryAdminNotifier; + private final SwingNotifier swingNotifier; private final FileService fileService; @Override @@ -142,7 +142,7 @@ public class InquiryFacadeImpl implements InquiryFacade { params.put("inquiryId", inquiry.getId()); params.put("inquirySubject", inquiry.getInquirySubject()); params.put("writerName", current.getUserName()); - inquiryAdminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_CREATED, params); + swingNotifier.notifyPortalAdmins(MessageCode.INQUIRY_CREATED, params); } @Override diff --git a/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java b/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java index e1796e1..58525ab 100644 --- a/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java +++ b/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/repository/UserInfoRepository.java @@ -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.eai.data.jpa.BaseRepository; 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 조회. + * + *

역할 기반 알림 대상 조회는 + * {@code com.eactive.apim.portal.djb.swing.repository.SwingStaffRepository} 로 분리했다.

+ */ @EMSDataSource public interface UserInfoRepository extends BaseRepository { - - /** - * TSEAIRM02.roleidnfiname 컬럼은 콤마로 구분된 복수 역할을 저장한다. - * (예: {@code "admin,portal-admin"}). Oracle native query로 콤마 토큰 매치. - */ - @Query(value = "SELECT * FROM TSEAIRM02 t" - + " WHERE ',' || t.ROLEIDNFINAME || ',' LIKE '%,' || :role || ',%'", - nativeQuery = true) - List findByRoleContaining(@Param("role") String role); } diff --git a/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/CommunityAdminNotifier.java b/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/CommunityAdminNotifier.java deleted file mode 100644 index d09e88f..0000000 --- a/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/CommunityAdminNotifier.java +++ /dev/null @@ -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 params) { - try { - List 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; - } -} diff --git a/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java b/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java index d646946..b5ea437 100644 --- a/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/djb/community/qna/comment/service/InquiryCommentFacadeImpl.java @@ -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.UserInfoRepository; 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.portalorg.entity.PortalOrg; import com.eactive.apim.portal.portaluser.entity.PortalUser; @@ -47,7 +48,7 @@ public class InquiryCommentFacadeImpl implements InquiryCommentFacade { private final InquiryCommentService commentService; private final InquiryCommentRepository commentRepository; private final InquiryCommentPermissionChecker permissionChecker; - private final CommunityAdminNotifier adminNotifier; + private final SwingNotifier swingNotifier; private final PortalUserRepository portalUserRepository; private final UserInfoRepository userInfoRepository; @@ -107,7 +108,7 @@ public class InquiryCommentFacadeImpl implements InquiryCommentFacade { params.put("inquirySubject", inquiry.getInquirySubject()); params.put("commentContent", comment.getCommentDetail()); params.put("writerName", current.getUserName()); - adminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_COMMENT_CREATED, params); + swingNotifier.notifyPortalAdmins(MessageCode.INQUIRY_COMMENT_CREATED, params); } private Map resolveWriters(List comments) { diff --git a/src/main/java/com/eactive/apim/portal/djb/community/qna/constant/DjbAdminRole.java b/src/main/java/com/eactive/apim/portal/djb/community/qna/constant/DjbAdminRole.java deleted file mode 100644 index da10892..0000000 --- a/src/main/java/com/eactive/apim/portal/djb/community/qna/constant/DjbAdminRole.java +++ /dev/null @@ -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() { - } -} diff --git a/src/main/java/com/eactive/apim/portal/djb/swing/EmployeeIdPolicy.java b/src/main/java/com/eactive/apim/portal/djb/swing/EmployeeIdPolicy.java new file mode 100644 index 0000000..2f82978 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/swing/EmployeeIdPolicy.java @@ -0,0 +1,40 @@ +package com.eactive.apim.portal.djb.swing; + +/** + * Swing 메신저 수신자 ID 가 행번(직원번호)이 아닐 때의 처리 정책. + * + *

Swing 메신저는 {@code TSEAIRM02.USERID} 를 수신자 코드로 사용하며 운영 직원은 숫자 행번을 쓴다. + * portal-admin 역할이 부여된 개발용 계정(영문 ID 등)은 Swing 에 존재하지 않아 발송이 무의미하므로 + * 이 정책으로 처리 방식을 선택한다. PTL_PROPERTY + * {@code Portal / djb.swing.notify.non-employee.policy} 로 지정한다.

+ */ +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; + } +} diff --git a/src/main/java/com/eactive/apim/portal/djb/swing/SwingMessageWriter.java b/src/main/java/com/eactive/apim/portal/djb/swing/SwingMessageWriter.java new file mode 100644 index 0000000..b80bc11 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/swing/SwingMessageWriter.java @@ -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 에 직접 적재한다. + * + *

TSEAIRM02(직원) → PTL_MESSAGE_TEMPLATE(본문) → PTL_MESSAGE_REQUEST(발송 요청) 순으로 + * 처리하며, 실제 발송은 기존과 동일하게 eapim-admin 의 {@code UmsDispatchJob}(5초 주기)이 + * {@code REQUEST_STATUS='PENDING'} 행을 수집해 수행한다.

+ */ +@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 params) { + String role = properties.getTargetRole(); + List 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 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 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 toStringParams(Map params) { + Map result = new HashMap<>(); + if (params != null) { + for (Map.Entry 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 params) { + if (!StringUtils.hasText(contents)) { + return ""; + } + + String result = contents; + for (Map.Entry entry : params.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + result = result.replace("%" + entry.getKey() + "%", entry.getValue()); + } + return result; + } +} diff --git a/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifier.java b/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifier.java new file mode 100644 index 0000000..b86923d --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifier.java @@ -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(사내 메신저) 전용 알림 발행자. + * + *

포털 사용자용 범용 발송 경로({@code MessageHandlerService} → {@code MessageSendService})를 + * 타지 않고 TSEAIRM02 / PTL_MESSAGE_TEMPLATE / PTL_MESSAGE_REQUEST 를 직접 다룬다. + * 내부 직원은 수신자 ID 체계(행번)와 채널(메신저 단일)이 포털 사용자와 완전히 달라 분리했다.

+ * + *

비동기 실행이므로 게시물 등록 트랜잭션과 분리된다. 발송 적재 실패가 등록을 되돌리지 않는 대신, + * 등록이 롤백돼도 알림은 남을 수 있다.

+ */ +@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 params) { + try { + swingMessageWriter.write(code, params); + } catch (Exception e) { + log.warn("Swing 알림 발행 실패 — code={}", code == null ? null : code.name(), e); + } + } +} diff --git a/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifyProperties.java b/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifyProperties.java new file mode 100644 index 0000000..2319eca --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/swing/SwingNotifyProperties.java @@ -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 접근 래퍼. + * + *

그룹 {@code Portal}, 점 구분 소문자 키 관례를 따른다. + * {@link PortalPropertyService#getOrCreateProperty} 는 최초 접근 시 기본값으로 DB row 를 + * 생성하므로 별도 초기 데이터 없이도 동작한다.

+ */ +@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 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; + } + } +} diff --git a/src/main/java/com/eactive/apim/portal/djb/swing/config/SwingAsyncConfig.java b/src/main/java/com/eactive/apim/portal/djb/swing/config/SwingAsyncConfig.java new file mode 100644 index 0000000..207c469 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/swing/config/SwingAsyncConfig.java @@ -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 알림 전용 비동기 실행기. + * + *

게시물 등록 트랜잭션이 메신저 알림 적재를 기다리지 않도록 분리한다. + * eapim-admin 의 {@code WebhookAsyncConfig} 와 동일한 구성.

+ */ +@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; + } +} diff --git a/src/main/java/com/eactive/apim/portal/djb/swing/repository/SwingStaffRepository.java b/src/main/java/com/eactive/apim/portal/djb/swing/repository/SwingStaffRepository.java new file mode 100644 index 0000000..e3c8030 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/swing/repository/SwingStaffRepository.java @@ -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 { + + /** + * TSEAIRM02.roleidnfiname 컬럼은 콤마로 구분된 복수 역할을 저장한다. + * (예: {@code "admin,portal-admin"}). Oracle native query로 콤마 토큰 매치. + */ + @Query(value = "SELECT * FROM TSEAIRM02 t" + + " WHERE ',' || t.ROLEIDNFINAME || ',' LIKE '%,' || :role || ',%'", + nativeQuery = true) + List findByRole(@Param("role") String role); +}