API 목록 페이징 로직 테스트 및 페이지 크기 설정 기능 추가:
- 기본 페이지 크기 설정 ApiListProperties 도입 - Controller, 템플릿, JS에 설정값 연결 및 테스트 추가
This commit is contained in:
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.apis.controller;
|
||||
|
||||
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiListProperties;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
@@ -17,6 +18,7 @@ import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -37,6 +39,7 @@ public class ApiController {
|
||||
private final ApiServiceService apiServiceService;
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
private final ApiStatusCatalogService apiStatusCatalogService;
|
||||
private final ApiListProperties apiListProperties;
|
||||
private static final String DEFAULT_TOKEN_API_ID = "default-token-api-spec";
|
||||
private static final String DEFAULT_TOKEN_API_NAME = "인증";
|
||||
|
||||
@@ -88,7 +91,8 @@ public class ApiController {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<ApiSpecInfoDto> allApis = (List<ApiSpecInfoDto>) searchResult.get("apis");
|
||||
Page<ApiSpecInfoDto> apiPage = slicePage(allApis, pageable);
|
||||
Pageable listPageable = PageRequest.of(pageable.getPageNumber(), apiListProperties.getPageSize(), pageable.getSort());
|
||||
Page<ApiSpecInfoDto> apiPage = slicePage(allApis, listPageable);
|
||||
|
||||
model.addAttribute("search", search);
|
||||
model.addAttribute("services", searchResult.get("services"));
|
||||
@@ -109,8 +113,8 @@ public class ApiController {
|
||||
*/
|
||||
private Page<ApiSpecInfoDto> slicePage(List<ApiSpecInfoDto> apis, Pageable pageable) {
|
||||
int total = apis == null ? 0 : apis.size();
|
||||
int fromIndex = Math.min(pageable.getPageNumber() * pageable.getPageSize(), total);
|
||||
int toIndex = Math.min(fromIndex + pageable.getPageSize(), total);
|
||||
int fromIndex = (int) Math.min(pageable.getOffset(), total);
|
||||
int toIndex = (int) Math.min((long) fromIndex + pageable.getPageSize(), total);
|
||||
List<ApiSpecInfoDto> content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex);
|
||||
|
||||
return new PageImpl<>(content, pageable, total);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.eactive.apim.portal.apps.apis.service;
|
||||
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** OPEN API 목록 및 앱/Webhook 신청·수정 API 선택 목록의 공통 페이지 크기. */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ApiListProperties {
|
||||
|
||||
private static final String PAGE_SIZE_PROPERTY = "api.list.page-size";
|
||||
private static final int DEFAULT_PAGE_SIZE = 15;
|
||||
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
/** PTL_PROPERTY(Portal / api.list.page-size)를 조회하고, 없으면 기본값으로 생성한다. */
|
||||
public int getPageSize() {
|
||||
String value = portalPropertyService.getOrCreateProperty(
|
||||
"Portal", PAGE_SIZE_PROPERTY, String.valueOf(DEFAULT_PAGE_SIZE),
|
||||
"API 목록 및 앱/Webhook 신청·수정 API 선택 목록의 페이지당 노출 개수 (양의 정수, 기본 15)");
|
||||
try {
|
||||
int size = Integer.parseInt(value == null ? "" : value.trim());
|
||||
if (size > 0) {
|
||||
return size;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// 비어 있거나 정수가 아닌 설정은 기본값을 사용한다.
|
||||
}
|
||||
log.warn("{} 값이 올바르지 않음('{}') - 기본값 {} 사용", PAGE_SIZE_PROPERTY, value, DEFAULT_PAGE_SIZE);
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* - 기선택: window.API_SELECTOR_SELECTED / 목록 URL: window.API_SELECTOR_LIST_URL (fragment 인라인 주입)
|
||||
* - "이전" 버튼: 호출 페이지의 #btnPrevStep (없으면 스킵)
|
||||
* - 카트/모달: fragment `apiSelectorPopups` 를 pagePopups 슬롯에서 호출(body 직속)
|
||||
* - 페이징: #apiPagination (PAGE_SIZE 건/페이지) — 카테고리/검색은 재조회 없이 클라이언트에서 처리
|
||||
* - 페이징: #apiPagination (window.API_SELECTOR_PAGE_SIZE 건/페이지, 기본 15) — 카테고리/검색은 클라이언트에서 처리
|
||||
*
|
||||
* design(figma s2) 인라인 스크립트 대비 패치 4건:
|
||||
* 1) 모달 열 때마다 updateModalList() 재빌드 — 세션 복원 직후(카드 렌더 전) 빈 모달 방지
|
||||
@@ -22,7 +22,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return; // 모듈 미사용 페이지
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
const configuredPageSize = Number(window.API_SELECTOR_PAGE_SIZE);
|
||||
const PAGE_SIZE = Number.isInteger(configuredPageSize) && configuredPageSize > 0 ? configuredPageSize : 15;
|
||||
|
||||
// DOM Elements
|
||||
const searchInput = document.getElementById('apiSearch');
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- "이전" 버튼은 호출 페이지에 id="btnPrevStep" — 모듈 JS가 data-save-action 경로로 저장 POST 후 step1 복귀.
|
||||
- 추가 hidden 필드는 호출 페이지에서 form="apiSelectorForm" 속성으로 주입(예: apikey 수정 clientId).
|
||||
- API 목록: GET /apis/for_request (ROLE_API_KEY_REQUEST) AJAX. 카테고리/검색 전환 시 재조회 없이
|
||||
클라이언트에서 12건/페이지로 페이징(#apiPagination, api-selector.js PAGE_SIZE) — 전체선택/모달은 페이징과
|
||||
PTL_PROPERTY(Portal / api.list.page-size, 기본 15) 기준으로 클라이언트 페이징 — 전체선택/모달은 페이징과
|
||||
무관하게 필터된 전체 목록 기준으로 동작.
|
||||
- 스타일: design s2-* (_apikey-register.scss step2 재작업분) + 전역 .pagination(_pagination.scss) 재사용.
|
||||
*/-->
|
||||
@@ -107,6 +107,7 @@
|
||||
<script th:inline="javascript">
|
||||
window.API_SELECTOR_SELECTED = /*[[${selectedApis}]]*/ [];
|
||||
window.API_SELECTOR_LIST_URL = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||
window.API_SELECTOR_PAGE_SIZE = /*[[${@apiListProperties.pageSize}]]*/ 15;
|
||||
</script>
|
||||
<script th:src="@{/js/api-selector.js}"></script>
|
||||
</th:block>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.eactive.apim.portal.apps.apis.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiListProperties;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
import com.eactive.apim.portal.djb.apistatus.service.ApiStatusCatalogService;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ApiControllerTest {
|
||||
|
||||
@Mock private ApiService apiService;
|
||||
@Mock private ApiServiceService apiServiceService;
|
||||
@Mock private ApiSearchFacade apiSearchFacade;
|
||||
@Mock private ApiStatusCatalogService apiStatusCatalogService;
|
||||
@Mock private PortalPropertyService portalPropertyService;
|
||||
private ApiController controller;
|
||||
|
||||
private final List<ApiSpecInfoDto> apis = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new ApiController(apiService, apiServiceService, apiSearchFacade,
|
||||
apiStatusCatalogService, new ApiListProperties(portalPropertyService));
|
||||
for (int i = 0; i < 32; i++) {
|
||||
ApiSpecInfoDto api = new ApiSpecInfoDto();
|
||||
api.setApiId("api-" + i);
|
||||
apis.add(api);
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("apis", apis);
|
||||
result.put("totalApiCount", apis.size());
|
||||
result.put("selectedApiCount", apis.size());
|
||||
when(apiSearchFacade.searchApis(any(ApiGroupSearch.class))).thenReturn(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultSizePaginatesByFifteen() {
|
||||
stubPageSize("15");
|
||||
|
||||
Page<?> first = listPage(0);
|
||||
assertEquals(15, first.getNumberOfElements());
|
||||
assertEquals(3, first.getTotalPages());
|
||||
assertEquals(32, first.getTotalElements());
|
||||
assertEquals(apis.subList(0, 15), first.getContent());
|
||||
assertEquals(apis.subList(15, 30), listPage(1).getContent());
|
||||
assertEquals(apis.subList(30, 32), listPage(2).getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertyChangesApplyOnNextRequestAndOverrideRequestSize() {
|
||||
stubPageSize(" 6 ");
|
||||
Page<?> page = listPage(1);
|
||||
assertEquals(6, page.getSize());
|
||||
assertEquals(apis.subList(6, 12), page.getContent());
|
||||
|
||||
stubPageSize("20");
|
||||
assertEquals(apis.subList(20, 32), listPage(1).getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidPropertyFallsBackToFifteen() {
|
||||
for (String value : new String[] {null, "", " ", "0", "-1", "invalid", "1.5", "2147483648"}) {
|
||||
stubPageSize(value);
|
||||
assertEquals(15, listPage(0).getSize(), "설정값: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void largePageOffsetDoesNotOverflow() {
|
||||
stubPageSize("15");
|
||||
assertTrue(listPage(Integer.MAX_VALUE - 1).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptySearchResultsProduceEmptyPage() {
|
||||
stubPageSize("15");
|
||||
apis.clear();
|
||||
Page<?> page = listPage(0);
|
||||
assertTrue(page.isEmpty());
|
||||
assertEquals(0, page.getTotalElements());
|
||||
}
|
||||
|
||||
private void stubPageSize(String value) {
|
||||
when(portalPropertyService.getOrCreateProperty(
|
||||
eq("Portal"), eq("api.list.page-size"), eq("15"), anyString())).thenReturn(value);
|
||||
}
|
||||
|
||||
private Page<?> listPage(int pageNumber) {
|
||||
ExtendedModelMap model = new ExtendedModelMap();
|
||||
assertEquals("apps/apis/mainApiList", controller.apiList(
|
||||
new ApiGroupSearch(), PageRequest.of(pageNumber, 10), model));
|
||||
Page<?> page = (Page<?>) model.get("page");
|
||||
assertEquals(page.getContent(), model.get("apis"));
|
||||
return page;
|
||||
}
|
||||
}
|
||||
+30
@@ -1,5 +1,7 @@
|
||||
package com.eactive.apim.portal.common.compatibility;
|
||||
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiListProperties;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -37,6 +39,10 @@ import org.thymeleaf.spring5.view.ThymeleafView;
|
||||
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ThymeleafBootMvcCompatibilityTest {
|
||||
|
||||
@@ -121,6 +127,30 @@ class ThymeleafBootMvcCompatibilityTest {
|
||||
.contains("회원 메뉴", "관리자 메뉴", ">portal-admin</span>").doesNotContain("로그인 안내"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void apiSelectorUsesSharedPageSizeForClientAndWebhookRegistrationAndModification() {
|
||||
PortalPropertyService properties = mock(PortalPropertyService.class);
|
||||
runner.withBean("apiListProperties", ApiListProperties.class, () -> new ApiListProperties(properties))
|
||||
.run(context -> {
|
||||
for (int pageSize : new int[]{15, 6}) {
|
||||
when(properties.getOrCreateProperty(eq("Portal"), eq("api.list.page-size"),
|
||||
eq("15"), anyString())).thenReturn(String.valueOf(pageSize));
|
||||
for (String action : new String[]{"/clients/register/step2", "/clients/modify/step2",
|
||||
"/webhook/register/step2", "/webhook/modify/step2"}) {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("apiServices", Collections.emptyList());
|
||||
model.put("selectedApis", Collections.singletonList("selected-api"));
|
||||
model.put("formAction", action);
|
||||
model.put("saveAction", action + "/save");
|
||||
String html = render(context, "views/fragment/api_selector :: apiSelector", model);
|
||||
assertThat(html).contains("window.API_SELECTOR_PAGE_SIZE = " + pageSize + ";",
|
||||
"action=\"/portal" + action + "\"",
|
||||
"window.API_SELECTOR_SELECTED = [\"selected-api\"];");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String render(WebApplicationContext context, String template, Map<String, Object> model) throws Exception {
|
||||
context.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest(context.getServletContext());
|
||||
|
||||
Reference in New Issue
Block a user