Merge Gitea master into z-eactive master
eapim-admin CI / build (push) Has been cancelled

This commit is contained in:
Rinjae
2026-06-22 17:24:22 +09:00
5 changed files with 116 additions and 5 deletions
@@ -29,6 +29,16 @@
// 원본 행 데이터를 ID로 조회하기 위한 맵
var gridRowDataMap = {};
// 휴대폰번호 중복 조회 모드 여부
var isDupView = false;
// 현재 검색조건 + 중복 조회 모드 플래그를 합쳐 LIST 요청 파라미터를 생성
function buildListPostData() {
var postData = getSearchForJqgrid("cmd", "LIST");
postData.onlyDuplicateMobile = isDupView;
return postData;
}
/* 선택된 사용자 삭제 - 상태에 따라 Soft/Hard Delete 자동 분기 */
function deleteSelectedUsers() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
@@ -166,6 +176,13 @@
$('#grid').jqGrid('setColProp', 'approvalStatus', {editoptions: {value: select_approvalStatus}});
$('#grid').jqGrid('setColProp', 'userStatus', {editoptions: {value: select_userStatus}});
// 휴대폰번호 중복 허용안함(기능 ON) + 중복 사용자 존재 시 배너 노출
if (json.mobileDuplicateCheckEnabled && json.mobileDuplicateUserCount > 0) {
$("#dupMobileCount").text(json.mobileDuplicateUserCount);
$("#dupMobileBanner").show();
}
$('#grid').trigger('reloadGrid');
},
@@ -176,7 +193,7 @@
}
$(document).ready(function () {
var gridPostData = getSearchForJqgrid("cmd", "LIST");
var gridPostData = buildListPostData();
$('#grid').jqGrid({
datatype: "json",
@@ -188,7 +205,7 @@
'<%= localeMessage.getString("portalUser.orgName") %>',
'<%= localeMessage.getString("portalUser.name") %>',
'<%= localeMessage.getString("portalUser.userId") %>',
<%--'<%= localeMessage.getString("portalUser.mobile") %>',--%>
'<%= localeMessage.getString("portalUser.mobileNumber") %>',
'<%= localeMessage.getString("portalUser.roleName") %>',
'<%= localeMessage.getString("portalUser.userStatus") %>',
'<%= localeMessage.getString("portalUser.createOn") %>',
@@ -201,7 +218,7 @@
{name: 'portalOrgUIs.orgName', align: 'center', width: "120"},
{name: 'userName', align: 'center', width: "80" },
{name: 'loginId', align: 'center', width: "150" },
// {name: 'mobileNumber', align: 'center', width: "100" },
{name: 'mobileNumber', align: 'center', width: "110", hidden: true},
{name: 'roleCodeDescription', align: 'center', width: "80"},
{name: 'userStatusDescription', align: 'center', width: "50"},
{name: 'createdDate', align: 'center', width: "100", formatter: timeStampFormat},
@@ -274,9 +291,20 @@
init();
$("#btn_search").click(function () {
var postData = getSearchForJqgrid("cmd", "LIST");
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
});
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
// 중복 휴대폰번호 사용자만 보기 / 전체 보기 토글
$("#btn_dup_toggle").click(function () {
isDupView = !isDupView;
if (isDupView) {
$(this).text("전체 보기");
$("#grid").jqGrid('showCol', 'mobileNumber');
} else {
$(this).text("중복만 보기");
$("#grid").jqGrid('hideCol', 'mobileNumber');
}
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
});
$("#btn_new").click(function () {
@@ -334,6 +362,11 @@
</div>
<% } %>
<div id="dupMobileBanner" style="display:none; background:#f8d7da; border:1px solid #dc3545; padding:8px 15px; margin:5px 0; border-radius:4px; color:#721c24;">
<strong>&#9888; 중복 휴대폰번호 사용자 <span id="dupMobileCount">0</span>건 발견</strong>
<button type="button" class="cssbtn" id="btn_dup_toggle" style="margin-left:10px;">중복만 보기</button>
</div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
@@ -8,12 +8,14 @@ import com.eactive.apim.portal.template.entity.QMessageRequest;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@@ -54,9 +56,40 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
predicate.and(qPortalUser.roleCode.eq(PortalUserEnums.RoleCode.valueOf(portalUserUISearch.getSearchRoleCode())));
}
// 휴대폰번호 중복(2명 이상 공유) 사용자만 조회
if (portalUserUISearch.isOnlyDuplicateMobile()) {
// 자바 리스트 .in() 은 컨버터가 IN 파라미터를 암호화하므로 레거시 평문 데이터와 매칭되지 않는다.
// 대신 서브쿼리 .in() 으로 컬럼 대 컬럼(SQL 내부) 비교를 하여 평문/암호문 저장 방식과 무관하게 동작시킨다.
QPortalUser dupUser = new QPortalUser("dupUser");
predicate.and(qPortalUser.mobileNumber.in(
JPAExpressions.select(dupUser.mobileNumber)
.from(dupUser)
.where(dupUser.mobileNumber.isNotNull()
.and(dupUser.userStatus.ne(PortalUserEnums.UserStatus.REMOVED)))
.groupBy(dupUser.mobileNumber)
.having(dupUser.count().gt(1L))));
}
return repository.findAll(predicate, pageable);
}
/**
* 휴대폰번호가 중복되는 사용자 총 인원수(중복 그룹에 속한 사용자 수의 합)를 반환한다.
*/
public long countDuplicateMobileUsers() {
QPortalUser qPortalUser = QPortalUser.portalUser;
// Oracle 빈 문자열=NULL 이슈로 isNotNull 만 사용 (findDuplicateMobileNumbers 와 동일 기준)
List<Long> duplicateGroupCounts = getJPAQueryFactory()
.select(qPortalUser.count())
.from(qPortalUser)
.where(qPortalUser.mobileNumber.isNotNull()
.and(qPortalUser.userStatus.ne(PortalUserEnums.UserStatus.REMOVED)))
.groupBy(qPortalUser.mobileNumber)
.having(qPortalUser.count().gt(1L))
.fetch();
return duplicateGroupCounts.stream().mapToLong(Long::longValue).sum();
}
public boolean existsByLoginId(String loginId) {
BooleanExpression predicate = QPortalUser.portalUser.loginId.eq(loginId);
return repository.exists(predicate);
@@ -23,4 +23,7 @@ public class PortalUserUISearch {
private boolean includeRemoved;
/** true 인 경우 휴대폰번호가 중복(2명 이상 공유)되는 사용자만 조회 */
private boolean onlyDuplicateMobile;
}
@@ -102,6 +102,12 @@ public class PortalUserManController extends BaseAnnotationController {
resultMap.put("userStatusRows", userStatusList);
resultMap.put("roleCodeRows", roleCodeList);
resultMap.put("isProdServer", SystemUtil.isProdServer());
// 휴대폰번호 중복 허용안함 여부 및 중복 사용자 수 (배너 노출용)
Map<String, Object> mobileDuplicateInfo = portalUserManService.getMobileDuplicateInfo();
resultMap.put("mobileDuplicateCheckEnabled", mobileDuplicateInfo.get("enabled"));
resultMap.put("mobileDuplicateUserCount", mobileDuplicateInfo.get("count"));
return ResponseEntity.ok(resultMap);
}
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.file.entity.FileDetail;
import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.service.FileService;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.portaluser.entity.PortalUserPrivacyAgreement;
@@ -31,7 +32,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
@@ -39,6 +42,11 @@ import java.util.List;
@Slf4j
public class PortalUserManService extends BaseService {
/** PortalProperty 그룹명 */
private static final String PORTAL_PROPERTY_GROUP = "Portal";
/** 사용자 별 휴대폰 번호 중복 허용 프로퍼티 키 (Y:허용, N:허용안함) */
private static final String PROP_MOBILE_DUPLICATE_ALLOW = "user.mobile.duplicate.allow";
private final PortalUserService portalUserService;
private final PortalUserUIMapper portalUserUIMapper;
private final LocaleMessage localeMessage;
@@ -50,6 +58,7 @@ public class PortalUserManService extends BaseService {
private final PortalUserTermsService portalUserTermsService;
private final ApprovalService approvalService;
private final PortalInquiryService portalInquiryService;
private final PortalPropertyService portalPropertyService;
public Page<PortalUserUI> selectList(Pageable pageable, PortalUserUISearch portalUserUISearch) {
@@ -57,6 +66,11 @@ public class PortalUserManService extends BaseService {
return portalUser.map(entity -> {
PortalUserUI ui = convertToUIWithMaskOption(entity, false);
// 목록의 휴대폰번호는 항상 마스킹 (중복 조회 모드에서만 컬럼이 노출되며 개인정보 보호)
if (ui != null && StringUtils.isNotBlank(ui.getMobileNumber())) {
ui.setMobileNumber(MaskingUtils.maskPhoneNumber(ui.getMobileNumber()));
}
if (entity.getPortalOrg() != null) {
setPortalOrgUI(entity, ui);
}
@@ -64,6 +78,28 @@ public class PortalUserManService extends BaseService {
});
}
/**
* 사용자 별 휴대폰 번호 중복 허용 프로퍼티(Portal/user.mobile.duplicate.allow)를 확인한다.
* 프로퍼티가 없으면 기본값 'N'(허용안함)으로 생성하며, '허용안함'일 때 중복 휴대폰번호 사용자 수를 함께 반환한다.
*
* @return enabled(boolean): 중복 조회 기능 활성 여부, count(long): 중복 휴대폰번호 사용자 수
*/
public Map<String, Object> getMobileDuplicateInfo() {
String allowValue = portalPropertyService.getOrCreateProperty(
PORTAL_PROPERTY_GROUP,
PROP_MOBILE_DUPLICATE_ALLOW,
"N",
"사용자 별 휴대폰 번호 중복 허용 여부 (Y:허용, N:허용안함). 허용안함일 때 사용자 관리 목록에 중복 휴대폰번호 사용자 조회 기능을 노출");
boolean checkEnabled = "N".equalsIgnoreCase(allowValue);
long duplicateUserCount = checkEnabled ? portalUserService.countDuplicateMobileUsers() : 0L;
Map<String, Object> info = new HashMap<>();
info.put("enabled", checkEnabled);
info.put("count", duplicateUserCount);
return info;
}
public PortalUserUI selectDetail(String id, boolean isMasked) {
PortalUser portalUser = portalUserService.getById(id);
PortalUserUI portalUserUI = convertToUIWithMaskOption(portalUser, isMasked);