앱 관리
This commit is contained in:
@@ -4,18 +4,11 @@ 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.ApiService;
|
||||
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.apiservice.service.ApiServiceService;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.app.service.AppServiceFacade;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -28,11 +21,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RequiredArgsConstructor
|
||||
public class ApiAjaxController {
|
||||
|
||||
private final static String STG_SERVER = "STG";
|
||||
|
||||
private final ApiService apiService;
|
||||
private final ApiServiceService apiServiceService;
|
||||
private final AppServiceFacade appServiceFacade;
|
||||
|
||||
public Map<String, ApiServiceDTO> getMainIconsFromServiceDtos(List<ApiServiceDTO> apiServices) {
|
||||
Map<String, ApiServiceDTO> mainIconsMap = new HashMap<>();
|
||||
@@ -54,53 +44,15 @@ public class ApiAjaxController {
|
||||
Map<String, ApiServiceDTO> mainIconsMap = getMainIconsFromServiceDtos(apiServices);
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = apiService.findApis();
|
||||
|
||||
for (ApiSpecInfoDto spec : apiSpecInfoDtos) {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
if (serviceDTO != null) {
|
||||
String mainIcon = serviceDTO.getMainIcon();
|
||||
String service = serviceDTO.getGroupName();
|
||||
spec.setMainIcon(mainIcon);
|
||||
spec.setService(service);
|
||||
}
|
||||
}
|
||||
return apiSpecInfoDtos;
|
||||
}
|
||||
|
||||
@GetMapping("/for_request_prod")
|
||||
@Secured("ROLE_API_KEY_REQUEST")
|
||||
public List<ApiSpecInfoDto> apiRequestProd(@ModelAttribute ApiGroupSearch search) {
|
||||
|
||||
List<ApiServiceDTO> apiServices = apiServiceService.searchApiGroups(search);
|
||||
Map<String, ApiServiceDTO> mainIconsMap = getMainIconsFromServiceDtos(apiServices);
|
||||
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
|
||||
List<ClientDTO> apikeys = appServiceFacade.getApiKeyList(user.getPortalOrg(), STG_SERVER);
|
||||
|
||||
List<ApiServiceApiDTO> apis = apikeys.stream()
|
||||
.flatMap(client -> client.getApiList().stream()) // flatten all apiLists into a single stream of ApiServiceApiDTO
|
||||
.distinct() // remove duplicates
|
||||
.collect(Collectors.toList()); // collect to List
|
||||
|
||||
List<ApiSpecInfoDto> apiSpecInfoDtos = new ArrayList<>();
|
||||
|
||||
apis.forEach(api -> {
|
||||
ApiSpecInfoDto specInfoDto = new ApiSpecInfoDto();
|
||||
specInfoDto.setApiId(api.getApiId());
|
||||
specInfoDto.setApiName(api.getApiDesc());
|
||||
apiSpecInfoDtos.add(specInfoDto);
|
||||
});
|
||||
|
||||
for (ApiSpecInfoDto spec : apiSpecInfoDtos) {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
if (serviceDTO != null) {
|
||||
String mainIcon = serviceDTO.getMainIcon();
|
||||
String service = serviceDTO.getGroupName();
|
||||
spec.setMainIcon(mainIcon);
|
||||
spec.setService(service);
|
||||
}
|
||||
}
|
||||
return apiSpecInfoDtos;
|
||||
// Filter APIs that have matching service information
|
||||
return apiSpecInfoDtos.stream()
|
||||
.filter(spec -> mainIconsMap.containsKey(spec.getApiId()))
|
||||
.peek(spec -> {
|
||||
ApiServiceDTO serviceDTO = mainIconsMap.get(spec.getApiId());
|
||||
spec.setMainIcon(serviceDTO.getMainIcon());
|
||||
spec.setService(serviceDTO.getGroupName());
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.eactive.apim.portal.apps.apis.controller;
|
||||
|
||||
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSearchData;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
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.dto.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package com.eactive.apim.portal.apps.app.controller;
|
||||
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSearchData;
|
||||
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
|
||||
import com.eactive.apim.portal.apps.app.dto.AppRequestDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientSimpleDTO;
|
||||
import com.eactive.apim.portal.apps.app.mapper.CredentialMapper;
|
||||
import com.eactive.apim.portal.apps.app.service.AppServiceFacade;
|
||||
import com.eactive.apim.portal.common.dto.ResponseDTO;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.EncryptionUtil;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.ValidationUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/myapikey")
|
||||
@RequiredArgsConstructor
|
||||
@Secured("ROLE_API_KEY_REQUEST")
|
||||
public class ApiKeyRestController {
|
||||
|
||||
private final static String PROD_SERVER = "PROD";
|
||||
private final static String STG_SERVER = "STG";
|
||||
|
||||
private final AppServiceFacade appServiceFacade;
|
||||
private final CredentialMapper credentialMapper;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
|
||||
@PostMapping("/api_key_request")
|
||||
public ResponseDTO apiRequest(@RequestBody AppRequestDTO appRequest) {
|
||||
appRequest.setOrg(SecurityUtil.getUserOrg());
|
||||
if (appRequest.getType() == null) {
|
||||
appRequest.setType(AppRequestType.NEW);
|
||||
}
|
||||
AppRequestDTO appRequestDTO = appServiceFacade.createAppRequest(appRequest);
|
||||
appServiceFacade.beginApproval(appRequestDTO.getApproval().getId());
|
||||
return new ResponseDTO(200, "SUCCESS", appRequestDTO.getType().getDescription() + " 신청이 완료되었습니다.");
|
||||
}
|
||||
|
||||
@PostMapping("/api_key_request_prod")
|
||||
public ResponseDTO requestProdApiKey(@RequestBody AppRequestDTO appRequest) {
|
||||
appRequest.setOrg(SecurityUtil.getUserOrg());
|
||||
|
||||
if (appRequest.getClientId() != null && !appRequest.getClientId().isEmpty()) {
|
||||
try {
|
||||
appRequest.setClientId(encryptionUtil.decrypt(appRequest.getClientId()));
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
return new ResponseDTO(400, "FAILURE", "API 키 정보가 올바르지 않습니다.");
|
||||
}
|
||||
appRequest.setType(AppRequestType.PROD_MODIFY);
|
||||
} else {
|
||||
appRequest.setType(AppRequestType.PROD_NEW);
|
||||
}
|
||||
|
||||
AppRequestDTO appRequestDTO = appServiceFacade.createAppRequest(appRequest);
|
||||
appServiceFacade.beginApproval(appRequestDTO.getApproval().getId());
|
||||
|
||||
return new ResponseDTO(200, "SUCCESS", appRequestDTO.getType().getDescription() + " 신청이 완료되었습니다.");
|
||||
}
|
||||
|
||||
@GetMapping("/dev")
|
||||
public List<ClientSimpleDTO> apiKeyList() {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
List<ClientDTO> apikeys = appServiceFacade.getApiKeyList(user.getPortalOrg(), STG_SERVER);
|
||||
|
||||
ApiSearchData apiSearchData = apiSearchFacade.loadAllApiData();
|
||||
for (ClientDTO apikey : apikeys) {
|
||||
apikey.getApiList().forEach(api -> {
|
||||
api.setService(apiSearchData.getServicesByApiId().get(api.getApiId()).getGroupName());
|
||||
});
|
||||
}
|
||||
|
||||
return apikeys.stream().map(credentialMapper::mapToClientSimpleDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/prod")
|
||||
public ClientSimpleDTO apiKeyProdList(@RequestBody AppRequestDTO appRequest) {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
|
||||
try {
|
||||
ClientDTO apiKey = appServiceFacade.getApiKey(user.getPortalOrg().getId(), encryptionUtil.decrypt(appRequest.getClientId()), PROD_SERVER);
|
||||
ApiSearchData apiSearchData = apiSearchFacade.loadAllApiData();
|
||||
apiKey.getApiList().forEach(api -> {
|
||||
api.setService(apiSearchData.getServicesByApiId().get(api.getApiId()).getGroupName());
|
||||
});
|
||||
apiKey.setClientid(encryptionUtil.encrypt(apiKey.getClientid()));
|
||||
return credentialMapper.mapToClientSimpleDTO(apiKey);
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
return new ClientSimpleDTO();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
package com.eactive.apim.portal.apps.app.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* API Key Registration Session Data
|
||||
* Holds data across multiple registration steps
|
||||
*/
|
||||
@Data
|
||||
public class ApiKeyRegistrationDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// Client ID for modification operations
|
||||
private String clientId;
|
||||
|
||||
// Request type for tracking operation type
|
||||
private String requestType; // "NEW" or "MODIFY"
|
||||
|
||||
// Existing app icon file ID (for modifications)
|
||||
private String appIconFileId;
|
||||
|
||||
// Step 1: Basic Information
|
||||
// Transient field for file upload (not serialized in session)
|
||||
private transient MultipartFile appIcon;
|
||||
|
||||
// Persisted file data
|
||||
private String appIconFileName;
|
||||
private byte[] appIconData;
|
||||
private String appIconContentType;
|
||||
|
||||
@NotBlank(message = "앱 이름을 입력해주세요.")
|
||||
@Size(max = 100, message = "앱 이름은 100자를 초과할 수 없습니다.")
|
||||
private String appName;
|
||||
|
||||
@NotBlank(message = "앱 설명을 입력해주세요.")
|
||||
@Size(max = 500, message = "앱 설명은 500자를 초과할 수 없습니다.")
|
||||
private String appDescription;
|
||||
|
||||
// Optional field - allows empty string or valid URL with http(s):// protocol
|
||||
@Pattern(regexp = "^$|^https?://.*", message = "올바른 URL 형식이 아닙니다.")
|
||||
private String callbackUrl;
|
||||
|
||||
private List<String> ipWhitelist = new ArrayList<>();
|
||||
|
||||
// Step 2: API Selection
|
||||
private List<String> selectedApis = new ArrayList<>();
|
||||
|
||||
// Helper method to add IP to whitelist
|
||||
public void addIpAddress(String ip) {
|
||||
if (ip != null && !ip.trim().isEmpty() && !ipWhitelist.contains(ip)) {
|
||||
ipWhitelist.add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to set IP whitelist from comma-separated string
|
||||
public void setIpWhitelistFromString(String ipString) {
|
||||
ipWhitelist.clear();
|
||||
if (ipString != null && !ipString.trim().isEmpty()) {
|
||||
String[] ips = ipString.split(",");
|
||||
for (String ip : ips) {
|
||||
String trimmedIp = ip.trim();
|
||||
if (!trimmedIp.isEmpty()) {
|
||||
ipWhitelist.add(trimmedIp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get IP whitelist as comma-separated string
|
||||
public String getIpWhitelistAsString() {
|
||||
return String.join(",", ipWhitelist);
|
||||
}
|
||||
|
||||
// Helper method to get app icon as base64 data URI for image display
|
||||
public String getAppIconBase64() {
|
||||
if (appIconData == null || appIconData.length == 0) {
|
||||
return null;
|
||||
}
|
||||
String base64Data = Base64.getEncoder().encodeToString(appIconData);
|
||||
String contentType = appIconContentType != null ? appIconContentType : "image/png";
|
||||
return "data:" + contentType + ";base64," + base64Data;
|
||||
}
|
||||
|
||||
// Validation helpers
|
||||
public boolean isStep1Complete() {
|
||||
// callbackUrl is optional, so we only check required fields
|
||||
// For modifications, appIconFileId indicates an existing icon
|
||||
boolean hasIcon = (appIconData != null && appIconData.length > 0)
|
||||
|| (appIconFileId != null && !appIconFileId.trim().isEmpty());
|
||||
return hasIcon &&
|
||||
appName != null && !appName.trim().isEmpty() &&
|
||||
appDescription != null && !appDescription.trim().isEmpty();
|
||||
}
|
||||
|
||||
public boolean isStep2Complete() {
|
||||
return selectedApis != null && !selectedApis.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return isStep1Complete() && isStep2Complete();
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,14 @@ public class AppRequestDTO {
|
||||
|
||||
private String reason;
|
||||
|
||||
private String appDescription;
|
||||
|
||||
private String callbackUrl;
|
||||
|
||||
private String ipWhitelist;
|
||||
|
||||
private String appIconFileId;
|
||||
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
private String prevApiList;
|
||||
|
||||
@@ -45,6 +45,10 @@ public class ClientDTO {
|
||||
|
||||
private String appstatus;
|
||||
|
||||
private String appDescription;
|
||||
|
||||
private String appIconFileId;
|
||||
|
||||
private List<ApiServiceApiDTO> apiList = new ArrayList<>();
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package com.eactive.apim.portal.apps.app.mapper;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||
import com.eactive.apim.portal.apps.apiservice.mapper.ApiServiceApiMapper;
|
||||
import com.eactive.apim.portal.apps.app.dto.AppRequestDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.approval.mapper.ApprovalMapper;
|
||||
import com.eactive.apim.portal.apps.user.mapper.PortalOrgMapper;
|
||||
import com.eactive.eai.data.mapper.BaseMapperConfig;
|
||||
@@ -13,4 +14,53 @@ import org.mapstruct.Mapper;
|
||||
@Mapper(config = BaseMapperConfig.class, uses = {PortalOrgMapper.class, ApiServiceApiMapper.class, ApprovalMapper.class})
|
||||
public interface AppRequestMapper extends GenericMapper<AppRequestDTO, AppRequest> {
|
||||
|
||||
/**
|
||||
* AppRequest를 ClientDTO로 간단하게 매핑합니다.
|
||||
* 뷰에서 AppRequest를 ClientDTO와 동일한 형식으로 표시하기 위해 사용됩니다.
|
||||
*
|
||||
* @param appRequest 앱 요청 엔티티
|
||||
* @return ClientDTO 표시용 DTO
|
||||
*/
|
||||
default ClientDTO mapToClientDTO(AppRequest appRequest) {
|
||||
if (appRequest == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClientDTO dto = new ClientDTO();
|
||||
|
||||
// 기본 식별 정보
|
||||
dto.setClientid(appRequest.getId()); // AppRequest ID를 임시 client ID로 사용
|
||||
dto.setClientname(appRequest.getClientName());
|
||||
|
||||
// 앱 상세 정보
|
||||
dto.setAppDescription(appRequest.getAppDescription());
|
||||
dto.setAppIconFileId(appRequest.getAppIconFileId());
|
||||
|
||||
// 네트워크 설정
|
||||
dto.setRedirecturi(appRequest.getCallbackUrl());
|
||||
dto.setAllowedips(appRequest.getIpWhitelist());
|
||||
|
||||
// 상태 정보 (ApprovalStatus → appstatus 매핑)
|
||||
if (appRequest.getStatus() != null) {
|
||||
switch (appRequest.getStatus()) {
|
||||
case APPROVED:
|
||||
dto.setAppstatus("1"); // 승인됨 = 활성화
|
||||
break;
|
||||
case DENIED:
|
||||
dto.setAppstatus("0"); // 거부됨 = 비활성화
|
||||
break;
|
||||
default:
|
||||
dto.setAppstatus("2"); // 대기중/진행중 = 보류
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 수정 날짜 (createdDate를 modifiedon으로 사용)
|
||||
dto.setModifiedon(appRequest.getCreatedDate());
|
||||
|
||||
// API 목록은 별도 처리 필요 (콤마로 구분된 문자열 → List<ApiServiceApiDTO>)
|
||||
// 필요시 컨트롤러에서 추가 처리
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.eactive.apim.portal.apps.app.mapper;
|
||||
|
||||
import com.eactive.apim.portal.app.entity.ProdClient;
|
||||
import com.eactive.apim.portal.apps.apiservice.mapper.ApiSpecInfoMapper;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.eai.data.mapper.BaseMapperConfig;
|
||||
import com.eactive.eai.data.mapper.GenericMapper;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(config = BaseMapperConfig.class, uses = {ApiSpecInfoMapper.class})
|
||||
public interface ProdClientMapper extends GenericMapper<ClientDTO, ProdClient> {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -7,12 +7,14 @@ import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.Approval;
|
||||
import com.eactive.apim.portal.approval.statemachine.ProcessingState;
|
||||
import com.eactive.apim.portal.approval.statemachine.RequestedState;
|
||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||
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.ApiServiceDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceApiDTO;
|
||||
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
|
||||
import com.eactive.apim.portal.apps.app.dto.ApiKeyRegistrationDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.AppRequestDTO;
|
||||
import com.eactive.apim.portal.apps.app.dto.ClientDTO;
|
||||
import com.eactive.apim.portal.apps.app.mapper.AppRequestMapper;
|
||||
@@ -21,16 +23,24 @@ import com.eactive.apim.portal.apps.approval.service.ApprovalService;
|
||||
import com.eactive.apim.portal.apps.user.dto.PortalOrgDTO;
|
||||
import com.eactive.apim.portal.apps.user.mapper.PortalOrgMapper;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.ApiServiceHelper;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -47,53 +57,64 @@ public class AppServiceFacade {
|
||||
private final AppRequestMapper appRequestMapper;
|
||||
private final PortalOrgMapper portalOrgMapper;
|
||||
private final ApiServiceHelper apiServiceHelper;
|
||||
private final FileService fileService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public Page<ClientDTO> getApiKeyPageForServer(Pageable pageable, PortalOrg portalOrg, String server) {
|
||||
Page<Credential> clients = credentialRepository.findAllByOrgidAndServer(pageable, portalOrg.getId(), server);
|
||||
return clients.map(credentialMapper::toVo);
|
||||
}
|
||||
public List<ClientDTO> getApikeyList(PortalOrg portalOrg) {
|
||||
|
||||
public List<ClientDTO> getApiKeyList(PortalOrg portalOrg, String server) {
|
||||
List<Credential> clients = credentialRepository.findAllByOrgidAndServer(portalOrg.getId(), server);
|
||||
List<Credential> clients = credentialRepository.findAllByOrgid(portalOrg.getId());
|
||||
return clients.stream().map(credentialMapper::toVo).collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
public ClientDTO getApiKey(String orgid, String clientId, String server) {
|
||||
return credentialRepository.findByClientidAndOrgidAndServer(clientId, orgid, server).map(credentialMapper::toVo).orElse(null);
|
||||
public List<AppRequest> getPendingApiKeyList(PortalOrg portalOrg) {
|
||||
List<AppRequest> appRequests = appRequestRepository.findAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(portalOrg, Arrays.asList(AppRequestType.NEW, AppRequestType.MODIFY, AppRequestType.DELETE),
|
||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||
|
||||
return appRequests;
|
||||
}
|
||||
|
||||
public ClientDTO getApiKey(String orgid, String clientId) {
|
||||
return credentialRepository.findByClientidAndOrgid(clientId, orgid).map(credentialMapper::toVo).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AppRequest ID와 조직 정보로 AppRequest를 조회합니다.
|
||||
* API 그룹 및 API 목록 정보를 함께 처리하여 반환합니다.
|
||||
*
|
||||
* @param id AppRequest ID
|
||||
* @param portalOrg 포털 조직
|
||||
* @return AppRequest DTO (없으면 null)
|
||||
*/
|
||||
public AppRequestDTO getAppRequestById(String id, PortalOrg portalOrg) {
|
||||
return getApiRequestByIdAndPortalOrg(id, portalOrg);
|
||||
}
|
||||
|
||||
|
||||
public Page<AppRequestDTO> getApiRequestHistory(Pageable pageable, PortalOrgDTO portalOrg) {
|
||||
return appRequestRepository.findAllByOrgAndTypeIsIn(pageable, portalOrgMapper.toEntity(portalOrg), Arrays.asList(AppRequestType.NEW,AppRequestType.MODIFY, AppRequestType.DELETE)).map(appRequestMapper::toVo);
|
||||
}
|
||||
|
||||
public Page<AppRequestDTO> getApiRequestProdHistory(Pageable pageable, PortalOrgDTO portalOrg) {
|
||||
return appRequestRepository.findAllByOrgAndTypeIsIn(pageable, portalOrgMapper.toEntity(portalOrg), Arrays.asList(AppRequestType.PROD_NEW,AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE)).map(appRequestMapper::toVo);
|
||||
return appRequestRepository.findAllByOrgAndTypeIsIn(pageable, portalOrgMapper.toEntity(portalOrg), Arrays.asList(AppRequestType.NEW, AppRequestType.MODIFY, AppRequestType.DELETE)).map(appRequestMapper::toVo);
|
||||
}
|
||||
|
||||
public AppRequestDTO createAppRequest(AppRequestDTO appRequest) {
|
||||
|
||||
setApiGroupsFromApiList(appRequest);
|
||||
AppRequest request = appRequestRepository.save(appRequestMapper.toEntity(appRequest));
|
||||
|
||||
if (request.getType() != null && (request.getType().equals(AppRequestType.MODIFY) || request.getType().equals(AppRequestType.DELETE) ||
|
||||
request.getType().equals(AppRequestType.PROD_MODIFY) || request.getType().equals(AppRequestType.PROD_DELETE))) {
|
||||
if (request.getType() != null && (request.getType().equals(AppRequestType.MODIFY) || request.getType().equals(AppRequestType.DELETE))) {
|
||||
|
||||
Optional<Credential> client = credentialRepository.findById(request.getClientId());
|
||||
|
||||
if (client.isPresent()) {
|
||||
request.setClientName(client.get().getClientname());
|
||||
|
||||
if(client.get().getApiList() != null) {
|
||||
if (client.get().getApiList() != null) {
|
||||
// 기존 API 목록을 저장
|
||||
String existingApiList = client.get().getApiList().stream()
|
||||
.map(ApiSpecInfo::getApiId)
|
||||
.collect(Collectors.joining(","));
|
||||
.map(ApiSpecInfo::getApiId)
|
||||
.collect(Collectors.joining(","));
|
||||
request.setPrevApiList(existingApiList);
|
||||
}
|
||||
|
||||
if(AppRequestType.DELETE.equals(request.getType()) || AppRequestType.PROD_DELETE.equals(request.getType())) {
|
||||
if (AppRequestType.DELETE.equals(request.getType())) {
|
||||
request.setApiList(request.getPrevApiList());
|
||||
}
|
||||
} else {
|
||||
@@ -108,22 +129,6 @@ public class AppServiceFacade {
|
||||
return appRequestMapper.toVo(request);
|
||||
}
|
||||
|
||||
private void setApiGroupsFromApiList(AppRequestDTO appRequest) {
|
||||
if (StringUtils.isEmpty(appRequest.getApiList())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> uniqueGroupIds = Arrays.stream(appRequest.getApiList().split(","))
|
||||
.map(apiId -> apiServiceService.findApiServiceByApiId(apiId))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(apiService -> apiService.getApiGroupApiList().stream())
|
||||
.map(ApiServiceApiDTO::getApiGroupId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
appRequest.setApiGroupList(String.join(",", uniqueGroupIds));
|
||||
}
|
||||
|
||||
|
||||
public void beginApproval(String approvalId) {
|
||||
approvalService.beginApproval(approvalId);
|
||||
}
|
||||
@@ -132,27 +137,11 @@ public class AppServiceFacade {
|
||||
appRequestRepository.findByIdAndOrg(id, portalOrg).ifPresent(approvalService::cancelAppApproval);
|
||||
}
|
||||
|
||||
public List<AppRequestDTO> getProdApiRequestHistory(String clientid) {
|
||||
return appRequestRepository.findAllByClientIdsContainsAndTypeIsIn(clientid,Arrays.asList(AppRequestType.PROD_NEW,AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE)).stream().map(appRequestMapper::toVo).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public AppRequestDTO getApiRequestProdByIdAndPortalOrg(String id, PortalOrg portalOrg) {
|
||||
AppRequestDTO appRequest = appRequestRepository.findByIdAndOrgAndTypeIsIn(id, portalOrg,
|
||||
Arrays.asList(AppRequestType.PROD_NEW,AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE)).map(this::convertToAppRequestDTO).orElse(null);
|
||||
|
||||
if(appRequest != null) {
|
||||
processApiGroups(appRequest);
|
||||
processApiList(appRequest);
|
||||
}
|
||||
return appRequest;
|
||||
}
|
||||
|
||||
public AppRequestDTO getApiRequestByIdAndPortalOrg(String id, PortalOrg org) {
|
||||
AppRequestDTO appRequest = appRequestRepository.findByIdAndOrg(id, org).map(this::convertToAppRequestDTO).orElse(null);
|
||||
|
||||
if(appRequest != null) {
|
||||
processApiGroups(appRequest);
|
||||
if (appRequest != null) {
|
||||
processApiList(appRequest);
|
||||
}
|
||||
return appRequest;
|
||||
@@ -167,16 +156,6 @@ public class AppServiceFacade {
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void processApiGroups(AppRequestDTO appRequest) {
|
||||
|
||||
if (!StringUtils.isEmpty(appRequest.getApiGroupList())) {
|
||||
String[] apiGroupList = appRequest.getApiGroupList().split(",");
|
||||
for (String apiGroupId : apiGroupList) {
|
||||
appRequest.getApiServiceDTOList().add(apiServiceService.getApiGroupById(apiGroupId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processApiList(AppRequestDTO appRequest) {
|
||||
|
||||
if (!StringUtils.isEmpty(appRequest.getApiList())) {
|
||||
@@ -192,4 +171,140 @@ public class AppServiceFacade {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiKeyRegistrationDTO를 기반으로 AppRequest를 생성합니다.
|
||||
*
|
||||
* @param registration API Key 등록 정보
|
||||
* @param portalOrg 포털 조직
|
||||
* @return 생성된 AppRequest DTO
|
||||
* @throws IOException 파일 처리 중 오류 발생 시
|
||||
*/
|
||||
public AppRequestDTO createAppRequestFromRegistration(ApiKeyRegistrationDTO registration, PortalOrg portalOrg) throws IOException {
|
||||
// 1. 앱 아이콘 파일 저장
|
||||
String appIconFileId = null;
|
||||
|
||||
if (registration.getAppIconData() != null && registration.getAppIconData().length > 0) {
|
||||
FileInfo fileInfo = fileService.createSingleFileFromBytes(registration.getAppIconFileName(), registration.getAppIconContentType(), registration.getAppIconData());
|
||||
appIconFileId = fileInfo.getFileId();
|
||||
}
|
||||
|
||||
// 2. AppRequestDTO 생성 및 필드 매핑
|
||||
AppRequestDTO appRequestDTO = new AppRequestDTO();
|
||||
appRequestDTO.setOrg(portalOrgMapper.toVo(portalOrg));
|
||||
appRequestDTO.setType(AppRequestType.NEW);
|
||||
|
||||
// 기본 정보 매핑
|
||||
appRequestDTO.setClientName(registration.getAppName());
|
||||
appRequestDTO.setAppDescription(registration.getAppDescription());
|
||||
appRequestDTO.setCallbackUrl(registration.getCallbackUrl());
|
||||
appRequestDTO.setAppIconFileId(appIconFileId);
|
||||
|
||||
// IP 화이트리스트 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getIpWhitelist() != null && !registration.getIpWhitelist().isEmpty()) {
|
||||
appRequestDTO.setIpWhitelist(String.join(",", registration.getIpWhitelist()));
|
||||
}
|
||||
|
||||
// 선택된 API 목록 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getSelectedApis() != null && !registration.getSelectedApis().isEmpty()) {
|
||||
appRequestDTO.setApiList(String.join(",", registration.getSelectedApis()));
|
||||
}
|
||||
|
||||
// 3. 기존 createAppRequest 메소드 호출하여 승인 프로세스 시작
|
||||
return createAppRequest(appRequestDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiKeyRegistrationDTO를 기반으로 수정 AppRequest를 생성합니다.
|
||||
*
|
||||
* @param registration API Key 수정 정보
|
||||
* @param portalOrg 포털 조직
|
||||
* @return 생성된 AppRequest DTO
|
||||
* @throws IOException 파일 처리 중 오류 발생 시
|
||||
*/
|
||||
public AppRequestDTO createModifyRequestFromRegistration(ApiKeyRegistrationDTO registration, PortalOrg portalOrg) throws IOException {
|
||||
// 1. 기존 Credential 정보 조회
|
||||
ClientDTO existingClient = getApiKey(portalOrg.getId(), registration.getClientId());
|
||||
if (existingClient == null) {
|
||||
throw new NotFoundException("Client not found: " + registration.getClientId());
|
||||
}
|
||||
|
||||
// 2. 앱 아이콘 파일 저장 (변경된 경우)
|
||||
String appIconFileId = null;
|
||||
|
||||
// 새로운 아이콘 데이터가 있으면 저장
|
||||
if (registration.getAppIconData() != null && registration.getAppIconData().length > 0) {
|
||||
FileInfo fileInfo = fileService.createSingleFileFromBytes(registration.getAppIconFileName(), registration.getAppIconContentType(), registration.getAppIconData());
|
||||
appIconFileId = fileInfo.getFileId();
|
||||
}
|
||||
// 새로운 아이콘이 없으면 기존 아이콘 ID 사용 (registration에서 가져오거나 기존 클라이언트에서 가져옴)
|
||||
else if (registration.getAppIconFileId() != null) {
|
||||
appIconFileId = registration.getAppIconFileId();
|
||||
} else {
|
||||
appIconFileId = existingClient.getAppIconFileId();
|
||||
}
|
||||
|
||||
// 3. AppRequestDTO 생성 및 필드 매핑
|
||||
AppRequestDTO appRequestDTO = new AppRequestDTO();
|
||||
appRequestDTO.setOrg(portalOrgMapper.toVo(portalOrg));
|
||||
appRequestDTO.setType(AppRequestType.MODIFY);
|
||||
appRequestDTO.setClientId(registration.getClientId()); // 수정할 클라이언트 ID 설정
|
||||
|
||||
// 기본 정보 매핑
|
||||
appRequestDTO.setClientName(registration.getAppName());
|
||||
appRequestDTO.setAppDescription(registration.getAppDescription());
|
||||
appRequestDTO.setCallbackUrl(registration.getCallbackUrl());
|
||||
appRequestDTO.setAppIconFileId(appIconFileId);
|
||||
|
||||
// IP 화이트리스트 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getIpWhitelist() != null && !registration.getIpWhitelist().isEmpty()) {
|
||||
appRequestDTO.setIpWhitelist(String.join(",", registration.getIpWhitelist()));
|
||||
}
|
||||
|
||||
// 선택된 API 목록 매핑 (List<String> → comma-separated String)
|
||||
if (registration.getSelectedApis() != null && !registration.getSelectedApis().isEmpty()) {
|
||||
appRequestDTO.setApiList(String.join(",", registration.getSelectedApis()));
|
||||
}
|
||||
|
||||
// 4. 기존 createAppRequest 메소드 호출하여 승인 프로세스 시작
|
||||
// createAppRequest 내부에서 MODIFY 타입인 경우 기존 API 목록을 prevApiList에 저장함
|
||||
return createAppRequest(appRequestDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 비밀번호를 검증합니다.
|
||||
* Client Secret 조회 등 민감한 작업 수행 시 본인 확인을 위해 사용됩니다.
|
||||
*
|
||||
* @param user 인증된 사용자 정보
|
||||
* @param password 검증할 비밀번호 (평문)
|
||||
* @return 비밀번호 일치 여부
|
||||
*/
|
||||
public boolean verifyUserPassword(PortalAuthenticatedUser user, String password) {
|
||||
if (user == null || password == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// PasswordEncoder를 사용하여 평문 비밀번호와 저장된 해시를 비교
|
||||
return passwordEncoder.matches(password, user.getPassword());
|
||||
} catch (Exception e) {
|
||||
// 비밀번호 검증 중 오류 발생 시 false 반환 (보안상 구체적인 오류 노출 방지)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key(Credential)를 즉시 삭제합니다.
|
||||
* 승인 프로세스 없이 바로 삭제 처리됩니다.
|
||||
*
|
||||
* @param orgId 조직 ID
|
||||
* @param clientId 삭제할 클라이언트 ID
|
||||
* @throws NotFoundException 클라이언트를 찾을 수 없는 경우
|
||||
*/
|
||||
public void deleteApp(String orgId, String clientId) {
|
||||
Credential credential = credentialRepository.findByClientidAndOrgid(clientId, orgId)
|
||||
.orElseThrow(() -> new NotFoundException("Client not found: " + clientId));
|
||||
|
||||
credentialRepository.delete(credential);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -4,7 +4,6 @@ import com.eactive.apim.portal.apps.dashboard.service.DashboardService;
|
||||
import com.eactive.apim.portal.common.pagerouter.PageHandler;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -22,9 +21,7 @@ public class DashboardHandler implements PageHandler {
|
||||
if (SecurityUtil.getPortalAuthenticatedUser() != null && SecurityUtil.getPortalAuthenticatedUser().getPortalOrg() != null) {
|
||||
PortalOrg org = SecurityUtil.getPortalAuthenticatedUser().getPortalOrg();
|
||||
model.addAttribute("pendingKeyCount", dashboardService.getPendingApiKeyRequestCount(org));
|
||||
model.addAttribute("testKeyCount", dashboardService.getTestApiKeyCount(org));
|
||||
model.addAttribute("pendingProdCount", dashboardService.getProdApiKeyRequestCount(org));
|
||||
model.addAttribute("prodKeyCount", dashboardService.getProdApiKeyCount(org));
|
||||
model.addAttribute("testKeyCount", dashboardService.getApiKeyCount(org));
|
||||
|
||||
model.addAttribute("totalApi", dashboardService.getTotalApi(org));
|
||||
model.addAttribute("dailyTotal", dashboardService.getDailyTotalApiUsage(org));
|
||||
|
||||
@@ -5,24 +5,18 @@ import com.eactive.apim.portal.app.entity.Credential;
|
||||
import com.eactive.apim.portal.app.repository.CredentialRepository;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.ApprovalStatus;
|
||||
import com.eactive.apim.portal.approval.statemachine.ProcessingState;
|
||||
import com.eactive.apim.portal.approval.statemachine.RequestedState;
|
||||
import com.eactive.apim.portal.apps.approval.service.ApprovalService;
|
||||
import com.eactive.apim.portal.apps.dashboard.ApiDailyStatistics;
|
||||
import com.eactive.apim.portal.apps.dashboard.repository.PortalStatisticsRepository;
|
||||
import com.eactive.apim.portal.apps.dashboard.repository.PortalStatisticsRepository.TopApiUsage;
|
||||
import com.eactive.apim.portal.common.util.DateUtil;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -39,15 +33,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class DashboardService {
|
||||
|
||||
private final static String PROD_SERVER = "PROD";
|
||||
private final static String STG_SERVER = "STG";
|
||||
|
||||
private final AppRequestRepository appRequestRepository;
|
||||
private final CredentialRepository credentialRepository;
|
||||
private final PortalStatisticsRepository portalStatisticsRepository;
|
||||
|
||||
public int getTestApiKeyCount(PortalOrg org) {
|
||||
return credentialRepository.countAllByOrgidAndServer(org.getId(), STG_SERVER);
|
||||
public int getApiKeyCount(PortalOrg org) {
|
||||
return credentialRepository.countAllByOrgid(org.getId());
|
||||
}
|
||||
|
||||
public int getPendingApiKeyRequestCount(PortalOrg org) {
|
||||
@@ -55,18 +46,9 @@ public class DashboardService {
|
||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||
}
|
||||
|
||||
public int getProdApiKeyRequestCount(PortalOrg org) {
|
||||
return appRequestRepository.countAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(org, Arrays.asList(AppRequestType.PROD_NEW, AppRequestType.PROD_MODIFY, AppRequestType.PROD_DELETE),
|
||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||
}
|
||||
|
||||
public int getProdApiKeyCount(PortalOrg org) {
|
||||
return credentialRepository.findAllByOrgidAndServer(org.getId(), PROD_SERVER).size();
|
||||
}
|
||||
|
||||
public int getTotalApi(PortalOrg org) {
|
||||
|
||||
List<Credential> credentials = credentialRepository.findAllByOrgidAndServer(org.getId(), PROD_SERVER);
|
||||
List<Credential> credentials = credentialRepository.findAllByOrgid(org.getId());
|
||||
return (int) credentials.stream()
|
||||
.flatMap(credential -> credential.getApiList().stream())
|
||||
.map(ApiSpecInfo::getApiId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
title.html=Kbank API Portal
|
||||
title.html=KJbank API Portal
|
||||
apim.route.register.file=\uCCA8\uBD80\uD30C\uC77C
|
||||
field.required=\uD544\uC218 \uC785\uB825\uD56D\uBAA9\uC785\uB2C8\uB2E4.
|
||||
companyName=\uD68C\uC0AC\uBA85
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -26,6 +26,133 @@ const customPopups = {
|
||||
// 이벤트 리스너 제거
|
||||
$(document).off('keydown.customAlert');
|
||||
},
|
||||
|
||||
/**
|
||||
* 비밀번호 입력 팝업 표시
|
||||
* @param {Object} options - 팝업 옵션
|
||||
* @param {string} options.title - 팝업 제목 (기본값: "비밀번호 입력")
|
||||
* @param {string} options.message - 안내 메시지 (기본값: "계속하려면 비밀번호를 입력해주세요.")
|
||||
* @param {Function} options.onConfirm - 확인 버튼 클릭 시 호출되는 콜백 (파라미터: password)
|
||||
* @param {Function} options.onCancel - 취소 버튼 클릭 시 호출되는 콜백 (선택사항)
|
||||
*/
|
||||
showPasswordInput: function (options) {
|
||||
options = options || {};
|
||||
|
||||
// 기본값 설정
|
||||
const title = options.title || '비밀번호 입력';
|
||||
const message = options.message || '계속하려면 비밀번호를 입력해주세요.';
|
||||
const onConfirm = options.onConfirm;
|
||||
const onCancel = options.onCancel;
|
||||
|
||||
// 팝업 내용 설정
|
||||
$('#passwordPopupTitle').text(title);
|
||||
$('#passwordPopupMessage').text(message);
|
||||
|
||||
// 입력 필드 및 에러 초기화
|
||||
$('#passwordPopupInput').val('').removeClass('error');
|
||||
$('#passwordPopupError').removeClass('show').text('');
|
||||
|
||||
// 팝업 표시 (modal 구조 사용)
|
||||
$('#passwordInputPopup').show();
|
||||
setTimeout(function() {
|
||||
$('#passwordModalBackdrop').addClass('show');
|
||||
$('#passwordModal').addClass('show');
|
||||
}, 10);
|
||||
|
||||
// Body 스크롤 방지
|
||||
$('body').css('overflow', 'hidden');
|
||||
|
||||
// 입력 필드에 포커스
|
||||
setTimeout(function() {
|
||||
$('#passwordPopupInput').focus();
|
||||
}, 350);
|
||||
|
||||
// 확인 버튼 이벤트 (기존 이벤트 제거 후 재등록)
|
||||
$('#passwordPopupConfirmButton').off('click').on('click', function () {
|
||||
const password = $('#passwordPopupInput').val().trim();
|
||||
|
||||
if (!password) {
|
||||
customPopups.showPasswordError('비밀번호를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm(password);
|
||||
}
|
||||
});
|
||||
|
||||
// 취소 버튼 이벤트
|
||||
$('#passwordPopupCancelButton').off('click').on('click', function () {
|
||||
customPopups.hidePasswordInput();
|
||||
if (typeof onCancel === 'function') {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
// 닫기 버튼 이벤트
|
||||
$('#passwordPopupCloseButton').off('click').on('click', function () {
|
||||
customPopups.hidePasswordInput();
|
||||
if (typeof onCancel === 'function') {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
// Enter 키 이벤트
|
||||
$('#passwordPopupInput').off('keypress').on('keypress', function (e) {
|
||||
if (e.which === 13) {
|
||||
$('#passwordPopupConfirmButton').click();
|
||||
}
|
||||
});
|
||||
|
||||
// 입력 시 에러 초기화
|
||||
$('#passwordPopupInput').off('input').on('input', function () {
|
||||
customPopups.clearPasswordError();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 비밀번호 입력 팝업 숨기기
|
||||
*/
|
||||
hidePasswordInput: function () {
|
||||
// Modal 숨김 애니메이션
|
||||
$('#passwordModalBackdrop').removeClass('show');
|
||||
$('#passwordModal').removeClass('show');
|
||||
|
||||
// 애니메이션 완료 후 숨김
|
||||
setTimeout(function() {
|
||||
$('#passwordInputPopup').hide();
|
||||
}, 300);
|
||||
|
||||
// Body 스크롤 복원
|
||||
$('body').css('overflow', '');
|
||||
|
||||
// 입력 필드 초기화
|
||||
$('#passwordPopupInput').val('').removeClass('error');
|
||||
$('#passwordPopupError').removeClass('show').text('');
|
||||
|
||||
// 이벤트 리스너 제거
|
||||
$('#passwordPopupConfirmButton').off('click');
|
||||
$('#passwordPopupCancelButton').off('click');
|
||||
$('#passwordPopupCloseButton').off('click');
|
||||
$('#passwordPopupInput').off('keypress').off('input');
|
||||
},
|
||||
|
||||
/**
|
||||
* 비밀번호 입력 에러 메시지 표시
|
||||
* @param {string} message - 에러 메시지
|
||||
*/
|
||||
showPasswordError: function (message) {
|
||||
$('#passwordPopupError').text(message).addClass('show');
|
||||
$('#passwordPopupInput').addClass('error');
|
||||
},
|
||||
|
||||
/**
|
||||
* 비밀번호 입력 에러 메시지 제거
|
||||
*/
|
||||
clearPasswordError: function () {
|
||||
$('#passwordPopupError').removeClass('show');
|
||||
$('#passwordPopupInput').removeClass('error');
|
||||
},
|
||||
showConfirm: function (message, callback) {
|
||||
$('#customConfirmMessage').text(message);
|
||||
$('#customConfirm').css('display', 'flex');
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// ============================================
|
||||
// Password Input Popup Component
|
||||
// Extends base modal styles from _modals.scss
|
||||
// ============================================
|
||||
|
||||
// Password popup specific styles (using modal structure)
|
||||
#passwordInputPopup {
|
||||
// Modal dialog size override
|
||||
.modal-dialog {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
// Password input group
|
||||
.pop_input_group {
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pop_input_field {
|
||||
width: 100%;
|
||||
padding: 12px $spacing-md;
|
||||
border: 1px solid $border-gray;
|
||||
border-radius: $border-radius-md;
|
||||
font-size: $font-size-sm;
|
||||
font-family: $font-family-primary;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary-blue;
|
||||
box-shadow: 0 0 0 3px rgba($primary-blue, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
// Error state
|
||||
&.error {
|
||||
border-color: $accent-orange;
|
||||
|
||||
&:focus {
|
||||
border-color: $accent-orange;
|
||||
box-shadow: 0 0 0 3px rgba($accent-orange, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: $accent-orange;
|
||||
font-size: $font-size-xs;
|
||||
margin-top: $spacing-xs;
|
||||
display: none;
|
||||
animation: fadeInDown 0.3s ease;
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation for error message
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 80px;
|
||||
max-width: 1280px;
|
||||
max-width: $container-max-width;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
@import 'components/navigation';
|
||||
@import 'components/partners';
|
||||
@import 'components/cta';
|
||||
@import 'components/password-popup';
|
||||
|
||||
// 5. Page-specific styles
|
||||
@import 'pages/index';
|
||||
@@ -36,6 +37,9 @@
|
||||
@import 'pages/api-market';
|
||||
@import 'pages/documentation';
|
||||
@import 'pages/login';
|
||||
@import 'pages/mypage';
|
||||
@import 'pages/apikey-register';
|
||||
@import 'pages/apikey-detail';
|
||||
|
||||
// 6. Themes
|
||||
@import 'themes/dark';
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
@charset "utf-8";
|
||||
|
||||
// ====================================
|
||||
// API Key Detail Pages Styles
|
||||
// For appRequestDetail.html and credentialDetail.html
|
||||
// ====================================
|
||||
|
||||
// Container Styles
|
||||
.apikey-detail-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: $spacing-4xl;
|
||||
padding-top: 0;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
padding: $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
.register-header {
|
||||
text-align: center;
|
||||
margin-bottom: $spacing-5xl;
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-4xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.header-description {
|
||||
font-size: $font-size-base;
|
||||
color: $text-gray;
|
||||
}
|
||||
}
|
||||
|
||||
// Status Section
|
||||
.status-section {
|
||||
text-align: center;
|
||||
margin-bottom: $spacing-4xl;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
border-radius: $border-radius-full;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: $font-size-sm;
|
||||
|
||||
&.badge-pending {
|
||||
background-color: #FFF3CD;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
&.badge-approved {
|
||||
background-color: #D4EDDA;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
&.badge-rejected {
|
||||
background-color: #F8D7DA;
|
||||
color: #721C24;
|
||||
}
|
||||
}
|
||||
|
||||
// Detail Section
|
||||
.detail-section {
|
||||
background: $white;
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-4xl;
|
||||
margin-bottom: $spacing-lg;
|
||||
box-shadow: $shadow-md;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
padding: $spacing-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin-bottom: $spacing-lg;
|
||||
padding-bottom: $spacing-md;
|
||||
border-bottom: 2px solid $border-gray;
|
||||
}
|
||||
|
||||
// Detail Grid
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-lg;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
|
||||
&.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-gray;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: $font-size-base;
|
||||
color: $text-dark;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
// App Icon
|
||||
.app-icon-display {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.app-icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 2px solid $border-gray;
|
||||
}
|
||||
|
||||
.app-icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $gray-bg;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 2px dashed $text-light;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
// IP List
|
||||
.ip-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.ip-tag {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-md;
|
||||
background-color: $light-bg;
|
||||
color: $secondary-blue;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-family: $font-family-mono;
|
||||
}
|
||||
|
||||
// Credential Code
|
||||
.credential-code {
|
||||
display: inline-block;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
background-color: $gray-bg;
|
||||
border: 1px solid $border-gray;
|
||||
border-radius: $border-radius-sm;
|
||||
font-family: $font-family-mono;
|
||||
font-size: $font-size-sm;
|
||||
color: $secondary-blue;
|
||||
}
|
||||
|
||||
// Secret Box
|
||||
.secret-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
background-color: $primary-blue;
|
||||
color: $white;
|
||||
border: none;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: $secondary-blue;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Status Indicator
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-xs $spacing-md;
|
||||
border-radius: $border-radius-xl;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
|
||||
&.status-active {
|
||||
background-color: #D4EDDA;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
background-color: #F8D7DA;
|
||||
color: #721C24;
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: $border-radius-circle;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
// API Detail List
|
||||
.api-detail-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.api-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
background: $white;
|
||||
border: 1px solid $border-gray;
|
||||
border-radius: $border-radius-md;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-blue;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
padding: $spacing-md;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.api-method {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-md;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-bold;
|
||||
font-family: $font-family-mono;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
|
||||
&.method-get {
|
||||
background-color: #D1FAE5;
|
||||
color: #065F46;
|
||||
}
|
||||
|
||||
&.method-post {
|
||||
background-color: #DBEAFE;
|
||||
color: #1E40AF;
|
||||
}
|
||||
|
||||
&.method-put {
|
||||
background-color: #FEF3C7;
|
||||
color: #92400E;
|
||||
}
|
||||
|
||||
&.method-delete {
|
||||
background-color: #FEE2E2;
|
||||
color: #991B1B;
|
||||
}
|
||||
|
||||
&.method-other {
|
||||
background-color: $border-gray;
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.api-list-item .api-name {
|
||||
flex: 1;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.api-status {
|
||||
padding: $spacing-xs $spacing-md;
|
||||
border-radius: $border-radius-lg;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
|
||||
&.status-pending {
|
||||
background-color: #FFF3CD;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
&.status-active {
|
||||
background-color: #D4EDDA;
|
||||
color: #155724;
|
||||
}
|
||||
}
|
||||
|
||||
// History Table
|
||||
.history-table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
background-color: $gray-bg;
|
||||
padding: $spacing-md $spacing-md;
|
||||
text-align: left;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-gray;
|
||||
border-bottom: 2px solid $border-gray;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.history-status {
|
||||
display: inline-block;
|
||||
padding: $spacing-xs $spacing-md;
|
||||
background-color: $light-bg;
|
||||
color: $secondary-blue;
|
||||
border-radius: $border-radius-lg;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
// Empty State
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 48px $spacing-lg;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-size-base;
|
||||
color: $text-gray;
|
||||
}
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
margin-top: $spacing-4xl;
|
||||
padding-top: $spacing-4xl;
|
||||
border-top: 1px solid $border-gray;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-action,
|
||||
.btn-secondary {
|
||||
padding: $spacing-md $spacing-4xl;
|
||||
border-radius: $border-radius-md;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: $font-size-base;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
&.btn-primary {
|
||||
background-color: $primary-blue;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: $secondary-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-info {
|
||||
background-color: $accent-cyan;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($accent-cyan, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
background-color: $accent-orange;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($accent-orange, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-cancel {
|
||||
background-color: $gray-bg;
|
||||
color: $text-dark;
|
||||
border: 1px solid $border-gray;
|
||||
|
||||
&:hover {
|
||||
background-color: $border-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: $spacing-md $spacing-4xl;
|
||||
border-radius: $border-radius-md;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: $font-size-base;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background-color: #FEE2E2;
|
||||
color: #991B1B;
|
||||
border: 1px solid #FCA5A5;
|
||||
|
||||
&:hover {
|
||||
background-color: #FCA5A5;
|
||||
color: #7F1D1D;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,343 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// MyPage - API Key List Card Layout
|
||||
// Reference: apps/mypage/apiKeyList.html
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Container
|
||||
.apikey-container {
|
||||
max-width: $container-max-width;
|
||||
margin: 0 auto;
|
||||
padding: $spacing-2xl $spacing-lg;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
padding: $spacing-lg $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Header Section
|
||||
.apikey-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: $spacing-2xl;
|
||||
padding-bottom: $spacing-lg;
|
||||
border-bottom: 2px solid $border-gray;
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.apikey-title {
|
||||
h1 {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-gray;
|
||||
font-weight: $font-weight-regular;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.apikey-actions {
|
||||
.btn-create-app {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: 12px 24px;
|
||||
background: $gradient-primary;
|
||||
color: $white;
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-semibold;
|
||||
cursor: pointer;
|
||||
transition: $transition-base;
|
||||
box-shadow: $shadow-md;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result Count
|
||||
.apikey-result-count {
|
||||
font-size: $font-size-base;
|
||||
color: $text-gray;
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
strong {
|
||||
color: $primary-blue;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
// Card Grid - Single column layout (one card per row)
|
||||
.apikey-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: $spacing-lg;
|
||||
margin-bottom: $spacing-2xl;
|
||||
}
|
||||
|
||||
// API Key Card - Simplified horizontal layout
|
||||
.apikey-card {
|
||||
background: $white;
|
||||
border: 1px solid $border-gray;
|
||||
border-radius: $border-radius-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
transition: $transition-base;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-lg;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: $gradient-primary;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow-lg;
|
||||
transform: translateX(4px);
|
||||
border-color: rgba($primary-blue, 0.3);
|
||||
|
||||
&::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
// App Icon
|
||||
.apikey-card-icon {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: $border-radius-md;
|
||||
overflow: hidden;
|
||||
background: $gray-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid $border-gray;
|
||||
|
||||
.app-icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.app-icon-placeholder {
|
||||
color: $text-gray;
|
||||
opacity: 0.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
// App Content
|
||||
.apikey-card-content {
|
||||
flex: 1;
|
||||
min-width: 0; // Allow text truncation
|
||||
|
||||
.apikey-card-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-dark;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.apikey-card-description {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-gray;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// Status Wrapper
|
||||
.apikey-card-status-wrapper {
|
||||
flex-shrink: 0;
|
||||
|
||||
.apikey-card-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
border-radius: $border-radius-full;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-active {
|
||||
background-color: rgba($accent-green, 0.15);
|
||||
color: #4FA065;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
background-color: rgba($accent-orange, 0.15);
|
||||
color: #D9534F;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background-color: rgba($accent-yellow, 0.15);
|
||||
color: #F0AD4E;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Empty State
|
||||
.apikey-empty-state {
|
||||
text-align: center;
|
||||
padding: $spacing-5xl $spacing-lg;
|
||||
background: $white;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 2px dashed $border-gray;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: $spacing-lg;
|
||||
opacity: 0.6;
|
||||
display: inline-block;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $text-dark;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-size-base;
|
||||
color: $text-gray;
|
||||
margin-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.btn-create-app-large {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: 14px 32px;
|
||||
background: $gradient-primary;
|
||||
color: $white;
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-semibold;
|
||||
cursor: pointer;
|
||||
transition: $transition-base;
|
||||
box-shadow: $shadow-lg;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: $shadow-xl;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce animation for empty state icon
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
// Loading State (if needed)
|
||||
.apikey-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: $spacing-5xl;
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid $border-gray;
|
||||
border-top-color: $primary-blue;
|
||||
border-radius: $border-radius-circle;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// API Key Registration Wizard Styles - Moved to _apikey-register.scss
|
||||
// -----------------------------------------------------------------------------
|
||||
// All registration-related styles have been moved to _apikey-register.scss
|
||||
// This file now contains only API Key List styles
|
||||
|
||||
// Responsive adjustments for smaller screens
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
.apikey-empty-state {
|
||||
padding: $spacing-3xl $spacing-md;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
|
||||
<body>
|
||||
<section layout:fragment="contentFragment" class="content">
|
||||
<div class="content_wrap">
|
||||
<div class="sub_title3">
|
||||
<h2 class="title">인증키 운영 전환 신청</h2>
|
||||
</div>
|
||||
<div class="inner i_cs h_inner5">
|
||||
<div class="form_type">
|
||||
<div class="info1 form_top">
|
||||
<p class="title w_tit">
|
||||
<span class="dot">API키 이름</span>
|
||||
</p>
|
||||
<div class="info_line info_add">
|
||||
<div class="info_box1 w_inp4">
|
||||
<input type="text" name="name" id="appRequestProdClientName" class="common_input_type_1 h_inp2" style="width: 300px;" placeholder="API키 이름 입력">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="infor api_dot top" style="margin-top: 10px;">
|
||||
<ul>
|
||||
<li>
|
||||
<p>인증키가 없을 경우, 승인 시, 인증키는 신규로 발급 됩니다.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form_type">
|
||||
<div class="info1 form_top" style="margin-top: 10px;">
|
||||
<p class="title w_tit">
|
||||
<span class="">API키 선택</span>
|
||||
</p>
|
||||
<div class="info_line info_add">
|
||||
<div class="info_box1 w_inp4">
|
||||
<div class="custom_select select_w h_inp" style="width: 300px;">
|
||||
<input type="hidden" id="clientId" />
|
||||
<div class="select_selected h_inp" id="selectedDevApiKey">API키 선택</div>
|
||||
<ul class="common_selecttype select_items select_hide" id="devApiKeyList">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pop_text l_text m-only" style="margin-top: 10px; font-weight: 700;">API</p>
|
||||
<div class="board_search sh_box top2">
|
||||
<p class="pop_text l_text pc-only">API</p>
|
||||
<div class="search_txt ser_api w_inp">
|
||||
<input type="text" class="common_input_type_1 h_inp2" id="prod_api_keyword" placeholder="API 검색">
|
||||
</div>
|
||||
<div class="btn_wrap search_btn">
|
||||
<button type="button" class="btn_search" id="prod_api_search">검색</button>
|
||||
<i class="i_search"></i>
|
||||
</div>
|
||||
<p class="pop_text l_text pop_text2 pc-only" style="margin-left: 145px;">운영 이관 신청 API</p>
|
||||
</div>
|
||||
<div class="tb_conbox top2">
|
||||
<div class="box table_type2 top_type">
|
||||
<table class="table_col table_min tb_bdtop" id="prodSourceTable" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="140px">
|
||||
<col width="230px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="total_wrap">
|
||||
<span class="inp_check b_tit">
|
||||
<input type="checkbox" class="agree_all" id="selectAllProdSourceTable" name="selectall" data-group="total">
|
||||
<label for="selectAllProdSourceTable"></label>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>서비스</th>
|
||||
<th>API</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box pop_arrow m-only" style="text-align:center;">
|
||||
<a href="#none" class="add_apis"><img th:src="@{/img/icon/Icon_arrow_btn_down.png}" alt="올리기"></a>
|
||||
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/Icon_arrow_btn_up.png}" alt="내리기"></a>
|
||||
</div>
|
||||
<div class="box pop_arrow pc-only" style="text-align:center;">
|
||||
<p>
|
||||
<a href="#none" class="add_apis"><img th:src="@{/img/icon/icon_arrow_btn_r.png}" alt="넣기"></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/icon_arrow_btn_l.png}" alt="빼기"></a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="pop_text l_text2 pt_top m-only">운영 이관 신청 API</p>
|
||||
<div class="box table_type2 top_type">
|
||||
|
||||
<table class="table_col table_min tb_bdtop" id="prodTargetTable" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="140px">
|
||||
<col width="230px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="total_wrap">
|
||||
<span class="inp_check b_tit">
|
||||
<input type="checkbox" class="agree_all" id="selectAllProdTargetTable" name="selectall2" data-group="total2">
|
||||
<label for="selectAllProdTargetTable"></label>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>서비스</th>
|
||||
<th>API</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="pop_text l_text2">신청사유</p>
|
||||
<div class="pop_textbox">
|
||||
<textarea name="reason" id="prod_reason" rows="5" cols="30" class="common_textareaType_1" placeholder="신청사유를 입력해 주세요."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form_type">
|
||||
<div class="btn_application btn_top3">
|
||||
<button class="common_btn_type_1 gray btn_cancel">취소</button>
|
||||
<div class="btn_gap"></div>
|
||||
<button class="common_btn_type_1 request_prod_apis">운영 전환 신청</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section layout:fragment="pagePopups" class="content pop">
|
||||
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
activateSelect();
|
||||
|
||||
$('.btn_cancel').on('click', function(){
|
||||
window.location.href = '/apis';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</html>
|
||||
@@ -1,92 +1,247 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="contentFragment" class="content">
|
||||
<div class="content_wrap">
|
||||
<div class="sub_title3">
|
||||
<h2 class="title">인증키 목록</h2>
|
||||
</div>
|
||||
<div class="inner i_cs h_inner8">
|
||||
<div class="tab-wrap">
|
||||
<div class="tabs">
|
||||
<ul class="tab_nav tab_bt">
|
||||
<li class="active">
|
||||
<a th:href="@{/myapikey}">개발</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/myapikey/api_key_prod_page}">운영</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab active">
|
||||
<div class="tb_board_list">
|
||||
<div class="table_box">
|
||||
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="86px">
|
||||
<col width="230px">
|
||||
<col width="120px">
|
||||
<col width="100px">
|
||||
<col width="120px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>이름</th>
|
||||
<th>인증키</th>
|
||||
<th>인증키상태</th>
|
||||
<th>사용 API 수</th>
|
||||
<th>마지막수정일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="apikey, status : ${apiKeys}">
|
||||
<td th:text="${status.index + 1}">1</td>
|
||||
<td>
|
||||
<a th:href="@{/myapikey/api_key_detail(id=${apikey.clientid})}" th:text="${apikey.clientname}">수신</a>
|
||||
</td>
|
||||
<td>
|
||||
<a th:href="@{/myapikey/api_key_detail(id=${apikey.clientid})}" th:text="${apikey.clientid}"></a>
|
||||
</td>
|
||||
<td th:text="${apikey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</td>
|
||||
<td th:text="${apikey.apiList.size()}">1</td>
|
||||
<td th:text="${#temporals.format(apikey.modifiedon, 'yyyy.MM.dd')}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn_wrap bt_location">
|
||||
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" type="button" class="btn_key" id="requestApiKey">인증키 신청</button>
|
||||
</div>
|
||||
<div class="pagination page_top" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
|
||||
<form name="listForm" th:action="@{/myapikey}" method="get">
|
||||
<input type="hidden" id="page" name="page"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-container">
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="apikey-header">
|
||||
<div class="apikey-title">
|
||||
<h1>마이페이지</h1>
|
||||
<h2>인증키 목록</h2>
|
||||
</div>
|
||||
<div class="apikey-actions">
|
||||
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
type="button"
|
||||
class="btn-create-app"
|
||||
id="requestApiKey">
|
||||
<span class="btn-icon">➕</span>
|
||||
앱 생성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
|
||||
<div th:insert="~{fragment/apikey-request :: apiKeyRequest}" class="popup_total" id="apiKeyRequestPopup" style="display:none; z-index: 1000;"></div>
|
||||
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
|
||||
</section>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
activateSelect();
|
||||
});
|
||||
|
||||
function fn_select_page(pageNo) {
|
||||
document.listForm.page.value = pageNo;
|
||||
document.listForm.action = '[[@{/myapikey}]]';
|
||||
document.listForm.submit();
|
||||
}
|
||||
<!-- Result Count -->
|
||||
<div class="apikey-result-count">
|
||||
총 <strong th:text="${(appRequests != null ? appRequests.size() : 0) + (apiKeys != null ? apiKeys.size() : 0)}">0</strong>개의 인증키
|
||||
</div>
|
||||
|
||||
<!-- App Requests Cards Grid (Pending) -->
|
||||
<div class="apikey-card-grid" th:if="${appRequests != null and !appRequests.isEmpty()}">
|
||||
|
||||
<a class="apikey-card"
|
||||
th:each="request : ${appRequests}"
|
||||
th:href="@{/myapikey/app_request_detail(id=${request.id})}">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="apikey-card-icon">
|
||||
<img th:if="${request.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${request.appIconFileId})}"
|
||||
alt="App Icon"
|
||||
class="app-icon-image">
|
||||
<div th:unless="${request.appIconFileId != null}" class="app-icon-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Content -->
|
||||
<div class="apikey-card-content">
|
||||
<!-- App Name -->
|
||||
<h3 class="apikey-card-title" th:text="${request.clientName}">앱 이름</h3>
|
||||
|
||||
<!-- App Description -->
|
||||
<p class="apikey-card-description"
|
||||
th:text="${request.appDescription != null ? request.appDescription : '설명 없음'}">
|
||||
앱 설명이 여기에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- App Status Badge (Using Approval State) -->
|
||||
<div class="apikey-card-status-wrapper">
|
||||
<span class="apikey-card-status status-pending"
|
||||
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '상태 없음'}">
|
||||
승인 대기
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- API Key Cards Grid (Approved) -->
|
||||
<div class="apikey-card-grid" th:if="${apiKeys != null and !apiKeys.isEmpty()}">
|
||||
|
||||
<a class="apikey-card"
|
||||
th:each="apikey, status : ${apiKeys}"
|
||||
th:href="@{/myapikey/credential_detail(id=${apikey.clientid})}">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="apikey-card-icon">
|
||||
<img th:if="${apikey.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apikey.appIconFileId})}"
|
||||
alt="App Icon"
|
||||
class="app-icon-image">
|
||||
<div th:unless="${apikey.appIconFileId != null}" class="app-icon-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Content -->
|
||||
<div class="apikey-card-content">
|
||||
<!-- App Name -->
|
||||
<h3 class="apikey-card-title" th:text="${apikey.clientname}">앱 이름</h3>
|
||||
|
||||
<!-- App Description -->
|
||||
<p class="apikey-card-description"
|
||||
th:text="${apikey.appDescription != null ? apikey.appDescription : '설명 없음'}">
|
||||
앱 설명이 여기에 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- App Status Badge -->
|
||||
<div class="apikey-card-status-wrapper">
|
||||
<span class="apikey-card-status"
|
||||
th:classappend="${apikey.appstatus == '1' ? 'status-active' : apikey.appstatus == '0' ? 'status-inactive' : 'status-pending'}"
|
||||
th:text="${apikey.appstatus == '1' ? '활성화' : apikey.appstatus == '0' ? '비활성화' : '승인 대기'}">
|
||||
활성화
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="apikey-empty-state" th:if="${(appRequests == null or appRequests.isEmpty()) and (apiKeys == null or apiKeys.isEmpty())}">
|
||||
<div class="empty-icon">🔑</div>
|
||||
<h3>등록된 인증키가 없습니다</h3>
|
||||
<p>새로운 앱을 생성하여 API를 사용해보세요.</p>
|
||||
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
type="button"
|
||||
class="btn-create-app-large"
|
||||
id="requestApiKeyEmpty">
|
||||
<span class="btn-icon">➕</span>
|
||||
첫 앱 생성하기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Card click handlers
|
||||
const apikeyCards = document.querySelectorAll('.apikey-card');
|
||||
apikeyCards.forEach(function(card) {
|
||||
// Prevent navigation when clicking on specific elements
|
||||
const preventNavElements = card.querySelectorAll('.btn-copy-key, .btn-view-detail, a');
|
||||
preventNavElements.forEach(function(element) {
|
||||
element.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
// Card click handler
|
||||
card.addEventListener('click', function(e) {
|
||||
// Don't navigate if clicking on interactive elements
|
||||
if (e.target.closest('.btn-copy-key') || e.target.closest('.btn-view-detail')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard accessibility
|
||||
card.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const href = this.getAttribute('data-href');
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Copy to clipboard functionality
|
||||
const copyButtons = document.querySelectorAll('.btn-copy-key');
|
||||
copyButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const textToCopy = this.getAttribute('data-clipboard-text');
|
||||
|
||||
// Create temporary input element
|
||||
const tempInput = document.createElement('input');
|
||||
tempInput.value = textToCopy;
|
||||
document.body.appendChild(tempInput);
|
||||
tempInput.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
|
||||
// Visual feedback
|
||||
const originalContent = this.innerHTML;
|
||||
this.innerHTML = '<span class="copy-icon">✓</span>';
|
||||
this.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalContent;
|
||||
this.classList.remove('copied');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(tempInput);
|
||||
});
|
||||
});
|
||||
|
||||
// App creation button handlers
|
||||
const requestApiKeyBtn = document.getElementById('requestApiKey');
|
||||
const requestApiKeyEmptyBtn = document.getElementById('requestApiKeyEmpty');
|
||||
|
||||
function handleApiKeyRequest() {
|
||||
// Clear any existing registration data from sessionStorage
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key && key.startsWith('registration_')) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
||||
|
||||
// Redirect to the API key registration wizard with clear parameter
|
||||
// This tells the server to clear the session and start fresh
|
||||
window.location.href = '/myapikey/register/step1?clear=true';
|
||||
}
|
||||
|
||||
if (requestApiKeyBtn) {
|
||||
requestApiKeyBtn.addEventListener('click', handleApiKeyRequest);
|
||||
}
|
||||
|
||||
if (requestApiKeyEmptyBtn) {
|
||||
requestApiKeyEmptyBtn.addEventListener('click', handleApiKeyRequest);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step active">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-label">기본 정보</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="register-header">
|
||||
<h1>API Key 수정</h1>
|
||||
<h2>Step 1: 기본 정보 수정</h2>
|
||||
<p class="header-description">
|
||||
앱의 기본 정보를 수정해주세요. 변경 사항은 승인 후 적용됩니다.
|
||||
</p>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-error" style="margin-top: 20px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Content -->
|
||||
<div class="register-content">
|
||||
<form id="modifyStep1Form" method="post" th:action="@{/myapikey/modify/step1}" th:object="${apiKeyModification}" class="register-form" enctype="multipart/form-data">
|
||||
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<div class="form-card">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="form-group">
|
||||
<label for="appIcon" class="form-label required">
|
||||
앱 아이콘
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<div class="icon-upload-wrapper">
|
||||
<div class="icon-preview" id="iconPreview">
|
||||
<div class="icon-placeholder"
|
||||
th:if="${apiKeyModification.appIconData == null and apiKeyModification.appIconFileId == null}">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<span>이미지 선택</span>
|
||||
</div>
|
||||
<!-- Show image from file service if available -->
|
||||
<img id="iconPreviewImage"
|
||||
th:if="${apiKeyModification.appIconFileId != null}"
|
||||
th:src="@{/file/download(fileSn=1,fileId=${apiKeyModification.appIconFileId})}"
|
||||
alt="Icon Preview">
|
||||
<!-- Show image from data if available (newly uploaded) -->
|
||||
<img id="iconPreviewImage"
|
||||
th:if="${apiKeyModification.appIconData != null and apiKeyModification.appIconFileId == null}"
|
||||
th:src="'data:' + ${apiKeyModification.appIconContentType} + ';base64,' + ${T(java.util.Base64).getEncoder().encodeToString(apiKeyModification.appIconData)}"
|
||||
alt="Icon Preview">
|
||||
<button type="button" class="btn-remove-icon" id="btnRemoveIcon"
|
||||
th:style="${(apiKeyModification.appIconData != null or apiKeyModification.appIconFileId != null) ? 'display: block;' : 'display: none;'}"
|
||||
onclick="removeAppIcon()" title="아이콘 삭제">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input type="file"
|
||||
id="appIcon"
|
||||
name="appIcon"
|
||||
class="icon-input"
|
||||
accept="image/png,image/jpeg,image/jpg">
|
||||
<button type="button" class="btn-icon-select" onclick="document.getElementById('appIcon').click()">
|
||||
파일 선택
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-hint">PNG, JPG 형식 (최대 2MB, 권장 크기: 512x512px)</small>
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<div class="form-group">
|
||||
<label for="appName" class="form-label required">
|
||||
앱 이름
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="appName"
|
||||
th:field="*{appName}"
|
||||
class="form-control"
|
||||
placeholder="예: My Application"
|
||||
maxlength="50"
|
||||
required>
|
||||
<small class="form-hint">앱을 구별할 수 있는 고유한 이름을 입력해주세요. (최대 50자)</small>
|
||||
</div>
|
||||
|
||||
<!-- App Description -->
|
||||
<div class="form-group">
|
||||
<label for="appDescription" class="form-label">앱 설명</label>
|
||||
<textarea id="appDescription"
|
||||
th:field="*{appDescription}"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder="앱의 주요 기능과 용도를 간단히 설명해주세요."
|
||||
maxlength="500"></textarea>
|
||||
<small class="form-hint">앱의 용도와 주요 기능을 설명해주세요. (최대 500자)</small>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="form-group">
|
||||
<label for="callbackUrl" class="form-label">Callback URL</label>
|
||||
<input type="url"
|
||||
id="callbackUrl"
|
||||
th:field="*{callbackUrl}"
|
||||
class="form-control"
|
||||
placeholder="https://example.com/callback">
|
||||
<small class="form-hint">OAuth 2.0 인증 시 사용할 콜백 URL을 입력해주세요.</small>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="form-group">
|
||||
<label for="ipWhitelist" class="form-label">
|
||||
IP 화이트리스트
|
||||
</label>
|
||||
<div class="ip-input-group">
|
||||
<input type="text"
|
||||
id="ipWhitelistInput"
|
||||
class="form-input"
|
||||
placeholder="예: 192.168.1.1 또는 10.0.0.0/24">
|
||||
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-hint">허용할 IP 주소 또는 CIDR 대역을 입력하세요. (선택사항)</small>
|
||||
|
||||
<!-- IP List Display -->
|
||||
<div class="ip-list" id="ipList" style="display: none;">
|
||||
<div class="ip-list-header">
|
||||
<span class="ip-count">등록된 IP: <strong id="ipCount">0</strong>개</span>
|
||||
</div>
|
||||
<div class="ip-items" id="ipItems">
|
||||
<!-- Dynamically added IP items -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input to store IP list -->
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="form-actions">
|
||||
<a th:href="@{/myapikey/modify/cancel(clientId=${apiKeyModification.clientId})}" class="btn-cancel">
|
||||
취소
|
||||
</a>
|
||||
<button type="submit" class="btn-primary">
|
||||
다음 단계
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
// IP address list management
|
||||
let ipAddresses = [];
|
||||
|
||||
// Remove app icon
|
||||
function removeAppIcon() {
|
||||
const appIconInput = document.getElementById('appIcon');
|
||||
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
||||
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
|
||||
// Clear file input
|
||||
appIconInput.value = '';
|
||||
|
||||
// Hide preview image and show placeholder
|
||||
iconPreviewImage.style.display = 'none';
|
||||
iconPreviewImage.src = '';
|
||||
iconPlaceholder.style.display = 'flex';
|
||||
|
||||
// Hide delete button
|
||||
btnRemoveIcon.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add IP address to the list
|
||||
function addIpAddress() {
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
// Validate IP format
|
||||
if (!ipValue) {
|
||||
alert('IP 주소를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic IP validation (supports IP and CIDR notation)
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
if (!ipPattern.test(ipValue)) {
|
||||
alert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if (ipAddresses.includes(ipValue)) {
|
||||
alert('이미 추가된 IP 주소입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to array
|
||||
ipAddresses.push(ipValue);
|
||||
|
||||
// Update UI
|
||||
updateIpList();
|
||||
|
||||
// Clear input
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
}
|
||||
|
||||
// Remove IP address from the list
|
||||
function removeIpAddress(ip) {
|
||||
const index = ipAddresses.indexOf(ip);
|
||||
if (index > -1) {
|
||||
ipAddresses.splice(index, 1);
|
||||
updateIpList();
|
||||
}
|
||||
}
|
||||
|
||||
// Update IP list display
|
||||
function updateIpList() {
|
||||
const ipList = document.getElementById('ipList');
|
||||
const ipItems = document.getElementById('ipItems');
|
||||
const ipCount = document.getElementById('ipCount');
|
||||
const ipWhitelistHidden = document.getElementById('ipWhitelist');
|
||||
|
||||
// Update count
|
||||
ipCount.textContent = ipAddresses.length;
|
||||
|
||||
// Show/hide list container
|
||||
if (ipAddresses.length > 0) {
|
||||
ipList.style.display = 'block';
|
||||
|
||||
// Build IP items HTML
|
||||
let itemsHTML = '';
|
||||
ipAddresses.forEach(function(ip) {
|
||||
itemsHTML += `
|
||||
<div class="ip-item">
|
||||
<span class="ip-address">${ip}</span>
|
||||
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')" title="제거">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
ipItems.innerHTML = itemsHTML;
|
||||
} else {
|
||||
ipList.style.display = 'none';
|
||||
ipItems.innerHTML = '';
|
||||
}
|
||||
|
||||
// Update hidden input with comma-separated values
|
||||
ipWhitelistHidden.value = ipAddresses.join(',');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initialize with existing icon data
|
||||
var hasExistingIcon = /*[[${apiKeyModification.appIconData != null}]]*/ false;
|
||||
var hasExistingIconFile = /*[[${apiKeyModification.appIconFileId != null}]]*/ false;
|
||||
|
||||
// If we have existing icon data, it's already shown by Thymeleaf
|
||||
// But we need to ensure the UI state is correct
|
||||
if (hasExistingIcon || hasExistingIconFile) {
|
||||
$('.icon-placeholder').hide();
|
||||
$('#btnRemoveIcon').show();
|
||||
}
|
||||
|
||||
// Restore existing IP whitelist
|
||||
const sessionIpWhitelist = /*[[${apiKeyModification.ipWhitelistAsString}]]*/ '';
|
||||
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
||||
const ips = sessionIpWhitelist.split(',');
|
||||
ips.forEach(function(ip) {
|
||||
const trimmedIp = ip.trim();
|
||||
if (trimmedIp && !ipAddresses.includes(trimmedIp)) {
|
||||
ipAddresses.push(trimmedIp);
|
||||
}
|
||||
});
|
||||
updateIpList();
|
||||
}
|
||||
|
||||
// File upload preview
|
||||
$('#appIcon').on('change', function(e) {
|
||||
var file = e.target.files[0];
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
||||
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
||||
|
||||
if (file) {
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('파일 크기는 2MB를 초과할 수 없습니다.');
|
||||
$(this).val('');
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
iconPreviewImage.src = e.target.result;
|
||||
iconPreviewImage.style.display = 'block';
|
||||
iconPlaceholder.style.display = 'none';
|
||||
btnRemoveIcon.style.display = 'flex';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// IP whitelist input - Enter key handler
|
||||
$('#ipWhitelistInput').on('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addIpAddress();
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission - ensure IP list is updated
|
||||
$('#modifyStep1Form').on('submit', function(e) {
|
||||
// IP whitelist is already updated via updateIpList()
|
||||
});
|
||||
});
|
||||
/*]]>*/
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,718 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">기본 정보</div>
|
||||
</div>
|
||||
<div class="progress-line filled"></div>
|
||||
<div class="progress-step active">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="register-header">
|
||||
<h1>API Key 수정</h1>
|
||||
<h2>Step 2: API 선택</h2>
|
||||
<p class="header-description">
|
||||
사용할 API를 선택해주세요. 여러 개의 API를 선택할 수 있습니다.
|
||||
</p>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-error" style="margin-top: 20px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Container with Sidebar Layout -->
|
||||
<section class="apikey-register-container with-sidebar">
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="apikey-register-sidebar" id="apiSidebar">
|
||||
<nav class="apikey-sidebar-nav">
|
||||
<!-- All APIs -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-title active" data-group="">
|
||||
전체
|
||||
<span class="api-count" id="totalApiCount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Categories -->
|
||||
<div class="menu-section" th:each="service : ${apiServices}">
|
||||
<div class="menu-title"
|
||||
th:data-group="${service.id}">
|
||||
[[${service.groupName}]]
|
||||
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Mobile Overlay -->
|
||||
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="apikey-register-content">
|
||||
<form id="modifyStep2Form" method="post" th:action="@{/myapikey/modify/step2}" th:object="${apiKeyModification}" class="register-form">
|
||||
|
||||
<!-- Hidden field for clientId -->
|
||||
<input type="hidden" th:field="*{clientId}"/>
|
||||
|
||||
<!-- Search Box with Select All -->
|
||||
<div class="api-filter-header">
|
||||
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
|
||||
<label class="select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox">
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="api-search-box">
|
||||
<input type="text"
|
||||
id="apiSearch"
|
||||
class="search-input"
|
||||
placeholder="API 검색...">
|
||||
<span class="search-icon">🔍</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Count -->
|
||||
<div class="api-result-count">
|
||||
총 <strong id="visibleCount">0</strong>건
|
||||
</div>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="api-selection-card-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (initially hidden) -->
|
||||
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- API cards will be dynamically loaded here -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<!-- Save and go back button -->
|
||||
<button type="submit" formaction="/myapikey/modify/step2/save" class="btn-secondary" id="btnPrevStep">
|
||||
<span class="icon">←</span>
|
||||
이전 단계
|
||||
</button>
|
||||
<button type="submit" class="btn-primary" id="btnNext">
|
||||
저장
|
||||
<span class="icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Floating Cart Button -->
|
||||
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1"></circle>
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
<span class="cart-text">선택된 API:</span>
|
||||
<span class="cart-badge" id="cartBadge">0</span>
|
||||
</button>
|
||||
|
||||
<!-- Selected APIs Modal -->
|
||||
<div class="selected-apis-modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="modal-close" id="modalCloseBtn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="selected-apis-list" id="modalSelectedList">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM Elements
|
||||
const form = document.getElementById('modifyStep2Form');
|
||||
const searchInput = document.getElementById('apiSearch');
|
||||
const sidebar = document.getElementById('apiSidebar');
|
||||
const mobileToggle = document.getElementById('mobileToggle');
|
||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||
const menuTitles = document.querySelectorAll('.menu-title');
|
||||
const visibleCount = document.getElementById('visibleCount');
|
||||
const apiCardGrid = document.getElementById('apiCardGrid');
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const totalApiCount = document.getElementById('totalApiCount');
|
||||
|
||||
let currentFilter = ''; // Empty string means "all"
|
||||
let currentServiceName = '전체'; // Store current service name
|
||||
let allApis = []; // Store loaded APIs
|
||||
let allLoadedApis = []; // Store ALL APIs from server for modal display
|
||||
let selectedApis = new Set(); // Track selected API IDs
|
||||
|
||||
// Restore selected APIs from session (for modification)
|
||||
const sessionSelectedApis = /*[[${apiKeyModification.selectedApis}]]*/ [];
|
||||
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
|
||||
sessionSelectedApis.forEach(function(apiId) {
|
||||
selectedApis.add(apiId);
|
||||
});
|
||||
}
|
||||
|
||||
// Load all APIs for modal display (used when APIs from different categories are selected)
|
||||
function loadAllApisForModal() {
|
||||
fetch('/apis/for_request')
|
||||
.then(response => response.json())
|
||||
.then(apis => {
|
||||
allLoadedApis = apis;
|
||||
console.log('Loaded', allLoadedApis.length, 'APIs for modal display');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load all APIs for modal:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load APIs via AJAX
|
||||
function loadApis(groupId) {
|
||||
// Show loading state
|
||||
loadingState.style.display = 'block';
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
// Remove existing API cards
|
||||
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
|
||||
|
||||
// Build URL with optional groupId filter
|
||||
let url = '/apis/for_request';
|
||||
if (groupId) {
|
||||
url += '?groupIds=' + encodeURIComponent(groupId);
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(apis => {
|
||||
allApis = apis;
|
||||
loadingState.style.display = 'none';
|
||||
|
||||
if (apis.length === 0) {
|
||||
emptyState.style.display = 'block';
|
||||
visibleCount.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
renderApiCards(apis);
|
||||
visibleCount.textContent = apis.length;
|
||||
|
||||
// Update total count
|
||||
if (!groupId || groupId === '') {
|
||||
totalApiCount.textContent = apis.length;
|
||||
}
|
||||
|
||||
// Update select all UI
|
||||
updateSelectAllUI();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load APIs:', error);
|
||||
loadingState.style.display = 'none';
|
||||
emptyState.querySelector('h3').textContent = 'API 로드 실패';
|
||||
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
|
||||
emptyState.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Update select all UI visibility and label
|
||||
function updateSelectAllUI() {
|
||||
const selectAllWrapper = document.getElementById('selectAllWrapper');
|
||||
const selectAllText = document.getElementById('selectAllText');
|
||||
|
||||
if (currentFilter === '') {
|
||||
// Hide select all when "전체" is selected
|
||||
selectAllWrapper.style.display = 'none';
|
||||
} else {
|
||||
// Show select all with service name
|
||||
selectAllWrapper.style.display = 'flex';
|
||||
selectAllText.textContent = currentServiceName + ' API 전체 선택';
|
||||
}
|
||||
|
||||
updateSelectAllCheckboxState();
|
||||
}
|
||||
|
||||
// Update select all checkbox state (checked/indeterminate/unchecked)
|
||||
function updateSelectAllCheckboxState() {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card'))
|
||||
.filter(card => card.style.display !== 'none');
|
||||
|
||||
if (visibleCards.length === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
|
||||
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else if (checkedCount === visibleCheckboxes.length) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Render API cards
|
||||
function renderApiCards(apis) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
apis.forEach(api => {
|
||||
const card = createApiCard(api);
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
|
||||
apiCardGrid.appendChild(fragment);
|
||||
|
||||
// Re-attach event listeners
|
||||
attachCardEventListeners();
|
||||
|
||||
// Update modal list after cards are rendered
|
||||
updateModalList();
|
||||
}
|
||||
|
||||
// Create API card element
|
||||
function createApiCard(api) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'api-selection-card';
|
||||
// Note: AJAX response doesn't have apiGroupId, use apiGroupName or service for grouping
|
||||
card.setAttribute('data-group', api.apiGroupName || '');
|
||||
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
||||
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
||||
card.setAttribute('data-api-id', api.apiId);
|
||||
|
||||
const isSelected = selectedApis.has(api.apiId);
|
||||
if (isSelected) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="api-checkbox"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
</div>
|
||||
|
||||
<label class="api-card-content" for="api-${api.apiId}">
|
||||
<div class="api-card-header">
|
||||
<span class="api-card-badge">${api.service || '카테고리'}</span>
|
||||
<span class="api-method">${api.apiMethod || 'GET'}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<code>${api.apiUrl || '/api/v1/example'}</code>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Attach event listeners to cards
|
||||
function attachCardEventListeners() {
|
||||
const checkboxes = document.querySelectorAll('.api-checkbox');
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
});
|
||||
|
||||
apiCards.forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('api-checkbox') || e.target.tagName === 'LABEL') {
|
||||
return;
|
||||
}
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
updateSelectAllCheckboxState();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update card visual state
|
||||
function updateCardSelection(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
if (checkbox.checked) {
|
||||
card.classList.add('selected');
|
||||
selectedApis.add(checkbox.value);
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
selectedApis.delete(checkbox.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected count
|
||||
function updateSelectedCount() {
|
||||
// Update floating cart button
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const cartBadge = document.getElementById('cartBadge');
|
||||
|
||||
if (selectedApis.size > 0) {
|
||||
floatingCartBtn.style.display = 'flex';
|
||||
cartBadge.textContent = selectedApis.size;
|
||||
} else {
|
||||
floatingCartBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update modal list
|
||||
updateModalList();
|
||||
}
|
||||
|
||||
// Update modal selected APIs list
|
||||
function updateModalList() {
|
||||
const modalSelectedList = document.getElementById('modalSelectedList');
|
||||
modalSelectedList.innerHTML = '';
|
||||
|
||||
if (selectedApis.size === 0) {
|
||||
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build list using selectedApis Set for consistency
|
||||
// This ensures we show all selected APIs regardless of current filter/view
|
||||
console.log('updateModalList: Building modal for', selectedApis.size, 'selected APIs');
|
||||
selectedApis.forEach(function(apiId) {
|
||||
// Try to find the checkbox in the DOM first
|
||||
const checkbox = document.querySelector('.api-checkbox[value="' + apiId + '"]');
|
||||
let apiName = apiId; // Default to ID if we can't find the name
|
||||
|
||||
if (checkbox) {
|
||||
// If checkbox exists in DOM, get the name from the card
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
if (card) {
|
||||
const nameElement = card.querySelector('.api-name');
|
||||
if (nameElement) {
|
||||
apiName = nameElement.textContent;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If checkbox not in DOM (different category is showing),
|
||||
// try to find the API in our loaded data
|
||||
let api = allApis.find(a => a.apiId === apiId);
|
||||
if (!api && allLoadedApis.length > 0) {
|
||||
api = allLoadedApis.find(a => a.apiId === apiId);
|
||||
}
|
||||
if (api && api.apiName) {
|
||||
apiName = api.apiName;
|
||||
}
|
||||
}
|
||||
|
||||
const apiPill = document.createElement('div');
|
||||
apiPill.className = 'api-pill';
|
||||
apiPill.innerHTML = `
|
||||
<span class="api-pill-name">${apiName}</span>
|
||||
<button type="button" class="api-pill-remove" data-value="${apiId}" aria-label="Remove ${apiName}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
console.log('Adding pill for:', apiId, '→', apiName);
|
||||
modalSelectedList.appendChild(apiPill);
|
||||
});
|
||||
|
||||
// Add remove handlers to newly created elements
|
||||
modalSelectedList.querySelectorAll('.api-pill-remove').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const value = this.getAttribute('data-value');
|
||||
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
} else {
|
||||
// If checkbox not found (maybe different category is showing),
|
||||
// still remove from selection
|
||||
selectedApis.delete(value);
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
let visibleCountNum = 0;
|
||||
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
apiCards.forEach(function(card) {
|
||||
const apiName = card.getAttribute('data-name');
|
||||
const apiDesc = card.getAttribute('data-desc');
|
||||
|
||||
// Check if matches search
|
||||
const matchesSearch = apiName.includes(searchTerm) || apiDesc.includes(searchTerm);
|
||||
|
||||
if (matchesSearch) {
|
||||
card.style.display = '';
|
||||
visibleCountNum++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
visibleCount.textContent = visibleCountNum;
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar category selection
|
||||
menuTitles.forEach(function(title) {
|
||||
title.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove active from all
|
||||
menuTitles.forEach(t => t.classList.remove('active'));
|
||||
// Add active to clicked
|
||||
this.classList.add('active');
|
||||
|
||||
const groupId = this.getAttribute('data-group');
|
||||
currentFilter = groupId;
|
||||
|
||||
// Store current service name (extract text before the count)
|
||||
currentServiceName = this.textContent.trim().split('\n')[0].trim();
|
||||
|
||||
// Load APIs for selected service
|
||||
loadApis(groupId);
|
||||
|
||||
// Clear search when changing category
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
// Close mobile menu if open
|
||||
if (window.innerWidth <= 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
function closeMobileMenu() {
|
||||
sidebar.classList.remove('mobile-open');
|
||||
mobileOverlay.classList.remove('active');
|
||||
mobileToggle.innerHTML = '☰';
|
||||
mobileToggle.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
|
||||
if (mobileToggle && sidebar && mobileOverlay) {
|
||||
mobileToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
mobileOverlay.classList.toggle('active');
|
||||
|
||||
if (sidebar.classList.contains('mobile-open')) {
|
||||
this.innerHTML = '✕';
|
||||
this.setAttribute('aria-label', '메뉴 닫기');
|
||||
} else {
|
||||
this.innerHTML = '☰';
|
||||
this.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu on overlay click
|
||||
mobileOverlay.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile menu on resize to desktop
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal control
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const selectedApisModal = document.getElementById('selectedApisModal');
|
||||
const modalOverlay = document.getElementById('modalOverlay');
|
||||
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
||||
const modalCancelBtn = document.getElementById('modalCancelBtn');
|
||||
|
||||
// Open modal
|
||||
function openModal() {
|
||||
selectedApisModal.style.display = 'block';
|
||||
// Add show class to backdrop and modal for visibility
|
||||
setTimeout(function() {
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.add('show');
|
||||
}
|
||||
selectedApisModal.classList.add('show');
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden'; // Prevent background scroll
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
// Remove show class first for transition
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.remove('show');
|
||||
}
|
||||
selectedApisModal.classList.remove('show');
|
||||
|
||||
// Hide modal after transition
|
||||
setTimeout(function() {
|
||||
selectedApisModal.style.display = 'none';
|
||||
}, 300);
|
||||
|
||||
document.body.style.overflow = ''; // Restore scroll
|
||||
}
|
||||
|
||||
// Floating cart button click
|
||||
if (floatingCartBtn) {
|
||||
floatingCartBtn.addEventListener('click', openModal);
|
||||
}
|
||||
|
||||
// Modal overlay click
|
||||
if (modalOverlay) {
|
||||
modalOverlay.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Modal close button click
|
||||
if (modalCloseBtn) {
|
||||
modalCloseBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Modal cancel button click
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Close modal on ESC key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && selectedApisModal.style.display === 'block') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Select all checkbox event handler
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card'))
|
||||
.filter(card => card.style.display !== 'none');
|
||||
|
||||
visibleCards.forEach(function(card) {
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
updateCardSelection(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Allow submission even without selected APIs (user might want to remove all)
|
||||
// But show confirmation
|
||||
if (selectedApis.size === 0) {
|
||||
if (!confirm('API를 선택하지 않으셨습니다. 계속하시겠습니까?')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
// Load all APIs for modal display FIRST (async)
|
||||
loadAllApisForModal();
|
||||
|
||||
// Load all APIs automatically on page load
|
||||
loadApis('');
|
||||
|
||||
// Update count after APIs are loaded (this will show count from restored session data)
|
||||
updateSelectedCount();
|
||||
|
||||
// Log restored selections for debugging
|
||||
if (selectedApis.size > 0) {
|
||||
console.log('Restored ' + selectedApis.size + ' selected APIs from session:', Array.from(selectedApis));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">기본 정보</div>
|
||||
</div>
|
||||
<div class="progress-line filled"></div>
|
||||
<div class="progress-step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
<div class="progress-line filled"></div>
|
||||
<div class="progress-step active completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Result -->
|
||||
<div class="register-result success" th:if="${modificationComplete}">
|
||||
|
||||
<!-- Success Icon Animation -->
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon success-icon">
|
||||
<svg class="checkmark" viewBox="0 0 52 52">
|
||||
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
|
||||
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div class="result-header">
|
||||
<h1>API Key 수정 요청 완료!</h1>
|
||||
<p class="result-description">
|
||||
API Key 수정 요청이 성공적으로 접수되었습니다. 관리자 승인 후 변경 사항이 적용됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Result Content -->
|
||||
<div class="result-content">
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="result-actions">
|
||||
<a th:href="@{/myapikey}" class="btn-secondary">
|
||||
<span class="icon">📋</span>
|
||||
API Key 목록으로
|
||||
</a>
|
||||
<a th:href="@{/myapikey/api_key_request/history}" class="btn-primary">
|
||||
<span class="icon">📄</span>
|
||||
요청 내역 확인
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Result (if needed) -->
|
||||
<div class="register-result error" th:unless="${modificationComplete}">
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon error-icon">
|
||||
<span>❌</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-header">
|
||||
<h1>잘못된 접근입니다</h1>
|
||||
<p class="result-description">
|
||||
수정 프로세스를 완료하지 않았거나 잘못된 경로로 접근하셨습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<a th:href="@{/myapikey}" class="btn-primary">
|
||||
목록으로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Success animation
|
||||
setTimeout(function() {
|
||||
const successIcon = document.querySelector('.success-icon');
|
||||
if (successIcon) {
|
||||
successIcon.classList.add('animate');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Success checkmark animation */
|
||||
.checkmark {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #6BCF7F;
|
||||
stroke-miterlimit: 10;
|
||||
animation: rotate 1s ease-in-out;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #6BCF7F;
|
||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,165 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/kbank_apikey_layout}">
|
||||
<head>
|
||||
<title>API 키 상세</title>
|
||||
</head>
|
||||
<body>
|
||||
<section layout:fragment="contentFragment" class="content">
|
||||
<div class="content_wrap">
|
||||
<div class="sub_title3">
|
||||
<h2 class="title">인증키 정보</h2>
|
||||
</div>
|
||||
|
||||
<div class="inner i_cs h_inner11">
|
||||
|
||||
<div class="form_type">
|
||||
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>인증키 이름</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="hidden" id="prodClientId" th:value="${@encryptionUtil.encrypt(apiKey.clientid)}" class="common_input_type_1" disabled="disabled">
|
||||
<input type="text" id="prodClientName" th:value="${apiKey.clientname}" class="common_input_type_1" disabled="disabled">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>변경일</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" th:value="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd')}" name="text" class="common_input_type_1" placeholder="2024.06.01" disabled="disabled">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>인증키 상태</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" th:value="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}" name="text" class="common_input_type_1" placeholder="활성화" disabled="disabled">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="terms_box title">
|
||||
<p>API 정보</p>
|
||||
</div>
|
||||
<div class="table_box">
|
||||
|
||||
<div class="pc-only">
|
||||
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="166px">
|
||||
<col width="230px">
|
||||
<col width="120px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>서비스</th>
|
||||
<th>API명</th>
|
||||
<th>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="api, apiStat : ${apiKey.apiList}">
|
||||
<td th:text="${apiStat.count}">14</td>
|
||||
<td th:text="${api.service}">공통</td>
|
||||
<td th:text="${api.apiDesc}">회원여부조회</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="m-only">
|
||||
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="46px">
|
||||
<col width="90px">
|
||||
<col width="140px">
|
||||
<col width="60px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>서비스</th>
|
||||
<th>API명</th>
|
||||
<th>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="api, apiStat : ${apiKey.apiList}">
|
||||
<td th:text="${apiStat.count}">14</td>
|
||||
<td th:text="${api.apiGroupId}">공통</td>
|
||||
<td th:text="${api.apiDesc}">회원여부조회</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn_wrap bt_location">
|
||||
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="btn_add btn_change_prod_api">API 변경</button>
|
||||
</div>
|
||||
<div class="btn_inventory btn_mtop">
|
||||
<a th:href="@{/myapikey/api_key_prod_page}" class="common_btn_type_1">목록</a>
|
||||
</div>
|
||||
<!-- // 기본 정보 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
|
||||
<div th:insert="~{fragment/apikey-request :: apiKeyRequest}" class="popup_total" id="apiKeyRequestPopup" style="display:none; z-index: 1000;"></div>
|
||||
<th:block th:replace="~{fragment/apikey-request :: apiKeyRequestScript}"></th:block>
|
||||
<div th:insert="~{fragment/apikey-request-prod :: prodApiKeyRequest}" class="popup_total" id="apiKeyProdRequestPopup" style="display:none; z-index: 1000;"></div>
|
||||
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
|
||||
</section>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
activateSelect();
|
||||
$('.btn_keydel').on('click', function(event) {
|
||||
event.preventDefault();
|
||||
$('.loading-overlay').show();
|
||||
const requestData = {
|
||||
clientId: document.getElementById("prodClientId").value,
|
||||
type: 'DELETE'
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/myapikey/api_key_request',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(requestData)
|
||||
}).done(function(response) {
|
||||
customPopups.showConfirm(response.msg, function() {
|
||||
window.location = '[[@{/myapikey/api_key_request/history}]]';
|
||||
});
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
customPopups.showAlert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
|
||||
}).always(function() {
|
||||
$('.loading-overlay').hide();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,89 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
|
||||
|
||||
<body>
|
||||
<section layout:fragment="contentFragment" class="content">
|
||||
<div class="content_wrap">
|
||||
<div class="sub_title3">
|
||||
<h2 class="title">인증키 목록</h2>
|
||||
</div>
|
||||
<div class="inner i_cs h_inner8">
|
||||
<div class="tab-wrap">
|
||||
<div class="tabs">
|
||||
<ul class="tab_nav tab_bt">
|
||||
<li>
|
||||
<a th:href="@{/myapikey}">개발</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a th:href="@{/myapikey/api_key_prod_page}">운영</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab active">
|
||||
<div class="tb_board_list">
|
||||
<div class="table_box">
|
||||
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="230px">
|
||||
<col width="120px">
|
||||
<col width="100px">
|
||||
<col width="120px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>이름</th>
|
||||
<th>인증키상태</th>
|
||||
<th>사용 API 수</th>
|
||||
<th>마지막수정일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="apikey, status : ${apiKeys}">
|
||||
<td th:text="${status.index + 1}">1</td>
|
||||
<td>
|
||||
<a th:href="@{/myapikey/api_key_prod_detail(id=${@encryptionUtil.encrypt(apikey.clientid)})}" th:text="${apikey.clientname}">수신</a>
|
||||
</td>
|
||||
<td th:text="${apikey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</td>
|
||||
<td th:text="${apikey.apiList.size()}">1</td>
|
||||
<td th:text="${#temporals.format(apikey.modifiedon, 'yyyy.MM.dd')}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn_wrap bt_location">
|
||||
<button sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" type="button" class="btn_key btn_request_prod_api" id="requestApiKey">운영 인증키 신청</button>
|
||||
</div>
|
||||
<div class="pagination page_top" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
|
||||
<form name="listForm" th:action="@{/myapikey/api_key_prod_page}" method="get">
|
||||
<input type="hidden" id="page" name="page"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" layout:fragment="pagePopups" class="content pop">
|
||||
<div th:insert="~{fragment/apikey-request-prod :: prodApiKeyRequest}" id="apiKeyProdRequestPopup" class="popup_total" style="display: none"></div>
|
||||
<th:block th:replace="~{fragment/apikey-request-prod :: prodApiKeyRequestScript}"></th:block>
|
||||
</section>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
activateSelect();
|
||||
});
|
||||
|
||||
function fn_select_page(pageNo) {
|
||||
document.listForm.page.value = pageNo;
|
||||
document.listForm.action = '[[@{/myapikey/api_key_prod_page}]]';
|
||||
document.listForm.submit();
|
||||
}
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,511 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step active">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-label">기본 정보</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="register-header">
|
||||
<h1>API Key 생성</h1>
|
||||
<h2>Step 1: 기본 정보 입력</h2>
|
||||
<p class="header-description">
|
||||
앱의 기본 정보를 입력해주세요. 이 정보는 API 관리 및 모니터링에 사용됩니다.
|
||||
</p>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-error" style="margin-top: 20px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Content -->
|
||||
<div class="register-content">
|
||||
<form id="registerStep1Form" method="post" th:action="@{/myapikey/register/step1}" th:object="${apiKeyRegistration}" class="register-form" enctype="multipart/form-data">
|
||||
|
||||
<div class="form-card">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="form-group">
|
||||
<label for="appIcon" class="form-label required">
|
||||
앱 아이콘
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<div class="icon-upload-wrapper">
|
||||
<div class="icon-preview" id="iconPreview">
|
||||
<div class="icon-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<span>이미지 선택</span>
|
||||
</div>
|
||||
<img id="iconPreviewImage" style="display: none;" alt="Icon Preview">
|
||||
<button type="button" class="btn-remove-icon" id="btnRemoveIcon" style="display: none;" onclick="removeAppIcon()" title="아이콘 삭제">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input type="file"
|
||||
id="appIcon"
|
||||
name="appIcon"
|
||||
class="icon-input"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
required>
|
||||
<button type="button" class="btn-icon-select" onclick="document.getElementById('appIcon').click()">
|
||||
파일 선택
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-hint">PNG, JPG 형식 (최대 2MB, 권장 크기: 512x512px)</small>
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<div class="form-group">
|
||||
<label for="appName" class="form-label required">
|
||||
앱 이름
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="appName"
|
||||
name="appName"
|
||||
th:field="*{appName}"
|
||||
class="form-input"
|
||||
placeholder="예: My Service API Client"
|
||||
required
|
||||
maxlength="100">
|
||||
<small class="form-hint">API Key를 식별할 수 있는 고유한 이름을 입력하세요.</small>
|
||||
</div>
|
||||
|
||||
<!-- App Description -->
|
||||
<div class="form-group">
|
||||
<label for="appDescription" class="form-label required">
|
||||
앱 설명
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<textarea id="appDescription"
|
||||
name="appDescription"
|
||||
th:field="*{appDescription}"
|
||||
class="form-textarea"
|
||||
placeholder="이 앱이 어떤 용도로 사용되는지 간단히 설명해주세요."
|
||||
rows="4"
|
||||
required
|
||||
maxlength="500"></textarea>
|
||||
<small class="form-hint">
|
||||
<span class="char-counter">0 / 500</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="form-group">
|
||||
<label for="callbackUrl" class="form-label">
|
||||
Callback URL
|
||||
</label>
|
||||
<input type="text"
|
||||
id="callbackUrl"
|
||||
name="callbackUrl"
|
||||
th:field="*{callbackUrl}"
|
||||
class="form-input"
|
||||
placeholder="https://example.com/api/callback">
|
||||
<small class="form-hint">OAuth 인증 후 리다이렉트될 URL을 입력하세요. (선택사항)</small>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="form-group">
|
||||
<label for="ipWhitelist" class="form-label">
|
||||
IP 화이트리스트
|
||||
</label>
|
||||
<div class="ip-input-group">
|
||||
<input type="text"
|
||||
id="ipWhitelistInput"
|
||||
class="form-input"
|
||||
placeholder="예: 192.168.1.1 또는 10.0.0.0/24">
|
||||
<button type="button" class="btn-add-ip" onclick="addIpAddress()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-hint">허용할 IP 주소 또는 CIDR 대역을 입력하세요. (선택사항)</small>
|
||||
|
||||
<!-- IP List Display -->
|
||||
<div class="ip-list" id="ipList" style="display: none;">
|
||||
<div class="ip-list-header">
|
||||
<span class="ip-count">등록된 IP: <strong id="ipCount">0</strong>개</span>
|
||||
</div>
|
||||
<div class="ip-items" id="ipItems">
|
||||
<!-- Dynamically added IP items -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input to store IP list -->
|
||||
<input type="hidden" id="ipWhitelist" name="ipWhitelist" value="">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<a href="/myapikey" class="btn-secondary" id="btnCancel">
|
||||
<span class="icon">←</span>
|
||||
취소
|
||||
</a>
|
||||
<button type="submit" class="btn-primary">
|
||||
다음 단계
|
||||
<span class="icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
// IP address list management
|
||||
let ipAddresses = [];
|
||||
|
||||
// Remove app icon
|
||||
function removeAppIcon() {
|
||||
const appIconInput = document.getElementById('appIcon');
|
||||
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
||||
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
|
||||
// Clear file input
|
||||
appIconInput.value = '';
|
||||
|
||||
// Hide preview image and show placeholder
|
||||
iconPreviewImage.style.display = 'none';
|
||||
iconPreviewImage.src = '';
|
||||
iconPlaceholder.style.display = 'flex';
|
||||
|
||||
// Hide delete button
|
||||
btnRemoveIcon.style.display = 'none';
|
||||
|
||||
// Add required attribute back since no icon is selected
|
||||
appIconInput.setAttribute('required', 'required');
|
||||
}
|
||||
|
||||
// Add IP address to the list
|
||||
function addIpAddress() {
|
||||
const ipInput = document.getElementById('ipWhitelistInput');
|
||||
const ipValue = ipInput.value.trim();
|
||||
|
||||
// Validate IP format
|
||||
if (!ipValue) {
|
||||
alert('IP 주소를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic IP validation (supports IP and CIDR notation)
|
||||
const ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
|
||||
if (!ipPattern.test(ipValue)) {
|
||||
alert('올바른 IP 주소 형식이 아닙니다.\n예: 192.168.1.1 또는 10.0.0.0/24');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if (ipAddresses.includes(ipValue)) {
|
||||
alert('이미 추가된 IP 주소입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to array
|
||||
ipAddresses.push(ipValue);
|
||||
|
||||
// Update UI
|
||||
updateIpList();
|
||||
|
||||
// Clear input
|
||||
ipInput.value = '';
|
||||
ipInput.focus();
|
||||
}
|
||||
|
||||
// Remove IP address from the list
|
||||
function removeIpAddress(ip) {
|
||||
const index = ipAddresses.indexOf(ip);
|
||||
if (index > -1) {
|
||||
ipAddresses.splice(index, 1);
|
||||
updateIpList();
|
||||
}
|
||||
}
|
||||
|
||||
// Update IP list display
|
||||
function updateIpList() {
|
||||
const ipList = document.getElementById('ipList');
|
||||
const ipItems = document.getElementById('ipItems');
|
||||
const ipCount = document.getElementById('ipCount');
|
||||
const ipWhitelistHidden = document.getElementById('ipWhitelist');
|
||||
|
||||
// Update count
|
||||
ipCount.textContent = ipAddresses.length;
|
||||
|
||||
// Show/hide list container
|
||||
if (ipAddresses.length > 0) {
|
||||
ipList.style.display = 'block';
|
||||
|
||||
// Build IP items HTML
|
||||
let itemsHTML = '';
|
||||
ipAddresses.forEach(function(ip) {
|
||||
itemsHTML += `
|
||||
<div class="ip-item">
|
||||
<span class="ip-address">${ip}</span>
|
||||
<button type="button" class="btn-remove-ip" onclick="removeIpAddress('${ip}')" title="제거">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
ipItems.innerHTML = itemsHTML;
|
||||
} else {
|
||||
ipList.style.display = 'none';
|
||||
ipItems.innerHTML = '';
|
||||
}
|
||||
|
||||
// Update hidden input with comma-separated values
|
||||
ipWhitelistHidden.value = ipAddresses.join(',');
|
||||
}
|
||||
|
||||
// Allow Enter key to add IP
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Only clear sessionStorage if starting a new registration (clear=true parameter)
|
||||
// This preserves data during page refresh or back navigation from Step 2
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const shouldClear = urlParams.get('clear') === 'true';
|
||||
|
||||
if (shouldClear) {
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key && key.startsWith('registration_')) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
||||
}
|
||||
|
||||
const form = document.getElementById('registerStep1Form');
|
||||
const appDescTextarea = document.getElementById('appDescription');
|
||||
const charCounter = document.querySelector('.char-counter');
|
||||
const appIconInput = document.getElementById('appIcon');
|
||||
const iconPreviewImage = document.getElementById('iconPreviewImage');
|
||||
const iconPlaceholder = document.querySelector('.icon-placeholder');
|
||||
const ipWhitelistInput = document.getElementById('ipWhitelistInput');
|
||||
|
||||
// Restore IP whitelist from session (Thymeleaf provides this)
|
||||
/*<![CDATA[*/
|
||||
const sessionIpWhitelist = /*[[${apiKeyRegistration.ipWhitelistAsString}]]*/ '';
|
||||
if (sessionIpWhitelist && sessionIpWhitelist.trim() !== '') {
|
||||
const ips = sessionIpWhitelist.split(',');
|
||||
ips.forEach(function(ip) {
|
||||
const trimmedIp = ip.trim();
|
||||
if (trimmedIp && !ipAddresses.includes(trimmedIp)) {
|
||||
ipAddresses.push(trimmedIp);
|
||||
}
|
||||
});
|
||||
updateIpList();
|
||||
}
|
||||
|
||||
// Restore app icon preview from session if exists
|
||||
const appIconFileName = /*[[${apiKeyRegistration.appIconFileName}]]*/ '';
|
||||
const appIconBase64 = /*[[${apiKeyRegistration.appIconBase64}]]*/ '';
|
||||
|
||||
if (appIconFileName && appIconFileName.trim() !== '' && appIconBase64) {
|
||||
// Display the actual uploaded image from session
|
||||
iconPreviewImage.src = appIconBase64;
|
||||
iconPreviewImage.style.display = 'block';
|
||||
iconPlaceholder.style.display = 'none';
|
||||
|
||||
// Show delete button
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
btnRemoveIcon.style.display = 'flex';
|
||||
|
||||
// Remove required attribute since we already have an image in session
|
||||
appIconInput.removeAttribute('required');
|
||||
}
|
||||
/*]]>*/
|
||||
|
||||
// Initialize character counter for existing description
|
||||
if (appDescTextarea && charCounter) {
|
||||
const currentLength = appDescTextarea.value.length;
|
||||
const maxLength = appDescTextarea.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
}
|
||||
|
||||
// Character counter for description
|
||||
if (appDescTextarea && charCounter) {
|
||||
appDescTextarea.addEventListener('input', function() {
|
||||
const currentLength = this.value.length;
|
||||
const maxLength = this.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
// Change color when near limit
|
||||
if (currentLength > maxLength * 0.9) {
|
||||
charCounter.style.color = '#FF6B6B';
|
||||
} else {
|
||||
charCounter.style.color = '#94A3B8';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// App icon preview
|
||||
if (appIconInput) {
|
||||
appIconInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
const btnRemoveIcon = document.getElementById('btnRemoveIcon');
|
||||
|
||||
if (file) {
|
||||
// Validate file size (max 2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('파일 크기는 2MB를 초과할 수 없습니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type - Only PNG and JPG allowed
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert('PNG, JPG 형식만 업로드 가능합니다.');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show preview
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
iconPreviewImage.src = event.target.result;
|
||||
iconPreviewImage.style.display = 'block';
|
||||
iconPlaceholder.style.display = 'none';
|
||||
|
||||
// Show delete button
|
||||
btnRemoveIcon.style.display = 'flex';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Remove required attribute since a file is now selected
|
||||
appIconInput.removeAttribute('required');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IP whitelist input - Enter key handler
|
||||
if (ipWhitelistInput) {
|
||||
ipWhitelistInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addIpAddress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel button handler - clear sessionStorage and notify server
|
||||
const btnCancel = document.getElementById('btnCancel');
|
||||
if (btnCancel) {
|
||||
btnCancel.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Clear all registration-related data from sessionStorage
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key && key.startsWith('registration_')) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => sessionStorage.removeItem(key));
|
||||
|
||||
// Navigate to cancel endpoint to clear server session
|
||||
window.location.href = '/myapikey/register/cancel';
|
||||
});
|
||||
}
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
const appIcon = document.getElementById('appIcon').files[0];
|
||||
const appName = document.getElementById('appName').value.trim();
|
||||
const appDescription = document.getElementById('appDescription').value.trim();
|
||||
const callbackUrl = document.getElementById('callbackUrl').value.trim();
|
||||
|
||||
// Check if app icon exists (either newly uploaded or from session)
|
||||
const hasSessionIcon = appIconFileName && appIconFileName.trim() !== '';
|
||||
if (!appIcon && !hasSessionIcon) {
|
||||
e.preventDefault();
|
||||
alert('앱 아이콘을 선택해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!appName) {
|
||||
e.preventDefault();
|
||||
alert('앱 이름을 입력해주세요.');
|
||||
document.getElementById('appName').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!appDescription) {
|
||||
e.preventDefault();
|
||||
alert('앱 설명을 입력해주세요.');
|
||||
document.getElementById('appDescription').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate URL format only if callbackUrl is provided (optional field)
|
||||
if (callbackUrl && callbackUrl.trim() !== '') {
|
||||
try {
|
||||
new URL(callbackUrl);
|
||||
} catch (error) {
|
||||
e.preventDefault();
|
||||
alert('올바른 URL 형식이 아닙니다.\n예: https://example.com/api/callback');
|
||||
document.getElementById('callbackUrl').focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Store data in sessionStorage for later steps
|
||||
sessionStorage.setItem('registration_appName', appName);
|
||||
sessionStorage.setItem('registration_appDescription', appDescription);
|
||||
sessionStorage.setItem('registration_callbackUrl', callbackUrl);
|
||||
sessionStorage.setItem('registration_ipWhitelist', ipAddresses.join(','));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,670 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">기본 정보</div>
|
||||
</div>
|
||||
<div class="progress-line filled"></div>
|
||||
<div class="progress-step active">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="register-header">
|
||||
<h1>API Key 생성</h1>
|
||||
<h2>Step 2: API 선택</h2>
|
||||
<p class="header-description">
|
||||
사용할 API를 선택해주세요. 나중에 추가하거나 변경할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Container with Sidebar Layout -->
|
||||
<section class="apikey-register-container with-sidebar">
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="apikey-register-sidebar" id="apiSidebar">
|
||||
<nav class="apikey-sidebar-nav">
|
||||
<!-- All APIs -->
|
||||
<div class="menu-section">
|
||||
<div class="menu-title active" data-group="">
|
||||
전체
|
||||
<span class="api-count" th:text="${totalApiCount ?: 0}">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Categories -->
|
||||
<div class="menu-section" th:each="service : ${apiServices}">
|
||||
<div class="menu-title"
|
||||
th:data-group="${service.id}">
|
||||
[[${service.groupName}]]
|
||||
<span class="api-count" th:text="${service.apiGroupApiList.size()}">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button class="apikey-mobile-toggle" id="mobileToggle" aria-label="메뉴 열기">
|
||||
☰
|
||||
</button>
|
||||
|
||||
<!-- Mobile Overlay -->
|
||||
<div class="apikey-mobile-overlay" id="mobileOverlay"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="apikey-register-content">
|
||||
<form id="registerStep2Form" method="post" th:action="@{/myapikey/register/step2}" class="register-form">
|
||||
|
||||
<!-- Search Box with Select All -->
|
||||
<div class="api-filter-header">
|
||||
<div class="select-all-wrapper" id="selectAllWrapper" style="display: none;">
|
||||
<label class="select-all-label">
|
||||
<input type="checkbox" id="selectAllCheckbox" class="select-all-checkbox">
|
||||
<span id="selectAllText">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="api-search-box">
|
||||
<input type="text"
|
||||
id="apiSearch"
|
||||
class="search-input"
|
||||
placeholder="API 검색...">
|
||||
<span class="search-icon">🔍</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Count -->
|
||||
<div class="api-result-count">
|
||||
총 <strong id="visibleCount">0</strong>건
|
||||
</div>
|
||||
|
||||
<!-- API Cards Grid -->
|
||||
<div class="api-selection-card-grid" id="apiCardGrid">
|
||||
<!-- Loading state -->
|
||||
<div class="loading-state" id="loadingState" style="grid-column: 1 / -1;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>API 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (initially hidden) -->
|
||||
<div class="empty-api-state" id="emptyState" style="display: none; grid-column: 1 / -1;">
|
||||
<div class="empty-icon">📦</div>
|
||||
<h3>API를 선택해주세요</h3>
|
||||
<p>왼쪽 메뉴에서 서비스를 선택하면 해당 API 목록이 표시됩니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- API cards will be dynamically loaded here -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<button type="button" id="btnPrevStep" class="btn-secondary">
|
||||
<span class="icon">←</span>
|
||||
이전 단계
|
||||
</button>
|
||||
<button type="submit" class="btn-primary">
|
||||
저장
|
||||
<span class="icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</main>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Floating Cart Button -->
|
||||
<button type="button" class="floating-cart-btn" id="floatingCartBtn" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1"></circle>
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
<span class="cart-text">선택된 API:</span>
|
||||
<span class="cart-badge" id="cartBadge">0</span>
|
||||
</button>
|
||||
|
||||
<!-- Selected APIs Modal - Uses base modal component -->
|
||||
<div class="selected-apis-modal" id="selectedApisModal" style="display: none;">
|
||||
<div class="modal-backdrop" id="modalOverlay"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">선택된 API 목록</h3>
|
||||
<button type="button" class="modal-close" id="modalCloseBtn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="selected-apis-list" id="modalSelectedList">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="modalCancelBtn">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM Elements
|
||||
const form = document.getElementById('registerStep2Form');
|
||||
const searchInput = document.getElementById('apiSearch');
|
||||
const sidebar = document.getElementById('apiSidebar');
|
||||
const mobileToggle = document.getElementById('mobileToggle');
|
||||
const mobileOverlay = document.getElementById('mobileOverlay');
|
||||
const menuTitles = document.querySelectorAll('.menu-title');
|
||||
const visibleCount = document.getElementById('visibleCount');
|
||||
const apiCardGrid = document.getElementById('apiCardGrid');
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
let currentFilter = ''; // Empty string means "all"
|
||||
let currentServiceName = '전체'; // Store current service name
|
||||
let allApis = []; // Store loaded APIs
|
||||
let selectedApis = new Set(); // Track selected API IDs
|
||||
|
||||
// Restore selected APIs from session
|
||||
const sessionSelectedApis = /*[[${apiKeyRegistration.selectedApis}]]*/ [];
|
||||
if (sessionSelectedApis && Array.isArray(sessionSelectedApis)) {
|
||||
sessionSelectedApis.forEach(function(apiId) {
|
||||
selectedApis.add(apiId);
|
||||
});
|
||||
}
|
||||
|
||||
// Load APIs via AJAX
|
||||
function loadApis(groupId) {
|
||||
// Show loading state
|
||||
loadingState.style.display = 'block';
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
// Remove existing API cards
|
||||
document.querySelectorAll('.api-selection-card').forEach(card => card.remove());
|
||||
|
||||
// Build URL with optional groupId filter
|
||||
let url = '/apis/for_request';
|
||||
if (groupId) {
|
||||
url += '?groupIds=' + encodeURIComponent(groupId);
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(apis => {
|
||||
allApis = apis;
|
||||
loadingState.style.display = 'none';
|
||||
|
||||
if (apis.length === 0) {
|
||||
emptyState.style.display = 'block';
|
||||
visibleCount.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
renderApiCards(apis);
|
||||
visibleCount.textContent = apis.length;
|
||||
updateSelectAllUI();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load APIs:', error);
|
||||
loadingState.style.display = 'none';
|
||||
emptyState.querySelector('h3').textContent = 'API 로드 실패';
|
||||
emptyState.querySelector('p').textContent = '다시 시도해주세요.';
|
||||
emptyState.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Render API cards
|
||||
function renderApiCards(apis) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
apis.forEach(api => {
|
||||
const card = createApiCard(api);
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
|
||||
apiCardGrid.appendChild(fragment);
|
||||
|
||||
// Re-attach event listeners
|
||||
attachCardEventListeners();
|
||||
}
|
||||
|
||||
// Create API card element
|
||||
function createApiCard(api) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'api-selection-card';
|
||||
card.setAttribute('data-group', api.apiGroupId || '');
|
||||
card.setAttribute('data-name', (api.apiName || '').toLowerCase());
|
||||
card.setAttribute('data-desc', (api.apiSimpleDescription || '').toLowerCase());
|
||||
card.setAttribute('data-api-id', api.apiId);
|
||||
|
||||
const isSelected = selectedApis.has(api.apiId);
|
||||
if (isSelected) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox"
|
||||
name="selectedApis"
|
||||
value="${api.apiId}"
|
||||
id="api-${api.apiId}"
|
||||
class="api-checkbox"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
</div>
|
||||
|
||||
<label class="api-card-content" for="api-${api.apiId}">
|
||||
<div class="api-card-header">
|
||||
<span class="api-card-badge">${api.service || '카테고리'}</span>
|
||||
<span class="api-method">${api.apiMethod || 'GET'}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="api-name">${api.apiName || 'API 이름'}</h3>
|
||||
<p class="api-description">${api.apiSimpleDescription || 'API 설명이 없습니다.'}</p>
|
||||
|
||||
<div class="api-endpoint">
|
||||
<code>${api.apiUrl || '/api/v1/example'}</code>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Attach event listeners to cards
|
||||
function attachCardEventListeners() {
|
||||
const checkboxes = document.querySelectorAll('.api-checkbox');
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
updateCardSelection(this);
|
||||
updateSelectedCount();
|
||||
});
|
||||
});
|
||||
|
||||
apiCards.forEach(card => {
|
||||
card.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('api-checkbox') || e.target.tagName === 'LABEL') {
|
||||
return;
|
||||
}
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update card visual state
|
||||
function updateCardSelection(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
if (checkbox.checked) {
|
||||
card.classList.add('selected');
|
||||
selectedApis.add(checkbox.value);
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
selectedApis.delete(checkbox.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected count
|
||||
function updateSelectedCount() {
|
||||
// Update floating cart button
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const cartBadge = document.getElementById('cartBadge');
|
||||
|
||||
if (selectedApis.size > 0) {
|
||||
floatingCartBtn.style.display = 'flex';
|
||||
cartBadge.textContent = selectedApis.size;
|
||||
} else {
|
||||
floatingCartBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update modal list
|
||||
updateModalList();
|
||||
|
||||
// Update select all checkbox state
|
||||
updateSelectAllCheckboxState();
|
||||
}
|
||||
|
||||
// Update select all UI visibility and text
|
||||
function updateSelectAllUI() {
|
||||
const selectAllWrapper = document.getElementById('selectAllWrapper');
|
||||
const selectAllText = document.getElementById('selectAllText');
|
||||
|
||||
if (currentFilter === '') {
|
||||
// Hide select all when "전체" is selected
|
||||
selectAllWrapper.style.display = 'none';
|
||||
} else {
|
||||
// Show select all with service name
|
||||
selectAllWrapper.style.display = 'flex';
|
||||
selectAllText.textContent = currentServiceName + ' API 전체 선택';
|
||||
}
|
||||
|
||||
updateSelectAllCheckboxState();
|
||||
}
|
||||
|
||||
// Update select all checkbox state based on visible cards
|
||||
function updateSelectAllCheckboxState() {
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
if (visibleCards.length === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCheckboxes = visibleCards.map(card => card.querySelector('.api-checkbox'));
|
||||
const checkedCount = visibleCheckboxes.filter(cb => cb.checked).length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else if (checkedCount === visibleCheckboxes.length) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update modal selected APIs list
|
||||
function updateModalList() {
|
||||
const modalSelectedList = document.getElementById('modalSelectedList');
|
||||
modalSelectedList.innerHTML = '';
|
||||
|
||||
if (selectedApis.size === 0) {
|
||||
modalSelectedList.innerHTML = '<p class="empty-message">선택된 API가 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all checked checkboxes
|
||||
const checkedBoxes = document.querySelectorAll('.api-checkbox:checked');
|
||||
checkedBoxes.forEach(function(checkbox) {
|
||||
const card = checkbox.closest('.api-selection-card');
|
||||
const apiName = card.querySelector('.api-name').textContent;
|
||||
|
||||
const apiPill = document.createElement('div');
|
||||
apiPill.className = 'api-pill';
|
||||
apiPill.innerHTML = `
|
||||
<span class="api-pill-name">${apiName}</span>
|
||||
<button type="button" class="api-pill-remove" data-value="${checkbox.value}" aria-label="Remove ${apiName}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
modalSelectedList.appendChild(apiPill);
|
||||
});
|
||||
|
||||
// Add remove handlers
|
||||
document.querySelectorAll('.api-pill-remove').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const value = this.getAttribute('data-value');
|
||||
const checkbox = document.querySelector('.api-checkbox[value="' + value + '"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateCardSelection(checkbox);
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
let visibleCountNum = 0;
|
||||
|
||||
const apiCards = document.querySelectorAll('.api-selection-card');
|
||||
apiCards.forEach(function(card) {
|
||||
const apiName = card.getAttribute('data-name');
|
||||
const apiDesc = card.getAttribute('data-desc');
|
||||
|
||||
// Check if matches search
|
||||
const matchesSearch = apiName.includes(searchTerm) || apiDesc.includes(searchTerm);
|
||||
|
||||
if (matchesSearch) {
|
||||
card.style.display = '';
|
||||
visibleCountNum++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
visibleCount.textContent = visibleCountNum;
|
||||
|
||||
// Update select all checkbox state after search
|
||||
updateSelectAllCheckboxState();
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar category selection
|
||||
menuTitles.forEach(function(title) {
|
||||
title.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove active from all
|
||||
menuTitles.forEach(t => t.classList.remove('active'));
|
||||
// Add active to clicked
|
||||
this.classList.add('active');
|
||||
|
||||
const groupId = this.getAttribute('data-group');
|
||||
currentFilter = groupId;
|
||||
|
||||
// Store service name for select all label
|
||||
currentServiceName = this.textContent.trim().split('\n')[0].trim();
|
||||
|
||||
// Load APIs for selected service
|
||||
loadApis(groupId);
|
||||
|
||||
// Clear search when changing category
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
// Close mobile menu if open
|
||||
if (window.innerWidth <= 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
function closeMobileMenu() {
|
||||
sidebar.classList.remove('mobile-open');
|
||||
mobileOverlay.classList.remove('active');
|
||||
mobileToggle.innerHTML = '☰';
|
||||
mobileToggle.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
|
||||
if (mobileToggle && sidebar && mobileOverlay) {
|
||||
mobileToggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
mobileOverlay.classList.toggle('active');
|
||||
|
||||
if (sidebar.classList.contains('mobile-open')) {
|
||||
this.innerHTML = '✕';
|
||||
this.setAttribute('aria-label', '메뉴 닫기');
|
||||
} else {
|
||||
this.innerHTML = '☰';
|
||||
this.setAttribute('aria-label', '메뉴 열기');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu on overlay click
|
||||
mobileOverlay.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile menu on resize to desktop
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal control
|
||||
const floatingCartBtn = document.getElementById('floatingCartBtn');
|
||||
const selectedApisModal = document.getElementById('selectedApisModal');
|
||||
const modalOverlay = document.getElementById('modalOverlay');
|
||||
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
||||
const modalCancelBtn = document.getElementById('modalCancelBtn');
|
||||
|
||||
// Open modal
|
||||
function openModal() {
|
||||
selectedApisModal.style.display = 'block';
|
||||
// Add show class to backdrop and modal for visibility
|
||||
setTimeout(function() {
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.add('show');
|
||||
}
|
||||
selectedApisModal.classList.add('show');
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden'; // Prevent background scroll
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
// Remove show class first for transition
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.remove('show');
|
||||
}
|
||||
selectedApisModal.classList.remove('show');
|
||||
|
||||
// Hide modal after transition
|
||||
setTimeout(function() {
|
||||
selectedApisModal.style.display = 'none';
|
||||
}, 300);
|
||||
|
||||
document.body.style.overflow = ''; // Restore scroll
|
||||
}
|
||||
|
||||
// Floating cart button click
|
||||
if (floatingCartBtn) {
|
||||
floatingCartBtn.addEventListener('click', openModal);
|
||||
}
|
||||
|
||||
// Modal overlay click
|
||||
if (modalOverlay) {
|
||||
modalOverlay.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Modal close button click
|
||||
if (modalCloseBtn) {
|
||||
modalCloseBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Modal cancel button click
|
||||
if (modalCancelBtn) {
|
||||
modalCancelBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// Close modal on ESC key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && selectedApisModal.style.display === 'block') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Select All checkbox event
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const visibleCards = Array.from(document.querySelectorAll('.api-selection-card')).filter(card => card.style.display !== 'none');
|
||||
|
||||
visibleCards.forEach(function(card) {
|
||||
const checkbox = card.querySelector('.api-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
updateCardSelection(checkbox);
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectedCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Previous step button - save current selections before going back
|
||||
const btnPrevStep = document.getElementById('btnPrevStep');
|
||||
if (btnPrevStep) {
|
||||
btnPrevStep.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Change form action to save endpoint
|
||||
const originalAction = form.action;
|
||||
form.action = '/myapikey/register/step2/save';
|
||||
|
||||
// Submit the form to save selections
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Form validation
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (selectedApis.size === 0) {
|
||||
e.preventDefault();
|
||||
if (confirm('API를 선택하지 않으셨습니다. 나중에 추가하시겠습니까?')) {
|
||||
// Allow form submission without selected APIs
|
||||
form.submit();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store selected APIs in sessionStorage
|
||||
sessionStorage.setItem('registration_selectedApis', JSON.stringify(Array.from(selectedApis)));
|
||||
});
|
||||
|
||||
// Initialize
|
||||
updateSelectedCount(); // This will show count from restored session data
|
||||
|
||||
// Load all APIs automatically on page load (empty string = all APIs)
|
||||
loadApis('');
|
||||
|
||||
// Load data from step 1 (if needed for display)
|
||||
const appName = sessionStorage.getItem('registration_appName');
|
||||
if (appName) {
|
||||
console.log('App Name from Step 1:', appName);
|
||||
}
|
||||
|
||||
// Log restored selections for debugging
|
||||
if (selectedApis.size > 0) {
|
||||
console.log('Restored ' + selectedApis.size + ' selected APIs from session:', Array.from(selectedApis));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,251 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kjbank_base_layout}">
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
<section class="apikey-register-container">
|
||||
|
||||
<!-- Progress Indicator -->
|
||||
<div class="register-progress">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">기본 정보</div>
|
||||
</div>
|
||||
<div class="progress-line filled"></div>
|
||||
<div class="progress-step completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">API 선택</div>
|
||||
</div>
|
||||
<div class="progress-line filled"></div>
|
||||
<div class="progress-step active completed">
|
||||
<div class="step-number">✓</div>
|
||||
<div class="step-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Result -->
|
||||
<div class="register-result success" th:if="${registrationComplete}">
|
||||
|
||||
<!-- Success Icon Animation -->
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon success-icon">
|
||||
<svg class="checkmark" viewBox="0 0 52 52">
|
||||
<circle class="checkmark-circle" cx="26" cy="26" r="25" fill="none"/>
|
||||
<path class="checkmark-check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div class="result-header">
|
||||
<h1>API Key 생성 완료!</h1>
|
||||
<p class="result-description">
|
||||
API Key가 성공적으로 생성되었습니다. 아래 정보를 안전한 곳에 보관해주세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- API Key Display -->
|
||||
<div class="result-content">
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="result-actions">
|
||||
<a href="/myapikey" class="btn-secondary">
|
||||
<span class="icon">📋</span>
|
||||
API Key 목록으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Result (if needed) -->
|
||||
<div class="register-result error" th:unless="${registrationComplete}" style="display: none;">
|
||||
<div class="result-icon-wrapper">
|
||||
<div class="result-icon error-icon">
|
||||
<span>❌</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-header">
|
||||
<h1>API Key 생성 실패</h1>
|
||||
<p class="result-description">
|
||||
API Key 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<a href="/myapikey/register/step1" class="btn-secondary">
|
||||
다시 시도
|
||||
</a>
|
||||
<a href="/myapikey" class="btn-primary">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Load data from sessionStorage
|
||||
const appName = sessionStorage.getItem('registration_appName');
|
||||
const selectedApis = JSON.parse(sessionStorage.getItem('registration_selectedApis') || '[]');
|
||||
|
||||
// Update display with actual data
|
||||
if (appName) {
|
||||
document.getElementById('resultAppName').textContent = appName;
|
||||
}
|
||||
|
||||
// Copy to clipboard functionality
|
||||
document.querySelectorAll('.btn-copy').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const targetId = this.getAttribute('data-copy');
|
||||
const targetElement = document.getElementById(targetId);
|
||||
const textToCopy = targetElement.textContent;
|
||||
|
||||
// Create temporary input
|
||||
const tempInput = document.createElement('input');
|
||||
tempInput.value = textToCopy;
|
||||
document.body.appendChild(tempInput);
|
||||
tempInput.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
|
||||
// Visual feedback
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '<span class="copy-icon">✓</span> 복사됨';
|
||||
this.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalText;
|
||||
this.classList.remove('copied');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
alert('복사에 실패했습니다. 수동으로 복사해주세요.');
|
||||
}
|
||||
|
||||
document.body.removeChild(tempInput);
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle visibility for secret key
|
||||
document.querySelectorAll('.btn-toggle-visibility').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const targetId = this.getAttribute('data-target');
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement.classList.contains('blurred')) {
|
||||
targetElement.classList.remove('blurred');
|
||||
this.innerHTML = '<span class="visibility-icon">🙈</span>';
|
||||
} else {
|
||||
targetElement.classList.add('blurred');
|
||||
this.innerHTML = '<span class="visibility-icon">👁️</span>';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Download key information
|
||||
document.getElementById('downloadKeyBtn')?.addEventListener('click', function() {
|
||||
const keyInfo = {
|
||||
appName: document.getElementById('resultAppName').textContent,
|
||||
clientId: document.getElementById('clientId').textContent,
|
||||
clientSecret: document.getElementById('clientSecret').textContent,
|
||||
createdAt: new Date().toISOString(),
|
||||
selectedApis: selectedApis
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(keyInfo, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
|
||||
|
||||
const exportFileDefaultName = 'apikey_' + keyInfo.appName.replace(/\s+/g, '_') + '.json';
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
linkElement.click();
|
||||
});
|
||||
|
||||
// Clear session storage after successful registration
|
||||
if (/*[[${registrationComplete}]]*/ false) {
|
||||
sessionStorage.removeItem('registration_appName');
|
||||
sessionStorage.removeItem('registration_appDescription');
|
||||
sessionStorage.removeItem('registration_appType');
|
||||
sessionStorage.removeItem('registration_environment');
|
||||
sessionStorage.removeItem('registration_selectedApis');
|
||||
}
|
||||
|
||||
// Success animation
|
||||
setTimeout(function() {
|
||||
const successIcon = document.querySelector('.success-icon');
|
||||
if (successIcon) {
|
||||
successIcon.classList.add('animate');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Blur effect for secret key */
|
||||
.key-value.blurred {
|
||||
filter: blur(5px);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Copy button feedback */
|
||||
.btn-copy.copied {
|
||||
background-color: #6BCF7F !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Success checkmark animation */
|
||||
.checkmark {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #6BCF7F;
|
||||
stroke-miterlimit: 10;
|
||||
animation: rotate 1s ease-in-out;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #6BCF7F;
|
||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
}
|
||||
|
||||
.checkmark-check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,191 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
|
||||
<head>
|
||||
<title>API 요청 상세</title>
|
||||
</head>
|
||||
<body>
|
||||
<section class="content" layout:fragment="contentFragment">
|
||||
<div class="content_wrap">
|
||||
<div class="sub_title3">
|
||||
<h2 class="title">운영 인증키 요청 정보</h2>
|
||||
</div>
|
||||
|
||||
<div class="inner i_cs h_inner11">
|
||||
<div class="form_type">
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>이름</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.clientName}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>유형</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.type.description}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>상태</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.approval.approvalStatus.getDescription()}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>사유</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${appRequest.reason}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info1 info_mypage">
|
||||
<p class="title">
|
||||
<span>요청일시</span>
|
||||
</p>
|
||||
<div class="info_line">
|
||||
<div class="info_box1 w_inp6">
|
||||
<input type="text" name="text" class="common_input_type_1" disabled="disabled" th:value="${#temporals.format(appRequest.approval.createdDate, 'yyyy-MM-dd HH:mm')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terms_box title">
|
||||
<p>API 정보</p>
|
||||
</div>
|
||||
|
||||
<div class="table_box">
|
||||
|
||||
<div class="pc-only">
|
||||
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="166px">
|
||||
<col width="230px">
|
||||
<col width="80px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>서비스</th>
|
||||
<th>API명</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="api,status : ${appRequest.apiSpecList}"
|
||||
th:class="${appRequest.type != null ?
|
||||
((appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
|
||||
(#strings.isEmpty(appRequest.prevApiList) ? '' :
|
||||
(!#strings.contains(appRequest.prevApiList, api.apiId) ? 'highlight_new' : '')) :
|
||||
((appRequest.type.toString() == 'DELETE' or appRequest.type.toString() == 'PROD_DELETE') ? 'highlight_delete' : '')) : ''}">
|
||||
<td th:text="${status.index + 1}"></td>
|
||||
<td th:text="${api.service}"></td>
|
||||
<td th:text="${api.apiName}"></td>
|
||||
<td th:class="status"
|
||||
th:text="${(appRequest.type.toString() =='MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
|
||||
(#strings.isEmpty(appRequest.prevApiList) ? '' : (#strings.contains(appRequest.prevApiList, api.apiId) ? '' : '추가')) : ''}"></td>
|
||||
</tr>
|
||||
<tr class="highlight_delete"
|
||||
th:if="${!#strings.isEmpty(appRequest.prevApiList) and (appRequest.type != null and
|
||||
(appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY'))}"
|
||||
th:each="apiId : ${#strings.arraySplit(appRequest.prevApiList,',')}"
|
||||
th:with="api=${@apiService.selectDetail(apiId)},
|
||||
serviceDto=${@apiServiceService.findApiServiceByApiId(apiId)}"
|
||||
th:unless="${#strings.contains(appRequest.apiList, apiId)}">
|
||||
<td></td>
|
||||
<td th:text="${serviceDto != null ? serviceDto.getGroupName() : ''}"></td>
|
||||
<td th:text="${api.apiName}"></td>
|
||||
<td th:class="status">삭제</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="m-only">
|
||||
<table class="table tb_key m_top" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="46px">
|
||||
<col width="166px">
|
||||
<col width="140px">
|
||||
<col width="40px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>서비스</th>
|
||||
<th>API명</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="api,status : ${appRequest.apiSpecList}"
|
||||
th:class="${appRequest.type != null ?
|
||||
((appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
|
||||
(#strings.isEmpty(appRequest.prevApiList) ? '' :
|
||||
(!#strings.contains(appRequest.prevApiList, api.apiId) ? 'highlight_new' : '')) :
|
||||
((appRequest.type.toString() == 'DELETE' or appRequest.type.toString() == 'PROD_DELETE') ? 'highlight_delete' : '')) : ''}">
|
||||
<td th:text="${status.index + 1}"></td>
|
||||
<td th:text="${api.service}"></td>
|
||||
<td th:text="${api.apiName}"></td>
|
||||
<td th:class="status"
|
||||
th:text="${(appRequest.type.toString() =='MODIFY' or appRequest.type.toString() == 'PROD_MODIFY') ?
|
||||
(#strings.isEmpty(appRequest.prevApiList) ? '' : (#strings.contains(appRequest.prevApiList, api.apiId) ? '' : '추가')) : ''}"></td>
|
||||
</tr>
|
||||
<tr class="highlight_delete"
|
||||
th:if="${!#strings.isEmpty(appRequest.prevApiList) and (appRequest.type != null and
|
||||
(appRequest.type.toString() == 'MODIFY' or appRequest.type.toString() == 'PROD_MODIFY'))}"
|
||||
th:each="apiId : ${#strings.arraySplit(appRequest.prevApiList,',')}"
|
||||
th:with="api=${@apiService.selectDetail(apiId)},
|
||||
serviceDto=${@apiServiceService.findApiServiceByApiId(apiId)}"
|
||||
th:unless="${#strings.contains(appRequest.apiList, apiId)}">
|
||||
<td></td>
|
||||
<td th:text="${serviceDto != null ? serviceDto.getGroupName() : ''}"></td>
|
||||
<td th:text="${api.apiName}"></td>
|
||||
<td th:class="status">삭제</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn_wrap bt_location" th:if="${appRequest.approval.approvalStatus.toString() == 'REQUESTED'}">
|
||||
<form th:action="@{/myapikey/api_key_request/cancel}" method="post">
|
||||
<input type="hidden" name="id" th:value="${appRequest.id}">
|
||||
<button type="submit" class="btn_del">취소</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="btn_inventory btn_mtop">
|
||||
<a th:href="@{/myapikey/api_key_request/prod_history}" class="common_btn_type_1">목록</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="copyToast" class="toast" style="position: absolute; top: 20px; right: 20px;" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto">알림</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
복사되었습니다
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script layout:fragment="script">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,114 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_apikey_layout}">
|
||||
<body>
|
||||
<section class="content" layout:fragment="contentFragment">
|
||||
<div class="content_wrap">
|
||||
<div class="sub_title3">
|
||||
<h2 class="title">인증키 신청 목록</h2>
|
||||
</div>
|
||||
<div class="inner i_cs h_inner8">
|
||||
<div class="tab-wrap">
|
||||
<div class="tabs">
|
||||
<ul class="tab_nav tab_bt">
|
||||
<li>
|
||||
<a th:href="@{/myapikey/api_key_request/history}">개발</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a th:href="@{/myapikey/api_key_request/prod_history}">운영</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab active">
|
||||
<div class="tb_board_list">
|
||||
<div class="table_box">
|
||||
<div class="pc-only">
|
||||
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="150px">
|
||||
<col width="90px">
|
||||
<col width="90px">
|
||||
<col width="90px">
|
||||
<col width="250px">
|
||||
<col width="100px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>신청명</th>
|
||||
<th>신청상태</th>
|
||||
<th>신청일</th>
|
||||
<th>처리일자</th>
|
||||
<th>신청사유</th>
|
||||
<th>사용 API 수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="request, status : ${requests}">
|
||||
<td th:text="${status.index + 1}">1</td>
|
||||
<td><a th:href="@{/myapikey/api_key_request/prod_detail(id=${request.id})}" class="btn btn-sm btn-primary" th:text="'[' + ${request.clientName} + '] ' + ${request.type.description}"></a></td>
|
||||
<td><span th:if="${request.approval != null}" th:text="${request.approval.approvalStatus.description}"></span></td>
|
||||
<td><span th:if="${request.approval != null}" th:text="${#temporals.format(request.approval.createdDate, 'yyyy-MM-dd')}"></span></td>
|
||||
<td><span th:if="${request.approval != null}" th:text="${#temporals.format(request.approval.approvalDate, 'yyyy-MM-dd')}"></span></td>
|
||||
<td th:text="${request.reason}">1</td>
|
||||
<td th:text="${#lists.size(#strings.arraySplit(request.apiList, ',').?[!#strings.isEmpty(#this)])}">0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="m-only">
|
||||
<table class="table table_min" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="110px">
|
||||
<col width="90px">
|
||||
<col width="90px">
|
||||
<col width="250px">
|
||||
<col width="100px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>신청명</th>
|
||||
<th>신청상태</th>
|
||||
<th>신청일</th>
|
||||
<th>신청사유</th>
|
||||
<th>사용 API 수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="request, status : ${requests}">
|
||||
<td th:text="${status.index + 1}">1</td>
|
||||
<td><a th:href="@{/myapikey/api_key_request/prod_detail(id=${request.id})}" class="btn btn-sm btn-primary" th:text="${request.clientName}"></a></td>
|
||||
<td th:text="${request.status}">1</td>
|
||||
<td th:text="${#temporals.format(request.createdDate, 'yyyy-MM-dd')}"></td>
|
||||
<td th:text="${request.reason}">1</td>
|
||||
<td th:text="${#lists.size(#strings.arraySplit(request.apiList, ',').?[!#strings.isEmpty(#this)])}">0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination" th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
|
||||
<form name="listForm" th:action="@{/myapikey/api_key_request/history}" method="get">
|
||||
<input type="hidden" id="page" name="page"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script>
|
||||
function fn_select_page(pageNo) {
|
||||
document.listForm.page.value = pageNo;
|
||||
document.listForm.action = '[[@{/myapikey/api_key_request/history}]]';
|
||||
document.listForm.submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,208 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/kjbank_base_layout}">
|
||||
<head>
|
||||
<title>API 키 신청 상세</title>
|
||||
</head>
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="register-header">
|
||||
<h1>API Key 신청 상세</h1>
|
||||
<p class="header-description">
|
||||
신청하신 API Key의 상세 정보를 확인할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="apikey-detail-container">
|
||||
|
||||
<!-- Section: 기본 정보 (From Step 1) -->
|
||||
<div class="detail-section">
|
||||
<h2 class="section-title">기본 정보</h2>
|
||||
<div class="detail-grid">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">앱 아이콘</label>
|
||||
<div class="detail-value">
|
||||
<div class="app-icon-display" th:if="${appRequest.appIconFileId != null}">
|
||||
<img th:src="@{/file/download(fileSn=1,fileId=${appRequest.appIconFileId})}"
|
||||
alt="앱 아이콘"
|
||||
class="app-icon-image">
|
||||
</div>
|
||||
<div class="app-icon-display" th:unless="${appRequest.appIconFileId != null}">
|
||||
<div class="app-icon-placeholder">
|
||||
<span>📱</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">앱 이름</label>
|
||||
<div class="detail-value" th:text="${appRequest.clientName}">
|
||||
My Application
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client ID (if exists) -->
|
||||
<div class="detail-item" th:if="${appRequest.clientId != null}">
|
||||
<label class="detail-label">Client ID</label>
|
||||
<div class="detail-value" th:text="${appRequest.clientId}">
|
||||
client-id-12345
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Description -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">앱 설명</label>
|
||||
<div class="detail-value" th:text="${appRequest.appDescription}">
|
||||
This is a sample app description.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">Callback URL</label>
|
||||
<div class="detail-value" th:text="${appRequest.callbackUrl}">
|
||||
https://example.com/callback
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="detail-item full-width" th:if="${appRequest.ipWhitelist != null and !appRequest.ipWhitelist.isEmpty()}">
|
||||
<label class="detail-label">IP 화이트리스트</label>
|
||||
<div class="detail-value">
|
||||
<div class="ip-list">
|
||||
<span class="ip-tag" th:each="ip : ${#strings.arraySplit(appRequest.ipWhitelist, ',')}">
|
||||
[[${ip.trim()}]]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Created Date -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">신청일시</label>
|
||||
<div class="detail-value" th:text="${#temporals.format(appRequest.createdDate, 'yyyy.MM.dd HH:mm')}">
|
||||
2024.01.01 12:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item" th:if="${appRequest.approval != null}">
|
||||
<label class="detail-label">승인 상태</label>
|
||||
<div class="detail-value" th:text="${appRequest.approval.approvalStatus.description}">
|
||||
승인 대기
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item" th:if="${appRequest.approval != null and appRequest.approval.approvalDate != null}">
|
||||
<label class="detail-label">승인일시</label>
|
||||
<div class="detail-value" th:text="${#temporals.format(appRequest.approval.approvalDate, 'yyyy.MM.dd HH:mm')}">
|
||||
2024.01.01 12:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item full-width" th:if="${appRequest.approval != null and appRequest.reason != null and !appRequest.reason.isEmpty()}">
|
||||
<label class="detail-label">사유</label>
|
||||
<div class="detail-value" th:text="${appRequest.reason}">
|
||||
Approval reason
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section: API 정보 (From Step 2) -->
|
||||
<div class="detail-section">
|
||||
<h2 class="section-title">선택된 API</h2>
|
||||
|
||||
<!-- API List -->
|
||||
<div class="api-detail-list">
|
||||
<div class="api-list-item" th:each="api : ${appRequest.apiSpecList}">
|
||||
<span class="api-method"
|
||||
th:classappend="${api.apiMethod == 'GET' ? 'method-get' :
|
||||
(api.apiMethod == 'POST' ? 'method-post' :
|
||||
(api.apiMethod == 'PUT' ? 'method-put' :
|
||||
(api.apiMethod == 'DELETE' ? 'method-delete' : 'method-other')))}">
|
||||
[[${api.apiMethod}]]
|
||||
</span>
|
||||
<span class="api-name" th:text="${api.apiName}">API Name</span>
|
||||
<span class="api-status status-pending">대기중</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" th:if="${appRequest.apiSpecList == null or appRequest.apiSpecList.isEmpty()}">
|
||||
<div class="empty-icon">📦</div>
|
||||
<p>선택된 API가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="detail-actions">
|
||||
<button type="button"
|
||||
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
class="btn-cancel"
|
||||
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
|
||||
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED')}"
|
||||
th:data-request-id="${appRequest.id}"
|
||||
onclick="cancelRequestById(this)">
|
||||
신청 취소
|
||||
</button>
|
||||
<a th:href="@{/myapikey}" class="btn-secondary">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
|
||||
// Cancel request function - called from button with data-request-id attribute
|
||||
function cancelRequestById(button) {
|
||||
var requestId = $(button).data('request-id');
|
||||
|
||||
if (!confirm('정말로 이 신청을 취소하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.loading-overlay').show();
|
||||
|
||||
$.ajax({
|
||||
url: '/myapikey/api_key_request/cancel',
|
||||
type: 'POST',
|
||||
data: {id: requestId},
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
|
||||
}
|
||||
}).done(function(response) {
|
||||
if (response.success) {
|
||||
alert(response.message || '신청이 취소되었습니다.');
|
||||
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
|
||||
} else {
|
||||
alert(response.message || '신청 취소 중 오류가 발생했습니다.');
|
||||
$('.loading-overlay').hide();
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
alert('신청 취소 중 오류가 발생했습니다.');
|
||||
console.error('Cancel error:', error);
|
||||
$('.loading-overlay').hide();
|
||||
});
|
||||
}
|
||||
|
||||
/*]]>*/
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,320 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/kjbank_base_layout}">
|
||||
<head>
|
||||
<title>API 키 상세</title>
|
||||
</head>
|
||||
<body>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="register-header">
|
||||
<h1>API Key 상세</h1>
|
||||
<p class="header-description">
|
||||
발급된 API Key의 상세 정보를 확인할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="apikey-detail-container">
|
||||
|
||||
<!-- Section 1: 앱 기본 정보 -->
|
||||
<div class="detail-section">
|
||||
<h2 class="section-title">앱 정보</h2>
|
||||
<div class="detail-grid">
|
||||
|
||||
<!-- App Icon -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">앱 아이콘</label>
|
||||
<div class="detail-value">
|
||||
<div class="app-icon-display" th:if="${apiKey.appIconFileId != null}">
|
||||
<img th:src="@{/file/download(fileSn=1,fileId=${apiKey.appIconFileId})}"
|
||||
alt="앱 아이콘"
|
||||
class="app-icon-image">
|
||||
</div>
|
||||
<div class="app-icon-display" th:unless="${apiKey.appIconFileId != null}">
|
||||
<div class="app-icon-placeholder">
|
||||
<span>📱</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">앱 이름</label>
|
||||
<div class="detail-value" th:text="${apiKey.clientname}">
|
||||
My Application
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Status -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">앱 상태</label>
|
||||
<div class="detail-value">
|
||||
<span class="status-indicator"
|
||||
th:classappend="${apiKey.appstatus == '1' ? 'status-active' : 'status-inactive'}">
|
||||
<span class="status-dot"></span>
|
||||
<span th:text="${apiKey.appstatus == '1' ? '활성화' : '비활성화'}">활성화</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Description -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">앱 설명</label>
|
||||
<div class="detail-value" th:text="${apiKey.appDescription != null ? apiKey.appDescription : '설명이 없습니다.'}">
|
||||
This is a sample app description.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: 인증 상세 정보 -->
|
||||
<div class="detail-section">
|
||||
<h2 class="section-title">인증 정보</h2>
|
||||
<div class="detail-grid">
|
||||
|
||||
<!-- Client ID -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">Client ID</label>
|
||||
<div class="detail-value">
|
||||
<div class="secret-box">
|
||||
<code class="credential-code" th:text="${apiKey.clientid}">client-id-12345</code>
|
||||
<button type="button" class="btn-copy" th:data-secret="${apiKey.clientid}" onclick="copyToClipboardFromButton(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
복사
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Secret -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">Client Secret</label>
|
||||
<div class="detail-value">
|
||||
<!-- Hidden Secret (shown initially) -->
|
||||
<div class="secret-box" id="hiddenSecretBox">
|
||||
<code class="credential-code">••••••••••••••••••••</code>
|
||||
<button type="button" class="btn-copy" onclick="showPasswordPrompt()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
보기
|
||||
</button>
|
||||
</div>
|
||||
<!-- Actual Secret (hidden initially) -->
|
||||
<div class="secret-box" id="revealedSecretBox" style="display: none;">
|
||||
<code class="credential-code" th:text="${apiKey.clientsecret}">secret-key-67890</code>
|
||||
<button type="button" class="btn-copy" th:data-secret="${apiKey.clientsecret}" onclick="copyToClipboardFromButton(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
복사
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">Callback URL</label>
|
||||
<div class="detail-value" th:text="${apiKey.redirecturi != null ? apiKey.redirecturi : '설정되지 않음'}">
|
||||
https://example.com/callback
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<div class="detail-item full-width" th:if="${apiKey.allowedips != null}">
|
||||
<label class="detail-label">IP 화이트리스트</label>
|
||||
<div class="detail-value">
|
||||
<div class="ip-list">
|
||||
<span class="ip-tag" th:each="ip : ${#strings.arraySplit(apiKey.allowedips, ',')}">
|
||||
[[${ip.trim()}]]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록된 API -->
|
||||
<div class="detail-item full-width">
|
||||
<label class="detail-label">등록된 API</label>
|
||||
<div class="detail-value">
|
||||
<!-- API List -->
|
||||
<div class="api-detail-list">
|
||||
<div class="api-list-item" th:each="api : ${apiKey.apiList}">
|
||||
<span class="api-method method-other">
|
||||
API
|
||||
</span>
|
||||
<span class="api-name" th:text="${api.apiDesc}">API Name</span>
|
||||
<span class="api-status status-active">활성</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" th:if="${apiKey.apiList == null or apiKey.apiList.isEmpty()}">
|
||||
<div class="empty-icon">📦</div>
|
||||
<p>등록된 API가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modified Date -->
|
||||
<div class="detail-item">
|
||||
<label class="detail-label">최종 수정일자</label>
|
||||
<div class="detail-value" th:text="${#temporals.format(apiKey.modifiedon, 'yyyy.MM.dd HH:mm')}">
|
||||
2024.01.01 12:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="detail-actions">
|
||||
<a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
class="btn-action btn-primary"
|
||||
th:href="@{/myapikey/modify/step1(clientId=${apiKey.clientid})}">
|
||||
수정
|
||||
</a>
|
||||
<button type="button"
|
||||
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||
class="btn-action btn-danger"
|
||||
th:data-client-id="${apiKey.clientid}"
|
||||
onclick="deleteApiKeyFromButton(this)">
|
||||
인증키 삭제
|
||||
</button>
|
||||
<a th:href="@{/myapikey}" class="btn-secondary">
|
||||
목록으로
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
|
||||
$(document).ready(function () {
|
||||
activateSelect();
|
||||
});
|
||||
|
||||
// Show password prompt for viewing client secret
|
||||
function showPasswordPrompt() {
|
||||
customPopups.showPasswordInput({
|
||||
title: 'Client Secret 조회',
|
||||
message: '보안을 위해 비밀번호를 입력해주세요.',
|
||||
onConfirm: function(password) {
|
||||
// Verify password via AJAX
|
||||
$.ajax({
|
||||
url: /*[[@{/myapikey/verify-password}]]*/ '/myapikey/verify-password',
|
||||
type: 'POST',
|
||||
data: { password: password },
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Hide password popup
|
||||
customPopups.hidePasswordInput();
|
||||
|
||||
// Reveal the client secret
|
||||
$('#hiddenSecretBox').hide();
|
||||
$('#revealedSecretBox').fadeIn(300);
|
||||
} else {
|
||||
// Show error message
|
||||
customPopups.showPasswordError(response.message || '비밀번호가 일치하지 않습니다.');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
customPopups.showPasswordError('오류가 발생했습니다. 다시 시도해주세요.');
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel: function() {
|
||||
// User cancelled - do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy to clipboard function - called from button with data-secret attribute
|
||||
function copyToClipboardFromButton(button) {
|
||||
var text = $(button).data('secret');
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
alert('클립보드에 복사되었습니다.');
|
||||
}).catch(function(err) {
|
||||
console.error('Failed to copy:', err);
|
||||
fallbackCopy(text);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
alert('클립보드에 복사되었습니다.');
|
||||
} catch (err) {
|
||||
alert('복사에 실패했습니다.');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Delete API Key function - called from button with data-client-id attribute
|
||||
function deleteApiKeyFromButton(button) {
|
||||
var clientId = $(button).data('client-id');
|
||||
|
||||
if (!confirm('정말로 이 인증키를 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.loading-overlay').show();
|
||||
|
||||
const requestData = {
|
||||
clientId: clientId
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/myapikey/api_key_delete',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(requestData),
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ 'token'
|
||||
}
|
||||
}).done(function (response) {
|
||||
alert(response.msg || 'API Key가 삭제되었습니다.');
|
||||
window.location.href = /*[[@{/myapikey}]]*/ '/myapikey';
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
alert('API 삭제 요청 중 오류가 발생했습니다: ' + errorThrown);
|
||||
}).always(function () {
|
||||
$('.loading-overlay').hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*]]>*/
|
||||
</script>
|
||||
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,9 +15,9 @@
|
||||
<meta content="max-age=0, public" http-equiv="Cache-Control"/>
|
||||
<meta content="index, follow" name="robots"/>
|
||||
<meta content="https://www.eactive.co.kr/" property="og:url"/>
|
||||
<link rel="icon" type="image/ico" th:href="@{/favicon_16px.png}">
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/css/style2.css}">
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/css/common2.css}">
|
||||
<link rel="icon" type="image/ico" th:href="@{/favicon.ico}">
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}">
|
||||
<!-- <link rel="stylesheet" type="text/css" th:href="@{/css/common2.css}">-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap" class="wrap_top">
|
||||
|
||||
@@ -1,628 +0,0 @@
|
||||
<div th:fragment="prodApiKeyRequest" class="pop_dim">
|
||||
<div class="inner">
|
||||
<div class="popup-content pop_w2">
|
||||
<div class="inner_txt">
|
||||
<div class="title">
|
||||
<h2>인증키 운영 전환 신청</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form_type">
|
||||
<div class="info1 form_top" style="margin-top: 10px;">
|
||||
<p class="title w_tit">
|
||||
<span class="dot">API키 이름</span>
|
||||
</p>
|
||||
<div class="info_line info_add">
|
||||
<div class="info_box1 w_inp4">
|
||||
<input type="text" style="max-width: 300px;" name="name" id="appRequestProdClientName" class="common_input_type_1 h_inp2" placeholder="API키 이름 입력">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="infor api_dot top">
|
||||
<ul>
|
||||
<li>
|
||||
<p>인증키가 없을 경우, 승인 시 인증키는 신규로 발급 됩니다.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form_type" id="apiKeySelect">
|
||||
<div class="info1 form_top" style="margin-top: 10px;">
|
||||
<p class="title w_tit">
|
||||
<span class="dot">API키 선택</span>
|
||||
</p>
|
||||
<div class="info_line info_add">
|
||||
<div class="info_box1 w_inp4">
|
||||
<div class="custom_select select_w h_inp" style="width: 300px;">
|
||||
<input type="hidden" id="clientId" />
|
||||
<div class="select_selected h_inp" id="selectedDevApiKey">API키 선택</div>
|
||||
<ul class="common_selecttype select_items select_hide" id="devApiKeyList">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pop_text l_text m-only">API</p>
|
||||
|
||||
<div class="board_search sh_box top2">
|
||||
<p class="pop_text l_text pc-only">API</p>
|
||||
<div class="search_txt ser_api w_inp">
|
||||
<input type="text" class="common_input_type_1 h_inp2" id="prod_api_keyword" placeholder="API 검색">
|
||||
</div>
|
||||
<div class="btn_wrap search_btn">
|
||||
<button type="button" class="btn_search" id="prod_api_search">검색</button>
|
||||
<i class="i_search"></i>
|
||||
</div>
|
||||
<p class="pop_text l_text pop_text2 pc-only" style="margin-left: 145px;">운영 이관 신청 API</p>
|
||||
</div>
|
||||
<div class="tb_conbox top2">
|
||||
<div class="box table_type2 top_type">
|
||||
<table class="table_col table_min tb_bdtop" id="prodSourceTable" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="140px">
|
||||
<col width="230px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="total_wrap">
|
||||
<span class="inp_check b_tit">
|
||||
<input type="checkbox" class="agree_all" id="selectAllProdSourceTable" name="selectall" data-group="total">
|
||||
<label for="selectAllProdSourceTable"></label>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>서비스</th>
|
||||
<th>API</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box pop_arrow m-only">
|
||||
<a href="#none" class="add_apis"><img th:src="@{/img/icon/Icon_arrow_btn_down.png}" alt="올리기"></a>
|
||||
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/Icon_arrow_btn_up.png}" alt="내리기"></a>
|
||||
</div>
|
||||
<div class="box pop_arrow pc-only">
|
||||
<p>
|
||||
<a href="#none" class="add_apis"><img th:src="@{/img/icon/icon_arrow_btn_r.png}" alt="넣기"></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#none" class="remove_apis"><img th:src="@{/img/icon/icon_arrow_btn_l.png}" alt="빼기"></a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="pop_text l_text2 pt_top m-only">운영 이관 신청 API</p>
|
||||
<div class="box table_type2 top_type">
|
||||
|
||||
<table class="table_col table_min tb_bdtop" id="prodTargetTable" cellspacing="0" cellpadding="0" width="100%">
|
||||
<colgroup>
|
||||
<col width="60px">
|
||||
<col width="140px">
|
||||
<col width="230px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="total_wrap">
|
||||
<span class="inp_check b_tit">
|
||||
<input type="checkbox" class="agree_all" id="selectAllProdTargetTable" name="selectall2" data-group="total2">
|
||||
<label for="selectAllProdTargetTable"></label>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>서비스</th>
|
||||
<th>API</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="pop_text l_text2">신청사유</p>
|
||||
<div class="pop_textbox">
|
||||
<textarea name="reason" id="prod_reason" rows="5" cols="30" class="common_textareaType_1" placeholder="신청사유를 입력해 주세요."></textarea>
|
||||
</div>
|
||||
<div class="pop_btnbox">
|
||||
<button class="popup_button_gray btn_cancel">취소</button>
|
||||
<button class="popup_button_blue request_prod_apis">운영 전환 신청</button>
|
||||
</div>
|
||||
|
||||
<a href="#none" class="btn_close" style="margin-top:0">
|
||||
<img th:src="@{/img/icon/icon_close.png}" alt="닫기">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<th:block th:fragment="prodApiKeyRequestScript">
|
||||
<script>
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('X-XSRF-TOKEN', '[[${_csrf.token}]]');
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
(function ($) {
|
||||
// Model
|
||||
const model = {
|
||||
apiKeys: [],
|
||||
clientId: '',
|
||||
sourceItems: [],
|
||||
targetItems: [],
|
||||
isUpdate: false,
|
||||
updateSourceItemsFromApiKey: function (clientId) {
|
||||
this.sourceItems = [];
|
||||
const selectedApiKey = this.apiKeys.find(key => key.clientid === clientId);
|
||||
console.log(selectedApiKey);
|
||||
if (selectedApiKey) {
|
||||
this.sourceItems = selectedApiKey.apiList.map(api => ({
|
||||
apiId : api.apiId,
|
||||
service: api.service || '',
|
||||
apiName: api.apiDesc,
|
||||
selected: false
|
||||
}));
|
||||
console.log(this.sourceItems);
|
||||
}
|
||||
},
|
||||
fetchDevApiKeys: function () {
|
||||
return $.ajax({
|
||||
url: '/myapikey/dev',
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).then((data) => {
|
||||
if (data.length === 0) {
|
||||
customPopups.showAlert(('개발 인증 키를 먼저 발급 받으세요.'));
|
||||
} else {
|
||||
this.apiKeys = data;
|
||||
var clientId = $('#clientId').val(); //개발 인증키에서 운영 전환 신청 할 때
|
||||
console.log('개발 인증키:', clientId);
|
||||
|
||||
if (clientId) {
|
||||
$('#apiKeySelect').css('display', 'none');
|
||||
model.sourceItems.forEach(item => {
|
||||
item.selected = false;
|
||||
item.inTarget = false;
|
||||
});
|
||||
|
||||
model.updateSourceItemsFromApiKey(clientId);
|
||||
model.targetItems = [];
|
||||
view.render();
|
||||
} else {
|
||||
const apiKeyList = $('#devApiKeyList');
|
||||
|
||||
apiKeyList.empty();
|
||||
const li = $('<li>').attr('data-id', '').addClass('api-item').text('API키 선택');
|
||||
apiKeyList.append(li);
|
||||
|
||||
|
||||
data.forEach(item => {
|
||||
const li = $('<li>').attr('data-id', item.clientid).addClass('api-item').text(item.clientname);
|
||||
apiKeyList.append(li);
|
||||
});
|
||||
|
||||
const selectedApiKey = document.getElementById('selectedDevApiKey');
|
||||
apiKeyList.on('click', function (e) {
|
||||
if (e.target && e.target.matches('li.api-item')) {
|
||||
const selectedName = e.target.textContent;
|
||||
const selectedId = e.target.getAttribute('data-id');
|
||||
$('#clientId').val(selectedId);
|
||||
selectedApiKey.textContent = selectedName;
|
||||
model.sourceItems.forEach(item => {
|
||||
|
||||
if (!model.targetItems.some(tItem => tItem.apiId === item.apiId)) {
|
||||
item.selected = false;
|
||||
item.inTarget = false;
|
||||
}
|
||||
});
|
||||
|
||||
model.updateSourceItemsFromApiKey(selectedId);
|
||||
view.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Error fetching source items: ', error);
|
||||
});
|
||||
},
|
||||
moveSelectedToTarget: function () {
|
||||
const selectedItems = this.sourceItems.filter(item => item.selected && !item.inTarget);
|
||||
|
||||
selectedItems.forEach(item => {
|
||||
|
||||
const isDuplicate = this.targetItems.some(targetItem => targetItem.apiId === item.apiId);
|
||||
|
||||
if (!isDuplicate){
|
||||
item.selected = false;
|
||||
item.inTarget = true;
|
||||
this.targetItems.push({
|
||||
apiId: item.apiId,
|
||||
service: item.service,
|
||||
apiName: item.apiName,
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
moveSelectedToSource: function () {
|
||||
const selectedItems = this.targetItems.filter(item => item.selected);
|
||||
|
||||
selectedItems.forEach(item => {
|
||||
const sourceItem = this.sourceItems.find(sItem => sItem.apiId === item.apiId);
|
||||
if (sourceItem) {
|
||||
sourceItem.inTarget = false;
|
||||
sourceItem.selected = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.targetItems = this.targetItems.filter(item => !item.selected);
|
||||
}
|
||||
};
|
||||
|
||||
// View
|
||||
const view = {
|
||||
init: function () {
|
||||
this.prodSourceTableBody = $('#prodSourceTable tbody');
|
||||
this.prodTargetTableBody = $('#prodTargetTable tbody');
|
||||
this.selectAllSourceCheckbox = $('#selectAllProdSourceTable');
|
||||
this.selectAllTargetCheckbox = $('#selectAllProdTargetTable');
|
||||
this.nameInput = $('#appRequestProdClientName');
|
||||
this.reasonTextarea = $('#prod_reason');
|
||||
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function () {
|
||||
|
||||
this.selectAllSourceCheckbox.on('change', function () {
|
||||
console.log('select all');
|
||||
const isChecked = $(this).is(':checked');
|
||||
model.sourceItems.forEach(item => {
|
||||
const keyword = $('#prod_api_keyword').val().trim().toLowerCase();
|
||||
if (keyword !== '') {
|
||||
item.selected = isChecked && (item.apiName.toLowerCase().includes(keyword) ||
|
||||
(item.service != null && item.service.toLowerCase().includes(keyword)));
|
||||
} else {
|
||||
item.selected = isChecked;
|
||||
}
|
||||
});
|
||||
view.renderProdSourceTable();
|
||||
});
|
||||
|
||||
|
||||
this.selectAllTargetCheckbox.on('change', function () {
|
||||
console.log('select all target')
|
||||
const isChecked = $(this).is(':checked');
|
||||
model.targetItems.forEach(item => item.selected = isChecked);
|
||||
view.renderProdTargetTable();
|
||||
});
|
||||
|
||||
// Add APIs button
|
||||
$('.add_apis').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
const selectedCount = model.sourceItems.filter(item => item.selected && !item.inTarget).length;
|
||||
|
||||
// if (selectedCount === 0) {
|
||||
// customPopups.showAlert('선택된 API가 없습니다.');
|
||||
// return;
|
||||
// }
|
||||
model.moveSelectedToTarget();
|
||||
view.render();
|
||||
});
|
||||
|
||||
// Remove APIs button
|
||||
$('.remove_apis').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
const selectedCount = model.targetItems.filter(item => item.selected && !item.inTarget).length;
|
||||
|
||||
// if (selectedCount === 0) {
|
||||
// customPopups.showAlert('선택된 API가 없습니다.');
|
||||
// return;
|
||||
// }
|
||||
model.moveSelectedToSource();
|
||||
view.render();
|
||||
});
|
||||
|
||||
// Request APIs button
|
||||
$('.request_prod_apis').off('click');
|
||||
$('.request_prod_apis').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
controller.requestApis();
|
||||
});
|
||||
|
||||
$('.btn_change_prod_api').off('click');
|
||||
$('.btn_change_prod_api').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
model.clientId = $('#prodClientId').val();
|
||||
model.isUpdate = true;
|
||||
|
||||
view.nameInput.val($('#prodClientName').val());
|
||||
|
||||
|
||||
model.fetchDevApiKeys().then(() => {
|
||||
controller.initializeExistingApis(function () {
|
||||
controller.openPopup();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Open popup
|
||||
$('.btn_request_prod_api').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
controller.openPopup();
|
||||
|
||||
});
|
||||
|
||||
// Close popup
|
||||
$('.btn_close, .btn_cancel').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
controller.closePopup();
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
$('.btn_search').on('click', function (event) {
|
||||
event.preventDefault();
|
||||
controller.searchApis();
|
||||
});
|
||||
|
||||
// Optional: Implement search on enter key
|
||||
$('.search_txt input').on('keyup', function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
controller.searchApis();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.renderProdSourceTable();
|
||||
this.renderProdTargetTable();
|
||||
this.updateSelectAllCheckbox('source');
|
||||
this.updateSelectAllCheckbox('target');
|
||||
},
|
||||
|
||||
bindCheckboxEvents: function(checkbox, item, tableType){
|
||||
checkbox.addClass('prod-table-checkbox');
|
||||
|
||||
checkbox.on('change', function(e){
|
||||
|
||||
if(!$(this).hasClass('prod-table-checkbox')){
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Prod ${tableType} checkbox changed ->`, this.checked);
|
||||
const isChecked = $(this).is(':checked');
|
||||
|
||||
if(tableType === 'source'){
|
||||
const sourceItem = model.sourceItems.find(sItem => sItem.apiId === item.apiId);
|
||||
if(sourceItem){
|
||||
sourceItem.selected = isChecked;
|
||||
|
||||
$(`#prod_agree_ch_source_${item.apiId}`).prop('checked', isChecked);
|
||||
}
|
||||
view.updateSelectAllCheckbox('source');
|
||||
} else {
|
||||
const targetItem = model.targetItems.find(tItem => tItem.apiId === item.apiId);
|
||||
if(targetItem){
|
||||
targetItem.selected = isChecked;
|
||||
|
||||
$(`#prod_agree_ch_target_${item.apiId}`).prop('checked', isChecked);
|
||||
}
|
||||
view.updateSelectAllCheckbox('target');
|
||||
}
|
||||
});
|
||||
},
|
||||
createTableRow: function(item, tableType){
|
||||
|
||||
const checkboxId = `prod_agree_ch_${tableType}_${item.apiId}`;
|
||||
const checkboxName = `prod_${tableType}Group`;
|
||||
|
||||
const tr = $('<tr>');
|
||||
const tdCheckbox = $('<td>');
|
||||
const divSuvCk = $('<div>').addClass('suv_ck');
|
||||
const ul = $('<ul>');
|
||||
const li = $('<li>');
|
||||
const spanInpCheck = $('<span>').addClass('inp_check s_tit');
|
||||
|
||||
|
||||
const checkbox = $('<input>', {
|
||||
type: 'checkbox',
|
||||
id: checkboxId,
|
||||
name: checkboxName,
|
||||
'data-api-id': item.apiId,
|
||||
checked: item.selected,
|
||||
class: 'prod-table-checkbox'
|
||||
});
|
||||
|
||||
const label = $('<label>', {
|
||||
for: checkboxId
|
||||
});
|
||||
|
||||
|
||||
this.bindCheckboxEvents(checkbox, item, tableType);
|
||||
|
||||
//assemble DOM
|
||||
spanInpCheck.append(checkbox, label);
|
||||
li.append(spanInpCheck);
|
||||
ul.append(li);
|
||||
divSuvCk.append(ul);
|
||||
tdCheckbox.append(divSuvCk);
|
||||
|
||||
|
||||
const tdService = $('<td>').text(item.service || '');
|
||||
const tdApiName = $('<td>').text(item.apiName);
|
||||
|
||||
tr.append(tdCheckbox, tdService, tdApiName);
|
||||
return tr;
|
||||
},
|
||||
|
||||
renderProdSourceTable: function () {
|
||||
const keyword = $('#prod_api_keyword').val().trim().toLowerCase();
|
||||
this.prodSourceTableBody.empty();
|
||||
const targetApiIds = new Set(model.targetItems.map(item => item.apiId));
|
||||
// Filter source items
|
||||
const filteredItems = model.sourceItems.filter(item => {
|
||||
const notInTarget = !targetApiIds.has(item.apiId) && !item.inTarget;
|
||||
if (keyword !== '') {
|
||||
return notInTarget && (
|
||||
item.apiName.toLowerCase().includes(keyword) ||
|
||||
(item.service && item.service.toLowerCase().includes(keyword))
|
||||
);
|
||||
}
|
||||
return notInTarget;
|
||||
});
|
||||
// Render filtered items
|
||||
filteredItems.forEach(item => {
|
||||
const row = view.createTableRow(item, 'source');
|
||||
view.prodSourceTableBody.append(row);
|
||||
});
|
||||
},
|
||||
|
||||
renderProdTargetTable: function () {
|
||||
this.prodTargetTableBody.empty();
|
||||
model.targetItems.forEach(item => {
|
||||
const row = this.createTableRow(item, 'target');
|
||||
this.prodTargetTableBody.append(row);
|
||||
});
|
||||
},
|
||||
|
||||
updateSelectAllCheckbox: function (tableType) {
|
||||
if (tableType === 'source') {
|
||||
|
||||
const allChecked = model.sourceItems.length > 0 && model.sourceItems.every(item => item.selected);
|
||||
$('#selectAllProdSourceTable').prop('checked', allChecked);
|
||||
} else if (tableType === 'target') {
|
||||
|
||||
const allChecked = model.targetItems.length > 0 && model.targetItems.every(item => item.selected);
|
||||
$('#selectAllProdTargetTable').prop('checked', allChecked);
|
||||
}
|
||||
},
|
||||
|
||||
resetForm: function () {
|
||||
this.nameInput.val('');
|
||||
this.reasonTextarea.val('');
|
||||
}
|
||||
};
|
||||
|
||||
// Controller
|
||||
const controller = {
|
||||
init: function () {
|
||||
view.init();
|
||||
model.fetchDevApiKeys().then(() => {
|
||||
view.render();
|
||||
});
|
||||
},
|
||||
|
||||
openPopup: function () {
|
||||
this.init();
|
||||
$('#apiKeyProdRequestPopup').show();
|
||||
},
|
||||
|
||||
closePopup: function () {
|
||||
$('#apiKeyProdRequestPopup').hide();
|
||||
},
|
||||
initializeExistingApis: function (callback) {
|
||||
let requestData = {clientId : model.clientId};
|
||||
|
||||
$.ajax({
|
||||
url: '/myapikey/prod',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(requestData)
|
||||
}).then((data) => {
|
||||
const existingApis = [];
|
||||
data.apiList.forEach(function (api) {
|
||||
existingApis.push({
|
||||
apiId: api.apiId,
|
||||
service: api.service,
|
||||
apiName: api.apiDesc,
|
||||
selected: true
|
||||
});
|
||||
|
||||
const sourceItem = model.sourceItems.find(sItem => sItem.apiId === api.apiId);
|
||||
if (sourceItem) {
|
||||
sourceItem.inTarget = true;
|
||||
}
|
||||
});
|
||||
|
||||
model.targetItems = existingApis;
|
||||
view.render();
|
||||
callback();
|
||||
});
|
||||
},
|
||||
requestApis: function () {
|
||||
const name = view.nameInput.val().trim();
|
||||
const reason = view.reasonTextarea.val().trim();
|
||||
const apis = model.targetItems.filter(item => item.selected).map(item => item.apiId).join(',');
|
||||
|
||||
if (!name) {
|
||||
customPopups.showAlert('이름을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (apis.length === 0) {
|
||||
customPopups.showAlert('API를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reason) {
|
||||
customPopups.showAlert('신청사유를 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
let refClient = $('#clientId').val();
|
||||
const requestData = {
|
||||
clientName: name,
|
||||
apiList: apis,
|
||||
reason: reason,
|
||||
refClient: refClient
|
||||
};
|
||||
|
||||
if (model.isUpdate) {
|
||||
requestData.clientId = model.clientId;
|
||||
}
|
||||
|
||||
$('.loading-overlay').show();
|
||||
$.ajax({
|
||||
url: '/myapikey/api_key_request_prod',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(requestData)
|
||||
}).done(function (response) {
|
||||
view.resetForm();
|
||||
model.targetItems = [];
|
||||
view.render();
|
||||
controller.closePopup();
|
||||
customPopups.showAlert(response.msg, function () {
|
||||
window.location.href = '/myapikey/api_key_request/prod_history';
|
||||
});
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
customPopups.showAlert('API 신청 중 오류가 발생했습니다: ' + errorThrown);
|
||||
}).always(function () {
|
||||
$('.loading-overlay').hide();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
searchApis: function () {
|
||||
view.renderProdSourceTable();
|
||||
}
|
||||
};
|
||||
|
||||
controller.init(); // We initialize when the popup is opened
|
||||
|
||||
})(jQuery);
|
||||
});
|
||||
|
||||
</script>
|
||||
</th:block>
|
||||
@@ -7,193 +7,137 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div th:fragment="headerFragment">
|
||||
<h1 class="logo pc-only"><a th:href="@{/}"><span class="blind">kbank</span></a></h1>
|
||||
<h1 class="sub_title m-only">
|
||||
<p th:text="${pageName}">API Portal</p>
|
||||
</h1>
|
||||
<div class="home">
|
||||
<a th:href="@{/}"><span class="blind">home</span></a>
|
||||
</div>
|
||||
<button type="button" class="btn-mobilemenu m-only">
|
||||
<span>모바일메뉴 열기</span>
|
||||
</button>
|
||||
<div class="m-nav m-only">
|
||||
<h1><a class="logo2" th:href="@{/}"><span class="blind">Kbank</span></a></h1>
|
||||
<div>
|
||||
<a class="home2" th:href="@{/}"><span class="blind">home</span></a>
|
||||
</div>
|
||||
<div class="login_btn_group">
|
||||
<div class="header_btn_group">
|
||||
<div class="login_wrap flex_no" sec:authorize="isAnonymous()">
|
||||
<!-- 로그인, 회원가입 -->
|
||||
<div class="login_prev">
|
||||
<button th:href="@{/login}" class="btn btn_loginbt">로그인</button>
|
||||
<button th:href="@{/signup}" class="btn btn_memberbt">회원가입</button>
|
||||
<div th:fragment="headerFragment" class="header-container">
|
||||
<!-- Unified Header -->
|
||||
<header id="page-header">
|
||||
<div class="header-content">
|
||||
<!-- Logo -->
|
||||
<h1 class="logo">
|
||||
<a th:href="@{/}"><span class="blind">kbank</span></a>
|
||||
</h1>
|
||||
|
||||
<!-- Mobile-only Subtitle -->
|
||||
<div class="sub_title m-only">
|
||||
<p th:text="${pageName}">API Portal</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Home Button -->
|
||||
<div class="home m-only">
|
||||
<a th:href="@{/}"><span class="blind">home</span></a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<button type="button" class="btn-mobilemenu m-only">
|
||||
<span>모바일메뉴 열기</span>
|
||||
</button>
|
||||
|
||||
<!-- Unified Navigation -->
|
||||
<nav class="main-nav m-nav">
|
||||
<!-- Login/Profile Section (Mobile) -->
|
||||
<div class="nav-header m-only">
|
||||
<div class="login_btn_group">
|
||||
<div class="header_btn_group">
|
||||
<div class="login_wrap flex_no" sec:authorize="isAnonymous()">
|
||||
<div class="login_prev">
|
||||
<a th:href="@{/login}" class="btn btn_loginbt">로그인</a>
|
||||
<a th:href="@{/signup}" class="btn btn_memberbt">회원가입</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login_wrap" sec:authorize="isAuthenticated()">
|
||||
<div class="profile_type">
|
||||
<a th:href="@{/mypage}">
|
||||
<p>[[${#authentication.principal.userName}]]님</p>
|
||||
<span>즐거운 하루 되세요!</span>
|
||||
</a>
|
||||
</div>
|
||||
<a th:href="@{/actionLogout.do}" class="btn btn_logout">로그아웃</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login_wrap" sec:authorize="isAuthenticated()">
|
||||
<div class="profile_type">
|
||||
<a th:href="@{/mypage}">
|
||||
<p>[[${#authentication.principal.userName}]]님</p>
|
||||
<span>즐거운 하루 되세요!</span>
|
||||
</a>
|
||||
</div>
|
||||
<button type="button" th:href="@{/actionLogout.do}" class="btn btn_logout">로그아웃</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="m-nav-cs">
|
||||
<li>
|
||||
<a th:href="@{/intro}">
|
||||
<img th:src="@{/img/icon/icon_gnb_info.png}" alt="">
|
||||
안내
|
||||
</a>
|
||||
<ul class="mc-sub-nav">
|
||||
<li>
|
||||
<a th:href="@{/intro}">API Portal 소개</a>
|
||||
</li>
|
||||
<li><a th:href="@{/guide}">이용 안내</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/apis}">
|
||||
<img th:src="@{/img/icon/icon_gnb_api.png}" alt="">
|
||||
API
|
||||
</a>
|
||||
<ul class="mc-sub-nav">
|
||||
<li><a th:href="@{/apis}">API 목록</a></li>
|
||||
<li><a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" th:href="@{/apis/apikey/new}">API키 신청</a></li>
|
||||
<li><a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" th:href="@{/apis/apikey/prod}">운영 API키 신청</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">
|
||||
<img th:src="@{/img/icon/icon_gnb_code.png}" alt="테스트 베드">
|
||||
테스트 베드
|
||||
</a>
|
||||
<ul class="mc-sub-nav">
|
||||
<li><a th:href="@{/apis/testbed}">테스트베드</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#none">
|
||||
<img th:src="@{/img/icon/icon_gnb_customer.png}" alt="">
|
||||
고객지원
|
||||
</a>
|
||||
<ul class="mc-sub-nav">
|
||||
<li><a th:href="@{/portalnotice}">공지사항</a></li>
|
||||
<li><a th:href="@{/faq_list}">FAQ</a></li>
|
||||
<li><a th:href="@{/inquiry}">Q&A</a>
|
||||
<li><a th:href="@{/partnership}">사업제휴</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li sec:authorize="isAuthenticated()" >
|
||||
<a>
|
||||
<img th:src="@{/img/icon/icon_gnb_mypage.png}" alt="">
|
||||
마이페이지
|
||||
</a>
|
||||
<ul class="mc-sub-nav">
|
||||
<li sec:authorize="hasRole('ROLE_DASHBOARD')"><a th:href="@{/dashboard}">대시보드</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">인증 키 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST_VIEW')"><a th:href="@{/myapikey/api_key_request/history}">인증키 신청 목록</a></li>
|
||||
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
||||
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="info_wrap">
|
||||
<p class="tit1">
|
||||
<a th:href="@{/agreements/terms(tab='terms')}">이용약관</a> <span>/</span> <a th:href="@{/agreements/terms(tab='privacy')}">개인정보처리방침</a>
|
||||
</p>
|
||||
<p class="tit2">
|
||||
<span>고객센터 : 1522-1000</span>
|
||||
<span>해외 : +82-2-3778-9111</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nav">
|
||||
<div id="main_nav">
|
||||
<ul>
|
||||
<li><a th:href="@{/intro}">안내</a></li>
|
||||
<li><a th:href="@{/apis}">API</a></li>
|
||||
<li><a th:href="@{/apis/testbed}">테스트베드</a></li>
|
||||
<li><a href="#">고객지원</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="sub">
|
||||
<div id="sub_menu">
|
||||
<ul class="menu">
|
||||
<li><a th:href="@{/intro}">API Portal 소개</a></li>
|
||||
<li><a th:href="@{/guide}">이용 안내</a></li>
|
||||
</ul>
|
||||
<ul class="menu">
|
||||
<li><a th:href="@{/apis}">API 목록</a></li>
|
||||
<li><a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" th:href="@{/apis/apikey/new}">API키 신청</a></li>
|
||||
<li><a sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" th:href="@{/apis/apikey/prod}">운영 API키 신청</a></li>
|
||||
</ul>
|
||||
<ul class="menu">
|
||||
<li><a th:href="@{/apis/testbed}">테스트베드</a></li>
|
||||
</ul>
|
||||
<ul class="menu">
|
||||
<li><a th:href="@{/portalnotice}">공지사항</a></li>
|
||||
<li><a th:href="@{/faq_list}">FAQ</a></li>
|
||||
<li><a th:href="@{/inquiry}">Q&A</a></li>
|
||||
<li><a th:href="@{/partnership}">사업제휴</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="login_group">
|
||||
<!-- 로그인하기전 -->
|
||||
<ul sec:authorize="isAnonymous()" class="login_type">
|
||||
<li><a th:href="@{/login}">로그인</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- 로그인 후 -->
|
||||
<ul sec:authorize="isAuthenticated()" class="logout_type">
|
||||
<li class="icon line"><a th:href="@{/mypage}">[[${#authentication.principal.userName}]]님</a></li>
|
||||
<li>
|
||||
<span>|</span>
|
||||
</li>
|
||||
<li><a th:href="@{/actionLogout.do}">로그아웃</a></li>
|
||||
<li>
|
||||
<span>|</span>
|
||||
</li>
|
||||
<li class="mp_arrow">
|
||||
<a th:href="@{/dashboard}">마이페이지</a>
|
||||
<div class="mp_type">
|
||||
<ul class="mp_list">
|
||||
<li sec:authorize="hasRole('ROLE_DASHBOARD')">
|
||||
<a th:href="@{/dashboard}">대시보드</a>
|
||||
</li>
|
||||
<li sec:authorize="hasRole('ROLE_USER_MANAGER')">
|
||||
<a th:href="@{/users}">이용자 관리</a>
|
||||
</li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')">
|
||||
<a th:href="@{/myapikey}">인증키 관리</a>
|
||||
</li>
|
||||
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST_VIEW')">
|
||||
<a th:href="@{/myapikey/api_key_request/history}">인증키 신청 목록</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/mypage}">내 정보 관리</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/change_password}">비밀번호 변경</a>
|
||||
</li>
|
||||
<!-- Main Menu List -->
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item has-submenu">
|
||||
<a th:href="@{/intro}">안내</a>
|
||||
<ul class="sub-menu">
|
||||
<li><a th:href="@{/intro}">API Portal 소개</a></li>
|
||||
<li><a th:href="@{/guide}">이용 안내</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- // 로그인 후 -->
|
||||
</li>
|
||||
<li class="nav-item has-submenu">
|
||||
<a th:href="@{/apis}">API</a>
|
||||
<ul class="sub-menu">
|
||||
<li><a th:href="@{/apis}">API 목록</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-submenu">
|
||||
<a th:href="@{/apis/testbed}">테스트 베드</a>
|
||||
<ul class="sub-menu">
|
||||
<li><a th:href="@{/apis/testbed}">테스트베드</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-submenu">
|
||||
<a href="#">고객지원</a>
|
||||
<ul class="sub-menu">
|
||||
<li><a th:href="@{/portalnotice}">공지사항</a></li>
|
||||
<li><a th:href="@{/faq_list}">FAQ</a></li>
|
||||
<li><a th:href="@{/inquiry}">Q&A</a></li>
|
||||
<li><a th:href="@{/partnership}">사업제휴</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-submenu" sec:authorize="isAuthenticated()">
|
||||
<a href="#">마이페이지</a>
|
||||
<ul class="sub-menu">
|
||||
<li sec:authorize="hasRole('ROLE_DASHBOARD')"><a th:href="@{/dashboard}">대시보드</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">인증 키 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST_VIEW')"><a th:href="@{/myapikey/api_key_request/history}">인증키 신청 목록</a></li>
|
||||
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
||||
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Mobile Footer Info -->
|
||||
<div class="info_wrap m-only">
|
||||
<p class="tit1">
|
||||
<a th:href="@{/agreements/terms(tab='terms')}">이용약관</a> <span>/</span> <a th:href="@{/agreements/terms(tab='privacy')}">개인정보처리방침</a>
|
||||
</p>
|
||||
<p class="tit2">
|
||||
<span>고객센터 : 1522-1000</span>
|
||||
<span>해외 : +82-2-3778-9111</span>
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Login/Profile Section (Desktop) -->
|
||||
<div id="login_group" class="pc-only">
|
||||
<ul sec:authorize="isAnonymous()" class="login_type">
|
||||
<li><a th:href="@{/login}">로그인</a></li>
|
||||
</ul>
|
||||
<ul sec:authorize="isAuthenticated()" class="logout_type">
|
||||
<li class="icon line"><a th:href="@{/mypage}">[[${#authentication.principal.userName}]]님</a></li>
|
||||
<li><span>|</span></li>
|
||||
<li><a th:href="@{/actionLogout.do}">로그아웃</a></li>
|
||||
<li><span>|</span></li>
|
||||
<li class="mp_arrow">
|
||||
<a th:href="@{/dashboard}">마이페이지</a>
|
||||
<div class="mp_type">
|
||||
<ul class="mp_list">
|
||||
<li sec:authorize="hasRole('ROLE_DASHBOARD')"><a th:href="@{/dashboard}">대시보드</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">인증키 관리</a></li>
|
||||
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST_VIEW')"><a th:href="@{/myapikey/api_key_request/history}">인증키 신청 목록</a></li>
|
||||
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
||||
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
<div th:replace="fragment/popup/emailValidationPopup :: #emailValidationPopup"></div>
|
||||
<div th:replace="fragment/popup/customPopup2 :: #customConfirm"></div>
|
||||
<div th:replace="fragment/popup/emailResendPopup :: emailResendPopup"></div>
|
||||
<div th:replace="fragment/popup/passwordInputPopup :: passwordInputPopup"></div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- views/fragment/popup/passwordInputPopup.html -->
|
||||
<div th:fragment="passwordInputPopup" id="passwordInputPopup" style="display: none;">
|
||||
<!-- Modal Backdrop -->
|
||||
<div class="modal-backdrop" id="passwordModalBackdrop"></div>
|
||||
|
||||
<!-- Modal Wrapper -->
|
||||
<div class="modal" id="passwordModal">
|
||||
<div class="modal-dialog">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="passwordPopupTitle">비밀번호 입력</h3>
|
||||
<button type="button" class="modal-close" id="passwordPopupCloseButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
<p id="passwordPopupMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
|
||||
계속하려면 비밀번호를 입력해주세요.
|
||||
</p>
|
||||
|
||||
<!-- Password Input Field -->
|
||||
<div class="pop_input_group">
|
||||
<input type="password"
|
||||
id="passwordPopupInput"
|
||||
class="pop_input_field"
|
||||
placeholder="비밀번호를 입력하세요">
|
||||
<div id="passwordPopupError" class="error-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="passwordPopupCancelButton">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="passwordPopupConfirmButton">확인</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,9 +10,8 @@
|
||||
<body>
|
||||
|
||||
<!-- header -->
|
||||
<header th:replace="fragment/header :: headerFragment"></header>
|
||||
<th:block th:replace="fragment/header :: headerScript"></th:block>
|
||||
<nav th:replace="fragment/header :: navFragment"></nav>
|
||||
<th:block th:replace="fragment/kjbank/header_container :: headerFragment"></th:block>
|
||||
<th:block th:replace="fragment/kjbank/header_container :: headerScript"></th:block>
|
||||
|
||||
<!-- csrf -->
|
||||
<div>
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
<body>
|
||||
|
||||
<!-- header -->
|
||||
<header th:replace="fragment/header :: headerFragment"></header>
|
||||
<th:block th:replace="fragment/header :: headerScript"></th:block>
|
||||
<nav th:replace="fragment/header :: navFragment"></nav>
|
||||
<th:block th:replace="fragment/kjbank/header_container :: headerFragment"></th:block>
|
||||
<th:block th:replace="fragment/kjbank/header_container :: headerScript"></th:block>
|
||||
|
||||
<div>
|
||||
<section layout:fragment="contentFragment">
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
<div class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
<th:block layout:fragment="contentFragment">
|
||||
</th:block>
|
||||
<div class="container">
|
||||
<th:block layout:fragment="contentFragment">
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
</th:block>
|
||||
|
||||
Reference in New Issue
Block a user