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.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.ApiSearchFacade;
|
||||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
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.dto.ApiGroupSearch;
|
||||||
@@ -17,6 +18,7 @@ import java.util.Optional;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageImpl;
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.web.PageableDefault;
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -37,6 +39,7 @@ public class ApiController {
|
|||||||
private final ApiServiceService apiServiceService;
|
private final ApiServiceService apiServiceService;
|
||||||
private final ApiSearchFacade apiSearchFacade;
|
private final ApiSearchFacade apiSearchFacade;
|
||||||
private final ApiStatusCatalogService apiStatusCatalogService;
|
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_ID = "default-token-api-spec";
|
||||||
private static final String DEFAULT_TOKEN_API_NAME = "인증";
|
private static final String DEFAULT_TOKEN_API_NAME = "인증";
|
||||||
|
|
||||||
@@ -88,7 +91,8 @@ public class ApiController {
|
|||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<ApiSpecInfoDto> allApis = (List<ApiSpecInfoDto>) searchResult.get("apis");
|
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("search", search);
|
||||||
model.addAttribute("services", searchResult.get("services"));
|
model.addAttribute("services", searchResult.get("services"));
|
||||||
@@ -109,8 +113,8 @@ public class ApiController {
|
|||||||
*/
|
*/
|
||||||
private Page<ApiSpecInfoDto> slicePage(List<ApiSpecInfoDto> apis, Pageable pageable) {
|
private Page<ApiSpecInfoDto> slicePage(List<ApiSpecInfoDto> apis, Pageable pageable) {
|
||||||
int total = apis == null ? 0 : apis.size();
|
int total = apis == null ? 0 : apis.size();
|
||||||
int fromIndex = Math.min(pageable.getPageNumber() * pageable.getPageSize(), total);
|
int fromIndex = (int) Math.min(pageable.getOffset(), total);
|
||||||
int toIndex = Math.min(fromIndex + pageable.getPageSize(), total);
|
int toIndex = (int) Math.min((long) fromIndex + pageable.getPageSize(), total);
|
||||||
List<ApiSpecInfoDto> content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex);
|
List<ApiSpecInfoDto> content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex);
|
||||||
|
|
||||||
return new PageImpl<>(content, pageable, total);
|
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 인라인 주입)
|
* - 기선택: window.API_SELECTOR_SELECTED / 목록 URL: window.API_SELECTOR_LIST_URL (fragment 인라인 주입)
|
||||||
* - "이전" 버튼: 호출 페이지의 #btnPrevStep (없으면 스킵)
|
* - "이전" 버튼: 호출 페이지의 #btnPrevStep (없으면 스킵)
|
||||||
* - 카트/모달: fragment `apiSelectorPopups` 를 pagePopups 슬롯에서 호출(body 직속)
|
* - 카트/모달: fragment `apiSelectorPopups` 를 pagePopups 슬롯에서 호출(body 직속)
|
||||||
* - 페이징: #apiPagination (PAGE_SIZE 건/페이지) — 카테고리/검색은 재조회 없이 클라이언트에서 처리
|
* - 페이징: #apiPagination (window.API_SELECTOR_PAGE_SIZE 건/페이지, 기본 15) — 카테고리/검색은 클라이언트에서 처리
|
||||||
*
|
*
|
||||||
* design(figma s2) 인라인 스크립트 대비 패치 4건:
|
* design(figma s2) 인라인 스크립트 대비 패치 4건:
|
||||||
* 1) 모달 열 때마다 updateModalList() 재빌드 — 세션 복원 직후(카드 렌더 전) 빈 모달 방지
|
* 1) 모달 열 때마다 updateModalList() 재빌드 — 세션 복원 직후(카드 렌더 전) 빈 모달 방지
|
||||||
@@ -22,7 +22,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return; // 모듈 미사용 페이지
|
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
|
// DOM Elements
|
||||||
const searchInput = document.getElementById('apiSearch');
|
const searchInput = document.getElementById('apiSearch');
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
- "이전" 버튼은 호출 페이지에 id="btnPrevStep" — 모듈 JS가 data-save-action 경로로 저장 POST 후 step1 복귀.
|
- "이전" 버튼은 호출 페이지에 id="btnPrevStep" — 모듈 JS가 data-save-action 경로로 저장 POST 후 step1 복귀.
|
||||||
- 추가 hidden 필드는 호출 페이지에서 form="apiSelectorForm" 속성으로 주입(예: apikey 수정 clientId).
|
- 추가 hidden 필드는 호출 페이지에서 form="apiSelectorForm" 속성으로 주입(예: apikey 수정 clientId).
|
||||||
- API 목록: GET /apis/for_request (ROLE_API_KEY_REQUEST) AJAX. 카테고리/검색 전환 시 재조회 없이
|
- 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) 재사용.
|
- 스타일: design s2-* (_apikey-register.scss step2 재작업분) + 전역 .pagination(_pagination.scss) 재사용.
|
||||||
*/-->
|
*/-->
|
||||||
@@ -107,6 +107,7 @@
|
|||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
window.API_SELECTOR_SELECTED = /*[[${selectedApis}]]*/ [];
|
window.API_SELECTOR_SELECTED = /*[[${selectedApis}]]*/ [];
|
||||||
window.API_SELECTOR_LIST_URL = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
window.API_SELECTOR_LIST_URL = /*[[@{/apis/for_request}]]*/ '/apis/for_request';
|
||||||
|
window.API_SELECTOR_PAGE_SIZE = /*[[${@apiListProperties.pageSize}]]*/ 15;
|
||||||
</script>
|
</script>
|
||||||
<script th:src="@{/js/api-selector.js}"></script>
|
<script th:src="@{/js/api-selector.js}"></script>
|
||||||
</th:block>
|
</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;
|
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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -37,6 +39,10 @@ import org.thymeleaf.spring5.view.ThymeleafView;
|
|||||||
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
|
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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 {
|
class ThymeleafBootMvcCompatibilityTest {
|
||||||
|
|
||||||
@@ -121,6 +127,30 @@ class ThymeleafBootMvcCompatibilityTest {
|
|||||||
.contains("회원 메뉴", "관리자 메뉴", ">portal-admin</span>").doesNotContain("로그인 안내"));
|
.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 {
|
private String render(WebApplicationContext context, String template, Map<String, Object> model) throws Exception {
|
||||||
context.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
|
context.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest(context.getServletContext());
|
MockHttpServletRequest request = new MockHttpServletRequest(context.getServletContext());
|
||||||
|
|||||||
Reference in New Issue
Block a user