Merged from master
This commit is contained in:
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+6
-13
@@ -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 조회.
|
||||
*
|
||||
* <p>역할 기반 알림 대상 조회는
|
||||
* {@code com.eactive.apim.portal.djb.swing.repository.SwingStaffRepository} 로 분리했다.</p>
|
||||
*/
|
||||
@EMSDataSource
|
||||
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);
|
||||
}
|
||||
|
||||
-57
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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<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);
|
||||
}
|
||||
@@ -1160,7 +1160,7 @@ hr {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.mobile-drawer .drawer-welcome .btn-drawer-login:hover {
|
||||
background: rgb(0%, 25.7647058824%, 63.5294117647%);
|
||||
background: rgb(0, 65.7, 162);
|
||||
}
|
||||
.mobile-drawer .drawer-welcome.authenticated {
|
||||
flex-direction: row;
|
||||
@@ -1692,6 +1692,19 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.footer-link--external {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.footer-link-external-icon {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
color: #D1D5DB;
|
||||
font-size: 16px;
|
||||
@@ -2457,7 +2470,7 @@ hr {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.btn-success:hover {
|
||||
background: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
|
||||
background: rgb(83.2897959184, 199.3102040816, 106.493877551);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.btn-danger {
|
||||
@@ -2465,7 +2478,7 @@ hr {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: rgb(100%, 27.7647058824%, 27.7647058824%);
|
||||
background: rgb(255, 70.8, 70.8);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.btn-ghost {
|
||||
@@ -2731,7 +2744,7 @@ hr {
|
||||
.action-btn-delete:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
|
||||
background: rgb(100%, 34.862745098%, 34.862745098%);
|
||||
background: rgb(255, 88.9, 88.9);
|
||||
}
|
||||
.action-btn-delete:active {
|
||||
transform: translateY(0);
|
||||
@@ -2849,7 +2862,7 @@ hr {
|
||||
background: #a4d6ea;
|
||||
}
|
||||
.btn-input-action.btn-change:hover {
|
||||
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
|
||||
background: rgb(131.6625, 199.4303571429, 226.5375);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
|
||||
}
|
||||
@@ -2924,7 +2937,7 @@ hr {
|
||||
border: none;
|
||||
}
|
||||
.btn-action-primary:hover {
|
||||
background: rgb(12.4992826399%, 36.3615494978%, 80.6771879484%);
|
||||
background: rgb(31.8731707317, 92.7219512195, 205.7268292683);
|
||||
transform: translateY(-2px);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -2974,7 +2987,7 @@ hr {
|
||||
}
|
||||
.status-badge.status-processing {
|
||||
background: rgba(255, 217, 61, 0.1);
|
||||
color: rgb(86.7450980392%, 69.7537901759%, 0%);
|
||||
color: rgb(221.2, 177.8721649485, 0);
|
||||
}
|
||||
.status-badge.status-failed {
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
@@ -3014,7 +3027,7 @@ hr {
|
||||
}
|
||||
.status-badge-header.status-processing {
|
||||
background: rgba(255, 217, 61, 0.1);
|
||||
color: rgb(86.7450980392%, 69.7537901759%, 0%);
|
||||
color: rgb(221.2, 177.8721649485, 0);
|
||||
}
|
||||
|
||||
.badge-sm {
|
||||
@@ -4202,7 +4215,7 @@ select.form-control {
|
||||
.file-upload-wrapper .file-remove-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
|
||||
background: rgb(100%, 34.862745098%, 34.862745098%);
|
||||
background: rgb(255, 88.9, 88.9);
|
||||
}
|
||||
.file-upload-wrapper .file-remove-btn:active {
|
||||
transform: translateY(0);
|
||||
@@ -4523,7 +4536,7 @@ select.form-control {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.form-actions--with-withdrawal .withdrawal-link:hover {
|
||||
background: rgb(82.4349376114%, 91.2174688057%, 95.2709447415%);
|
||||
background: rgb(210.2090909091, 232.6045454545, 242.9409090909);
|
||||
}
|
||||
.form-actions--with-withdrawal .withdrawal-link img {
|
||||
width: 22px;
|
||||
@@ -4678,7 +4691,7 @@ select.form-control {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.notice-content-box a:hover {
|
||||
color: rgb(0%, 25.7647058824%, 63.5294117647%);
|
||||
color: rgb(0, 65.7, 162);
|
||||
}
|
||||
|
||||
.form-row--content .form-label-wrapper {
|
||||
@@ -5612,7 +5625,7 @@ select.form-control {
|
||||
font-size: 16px;
|
||||
}
|
||||
.drawer-logout-btn:hover {
|
||||
background: rgb(100%, 27.7647058824%, 27.7647058824%);
|
||||
background: rgb(255, 70.8, 70.8);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
|
||||
}
|
||||
@@ -6325,7 +6338,7 @@ select.form-control {
|
||||
color: #64748b;
|
||||
}
|
||||
.list-table-btn--default:hover {
|
||||
background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
|
||||
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429);
|
||||
}
|
||||
.list-table-btn--primary {
|
||||
background-color: #ecf0fa;
|
||||
@@ -6333,7 +6346,7 @@ select.form-control {
|
||||
color: #2a69de;
|
||||
}
|
||||
.list-table-btn--primary:hover {
|
||||
background-color: rgb(85.0049019608%, 88.1617647059%, 96.0539215686%);
|
||||
background-color: rgb(216.7625, 224.8125, 244.9375);
|
||||
}
|
||||
.list-table-btn--secondary {
|
||||
background-color: #f5f5f4;
|
||||
@@ -6341,7 +6354,7 @@ select.form-control {
|
||||
color: #64748b;
|
||||
}
|
||||
.list-table-btn--secondary:hover {
|
||||
background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
|
||||
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429);
|
||||
}
|
||||
.list-table-btn--danger {
|
||||
background-color: #fbe7e9;
|
||||
@@ -6349,7 +6362,7 @@ select.form-control {
|
||||
color: #bb1026;
|
||||
}
|
||||
.list-table-btn--danger:hover {
|
||||
background-color: rgb(97.081232493%, 82.487394958%, 83.9467787115%);
|
||||
background-color: rgb(247.5571428571, 210.3428571429, 214.0642857143);
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
@@ -6999,7 +7012,7 @@ select.form-control {
|
||||
.alert.alert-error {
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
color: rgb(100%, 27.7647058824%, 27.7647058824%);
|
||||
color: rgb(255, 70.8, 70.8);
|
||||
align-items: center;
|
||||
}
|
||||
.alert.alert-error svg {
|
||||
@@ -7013,7 +7026,7 @@ select.form-control {
|
||||
.alert.alert-success {
|
||||
background: rgba(107, 207, 127, 0.1);
|
||||
border: 1px solid rgba(107, 207, 127, 0.3);
|
||||
color: rgb(24.12484994%, 74.3849539816%, 34.1768707483%);
|
||||
color: rgb(61.5183673469, 189.6816326531, 87.1510204082);
|
||||
}
|
||||
.alert.alert-info {
|
||||
background: rgba(0, 73, 180, 0.1);
|
||||
@@ -12022,10 +12035,10 @@ body.index-page-body {
|
||||
line-height: 20px;
|
||||
}
|
||||
.login-button:hover {
|
||||
background: rgb(10.0588235294%, 27.568627451%, 68.1764705882%);
|
||||
background: rgb(25.65, 70.3, 173.85);
|
||||
}
|
||||
.login-button:active {
|
||||
background: rgb(9.5294117647%, 26.1176470588%, 64.5882352941%);
|
||||
background: rgb(24.3, 66.6, 164.7);
|
||||
}
|
||||
.login-button:disabled {
|
||||
opacity: 0.6;
|
||||
@@ -12068,10 +12081,10 @@ body.index-page-body {
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
.login-links-container .link-btn:hover {
|
||||
background: rgb(86.5137254902%, 89.3529411765%, 96.4509803922%);
|
||||
background: rgb(220.61, 227.85, 245.95);
|
||||
}
|
||||
.login-links-container .link-btn:active {
|
||||
background: rgb(80.4784313725%, 84.5882352941%, 94.862745098%);
|
||||
background: rgb(205.22, 215.7, 241.9);
|
||||
}
|
||||
|
||||
.login-alert {
|
||||
@@ -12580,12 +12593,12 @@ body.index-page-body {
|
||||
}
|
||||
.auth-request-button:hover,
|
||||
.auth-verify-button:hover {
|
||||
background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
|
||||
background: rgb(37.3117757009, 153.9304672897, 235.0082242991);
|
||||
transform: none !important;
|
||||
}
|
||||
.auth-request-button:active,
|
||||
.auth-verify-button:active {
|
||||
background: rgb(8.3967014843%, 57.3774601429%, 91.4307494961%);
|
||||
background: rgb(21.411588785, 146.3125233645, 233.148411215);
|
||||
}
|
||||
.auth-request-button:disabled,
|
||||
.auth-verify-button:disabled {
|
||||
@@ -12634,10 +12647,10 @@ body.index-page-body {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.account-recovery-card .form-actions .cancel-button:hover {
|
||||
background: rgb(88.4117647059%, 89.9568627451%, 92.2745098039%);
|
||||
background: rgb(225.45, 229.39, 235.3);
|
||||
}
|
||||
.account-recovery-card .form-actions .cancel-button:active {
|
||||
background: rgb(82.7058823529%, 85.0117647059%, 88.4705882353%);
|
||||
background: rgb(210.9, 216.78, 225.6);
|
||||
}
|
||||
.account-recovery-card .form-actions .submit-button {
|
||||
color: #FFFFFF;
|
||||
@@ -12647,7 +12660,7 @@ body.index-page-body {
|
||||
background: rgb(6, 54, 125);
|
||||
}
|
||||
.account-recovery-card .form-actions .submit-button:active {
|
||||
background: rgb(0%, 25.7647058824%, 63.5294117647%);
|
||||
background: rgb(0, 65.7, 162);
|
||||
}
|
||||
.account-recovery-card .form-actions .submit-button:disabled {
|
||||
opacity: 0.6;
|
||||
@@ -12852,7 +12865,7 @@ body.index-page-body {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.result-info-box .info-text .info-link:hover {
|
||||
color: rgb(0%, 25.7647058824%, 63.5294117647%);
|
||||
color: rgb(0, 65.7, 162);
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.result-info-box .info-text {
|
||||
@@ -18035,7 +18048,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.btn-copy-action:hover {
|
||||
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
|
||||
background: rgb(131.6625, 199.4303571429, 226.5375);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.btn-copy-action {
|
||||
@@ -18062,7 +18075,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.btn-view-secret:hover {
|
||||
background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
|
||||
background: rgb(31.8897196262, 151.4130841121, 234.5102803738);
|
||||
}
|
||||
.btn-view-secret svg {
|
||||
width: 20px;
|
||||
@@ -18247,7 +18260,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.btn-copy-action:hover {
|
||||
background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
|
||||
background: rgb(131.6625, 199.4303571429, 226.5375);
|
||||
}
|
||||
.btn-view-secret {
|
||||
width: 100% !important;
|
||||
@@ -18263,7 +18276,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
height: 16px;
|
||||
}
|
||||
.btn-view-secret:hover {
|
||||
background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
|
||||
background: rgb(31.8897196262, 151.4130841121, 234.5102803738);
|
||||
}
|
||||
#revealedSecretBox {
|
||||
width: 100%;
|
||||
@@ -18536,7 +18549,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.detail-wrap .dt-btn-copy:hover {
|
||||
background: rgb(74.3529411765%, 90.2296918768%, 100%);
|
||||
background: rgb(189.6, 230.0857142857, 255);
|
||||
}
|
||||
.detail-wrap .dt-btn-copy svg {
|
||||
color: #2a69de;
|
||||
@@ -18713,7 +18726,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.detail-wrap .dt-btn-gray:hover {
|
||||
background: rgb(66.9250773994%, 71.9364293086%, 75.9455108359%);
|
||||
background: rgb(170.6589473684, 183.4378947368, 193.6610526316);
|
||||
}
|
||||
.detail-wrap .dt-btn-red {
|
||||
width: 156px;
|
||||
@@ -18731,7 +18744,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.detail-wrap .dt-btn-red:hover {
|
||||
background: rgb(100%, 27.4868759774%, 25.1921568627%);
|
||||
background: rgb(255, 70.0915337423, 64.24);
|
||||
}
|
||||
.detail-wrap .dt-btn-blue {
|
||||
width: 156px;
|
||||
@@ -20206,7 +20219,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
}
|
||||
.btn-inquiry-list:hover {
|
||||
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
|
||||
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
|
||||
}
|
||||
.btn-inquiry-list:active {
|
||||
transform: scale(0.98);
|
||||
@@ -20239,7 +20252,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
}
|
||||
.btn-inquiry-edit:hover {
|
||||
background: rgb(0%, 27.1960784314%, 67.0588235294%);
|
||||
background: rgb(0, 69.35, 171);
|
||||
}
|
||||
.btn-inquiry-edit:active {
|
||||
transform: scale(0.98);
|
||||
@@ -20272,7 +20285,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
}
|
||||
.btn-inquiry-delete:hover {
|
||||
background: rgb(85.4839910648%, 16.2218912882%, 22.8577810871%);
|
||||
background: rgb(217.9841772152, 41.3658227848, 58.2873417722);
|
||||
}
|
||||
.btn-inquiry-delete:active {
|
||||
transform: scale(0.98);
|
||||
@@ -20344,7 +20357,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.file-upload-inline .btn-remove-file-inline:hover {
|
||||
background: rgb(82.1236038719%, 14.2293373045%, 20.7341772152%);
|
||||
background: rgb(209.4151898734, 36.2848101266, 52.8721518987);
|
||||
}
|
||||
.file-upload-inline .btn-remove-file-inline svg {
|
||||
width: 12px;
|
||||
@@ -20376,7 +20389,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
}
|
||||
.file-upload-inline .btn-file-attach:hover {
|
||||
background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
|
||||
background: rgb(37.3117757009, 153.9304672897, 235.0082242991);
|
||||
}
|
||||
.file-upload-inline .btn-file-attach svg {
|
||||
width: 22px;
|
||||
@@ -20436,7 +20449,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
border: none;
|
||||
}
|
||||
.inquiry-form-container .form-actions .btn-secondary:hover {
|
||||
background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
|
||||
background: rgb(215.8869565217, 218.8956521739, 224.9130434783);
|
||||
}
|
||||
.inquiry-form-container .form-actions .btn-primary {
|
||||
background: #0049b4;
|
||||
@@ -20444,7 +20457,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
border: none;
|
||||
}
|
||||
.inquiry-form-container .form-actions .btn-primary:hover {
|
||||
background: rgb(0%, 27.1960784314%, 67.0588235294%);
|
||||
background: rgb(0, 69.35, 171);
|
||||
}
|
||||
.inquiry-form-container .file-upload-inline .file-input-display {
|
||||
min-height: 50px;
|
||||
@@ -21233,7 +21246,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
cursor: pointer;
|
||||
}
|
||||
.djb-board-write-container .form-actions .btn-submit:hover {
|
||||
background-color: rgb(13.193687231%, 38.3816355811%, 85.1592539455%);
|
||||
background-color: rgb(33.643902439, 97.8731707317, 217.156097561);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.djb-board-write-container .form-actions .btn-submit {
|
||||
@@ -21719,7 +21732,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.org-file-remove:hover {
|
||||
background: rgb(100%, 27.7647058824%, 27.7647058824%);
|
||||
background: rgb(255, 70.8, 70.8);
|
||||
}
|
||||
|
||||
.org-file-notice {
|
||||
@@ -22731,7 +22744,7 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
}
|
||||
.status-indicator.status-active {
|
||||
background-color: rgba(107, 207, 127, 0.1);
|
||||
color: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
|
||||
color: rgb(83.2897959184, 199.3102040816, 106.493877551);
|
||||
}
|
||||
.status-indicator.status-active .status-dot {
|
||||
background-color: #6BCF7F;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -65,6 +65,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.footer-link--external {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.footer-link-external-icon {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
color: #D1D5DB;
|
||||
font-size: 16px;
|
||||
|
||||
@@ -11,7 +11,13 @@
|
||||
<a th:href="@{/agreements/terms}" class="footer-link">이용약관</a>
|
||||
<span class="footer-separator">|</span>
|
||||
<a href="https://www.jejubank.co.kr/hmpg/csct/secuCenr/ptctPlcy/procsPlcy/ctnt.do"
|
||||
target="_blank" rel="noopener noreferrer" class="footer-link">개인정보처리방침</a>
|
||||
target="_blank" rel="noopener noreferrer" class="footer-link footer-link--external">개인정보처리방침<svg class="footer-link-external-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
||||
<path d="M6.5 2.5H3.2A1.2 1.2 0 0 0 2 3.7v9.1A1.2 1.2 0 0 0 3.2 14h9.1a1.2 1.2 0 0 0 1.2-1.2V9.5"
|
||||
stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.5 2h4.5v4.5M14 2 7.5 8.5" stroke="currentColor" stroke-width="1.3"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg><span class="sr-only">새 창으로 열림</span></a>
|
||||
</div>
|
||||
<p class="footer-copyright">Copyright © 2026 JEJU Bank. All Rights Reserved.</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user