diff --git a/src/main/java/com/eactive/apim/portal/apps/main/controller/IndexController.java b/src/main/java/com/eactive/apim/portal/apps/main/controller/IndexController.java index fc5e865..8456d14 100644 --- a/src/main/java/com/eactive/apim/portal/apps/main/controller/IndexController.java +++ b/src/main/java/com/eactive/apim/portal/apps/main/controller/IndexController.java @@ -1,33 +1,22 @@ package com.eactive.apim.portal.apps.main.controller; -import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto; -import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade; -import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch; -import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceApiDTO; import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO; -import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeDTO; -import com.eactive.apim.portal.apps.community.notice.service.PortalNoticeFacade; +import com.eactive.apim.portal.apps.main.dto.IndexStatisticsDTO; +import com.eactive.apim.portal.apps.main.service.IndexStatisticsService; import com.eactive.apim.portal.apps.main.service.MainApiFacade; -import com.eactive.apim.portal.common.util.SecurityUtil; - -import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; -import java.util.*; -import java.util.stream.Collectors; - +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; @Controller @RequiredArgsConstructor public class IndexController { - private final ApiSearchFacade apiSearchFacade; private final MainApiFacade mainApiFacade; - private final PortalPropertyService portalPropertyService; + private final IndexStatisticsService indexStatisticsService; /** * 메인 페이지 API는 Portal Property 에 main.service.list 에 등록된 그룹을 기준으로 API를 조회함. @@ -45,29 +34,23 @@ public class IndexController { List services = mainApiFacade.getOpenApiServices(); List hashTags = mainApiFacade.getHashTags(); - ApiGroupSearch search = new ApiGroupSearch(); - Map searchResult = apiSearchFacade.searchApis(search); + // 인덱스 페이지 하단 통계 조회 (캐싱 적용) + IndexStatisticsDTO statistics = indexStatisticsService.getStatistics(); - int selectedApiCount = (int) searchResult.get("selectedApiCount"); - - int serviceCount = ((List) searchResult.get("services")).size(); - - // 승인된 기업 건수 - int approvedOrgCount = mainApiFacade.getApprovedOrgCount(); - - addAttributesToModel(model, services, hashTags, serviceCount, approvedOrgCount, selectedApiCount); + addAttributesToModel(model, services, hashTags, statistics); return "apps/main/index"; } - private void addAttributesToModel(Model model, List apiServices, List hashTags, int serviceCount, int approvedOrgCount, int selectedApiCount) { + private void addAttributesToModel(Model model, List apiServices, List hashTags, IndexStatisticsDTO statistics) { model.addAttribute("services", apiServices); model.addAttribute("hashtags", hashTags); - model.addAttribute("serviceCount", serviceCount); - model.addAttribute("approvedOrgCount", approvedOrgCount); - model.addAttribute("selectedApiCount", selectedApiCount); + // 인덱스 페이지 하단 통계 + model.addAttribute("approvedOrgCount", statistics.getApprovedOrgCount()); + model.addAttribute("serviceCount", statistics.getActiveAppCount()); + model.addAttribute("totalApiCount", statistics.getTotalApiCount()); } } diff --git a/src/main/java/com/eactive/apim/portal/apps/main/dto/IndexStatisticsDTO.java b/src/main/java/com/eactive/apim/portal/apps/main/dto/IndexStatisticsDTO.java new file mode 100644 index 0000000..4457961 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/apps/main/dto/IndexStatisticsDTO.java @@ -0,0 +1,37 @@ +package com.eactive.apim.portal.apps.main.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 인덱스 페이지 하단 통계 DTO + * - API 활용 기업: 법인으로 등록된 수의 합계 (정상 상태) + * - 서비스 이용 수: 전체 법인이 생성한 앱의 합계 (이용 가능 상태) + * - API 이용 건수: 전체 법인이 생성한 앱의 API 수의 합계 (이용 가능 상태) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IndexStatisticsDTO { + + /** + * API 활용 기업 수 + * 법인으로 등록되고 정상 상태인 기업의 수 + */ + private int approvedOrgCount; + + /** + * 서비스 이용 수 + * 정상 상태 법인이 생성한 앱 중 이용 가능 상태인 앱의 수 + */ + private int activeAppCount; + + /** + * API 이용 건수 + * 정상 상태 법인이 생성한 이용 가능 앱에 연결된 API의 총 수 + */ + private int totalApiCount; +} diff --git a/src/main/java/com/eactive/apim/portal/apps/main/service/IndexStatisticsService.java b/src/main/java/com/eactive/apim/portal/apps/main/service/IndexStatisticsService.java new file mode 100644 index 0000000..80aac8d --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/apps/main/service/IndexStatisticsService.java @@ -0,0 +1,137 @@ +package com.eactive.apim.portal.apps.main.service; + +import com.eactive.apim.portal.app.entity.Credential; +import com.eactive.apim.portal.app.repository.CredentialRepository; +import com.eactive.apim.portal.apps.main.dto.IndexStatisticsDTO; +import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository; +import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.ApprovalStatus; +import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 인덱스 페이지 통계 서비스 + * 메모리 캐싱을 통해 DB 조회를 최소화 (1시간 TTL) + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class IndexStatisticsService { + + private static final long CACHE_TTL_HOURS = 1; + private static final String APP_STATUS_ACTIVE = "1"; + + private final PortalOrgRepository portalOrgRepository; + private final CredentialRepository credentialRepository; + + // 캐시된 통계 데이터 + private volatile IndexStatisticsDTO cachedStatistics; + // 마지막 집계 시간 + private volatile LocalDateTime lastAggregatedTime; + + /** + * 인덱스 페이지 통계 조회 + * 캐시가 없거나 1시간이 경과한 경우 재집계 + */ + @Transactional(readOnly = true) + public IndexStatisticsDTO getStatistics() { + if (shouldRefreshCache()) { + synchronized (this) { + // Double-check locking + if (shouldRefreshCache()) { + refreshStatistics(); + } + } + } + return cachedStatistics; + } + + /** + * 캐시 갱신이 필요한지 확인 + */ + private boolean shouldRefreshCache() { + if (cachedStatistics == null || lastAggregatedTime == null) { + return true; + } + return LocalDateTime.now().isAfter(lastAggregatedTime.plusHours(CACHE_TTL_HOURS)); + } + + /** + * 통계 재집계 + */ + private void refreshStatistics() { + log.info("인덱스 페이지 통계 집계 시작"); + long startTime = System.currentTimeMillis(); + + try { + // 1. API 활용 기업 수: 정상 상태이고 승인 완료된 기관 수 + int approvedOrgCount = (int) portalOrgRepository.countByOrgStatusAndApprovalStatus( + OrgStatus.ACTIVE, ApprovalStatus.COMPLETED); + + // 2. 정상 상태 기관의 ID 목록 조회 + List activeOrgIds = portalOrgRepository.findIdsByOrgStatusAndApprovalStatus( + OrgStatus.ACTIVE, ApprovalStatus.COMPLETED); + + int activeAppCount = 0; + int totalApiCount = 0; + + if (!activeOrgIds.isEmpty()) { + // 3. 서비스 이용 수: 정상 기관의 이용 가능 앱 수 + activeAppCount = (int) credentialRepository.countActiveAppsByOrgIds(activeOrgIds); + + // 4. API 이용 건수: 정상 기관의 이용 가능 앱에 연결된 API 수 + List activeApps = credentialRepository.findActiveAppsByOrgIds(activeOrgIds); + totalApiCount = activeApps.stream() + .mapToInt(credential -> credential.getApiList() != null ? credential.getApiList().size() : 0) + .sum(); + } + + cachedStatistics = IndexStatisticsDTO.builder() + .approvedOrgCount(approvedOrgCount) + .activeAppCount(activeAppCount) + .totalApiCount(totalApiCount) + .build(); + + lastAggregatedTime = LocalDateTime.now(); + + long elapsed = System.currentTimeMillis() - startTime; + log.info("인덱스 페이지 통계 집계 완료 - 소요시간: {}ms, 기업: {}, 앱: {}, API: {}", + elapsed, approvedOrgCount, activeAppCount, totalApiCount); + + } catch (Exception e) { + log.error("인덱스 페이지 통계 집계 실패", e); + // 기존 캐시가 없는 경우 기본값 설정 + if (cachedStatistics == null) { + cachedStatistics = IndexStatisticsDTO.builder() + .approvedOrgCount(0) + .activeAppCount(0) + .totalApiCount(0) + .build(); + } + } + } + + /** + * 캐시 강제 갱신 (관리자용) + */ + public void forceRefresh() { + synchronized (this) { + lastAggregatedTime = null; + refreshStatistics(); + } + } + + /** + * 캐시 만료 시간 조회 + */ + public LocalDateTime getCacheExpireTime() { + if (lastAggregatedTime == null) { + return null; + } + return lastAggregatedTime.plusHours(CACHE_TTL_HOURS); + } +} diff --git a/src/main/java/com/eactive/apim/portal/apps/user/repository/PortalOrgRepository.java b/src/main/java/com/eactive/apim/portal/apps/user/repository/PortalOrgRepository.java index c555858..38916a4 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/repository/PortalOrgRepository.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/repository/PortalOrgRepository.java @@ -5,7 +5,10 @@ import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums; import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.rms.data.EMSDataSource; +import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; @EMSDataSource public interface PortalOrgRepository extends BaseRepository { @@ -20,5 +23,13 @@ public interface PortalOrgRepository extends BaseRepository { long countByOrgStatusAndApprovalStatus(PortalOrgEnums.OrgStatus status, PortalOrgEnums.ApprovalStatus approvalStatus); Optional findById(String orgId); + + /** + * 정상 상태이고 승인 완료된 기관의 ID 목록 조회 + */ + @Query("SELECT o.id FROM PortalOrg o WHERE o.orgStatus = :status AND o.approvalStatus = :approvalStatus") + List findIdsByOrgStatusAndApprovalStatus( + @Param("status") PortalOrgEnums.OrgStatus status, + @Param("approvalStatus") PortalOrgEnums.ApprovalStatus approvalStatus); }