Compare commits
2 Commits
889d77b990
...
bac9a54dee
| Author | SHA1 | Date | |
|---|---|---|---|
| bac9a54dee | |||
| 603cba65a7 |
@@ -15,6 +15,10 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
@@ -79,12 +83,17 @@ public class ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String apiList(@ModelAttribute ApiGroupSearch search, Model model) {
|
public String apiList(@ModelAttribute ApiGroupSearch search, @PageableDefault Pageable pageable, Model model) {
|
||||||
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
|
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<ApiSpecInfoDto> allApis = (List<ApiSpecInfoDto>) searchResult.get("apis");
|
||||||
|
Page<ApiSpecInfoDto> apiPage = slicePage(allApis, pageable);
|
||||||
|
|
||||||
model.addAttribute("search", search);
|
model.addAttribute("search", search);
|
||||||
model.addAttribute("services", searchResult.get("services"));
|
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("totalApiCount", searchResult.get("totalApiCount"));
|
||||||
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
|
model.addAttribute("selectedApiCount", searchResult.get("selectedApiCount"));
|
||||||
model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1");
|
model.addAttribute("selected", search.getGroupIds().size() > 0 ? search.getGroupIds().get(0) : "-1");
|
||||||
@@ -94,6 +103,19 @@ public class ApiController {
|
|||||||
return "apps/apis/mainApiList";
|
return "apps/apis/mainApiList";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apiSearchFacade.searchApis() 는 다른 소비처(API Status 필터 목록 등)와 계약을 공유하므로
|
||||||
|
* 항상 전체 List 를 돌려준다. 목록 화면 렌더링에서만 결과를 잘라 Page 로 감싼다.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
List<ApiSpecInfoDto> content = total == 0 ? new ArrayList<>() : apis.subList(fromIndex, toIndex);
|
||||||
|
|
||||||
|
return new PageImpl<>(content, pageable, total);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/testbed/api")
|
@GetMapping("/testbed/api")
|
||||||
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
|
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
|
||||||
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
|
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
|
||||||
|
|||||||
@@ -46,4 +46,6 @@ public class ApiSpecInfoDto {
|
|||||||
private String displayRoleCode;
|
private String displayRoleCode;
|
||||||
|
|
||||||
private String apiGroupName;
|
private String apiGroupName;
|
||||||
|
|
||||||
|
private String apiGroupId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ public class ApiSearchFacadeImpl implements ApiSearchFacade {
|
|||||||
filteredApis.forEach(api -> {
|
filteredApis.forEach(api -> {
|
||||||
ApiServiceDTO service = apiData.getServicesByApiId().get(api.getApiId());
|
ApiServiceDTO service = apiData.getServicesByApiId().get(api.getApiId());
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
api.setMainIcon(service.getMainIcon());
|
// mainIcon(CLOB, base64)은 카드 수만큼 복제하지 않는다. 렌더링은
|
||||||
|
// apiGroupId 기준 /api-services/{id}/icon 스트리밍 엔드포인트를 사용한다.
|
||||||
|
api.setApiGroupId(service.getId());
|
||||||
api.setApiGroupName(service.getGroupName());
|
api.setApiGroupName(service.getGroupName());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+37
@@ -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.dto.ApiServiceTabInfo;
|
||||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -20,6 +29,10 @@ import org.springframework.web.servlet.ModelAndView;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ApiServiceController {
|
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;
|
private final ApiServiceService apiServiceService;
|
||||||
|
|
||||||
|
|
||||||
@@ -52,4 +65,28 @@ public class ApiServiceController {
|
|||||||
|
|
||||||
return "apps/apiservice/apiServiceDetail";
|
return "apps/apiservice/apiServiceDetail";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 그룹 아이콘 스트리밍. mainIcon(CLOB, Data URL 문자열)을 그대로 HTML에 인라인하면
|
||||||
|
* 카드 개수만큼 반복 전송돼 응답 용량이 폭증하므로, 그룹 id 당 1회만 내려주고
|
||||||
|
* 브라우저 캐시로 재사용시킨다.
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/icon")
|
||||||
|
public ResponseEntity<byte[]> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,5 +120,14 @@ public class ApiServiceService {
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 그룹 아이콘(mainIcon)만 가볍게 조회. {@link #getApiGroupById} 는 apiGroupApiList 까지
|
||||||
|
* 함께 로드해 무거우므로, 아이콘 스트리밍 엔드포인트 전용으로 CLOB 값만 꺼낸다.
|
||||||
|
*/
|
||||||
|
public String getMainIcon(String id) {
|
||||||
|
return apiServiceRepository.findById(id)
|
||||||
|
.map(ApiGroup::getMainIcon)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ public class GlobalControllerAdvice {
|
|||||||
return clientGuardService.isDevtoolsGuardEnabled();
|
return clientGuardService.isDevtoolsGuardEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상단 헤더 좌측 노출용 활성 프로파일 배지. prod 프로파일이면 노출하지 않는다(null).
|
||||||
|
*/
|
||||||
|
@ModelAttribute("activeProfileBadge")
|
||||||
|
public String activeProfileBadge() {
|
||||||
|
if (environment.acceptsProfiles(Profiles.of("prod"))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] activeProfiles = environment.getActiveProfiles();
|
||||||
|
return activeProfiles.length == 0 ? "default" : String.join(", ", activeProfiles);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 푸터 고객센터 연락처. PortalProperty(Portal/customer.center.contact)에서 조회.
|
* 푸터 고객센터 연락처. PortalProperty(Portal/customer.center.contact)에서 조회.
|
||||||
* 전화번호가 아닐 수도 있으므로 값 그대로 출력하되 템플릿에서 th:text(HTML escape)로 렌더한다.
|
* 전화번호가 아닐 수도 있으므로 값 그대로 출력하되 템플릿에서 th:text(HTML escape)로 렌더한다.
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ server:
|
|||||||
# RequestHeader set X-Forwarded-Proto "https" / X-Forwarded-Port "1443" (mod_wl_ohs 는 미전송)
|
# RequestHeader set X-Forwarded-Proto "https" / X-Forwarded-Port "1443" (mod_wl_ohs 는 미전송)
|
||||||
forward-headers-strategy: native
|
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:
|
spring:
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -25,15 +25,6 @@
|
|||||||
<property name="FILE_LOG_LEVEL" value="${FILE_LOG_LEVEL:-INFO}"/>
|
<property name="FILE_LOG_LEVEL" value="${FILE_LOG_LEVEL:-INFO}"/>
|
||||||
<property name="FILE_LOG_LEVEL_DEV" value="${FILE_LOG_LEVEL:-DEBUG}"/>
|
<property name="FILE_LOG_LEVEL_DEV" value="${FILE_LOG_LEVEL:-DEBUG}"/>
|
||||||
|
|
||||||
<!-- HTTP 세션 로그(eapim.portal.session) on/off JVM 파라미터 제어 -->
|
|
||||||
<!-- 사용: -DHTTP_SESSION_LOG_ENABLED=false / 기본값: true(켜짐, INFO) -->
|
|
||||||
<property name="HTTP_SESSION_LOG_ENABLED" value="${HTTP_SESSION_LOG_ENABLED:-true}"/>
|
|
||||||
<property name="__httpSessionLevel.true" value="INFO"/>
|
|
||||||
<property name="__httpSessionLevel.TRUE" value="INFO"/>
|
|
||||||
<property name="__httpSessionLevel.false" value="OFF"/>
|
|
||||||
<property name="__httpSessionLevel.FALSE" value="OFF"/>
|
|
||||||
<property name="HTTP_SESSION_EFFECTIVE_LEVEL" value="${__httpSessionLevel.${HTTP_SESSION_LOG_ENABLED}}"/>
|
|
||||||
|
|
||||||
<!-- API 테스터 감사 로그(eapim.portal.apitester.audit) on/off JVM 파라미터 제어 -->
|
<!-- API 테스터 감사 로그(eapim.portal.apitester.audit) on/off JVM 파라미터 제어 -->
|
||||||
<!-- 사용: -DAPI_TESTER_AUDIT_LOG_ENABLED=false / 기본값: true(켜짐, INFO) -->
|
<!-- 사용: -DAPI_TESTER_AUDIT_LOG_ENABLED=false / 기본값: true(켜짐, INFO) -->
|
||||||
<property name="API_TESTER_AUDIT_LOG_ENABLED" value="${API_TESTER_AUDIT_LOG_ENABLED:-true}"/>
|
<property name="API_TESTER_AUDIT_LOG_ENABLED" value="${API_TESTER_AUDIT_LOG_ENABLED:-true}"/>
|
||||||
@@ -119,7 +110,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
|
||||||
<logger name="eapim.portal.session" level="${HTTP_SESSION_EFFECTIVE_LEVEL}" additivity="false">
|
<logger name="eapim.portal.session" level="${FILE_LOG_LEVEL}" additivity="false">
|
||||||
<appender-ref ref="HTTP_SESSION" />
|
<appender-ref ref="HTTP_SESSION" />
|
||||||
</logger>
|
</logger>
|
||||||
|
|
||||||
|
|||||||
@@ -821,6 +821,50 @@ hr {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--accent-orange);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.env-badge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-strip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.env-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--accent-orange);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
z-index: 1100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -156,6 +156,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 활성 Spring 프로파일 표시 (prod 제외)
|
||||||
|
// 데스크톱: 로고 옆 인라인 뱃지 / 모바일·태블릿: 최상단 floating 바(fixed, 레이아웃 미영향)
|
||||||
|
.env-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--accent-orange);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.env-strip {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--accent-orange);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
z-index: 1100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Logo Wrapper
|
// Logo Wrapper
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
<form id="searchForm" th:action="@{/apis}" method="get">
|
<form id="searchForm" th:action="@{/apis}" method="get">
|
||||||
<input type="hidden" name="groupIds"
|
<input type="hidden" name="groupIds"
|
||||||
th:value="${search.groupIds != null and !search.groupIds.isEmpty() ? search.groupIds[0] : ''}" />
|
th:value="${search.groupIds != null and !search.groupIds.isEmpty() ? search.groupIds[0] : ''}" />
|
||||||
|
<input type="hidden" name="page" value="1" />
|
||||||
<input type="text" class="search-input" name="keyword" th:value="${search.keyword}"
|
<input type="text" class="search-input" name="keyword" th:value="${search.keyword}"
|
||||||
placeholder="검색어를 입력하세요.">
|
placeholder="검색어를 입력하세요.">
|
||||||
<button type="submit" class="search-submit-btn" aria-label="검색">
|
<button type="submit" class="search-submit-btn" aria-label="검색">
|
||||||
@@ -99,13 +100,17 @@
|
|||||||
|
|
||||||
<!-- API Image (Bottom Right) -->
|
<!-- API Image (Bottom Right) -->
|
||||||
<div class="api-card-image">
|
<div class="api-card-image">
|
||||||
<img th:if="${api.mainIcon}" th:src="${api.mainIcon}" th:alt="${api.apiGroupName}"
|
<img th:if="${api.apiGroupId}" th:src="@{/api-services/{id}/icon(id=${api.apiGroupId})}"
|
||||||
onerror="this.style.display='none'">
|
th:alt="${api.apiGroupName}" onerror="this.style.display='none'">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="pagination" th:if="${apis != null and !apis.isEmpty()}"
|
||||||
|
th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div class="api-empty-state" th:if="${apis == null or apis.isEmpty()}">
|
<div class="api-empty-state" th:if="${apis == null or apis.isEmpty()}">
|
||||||
<div class="empty-icon">🔍</div>
|
<div class="empty-icon">🔍</div>
|
||||||
@@ -121,6 +126,14 @@
|
|||||||
|
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
|
// fragment/pagination.html 의 인라인 onclick 에서 호출 (전역 스코프 필요)
|
||||||
|
window.fn_select_page = function (pageNo) {
|
||||||
|
const form = document.getElementById('searchForm');
|
||||||
|
const pageInput = form.querySelector('input[name="page"]');
|
||||||
|
if (pageInput) pageInput.value = pageNo;
|
||||||
|
form.submit();
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const sidebar = document.getElementById('apiSidebar');
|
const sidebar = document.getElementById('apiSidebar');
|
||||||
@@ -128,6 +141,7 @@
|
|||||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||||
const searchForm = document.getElementById('searchForm');
|
const searchForm = document.getElementById('searchForm');
|
||||||
const groupIdsInput = searchForm.querySelector('input[name="groupIds"]');
|
const groupIdsInput = searchForm.querySelector('input[name="groupIds"]');
|
||||||
|
const pageInput = searchForm.querySelector('input[name="page"]');
|
||||||
const searchInput = searchForm.querySelector('input[name="keyword"]');
|
const searchInput = searchForm.querySelector('input[name="keyword"]');
|
||||||
|
|
||||||
// API Card Click Handlers
|
// API Card Click Handlers
|
||||||
@@ -230,6 +244,9 @@
|
|||||||
groupIdsInput.value = '';
|
groupIdsInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 카테고리 변경 시 1페이지로 리셋
|
||||||
|
if (pageInput) pageInput.value = '1';
|
||||||
|
|
||||||
// Submit form
|
// Submit form
|
||||||
searchForm.submit();
|
searchForm.submit();
|
||||||
});
|
});
|
||||||
@@ -239,6 +256,7 @@
|
|||||||
searchInput.addEventListener('keypress', function (e) {
|
searchInput.addEventListener('keypress', function (e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (pageInput) pageInput.value = '1';
|
||||||
searchForm.submit();
|
searchForm.submit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -238,10 +238,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="card-illustration">
|
<div class="card-illustration">
|
||||||
<!-- 서비스별 아이콘 매핑 - base64 인코딩된 이미지 사용 -->
|
<!-- 서비스별 아이콘: base64 인라인 대신 그룹 id 기준 스트리밍 엔드포인트(캐시 가능) 사용 -->
|
||||||
<img
|
<img th:src="@{/api-services/{id}/icon(id=${service.id})}" th:alt="${service.groupName}">
|
||||||
th:src="${service.mainIcon != null and !#strings.isEmpty(service.mainIcon)} ? ${service.mainIcon} : @{/img/api_icon_default.png}"
|
|
||||||
th:alt="${service.groupName}">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,9 +558,6 @@
|
|||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
<!-- 메인 페이지 전용 스크립트 모듈 추가 -->
|
|
||||||
<script th:src="@{/js/main.js}"></script>
|
|
||||||
|
|
||||||
<!-- 로그인 후 처리 스크립트 -->
|
<!-- 로그인 후 처리 스크립트 -->
|
||||||
<script th:src="@{/js/login-success-handler.js}"></script>
|
<script th:src="@{/js/login-success-handler.js}"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
th:classappend="${isInvited ? 'input-readonly' : ''}" maxlength="50" th:value="${domain}"
|
th:classappend="${isInvited ? 'input-readonly' : ''}" maxlength="50" th:value="${domain}"
|
||||||
th:readonly="${isInvited}" th:placeholder="#{portalUser.Register.domain}">
|
th:readonly="${isInvited}" th:placeholder="#{portalUser.Register.domain}">
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-action-primary md" th:classappend="${isInvited ? 'hidden' : ''}">
|
<button type="button" class="btn-action-primary md btn_check_email" th:classappend="${isInvited ? 'hidden' : ''}">
|
||||||
중복체크
|
중복체크
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
||||||
<body>
|
<body>
|
||||||
<th:block th:fragment="headerFragment(headerClass)">
|
<th:block th:fragment="headerFragment(headerClass)">
|
||||||
|
<!-- 활성 프로파일 표시(prod 제외). 데스크톱은 로고 옆 뱃지, 모바일/태블릿은 최상단 floating 바(env-badge 는 그때 숨김) -->
|
||||||
|
<div th:if="${activeProfileBadge != null}" class="env-strip" th:text="${activeProfileBadge}">dev</div>
|
||||||
<!-- Global Header Container -->
|
<!-- Global Header Container -->
|
||||||
<header class="global-header" th:classappend="${headerClass}">
|
<header class="global-header" th:classappend="${headerClass}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
<img src="/img/logo/logo-djb.png" alt="DJBank" class="mobile-logo">
|
<img src="/img/logo/logo-djb.png" alt="DJBank" class="mobile-logo">
|
||||||
</a>
|
</a>
|
||||||
<a th:href="@{/}" class="mobile-logo-text" style="font-size: 22px; font-weight: 700; color: #212529; text-decoration: none; margin-left: 8px; vertical-align: middle;">API Portal</a>
|
<a th:href="@{/}" class="mobile-logo-text" style="font-size: 22px; font-weight: 700; color: #212529; text-decoration: none; margin-left: 8px; vertical-align: middle;">API Portal</a>
|
||||||
|
<span th:if="${activeProfileBadge != null}" class="env-badge" th:text="${activeProfileBadge}">dev</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user