API Spec 상태 조회 및 관리 기능 추가

- ApiSpecStatusRepository 신규 추가
- 스펙 상태 검색 조건/그룹 조회 및 UI 반영
This commit is contained in:
Rinjae
2026-09-09 11:05:46 +09:00
parent 145ad37499
commit 2a9dfaa093
8 changed files with 267 additions and 2 deletions
@@ -38,6 +38,13 @@
success: function (json) { success: function (json) {
new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering(); new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
var apiGroupSelect = $('select[name=searchApiGroupId]').empty();
apiGroupSelect.append($('<option>').val('').text('전체'));
apiGroupSelect.append($('<option>').val('UNREGISTERED').text('미등록'));
(json.apiGroupList || []).forEach(function (group) {
apiGroupSelect.append($('<option>').val(group.id).text(group.groupName));
});
setSearchable(selectName); // 콤보에 searchable 설정 setSearchable(selectName); // 콤보에 searchable 설정
putSelectFromParam(); putSelectFromParam();
@@ -86,6 +93,17 @@
} }
} }
function specStatusFormatter(cellvalue, options, rowObject) {
switch (cellvalue) {
case 'PUBLISHED':
var groupNames = rowObject.publishedGroupNames || [];
var tooltip = groupNames.length ? groupNames.join('\n') : '게시된 그룹 없음';
return $('<span>').text('게시(' + groupNames.length + ')').attr('title', tooltip).css('cursor', 'help').prop('outerHTML');
case 'REGISTERED': return '등록';
default: return '';
}
}
function list() { function list() {
detail() detail()
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
@@ -101,6 +119,7 @@
'요청', '요청',
'응답', '응답',
'상태', '상태',
'스펙 상태',
'작성자', '작성자',
'가상응답여부', '가상응답여부',
'변경일시(*)', '변경일시(*)',
@@ -113,6 +132,7 @@
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false}, {name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false}, {name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false}, {name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
{name: 'specStatus', align: 'center', width: '40', formatter: specStatusFormatter, sortable: false, title: false},
{name: 'author', align: 'center', width: '40', sortable: false}, {name: 'author', align: 'center', width: '40', sortable: false},
{name : 'simYn', align : 'center' , width:'40', hidden: true }, {name : 'simYn', align : 'center' , width:'40', hidden: true },
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true}, {name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
@@ -421,6 +441,14 @@
<td> <td>
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select> <select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
</td> </td>
<th style="width:180px;">당타발 구분</th>
<td>
<select name="searchInOutType" style="width: 100%">
<option value="">전체</option>
<option value="1">당발</option>
<option value="2">타발</option>
</select>
</td>
<th style="width:180px;">상태</th> <th style="width:180px;">상태</th>
<td> <td>
<select name="searchStatusCode" style="width: 100%"> <select name="searchStatusCode" style="width: 100%">
@@ -431,6 +459,24 @@
<option value="C">점검</option> <option value="C">점검</option>
</select> </select>
</td> </td>
</tr>
<tr>
<th style="width:180px;">API 스펙 상태</th>
<td>
<select name="searchSpecStatus" style="width: 100%">
<option value="">전체</option>
<option value="UNREGISTERED">미등록</option>
<option value="REGISTERED">등록</option>
<option value="PUBLISHED">게시</option>
</select>
</td>
<th style="width:180px;">API 그룹</th>
<td>
<select name="searchApiGroupId" style="width: 100%">
<option value="">전체</option>
<option value="UNREGISTERED">미등록</option>
</select>
</td>
<th></th><td></td> <th></th><td></td>
</tr> </tr>
</tbody> </tbody>
@@ -467,4 +513,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
@@ -8,9 +8,12 @@ import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
import com.eactive.eai.data.jpa.AbstractDataService; import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch; import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.Tuple;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -47,6 +50,24 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
}); });
} }
/** 검색 콤보용 그룹 ID/명칭. 게시 여부와 관계없이 모든 그룹을 제공한다. */
public List<Map<String, String>> findGroupOptions() {
QApiGroup group = QApiGroup.apiGroup;
List<Tuple> rows = getJPAQueryFactory()
.select(group.id, group.groupName)
.from(group)
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
.fetch();
List<Map<String, String>> options = new ArrayList<>();
for (Tuple row : rows) {
Map<String, String> option = new HashMap<>();
option.put("id", row.get(group.id));
option.put("groupName", StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
options.add(option);
}
return options;
}
public void deleteApiGroupApiByApiId(String apiId) { public void deleteApiGroupApiByApiId(String apiId) {
List<ApiGroup> apiGroups = repository.findAll(); List<ApiGroup> apiGroups = repository.findAll();
for (ApiGroup apiGroup : apiGroups) { for (ApiGroup apiGroup : apiGroups) {
@@ -77,6 +98,28 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
.fetch(); .fetch();
} }
/** 목록 툴팁용 게시 그룹명. Oracle IN 절 제한에 맞춰 페이지의 API들을 일괄 조회한다. */
public Map<String, List<String>> findPublishedGroupNamesByApiIds(List<String> apiIds) {
Map<String, List<String>> groupNames = new HashMap<>();
QApiGroup group = QApiGroup.apiGroup;
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
for (int from = 0; from < apiIds.size(); from += 1000) {
List<String> chunk = apiIds.subList(from, Math.min(from + 1000, apiIds.size()));
List<Tuple> rows = getJPAQueryFactory()
.select(membership.id.apiId, group.groupName)
.from(membership)
.join(membership.apiGroup, group)
.where(membership.id.apiId.in(chunk), group.displayYn.eq("1"))
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
.fetch();
for (Tuple row : rows) {
groupNames.computeIfAbsent(row.get(membership.id.apiId), key -> new ArrayList<>())
.add(StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
}
}
return groupNames;
}
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */ /** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
public void setApiGroupsForApi(String apiId, List<String> groupIds) { public void setApiGroupsForApi(String apiId, List<String> groupIds) {
deleteApiGroupApiByApiId(apiId); deleteApiGroupApiByApiId(apiId);
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
import java.util.Collection;
import java.util.List;
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
import com.eactive.eai.rms.data.EMSDataSource;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
/** EAI 목록과 스키마가 다른 EMS 스펙의 ID/게시 여부만 조회한다. */
@EMSDataSource
public interface ApiSpecStatusRepository extends Repository<ApiSpecInfo, String> {
@Query("SELECT a.apiId FROM ApiSpecInfo a WHERE "
+ "(:status = 'PUBLISHED' AND a.displayYn = 'Y') OR "
+ "(:status = 'REGISTERED' AND (a.displayYn IS NULL OR a.displayYn <> 'Y'))")
List<String> findApiIdsByStatus(@Param("status") String status);
@Query("SELECT a.apiId AS apiId, a.displayYn AS displayYn FROM ApiSpecInfo a WHERE a.apiId IN :apiIds")
List<SpecStatus> findStatusesByApiIds(@Param("apiIds") Collection<String> apiIds);
interface SpecStatus {
String getApiId();
String getDisplayYn();
}
}
@@ -1,6 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.eaimsg; package com.eactive.eai.rms.data.entity.onl.eaimsg;
import java.util.List; import java.util.List;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository; import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
import com.eactive.eai.rms.data.entity.onl.apim.apispec.ApiSpecStatusRepository;
import com.eactive.eai.data.entity.onl.adapter.QAdapter; import com.eactive.eai.data.entity.onl.adapter.QAdapter;
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup; import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi; import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
@@ -51,6 +53,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */ /** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */
private final ApiSpecInfoRepository apiSpecInfoRepository; private final ApiSpecInfoRepository apiSpecInfoRepository;
private final ApiSpecStatusRepository apiSpecStatusRepository;
public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) { public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) {
@@ -201,6 +204,32 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
predicate predicate
.and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath())); .and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath()));
} }
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchInOutType())) {
predicate.and(qeaiMessageEntity.eaisvcname.endsWith(eaiMessageUISearch.getSearchInOutType()));
}
String specStatus = eaiMessageUISearch.getSearchSpecStatus();
if ("UNREGISTERED".equals(specStatus)) {
predicate.and(apiSpecRegisteredExpression(qeaiMessageEntity.eaisvcname).not());
} else if ("REGISTERED".equals(specStatus) || "PUBLISHED".equals(specStatus)) {
predicate.and(apiIdInExpression(qeaiMessageEntity.eaisvcname,
apiSpecStatusRepository.findApiIdsByStatus(specStatus)));
}
String apiGroupId = eaiMessageUISearch.getSearchApiGroupId();
if (StringUtils.isNotBlank(apiGroupId)) {
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
QApiGroup group = QApiGroup.apiGroup;
JPQLQuery<Integer> groupQuery = JPAExpressions.selectOne()
.from(membership)
.join(membership.apiGroup, group)
.where(membership.id.apiId.eq(qeaiMessageEntity.eaisvcname));
if ("UNREGISTERED".equals(apiGroupId)) {
predicate.and(groupQuery.notExists());
} else {
predicate.and(groupQuery.where(group.id.eq(apiGroupId)).exists());
}
}
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchStatusCode())) { if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchStatusCode())) {
String statusCode = eaiMessageUISearch.getSearchStatusCode(); String statusCode = eaiMessageUISearch.getSearchStatusCode();
@@ -230,7 +259,10 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
* API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p> * API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p>
*/ */
private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) { private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) {
List<String> specApiIds = apiSpecInfoRepository.findAllApiIds(); return apiIdInExpression(eaiSvcNamePath, apiSpecInfoRepository.findAllApiIds());
}
private BooleanExpression apiIdInExpression(StringPath eaiSvcNamePath, List<String> specApiIds) {
if (specApiIds.isEmpty()) { if (specApiIds.isEmpty()) {
// in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다 // in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다
return Expressions.ONE.eq(Expressions.TWO); return Expressions.ONE.eq(Expressions.TWO);
@@ -245,6 +277,18 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
return expression; return expression;
} }
/** 현재 페이지의 스펙 상태를 일괄 조회한다. Oracle IN 절은 1,000개씩 나눈다. */
public Map<String, String> findApiSpecStatuses(List<String> apiIds) {
Map<String, String> statuses = new HashMap<>();
for (int from = 0; from < apiIds.size(); from += ORACLE_IN_LIMIT) {
int to = Math.min(from + ORACLE_IN_LIMIT, apiIds.size());
for (ApiSpecStatusRepository.SpecStatus spec : apiSpecStatusRepository.findStatusesByApiIds(apiIds.subList(from, to))) {
statuses.put(spec.getApiId(), "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED");
}
}
return statuses;
}
/** 노출 중인(display_yn='1') API 그룹에 편성된 API ID. API_GROUP 은 EAI 메시지와 같은 스키마다 */ /** 노출 중인(display_yn='1') API 그룹에 편성된 API ID. API_GROUP 은 EAI 메시지와 같은 스키마다 */
private JPQLQuery<String> displayedGroupApiIdSubQuery() { private JPQLQuery<String> displayedGroupApiIdSubQuery() {
QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi; QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi;
@@ -19,6 +19,12 @@ public class EAIMessageUISearch {
private String searchRefKey; private String searchRefKey;
private String searchApiFullPath; private String searchApiFullPath;
/** API ID 마지막 자리 (1: 당발, 2: 타발) */
private String searchInOutType;
/** UNREGISTERED: 미등록, REGISTERED: 등록(비게시), PUBLISHED: 게시 */
private String searchSpecStatus;
/** 그룹 ID. UNREGISTERED이면 소속 그룹이 없는 API */
private String searchApiGroupId;
/** API_STATUS.STATUS_CODE (E:장애, D:지연, C:점검). "N" 이면 정상(코드 없음)만 조회 */ /** API_STATUS.STATUS_CODE (E:장애, D:지연, C:점검). "N" 이면 정상(코드 없음)만 조회 */
private String searchStatusCode; private String searchStatusCode;
@@ -135,6 +135,15 @@ public class ApiInterfaceService extends OnlBaseService {
public Page<ApiInterfaceUI> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable,String sortname, String sortorder) { public Page<ApiInterfaceUI> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable,String sortname, String sortorder) {
Page<Tuple> tuples = eaiMessageQueryService.selectList(eaiMessageUISearch, pageable, sortname, sortorder ); Page<Tuple> tuples = eaiMessageQueryService.selectList(eaiMessageUISearch, pageable, sortname, sortorder );
List<String> apiIds = tuples.getContent().stream()
.map(tuple -> tuple.get(QEAIMessageEntity.eAIMessageEntity).getEaisvcname())
.distinct()
.collect(Collectors.toList());
Map<String, String> specStatuses = eaiMessageQueryService.findApiSpecStatuses(apiIds);
List<String> publishedApiIds = apiIds.stream()
.filter(apiId -> "PUBLISHED".equals(specStatuses.get(apiId)))
.collect(Collectors.toList());
Map<String, List<String>> publishedGroupNames = apiGroupService.findPublishedGroupNamesByApiIds(publishedApiIds);
return tuples.map(tuple -> { return tuples.map(tuple -> {
EAIMessageEntity eaiMessageEntity = tuple.get(QEAIMessageEntity.eAIMessageEntity); EAIMessageEntity eaiMessageEntity = tuple.get(QEAIMessageEntity.eAIMessageEntity);
@@ -153,6 +162,8 @@ public class ApiInterfaceService extends OnlBaseService {
apiInterfaceUI.setApiFullPath(apiFullPath); apiInterfaceUI.setApiFullPath(apiFullPath);
apiInterfaceUI.setBzwksvckeyname(bzwksvckeyname); apiInterfaceUI.setBzwksvckeyname(bzwksvckeyname);
apiInterfaceUI.setStatusCode(statusCode); apiInterfaceUI.setStatusCode(statusCode);
apiInterfaceUI.setSpecStatus(specStatuses.getOrDefault(eaiMessageEntity.getEaisvcname(), "UNREGISTERED"));
apiInterfaceUI.setPublishedGroupNames(publishedGroupNames.getOrDefault(eaiMessageEntity.getEaisvcname(), Collections.emptyList()));
return apiInterfaceUI; return apiInterfaceUI;
}); });
@@ -164,6 +175,7 @@ public class ApiInterfaceService extends OnlBaseService {
// 전체 목록수 얻기 // 전체 목록수 얻기
HashMap map = new HashMap(); HashMap map = new HashMap();
map.put("bizList", voList); map.put("bizList", voList);
map.put("apiGroupList", apiGroupService.findGroupOptions());
return map; return map;
} }
@@ -54,6 +54,8 @@ public class ApiInterfaceUI {
private String apiFullPath; private String apiFullPath;
private String bzwksvckeyname; private String bzwksvckeyname;
private String statusCode; private String statusCode;
private String specStatus;
private List<String> publishedGroupNames;
//ASYNC-SYNC 용 //ASYNC-SYNC 용
private String inboundResponseHttpMethod; private String inboundResponseHttpMethod;
@@ -0,0 +1,84 @@
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
class ApiSpecStatusRepositoryTest {
private static SessionFactory sessionFactory;
private static EntityManager entityManager;
private static ApiSpecStatusRepository repository;
@BeforeAll
static void setUp() {
sessionFactory = new Configuration()
.addAnnotatedClass(ApiSpecInfo.class)
.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
.setProperty("hibernate.connection.driver_class", "org.h2.Driver")
.setProperty("hibernate.connection.url", "jdbc:h2:mem:apiSpecStatus;DB_CLOSE_DELAY=-1")
.setProperty("hibernate.hbm2ddl.auto", "create-drop")
.buildSessionFactory();
entityManager = sessionFactory.createEntityManager();
repository = new JpaRepositoryFactory(entityManager).getRepository(ApiSpecStatusRepository.class);
entityManager.getTransaction().begin();
save("PUBLISHED1", "Y");
save("REGISTERED1", "N");
save("LEGACY2", null);
entityManager.getTransaction().commit();
entityManager.clear();
}
private static void save(String apiId, String displayYn) {
ApiSpecInfo spec = new ApiSpecInfo();
spec.setApiId(apiId);
spec.setDisplayYn(displayYn);
entityManager.persist(spec);
}
@AfterAll
static void tearDown() {
if (entityManager != null) entityManager.close();
if (sessionFactory != null) sessionFactory.close();
}
@Test
void publishedIncludesOnlyDisplayedSpecs() {
assertEquals(Arrays.asList("PUBLISHED1"), repository.findApiIdsByStatus("PUBLISHED"));
}
@Test
void registeredIncludesNonDisplayedAndLegacyNullSpecs() {
assertEquals(new HashSet<>(Arrays.asList("REGISTERED1", "LEGACY2")),
new HashSet<>(repository.findApiIdsByStatus("REGISTERED")));
}
@Test
void statusProjectionIncludesOnlyRequestedExistingSpecs() {
Map<String, String> statuses = repository.findStatusesByApiIds(
Arrays.asList("PUBLISHED1", "LEGACY2", "MISSING2")).stream()
.collect(Collectors.toMap(ApiSpecStatusRepository.SpecStatus::getApiId,
spec -> "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED"));
assertEquals(2, statuses.size());
assertEquals("PUBLISHED", statuses.get("PUBLISHED1"));
assertEquals("REGISTERED", statuses.get("LEGACY2"));
assertEquals("UNREGISTERED", statuses.getOrDefault("MISSING2", "UNREGISTERED"));
}
@Test
void missingApiHasNoSpecStatus() {
assertTrue(repository.findStatusesByApiIds(Arrays.asList("MISSING2")).isEmpty());
}
}