From 603cba65a70dfe1a05dd4b2e9e3e2579aeb2de97 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Fri, 21 Aug 2026 10:48:47 +0900 Subject: [PATCH] =?UTF-8?q?API=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20API=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EB=B0=8D=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20base64=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20URL=20=EC=BA=90=EC=8B=B1=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20response=20=EC=95=95=EC=B6=95=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80=20-=20application.yml=EC=97=90?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EA=B0=92=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/apis/controller/ApiController.java | 26 ++++++++++++- .../portal/apps/apis/dto/ApiSpecInfoDto.java | 2 + .../apis/service/ApiSearchFacadeImpl.java | 4 +- .../controller/ApiServiceController.java | 37 +++++++++++++++++++ .../apiservice/service/ApiServiceService.java | 9 +++++ src/main/resources/application.yml | 5 +++ .../views/apps/apis/mainApiList.html | 22 ++++++++++- .../templates/views/apps/main/index.html | 6 +-- .../components/commonUserInfoForm.html | 2 +- 9 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java b/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java index 8d93190..e93fe14 100644 --- a/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java +++ b/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java @@ -15,6 +15,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; @@ -79,12 +83,17 @@ public class ApiController { } @GetMapping - public String apiList(@ModelAttribute ApiGroupSearch search, Model model) { + public String apiList(@ModelAttribute ApiGroupSearch search, @PageableDefault Pageable pageable, Model model) { Map searchResult = apiSearchFacade.searchApis(search); + @SuppressWarnings("unchecked") + List allApis = (List) searchResult.get("apis"); + Page apiPage = slicePage(allApis, pageable); + model.addAttribute("search", search); model.addAttribute("services", searchResult.get("services")); - model.addAttribute("apis", searchResult.get("apis")); + model.addAttribute("apis", apiPage.getContent()); + model.addAttribute("page", apiPage); model.addAttribute("totalApiCount", searchResult.get("totalApiCount")); model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount")); model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1"); @@ -94,6 +103,19 @@ public class ApiController { return "apps/apis/mainApiList"; } + /** + * apiSearchFacade.searchApis() 는 다른 소비처(API Status 필터 목록 등)와 계약을 공유하므로 + * 항상 전체 List 를 돌려준다. 목록 화면 렌더링에서만 결과를 잘라 Page 로 감싼다. + */ + private Page slicePage(List 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); + List content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex); + + return new PageImpl<>(content, pageable, total); + } + @GetMapping("/testbed/api") public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) { // 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도. diff --git a/src/main/java/com/eactive/apim/portal/apps/apis/dto/ApiSpecInfoDto.java b/src/main/java/com/eactive/apim/portal/apps/apis/dto/ApiSpecInfoDto.java index fdeafca..ad0d19e 100644 --- a/src/main/java/com/eactive/apim/portal/apps/apis/dto/ApiSpecInfoDto.java +++ b/src/main/java/com/eactive/apim/portal/apps/apis/dto/ApiSpecInfoDto.java @@ -46,4 +46,6 @@ public class ApiSpecInfoDto { private String displayRoleCode; private String apiGroupName; + + private String apiGroupId; } diff --git a/src/main/java/com/eactive/apim/portal/apps/apis/service/ApiSearchFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/apis/service/ApiSearchFacadeImpl.java index f599f52..9e67411 100644 --- a/src/main/java/com/eactive/apim/portal/apps/apis/service/ApiSearchFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/apis/service/ApiSearchFacadeImpl.java @@ -115,7 +115,9 @@ public class ApiSearchFacadeImpl implements ApiSearchFacade { filteredApis.forEach(api -> { ApiServiceDTO service = apiData.getServicesByApiId().get(api.getApiId()); if (service != null) { - api.setMainIcon(service.getMainIcon()); + // mainIcon(CLOB, base64)은 카드 수만큼 복제하지 않는다. 렌더링은 + // apiGroupId 기준 /api-services/{id}/icon 스트리밍 엔드포인트를 사용한다. + api.setApiGroupId(service.getId()); api.setApiGroupName(service.getGroupName()); } }); diff --git a/src/main/java/com/eactive/apim/portal/apps/apiservice/controller/ApiServiceController.java b/src/main/java/com/eactive/apim/portal/apps/apiservice/controller/ApiServiceController.java index afd1364..1e95d83 100644 --- a/src/main/java/com/eactive/apim/portal/apps/apiservice/controller/ApiServiceController.java +++ b/src/main/java/com/eactive/apim/portal/apps/apiservice/controller/ApiServiceController.java @@ -5,8 +5,17 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO; import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceTabInfo; import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService; import java.util.Arrays; +import java.util.Base64; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -20,6 +29,10 @@ import org.springframework.web.servlet.ModelAndView; @RequiredArgsConstructor public class ApiServiceController { + // ApiGroup.mainIcon 은 "data:image/png;base64,...." 형태의 Data URL 문자열 그대로 저장돼 있다. + private static final Pattern DATA_URL_PATTERN = Pattern.compile("^data:(image/[a-zA-Z0-9+.-]+);base64,(.+)$", Pattern.DOTALL); + private static final String DEFAULT_ICON_PATH = "/img/api_icon_default.png"; + private final ApiServiceService apiServiceService; @@ -52,4 +65,28 @@ public class ApiServiceController { return "apps/apiservice/apiServiceDetail"; } + + /** + * API 그룹 아이콘 스트리밍. mainIcon(CLOB, Data URL 문자열)을 그대로 HTML에 인라인하면 + * 카드 개수만큼 반복 전송돼 응답 용량이 폭증하므로, 그룹 id 당 1회만 내려주고 + * 브라우저 캐시로 재사용시킨다. + */ + @GetMapping("/{id}/icon") + public ResponseEntity icon(@PathVariable String id) { + String mainIcon = apiServiceService.getMainIcon(id); + Matcher matcher = mainIcon == null ? null : DATA_URL_PATTERN.matcher(mainIcon); + + if (matcher == null || !matcher.matches()) { + return ResponseEntity.status(HttpStatus.FOUND) + .header(HttpHeaders.LOCATION, DEFAULT_ICON_PATH) + .build(); + } + + byte[] imageBytes = Base64.getDecoder().decode(matcher.group(2)); + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(matcher.group(1))) + .cacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic()) + .body(imageBytes); + } } diff --git a/src/main/java/com/eactive/apim/portal/apps/apiservice/service/ApiServiceService.java b/src/main/java/com/eactive/apim/portal/apps/apiservice/service/ApiServiceService.java index 3958f70..5e458dd 100644 --- a/src/main/java/com/eactive/apim/portal/apps/apiservice/service/ApiServiceService.java +++ b/src/main/java/com/eactive/apim/portal/apps/apiservice/service/ApiServiceService.java @@ -120,5 +120,14 @@ public class ApiServiceService { .orElse(null); } + /** + * 그룹 아이콘(mainIcon)만 가볍게 조회. {@link #getApiGroupById} 는 apiGroupApiList 까지 + * 함께 로드해 무거우므로, 아이콘 스트리밍 엔드포인트 전용으로 CLOB 값만 꺼낸다. + */ + public String getMainIcon(String id) { + return apiServiceRepository.findById(id) + .map(ApiGroup::getMainIcon) + .orElse(null); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d12e3ec..9552ad3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,6 +30,11 @@ server: # RequestHeader set X-Forwarded-Proto "https" / X-Forwarded-Port "1443" (mod_wl_ohs 는 미전송) forward-headers-strategy: native + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json + min-response-size: 1024 + spring: config: diff --git a/src/main/resources/templates/views/apps/apis/mainApiList.html b/src/main/resources/templates/views/apps/apis/mainApiList.html index c97a710..a240a3c 100644 --- a/src/main/resources/templates/views/apps/apis/mainApiList.html +++ b/src/main/resources/templates/views/apps/apis/mainApiList.html @@ -61,6 +61,7 @@
+