API Spec 자동 생성 기능 및 개발자포탈 반영 로직 추가
- DjbApiSpecAutoGenService 신규 추가 및 관련 규칙 구현 - PortalCredentialSyncService 도입으로 포탈 인증정보 동기화 로직 개선 - ClientManService 및 UI 업데이트: Spec 자동 생성 상태 반영
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE
|
UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE
|
AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
ApiInterfaceController_API관리_APIGW_INSERT,UPDATE,DELETE,CLONE
|
ApiInterfaceController_API관리_APIGW_INSERT,UPDATE,DELETE,CLONE
|
||||||
ClientController_Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE
|
ClientController_Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE,SYNC_PORTAL
|
||||||
RoleController_Role (역할)관리_APIGW_INSERT,UPDATE,DELETE
|
RoleController_Role (역할)관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE
|
MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE
|
PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
|
|||||||
@@ -308,7 +308,8 @@
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: {
|
data: {
|
||||||
cmd: 'SYNC_PORTAL',
|
cmd: 'SYNC_PORTAL',
|
||||||
clientId: clientId
|
clientId: clientId,
|
||||||
|
serviceType: 'APIGW'
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
showSyncResultModal(response);
|
showSyncResultModal(response);
|
||||||
@@ -453,8 +454,8 @@
|
|||||||
</div><!-- end content_top -->
|
</div><!-- end content_top -->
|
||||||
<div class="content_middle">
|
<div class="content_middle">
|
||||||
<div class="search_wrap">
|
<div class="search_wrap">
|
||||||
<!-- <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
<button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||||
</button> -->
|
</button>
|
||||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
||||||
@@ -741,7 +742,7 @@ function showSyncResultModal(response) {
|
|||||||
if (response.success) {
|
if (response.success) {
|
||||||
statusClass = "status-success";
|
statusClass = "status-success";
|
||||||
var createdCount = response.createdApiSpecCount || 0;
|
var createdCount = response.createdApiSpecCount || 0;
|
||||||
var createdMsg = createdCount > 0 ? "<br/><span style='color:#007bff;'>* " + createdCount + "개의 API Spec이 자동 생성되었습니다.</span>" : "";
|
var createdMsg = createdCount > 0 ? "<br/><span style='color:#007bff;'>* " + createdCount + "개의 API Spec을 비공개(N)로 자동 생성했습니다. API Spec 관리에서 확인 후 공개하세요.</span>" : "";
|
||||||
|
|
||||||
if (response.action === 'INSERTED') {
|
if (response.action === 'INSERTED') {
|
||||||
statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg;
|
statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg;
|
||||||
@@ -771,6 +772,7 @@ function showSyncResultModal(response) {
|
|||||||
var syncedApis = response.syncedApis || [];
|
var syncedApis = response.syncedApis || [];
|
||||||
var invalidApis = response.invalidApis || [];
|
var invalidApis = response.invalidApis || [];
|
||||||
var createdApiSpecs = response.createdApiSpecs || [];
|
var createdApiSpecs = response.createdApiSpecs || [];
|
||||||
|
var specGenFailed = response.specGenFailed || [];
|
||||||
|
|
||||||
$targetCount.text(targetApis.length);
|
$targetCount.text(targetApis.length);
|
||||||
|
|
||||||
@@ -784,8 +786,11 @@ function showSyncResultModal(response) {
|
|||||||
if (invalidApis.includes(apiId)) {
|
if (invalidApis.includes(apiId)) {
|
||||||
status = "미존재 (APIGW에 없음)";
|
status = "미존재 (APIGW에 없음)";
|
||||||
statusClass = "api-invalid";
|
statusClass = "api-invalid";
|
||||||
|
} else if (specGenFailed.includes(apiId)) {
|
||||||
|
status = "반영됨 (Spec 자동생성 실패·빈 항목)";
|
||||||
|
statusClass = "api-created";
|
||||||
} else if (createdApiSpecs.includes(apiId)) {
|
} else if (createdApiSpecs.includes(apiId)) {
|
||||||
status = "반영됨 (Spec 신규생성)";
|
status = "반영됨 (Spec 자동생성·비공개)";
|
||||||
statusClass = "api-created";
|
statusClass = "api-created";
|
||||||
} else if (syncedApis.includes(apiId)) {
|
} else if (syncedApis.includes(apiId)) {
|
||||||
status = "반영됨";
|
status = "반영됨";
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package com.eactive.eai.rms.onl.apim.approval.credential;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||||
|
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
||||||
|
import com.eactive.apim.portal.app.entity.Credential;
|
||||||
|
import com.eactive.eai.data.entity.onl.authserver.ClientEntity;
|
||||||
|
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ApiSpecManService;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.DjbApiSpecAutoGenService;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APIGW 인증 클라이언트(TSEAIAU01) → 개발자포탈(ptl_credential) 반영, 포털 쪽 쓰기 전담.
|
||||||
|
*
|
||||||
|
* {@link com.eactive.eai.rms.onl.apim.approval.app.GwClientSyncService}(포털→GW 방향)의 반대 방향 대응물.
|
||||||
|
* 트랜잭션 매니저를 EMS(MONITORING 스키마 고정)로 명시해, 호출부(APIGW 트랜잭션)의
|
||||||
|
* {@code DataSourceContextHolder} 를 건드리지 않는다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PortalCredentialSyncService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PortalCredentialSyncService.class);
|
||||||
|
|
||||||
|
private static final String DEFAULT_APP_DESCRIPTION = "APIGW에서 반영된 인증정보";
|
||||||
|
|
||||||
|
private final CredentialService credentialService;
|
||||||
|
private final ApiSpecInfoService apiSpecInfoService;
|
||||||
|
private final ApiSpecManService apiSpecManService;
|
||||||
|
private final DjbApiSpecAutoGenService autoGenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GW 클라이언트 정보를 포털 Credential 로 반영한다(없으면 신규, 있으면 갱신).
|
||||||
|
*
|
||||||
|
* @param gwClient APIGW 스키마에서 조회한 클라이언트(orgid 는 공백이 아니어야 한다 - 호출부 사전 검증)
|
||||||
|
* @return 반영 결과
|
||||||
|
*/
|
||||||
|
public Outcome apply(ClientEntity gwClient) {
|
||||||
|
String clientId = gwClient.getClientid();
|
||||||
|
String orgId = gwClient.getOrgid();
|
||||||
|
|
||||||
|
Credential existing = credentialService.findByClientidAndOrgid(clientId, orgId);
|
||||||
|
boolean isNew = existing == null;
|
||||||
|
Credential credential = isNew ? new Credential() : existing;
|
||||||
|
if (isNew) {
|
||||||
|
credential.setClientid(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyGwFields(gwClient, credential);
|
||||||
|
|
||||||
|
if (isNew && StringUtils.isBlank(credential.getAppDescription())) {
|
||||||
|
credential.setAppDescription(DEFAULT_APP_DESCRIPTION);
|
||||||
|
}
|
||||||
|
// appIconFileId / server 는 포털 고유 필드라 GW 에 대응값이 없다 - 기존 값 그대로 유지(신규면 null)
|
||||||
|
|
||||||
|
Outcome outcome = new Outcome(isNew);
|
||||||
|
credential.setApiList(resolveApiList(gwClient.getApiList(), outcome));
|
||||||
|
|
||||||
|
credentialService.save(credential);
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyGwFields(ClientEntity gwClient, Credential credential) {
|
||||||
|
credential.setClientname(gwClient.getClientname());
|
||||||
|
credential.setClientsecret(gwClient.getClientsecret());
|
||||||
|
credential.setScope(gwClient.getScope());
|
||||||
|
credential.setGranttypes(gwClient.getGranttypes());
|
||||||
|
credential.setAccesstokenvalidityseconds(gwClient.getAccesstokenvalidityseconds());
|
||||||
|
credential.setRefreshtokenvalidityseconds(gwClient.getRefreshtokenvalidityseconds());
|
||||||
|
credential.setAllowedips(gwClient.getAllowedips());
|
||||||
|
credential.setAuthorities(gwClient.getAuthorities());
|
||||||
|
credential.setRedirecturi(gwClient.getRedirecturi());
|
||||||
|
credential.setSecuritykey(gwClient.getSecuritykey());
|
||||||
|
credential.setAutoapprove(gwClient.getAutoapprove());
|
||||||
|
credential.setResourceids(gwClient.getResourceids());
|
||||||
|
credential.setOrgid(gwClient.getOrgid());
|
||||||
|
credential.setOrgname(gwClient.getOrgname());
|
||||||
|
credential.setDailytokenlimit(gwClient.getDailytokenlimit());
|
||||||
|
credential.setModifiedby(gwClient.getModifiedby());
|
||||||
|
credential.setModifiedon(gwClient.getModifiedon());
|
||||||
|
// appstatus 는 GW 값 그대로 복사(차단 상태도 포털에 반영). 공백/null 이면 정상(1)으로 간주.
|
||||||
|
credential.setAppstatus(StringUtils.isNotBlank(gwClient.getAppstatus()) ? gwClient.getAppstatus() : "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GW apiList 의 각 eaisvcname 을 포털 ApiSpecInfo(영속) 로 매핑, 미등록이면 자동생성 후 저장한다. */
|
||||||
|
private List<ApiSpecInfo> resolveApiList(List<EAIMessageEntity> gwApiList, Outcome outcome) {
|
||||||
|
List<ApiSpecInfo> result = new ArrayList<>();
|
||||||
|
if (gwApiList == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (EAIMessageEntity gwApi : gwApiList) {
|
||||||
|
String apiId = gwApi.getEaisvcname();
|
||||||
|
ApiSpecInfo spec = apiSpecInfoService.findById(apiId).orElse(null);
|
||||||
|
if (spec == null) {
|
||||||
|
spec = createApiSpec(apiId, gwApi, outcome);
|
||||||
|
}
|
||||||
|
result.add(spec);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiSpecInfo createApiSpec(String apiId, EAIMessageEntity gwApi, Outcome outcome) {
|
||||||
|
ApiSpecInfoUI ui;
|
||||||
|
try {
|
||||||
|
ui = autoGenService.generateApiSpecInfo(apiId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("API Spec 자동생성 실패, 최소 정보로 대체 생성 - apiId: {}", apiId, e);
|
||||||
|
ui = new ApiSpecInfoUI();
|
||||||
|
ui.setApiId(apiId);
|
||||||
|
ui.setApiName(StringUtils.isNotBlank(gwApi.getEaisvcdesc()) ? gwApi.getEaisvcdesc() : apiId);
|
||||||
|
ui.setApiSimpleDescription(gwApi.getEaisvcdesc());
|
||||||
|
outcome.specGenFailed.add(apiId);
|
||||||
|
}
|
||||||
|
ui.setDisplayYn("N"); // 비공개로 자동 생성 - 관리자가 API Spec 관리에서 확인 후 공개
|
||||||
|
apiSpecManService.save(ui);
|
||||||
|
outcome.createdApiSpecs.add(apiId);
|
||||||
|
|
||||||
|
return apiSpecInfoService.findById(apiId)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("API Spec 저장 직후 조회 실패: " + apiId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Outcome {
|
||||||
|
private final boolean inserted;
|
||||||
|
private final List<String> createdApiSpecs = new ArrayList<>();
|
||||||
|
private final List<String> specGenFailed = new ArrayList<>();
|
||||||
|
|
||||||
|
Outcome(boolean inserted) {
|
||||||
|
this.inserted = inserted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,11 @@ public class ClientController extends OnlBaseAnnotationController {
|
|||||||
response.put("createdApiSpecCount", result.getCreatedApiSpecCount());
|
response.put("createdApiSpecCount", result.getCreatedApiSpecCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spec 자동생성 실패(레이아웃/어댑터 미설정 등)로 최소 정보만 생성된 API 목록
|
||||||
|
if (result.getSpecGenFailed() != null && !result.getSpecGenFailed().isEmpty()) {
|
||||||
|
response.put("specGenFailed", result.getSpecGenFailed());
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 에러 처리
|
// 에러 처리
|
||||||
|
|||||||
+14
-135
@@ -1,21 +1,12 @@
|
|||||||
package com.eactive.eai.rms.onl.manage.authserver.client;
|
package com.eactive.eai.rms.onl.manage.authserver.client;
|
||||||
|
|
||||||
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
|
||||||
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
|
||||||
import com.eactive.apim.portal.app.entity.Credential;
|
|
||||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||||
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalorg.PortalOrgService;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalorg.PortalOrgService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
|
import com.eactive.eai.rms.onl.apim.approval.credential.PortalCredentialSyncService;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialService;
|
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
|
|
||||||
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUI;
|
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUI;
|
||||||
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUIMapper;
|
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUIMapper;
|
||||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -28,7 +19,6 @@ import com.eactive.eai.rms.common.base.OnlBaseService;
|
|||||||
import com.eactive.eai.rms.data.entity.onl.authserver.ClientEntityService;
|
import com.eactive.eai.rms.data.entity.onl.authserver.ClientEntityService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.authserver.ClientSearch;
|
import com.eactive.eai.rms.data.entity.onl.authserver.ClientSearch;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -42,28 +32,22 @@ public class ClientManService extends OnlBaseService {
|
|||||||
private ClientUIMapper clientUIMapper;
|
private ClientUIMapper clientUIMapper;
|
||||||
private PortalOrgService portalOrgService;
|
private PortalOrgService portalOrgService;
|
||||||
private PortalOrgUIMapper portalOrgUIMapper;
|
private PortalOrgUIMapper portalOrgUIMapper;
|
||||||
private CredentialManService credentialManService;
|
|
||||||
private CredentialService credentialService;
|
|
||||||
private EAIMessageService eaiMessageService;
|
private EAIMessageService eaiMessageService;
|
||||||
private ApiSpecInfoService apiSpecInfoService;
|
private PortalCredentialSyncService portalCredentialSyncService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ClientManService(ClientEntityService clientEntityService,
|
public ClientManService(ClientEntityService clientEntityService,
|
||||||
ClientUIMapper clientUIMapper,
|
ClientUIMapper clientUIMapper,
|
||||||
PortalOrgService portalOrgService,
|
PortalOrgService portalOrgService,
|
||||||
PortalOrgUIMapper portalOrgUIMapper,
|
PortalOrgUIMapper portalOrgUIMapper,
|
||||||
CredentialManService credentialManService,
|
|
||||||
CredentialService credentialService,
|
|
||||||
EAIMessageService eaiMessageService,
|
EAIMessageService eaiMessageService,
|
||||||
ApiSpecInfoService apiSpecInfoService) {
|
PortalCredentialSyncService portalCredentialSyncService) {
|
||||||
this.clientEntityService = clientEntityService;
|
this.clientEntityService = clientEntityService;
|
||||||
this.clientUIMapper = clientUIMapper;
|
this.clientUIMapper = clientUIMapper;
|
||||||
this.portalOrgService = portalOrgService;
|
this.portalOrgService = portalOrgService;
|
||||||
this.portalOrgUIMapper = portalOrgUIMapper;
|
this.portalOrgUIMapper = portalOrgUIMapper;
|
||||||
this.credentialManService = credentialManService;
|
|
||||||
this.credentialService = credentialService;
|
|
||||||
this.eaiMessageService = eaiMessageService;
|
this.eaiMessageService = eaiMessageService;
|
||||||
this.apiSpecInfoService = apiSpecInfoService;
|
this.portalCredentialSyncService = portalCredentialSyncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +111,10 @@ public class ClientManService extends OnlBaseService {
|
|||||||
* AGW 인증관리(ClientEntity)의 정보를 개발자포탈(Credential)에 반영합니다.
|
* AGW 인증관리(ClientEntity)의 정보를 개발자포탈(Credential)에 반영합니다.
|
||||||
* 포탈에 없으면 신규 생성(INSERT), 있으면 업데이트(UPDATE)합니다.
|
* 포탈에 없으면 신규 생성(INSERT), 있으면 업데이트(UPDATE)합니다.
|
||||||
*
|
*
|
||||||
|
* 검증(2,3단계)은 APIGW 스키마에서 이 메서드의 트랜잭션 안에서 수행하고, 실패 시 포탈은 전혀 건드리지
|
||||||
|
* 않는다. 실제 포탈 쓰기(Credential/ApiSpecInfo)는 {@link PortalCredentialSyncService}
|
||||||
|
* (별도 EMS 트랜잭션)에 위임한다.
|
||||||
|
*
|
||||||
* @param clientId 클라이언트 ID
|
* @param clientId 클라이언트 ID
|
||||||
* @return 반영 결과 (INSERTED, UPDATED, SKIPPED_NO_ORG, SKIPPED_INVALID_API, ERROR)
|
* @return 반영 결과 (INSERTED, UPDATED, SKIPPED_NO_ORG, SKIPPED_INVALID_API, ERROR)
|
||||||
*/
|
*/
|
||||||
@@ -181,73 +169,20 @@ public class ClientManService extends OnlBaseService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. MONITORING 스키마로 전환
|
// 4. 포탈 스키마에 반영 (별도 EMS 트랜잭션)
|
||||||
DataSourceType monitoringType = DataSourceTypeManager.getDataSourceType("MONITORING");
|
PortalCredentialSyncService.Outcome outcome = portalCredentialSyncService.apply(clientEntity);
|
||||||
DataSourceContextHolder.setDataSourceType(monitoringType);
|
|
||||||
|
|
||||||
// 5. PTL_API_SPEC_INFO에 없는 API 자동 생성
|
|
||||||
List<String> createdApiSpecs = new ArrayList<>();
|
|
||||||
if (clientEntity.getApiList() != null && !clientEntity.getApiList().isEmpty()) {
|
|
||||||
for (EAIMessageEntity apiEntity : clientEntity.getApiList()) {
|
|
||||||
String apiId = apiEntity.getEaisvcname();
|
|
||||||
if (!apiSpecInfoService.findById(apiId).isPresent()) {
|
|
||||||
// PTL_API_SPEC_INFO에 없으면 자동 생성
|
|
||||||
ApiSpecInfo newApiSpec = new ApiSpecInfo();
|
|
||||||
newApiSpec.setApiId(apiId);
|
|
||||||
newApiSpec.setApiName(StringUtils.isNotBlank(apiEntity.getEaisvcdesc())
|
|
||||||
? apiEntity.getEaisvcdesc() : apiId);
|
|
||||||
newApiSpec.setApiSimpleDescription(apiEntity.getEaisvcdesc());
|
|
||||||
newApiSpec.setDisplayYn("Y"); // 기본값: 공개
|
|
||||||
apiSpecInfoService.create(newApiSpec);
|
|
||||||
createdApiSpecs.add(apiId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 기존 Credential 확인
|
|
||||||
Credential existingCredential = credentialService.findByClientidAndOrgid(clientId, orgId);
|
|
||||||
|
|
||||||
// 7. 데이터 변환 (ClientEntity → CredentialUI)
|
|
||||||
CredentialUI credentialUI = convertToCredentialUI(clientEntity);
|
|
||||||
|
|
||||||
SyncResult.SyncAction action;
|
|
||||||
if (existingCredential != null) {
|
|
||||||
// 업데이트: 기존 값 유지해야 할 필드 처리
|
|
||||||
Credential existing = existingCredential;
|
|
||||||
if (StringUtils.isNotBlank(existing.getAppDescription())) {
|
|
||||||
credentialUI.setAppDescription(existing.getAppDescription());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(existing.getAppIconFileId())) {
|
|
||||||
credentialUI.setAppIconFileId(existing.getAppIconFileId());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(existing.getServer())) {
|
|
||||||
credentialUI.setServer(existing.getServer());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 포탈 스키마에 업데이트
|
|
||||||
credentialManService.update(credentialUI);
|
|
||||||
action = SyncResult.SyncAction.UPDATED;
|
|
||||||
} else {
|
|
||||||
// 신규 생성: 기본값 설정
|
|
||||||
if (StringUtils.isBlank(credentialUI.getAppDescription())) {
|
|
||||||
credentialUI.setAppDescription("APIGW에서 반영된 인증정보");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 포탈 스키마에 신규 생성
|
|
||||||
credentialManService.insert(credentialUI);
|
|
||||||
action = SyncResult.SyncAction.INSERTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SyncResult.builder()
|
return SyncResult.builder()
|
||||||
.success(true)
|
.success(true)
|
||||||
.action(action)
|
.action(outcome.isInserted() ? SyncResult.SyncAction.INSERTED : SyncResult.SyncAction.UPDATED)
|
||||||
.message("포탈 정보 반영 완료")
|
.message("포탈 정보 반영 완료")
|
||||||
.targetApis(requestedApiIds)
|
.targetApis(requestedApiIds)
|
||||||
.targetApiCount(requestedApiIds.size())
|
.targetApiCount(requestedApiIds.size())
|
||||||
.syncedApis(requestedApiIds)
|
.syncedApis(requestedApiIds)
|
||||||
.syncedApiCount(requestedApiIds.size())
|
.syncedApiCount(requestedApiIds.size())
|
||||||
.createdApiSpecs(createdApiSpecs)
|
.createdApiSpecs(outcome.getCreatedApiSpecs())
|
||||||
.createdApiSpecCount(createdApiSpecs.size())
|
.createdApiSpecCount(outcome.getCreatedApiSpecs().size())
|
||||||
|
.specGenFailed(outcome.getSpecGenFailed())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -259,60 +194,4 @@ public class ClientManService extends OnlBaseService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ClientEntity를 CredentialUI로 변환합니다.
|
|
||||||
*
|
|
||||||
* @param clientEntity APIGW 클라이언트 엔티티
|
|
||||||
* @return CredentialUI 포탈 인증정보 UI 객체
|
|
||||||
*/
|
|
||||||
private CredentialUI convertToCredentialUI(ClientEntity clientEntity) {
|
|
||||||
CredentialUI credentialUI = new CredentialUI();
|
|
||||||
|
|
||||||
// 기본 인증정보
|
|
||||||
credentialUI.setClientid(clientEntity.getClientid());
|
|
||||||
credentialUI.setClientname(clientEntity.getClientname());
|
|
||||||
credentialUI.setClientsecret(clientEntity.getClientsecret());
|
|
||||||
|
|
||||||
// OAuth 설정
|
|
||||||
credentialUI.setScope(clientEntity.getScope());
|
|
||||||
credentialUI.setGranttypes(clientEntity.getGranttypes());
|
|
||||||
credentialUI.setAccesstokenvalidityseconds(clientEntity.getAccesstokenvalidityseconds());
|
|
||||||
credentialUI.setRefreshtokenvalidityseconds(clientEntity.getRefreshtokenvalidityseconds());
|
|
||||||
|
|
||||||
// 보안 설정
|
|
||||||
credentialUI.setAllowedips(clientEntity.getAllowedips());
|
|
||||||
credentialUI.setAuthorities(clientEntity.getAuthorities());
|
|
||||||
credentialUI.setRedirecturi(clientEntity.getRedirecturi());
|
|
||||||
credentialUI.setSecuritykey(clientEntity.getSecuritykey());
|
|
||||||
credentialUI.setAutoapprove(clientEntity.getAutoapprove());
|
|
||||||
credentialUI.setResourceids(clientEntity.getResourceids());
|
|
||||||
|
|
||||||
// 조직 정보
|
|
||||||
credentialUI.setOrgid(clientEntity.getOrgid());
|
|
||||||
credentialUI.setOrgname(clientEntity.getOrgname());
|
|
||||||
|
|
||||||
// 앱 상태
|
|
||||||
credentialUI.setAppstatus(clientEntity.getAppstatus());
|
|
||||||
credentialUI.setDailytokenlimit(clientEntity.getDailytokenlimit());
|
|
||||||
|
|
||||||
// 수정 정보
|
|
||||||
credentialUI.setModifiedby(clientEntity.getModifiedby());
|
|
||||||
if (clientEntity.getModifiedon() != null) {
|
|
||||||
credentialUI.setModifiedon(clientEntity.getModifiedon().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 목록 변환
|
|
||||||
List<ApiSpecInfoUI> apiList = new ArrayList<>();
|
|
||||||
if (clientEntity.getApiList() != null) {
|
|
||||||
for (EAIMessageEntity apiEntity : clientEntity.getApiList()) {
|
|
||||||
ApiSpecInfoUI apiUI = new ApiSpecInfoUI();
|
|
||||||
apiUI.setApiId(apiEntity.getEaisvcname());
|
|
||||||
apiList.add(apiUI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
credentialUI.setApiList(apiList);
|
|
||||||
|
|
||||||
return credentialUI;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ public class SyncResult {
|
|||||||
*/
|
*/
|
||||||
private int createdApiSpecCount;
|
private int createdApiSpecCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spec 자동생성에 실패해 최소 정보(apiId만)로 대체 생성된 API 목록
|
||||||
|
* (레이아웃/어댑터 정보 부재 등. PTL_API_SPEC_INFO 행 자체는 생성됨 - displayYn='N')
|
||||||
|
*/
|
||||||
|
private List<String> specGenFailed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 반영 액션 타입
|
* 반영 액션 타입
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package com.eactive.eai.rms.onl.transaction.apim;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레이아웃+어댑터 기반 API Spec(OpenAPI 3.1) 자동생성 규칙(백엔드 단일 소스).
|
||||||
|
*
|
||||||
|
* {@link DjbApiSpecController}(6단계 편집 마법사)와 인증관리 "개발자포탈 정보 반영"
|
||||||
|
* (ClientManService.syncPortalData → PortalCredentialSyncService) 양쪽이 이 서비스를 공유한다.
|
||||||
|
* 규칙 자체는 원래 DjbApiSpecController 의 private 메서드였던 것을 그대로 옮긴 것으로, 동작 변경은 없다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DjbApiSpecAutoGenService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DjbApiSpecAutoGenService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApiInterfaceService apiInterfaceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApiSpecManService apiSpecManService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레이아웃/어댑터 정보를 조합해 OpenAPI 3.1 스펙 JSON 을 생성한다.
|
||||||
|
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
||||||
|
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
||||||
|
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
||||||
|
*/
|
||||||
|
public String generateSpecJson(String eaiSvcName) throws Exception {
|
||||||
|
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||||
|
if (apiInterfaceUI == null) {
|
||||||
|
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||||
|
}
|
||||||
|
LayoutUI requestLayoutUI = resolveLayout(apiInterfaceUI, true);
|
||||||
|
LayoutUI responseLayoutUI = resolveLayout(apiInterfaceUI, false);
|
||||||
|
return generateSpecJson(eaiSvcName, apiInterfaceUI, requestLayoutUI, responseLayoutUI);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateSpecJson(String eaiSvcName, ApiInterfaceUI apiInterfaceUI,
|
||||||
|
LayoutUI requestLayoutUI, LayoutUI responseLayoutUI) throws Exception {
|
||||||
|
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
||||||
|
if (inboundAdapterSpec == null) {
|
||||||
|
inboundAdapterSpec = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
||||||
|
|
||||||
|
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||||
|
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
||||||
|
return applyAutoRules(baseSpec, apiName, eaiSvcName, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동생성 결과를 저장 가능한 {@link ApiSpecInfoUI} 로 조립한다.
|
||||||
|
* displayYn 은 호출자가 정한다(비공개로 자동 생성하려면 호출 후 "N" 을 세팅).
|
||||||
|
*/
|
||||||
|
public ApiSpecInfoUI generateApiSpecInfo(String eaiSvcName) throws Exception {
|
||||||
|
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||||
|
if (apiInterfaceUI == null) {
|
||||||
|
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||||
|
LayoutUI requestLayoutUI = resolveLayout(apiInterfaceUI, true);
|
||||||
|
LayoutUI responseLayoutUI = resolveLayout(apiInterfaceUI, false);
|
||||||
|
String specJson = generateSpecJson(eaiSvcName, apiInterfaceUI, requestLayoutUI, responseLayoutUI);
|
||||||
|
|
||||||
|
ApiSpecInfoUI ui = new ApiSpecInfoUI();
|
||||||
|
ui.setApiId(eaiSvcName);
|
||||||
|
ui.setApiName(apiName);
|
||||||
|
ui.setApiSimpleDescription(apiInterfaceUI.getEaiSvcDesc());
|
||||||
|
ui.setTestbedSpec(specJson);
|
||||||
|
ui.setApiRequestSpec(apiSpecManService.generateSpecTableFromLayout(requestLayoutUI));
|
||||||
|
ui.setApiResponseSpec(apiSpecManService.generateSpecTableFromLayout(responseLayoutUI));
|
||||||
|
ui.setSampleRequest(apiSpecManService.generateSampleDataFromLayout(requestLayoutUI));
|
||||||
|
ui.setSampleResponse(apiSpecManService.generateSampleDataFromLayout(responseLayoutUI));
|
||||||
|
|
||||||
|
extractFirstOperation(specJson, ui);
|
||||||
|
return ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 생성된 spec 의 첫 path/method/requestBody mediaType 을 추출한다(마법사 프론트와 동일 규칙, app.js 의 _defScalar 대응). */
|
||||||
|
private void extractFirstOperation(String specJson, ApiSpecInfoUI ui) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(specJson);
|
||||||
|
JsonNode pathsNode = root.get("paths");
|
||||||
|
if (pathsNode == null || !pathsNode.isObject() || pathsNode.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Iterator<Map.Entry<String, JsonNode>> pathIt = pathsNode.fields();
|
||||||
|
if (!pathIt.hasNext()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map.Entry<String, JsonNode> pathEntry = pathIt.next();
|
||||||
|
ui.setApiUrl(pathEntry.getKey());
|
||||||
|
|
||||||
|
JsonNode pathItem = pathEntry.getValue();
|
||||||
|
if (pathItem != null && pathItem.isObject() && pathItem.fields().hasNext()) {
|
||||||
|
Map.Entry<String, JsonNode> methodEntry = pathItem.fields().next();
|
||||||
|
ui.setApiMethod(methodEntry.getKey().toUpperCase());
|
||||||
|
|
||||||
|
JsonNode op = methodEntry.getValue();
|
||||||
|
if (op != null && op.isObject()) {
|
||||||
|
JsonNode content = op.path("requestBody").path("content");
|
||||||
|
if (content.isObject() && content.fields().hasNext()) {
|
||||||
|
ui.setApiContentType(content.fields().next().getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("자동생성 spec 에서 apiUrl/apiMethod 추출 실패: " + ui.getApiId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LayoutUI resolveLayout(ApiInterfaceUI apiInterfaceUI, boolean request) throws Exception {
|
||||||
|
String layoutName = request ? apiInterfaceUI.getInboundRequestLayout() : apiInterfaceUI.getInboundResponseLayout();
|
||||||
|
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
||||||
|
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String contentType) {
|
||||||
|
try {
|
||||||
|
JsonNode parsed = objectMapper.readTree(specJson);
|
||||||
|
if (!parsed.isObject()) {
|
||||||
|
return specJson;
|
||||||
|
}
|
||||||
|
ObjectNode root = (ObjectNode) parsed;
|
||||||
|
|
||||||
|
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
||||||
|
getOrCreateObject(root, "info").put("title", apiName);
|
||||||
|
|
||||||
|
String tagName = "DJBank"; // 자동생성 태그명 고정
|
||||||
|
ArrayNode tags = objectMapper.createArrayNode();
|
||||||
|
ObjectNode tag = objectMapper.createObjectNode();
|
||||||
|
tag.put("name", tagName);
|
||||||
|
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
||||||
|
tags.add(tag);
|
||||||
|
root.set("tags", tags);
|
||||||
|
|
||||||
|
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
||||||
|
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
||||||
|
String adapterPath = "";
|
||||||
|
JsonNode serversNode = root.get("servers");
|
||||||
|
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
||||||
|
adapterPath = serversNode.get(0).get("url").asText("");
|
||||||
|
}
|
||||||
|
root.remove("servers");
|
||||||
|
|
||||||
|
JsonNode pathsNode = root.get("paths");
|
||||||
|
if (pathsNode != null && pathsNode.isObject()) {
|
||||||
|
ObjectNode paths = (ObjectNode) pathsNode;
|
||||||
|
List<String> pathKeys = new ArrayList<>();
|
||||||
|
Iterator<String> pit = paths.fieldNames();
|
||||||
|
while (pit.hasNext()) {
|
||||||
|
pathKeys.add(pit.next());
|
||||||
|
}
|
||||||
|
for (String pk : pathKeys) {
|
||||||
|
JsonNode pathItem = paths.get(pk);
|
||||||
|
if (pathItem != null && pathItem.isObject()) {
|
||||||
|
List<String> methods = new ArrayList<>();
|
||||||
|
Iterator<String> mit = pathItem.fieldNames();
|
||||||
|
while (mit.hasNext()) {
|
||||||
|
methods.add(mit.next());
|
||||||
|
}
|
||||||
|
for (String mk : methods) {
|
||||||
|
JsonNode opNode = pathItem.get(mk);
|
||||||
|
if (opNode != null && opNode.isObject()) {
|
||||||
|
ObjectNode op = (ObjectNode) opNode;
|
||||||
|
op.put("operationId", eaiSvcName);
|
||||||
|
op.put("summary", apiName);
|
||||||
|
ArrayNode opTags = objectMapper.createArrayNode();
|
||||||
|
opTags.add(tagName);
|
||||||
|
op.set("tags", opTags);
|
||||||
|
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
||||||
|
markGwOnOperation(op);
|
||||||
|
addResponseHeader(op, "200", "Content-Type", contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String newKey = adapterPath.isEmpty() ? pk
|
||||||
|
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
||||||
|
if (!newKey.equals(pk)) {
|
||||||
|
paths.set(newKey, pathItem);
|
||||||
|
paths.remove(pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectMapper.writeValueAsString(root);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
||||||
|
return specJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
||||||
|
private void markGwOnOperation(ObjectNode op) {
|
||||||
|
JsonNode body = op.get("requestBody");
|
||||||
|
if (body != null && body.isObject()) {
|
||||||
|
markGwInContent(body.get("content"));
|
||||||
|
}
|
||||||
|
JsonNode resps = op.get("responses");
|
||||||
|
if (resps != null && resps.isObject()) {
|
||||||
|
List<String> codes = new ArrayList<>();
|
||||||
|
Iterator<String> it = resps.fieldNames();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
codes.add(it.next());
|
||||||
|
}
|
||||||
|
for (String c : codes) {
|
||||||
|
JsonNode r = resps.get(c);
|
||||||
|
if (r != null && r.isObject()) {
|
||||||
|
markGwInContent(r.get("content"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markGwInContent(JsonNode content) {
|
||||||
|
if (content == null || !content.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> mts = new ArrayList<>();
|
||||||
|
Iterator<String> it = content.fieldNames();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
mts.add(it.next());
|
||||||
|
}
|
||||||
|
for (String mt : mts) {
|
||||||
|
JsonNode m = content.get(mt);
|
||||||
|
if (m != null && m.isObject()) {
|
||||||
|
markGwSchema(m.get("schema"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markGwSchema(JsonNode schema) {
|
||||||
|
if (schema == null || !schema.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectNode s = (ObjectNode) schema;
|
||||||
|
JsonNode props = s.get("properties");
|
||||||
|
if (props != null && props.isObject()) {
|
||||||
|
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
||||||
|
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
||||||
|
java.util.Set<String> existing = new java.util.HashSet<>();
|
||||||
|
required.forEach(n -> existing.add(n.asText()));
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
Iterator<String> it = props.fieldNames();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
keys.add(it.next());
|
||||||
|
}
|
||||||
|
for (String k : keys) {
|
||||||
|
JsonNode p = props.get(k);
|
||||||
|
if (p != null && p.isObject()) {
|
||||||
|
((ObjectNode) p).put("x-djb-gw", true);
|
||||||
|
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
||||||
|
required.add(k);
|
||||||
|
existing.add(k);
|
||||||
|
}
|
||||||
|
markGwSchema(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (required.size() > 0) {
|
||||||
|
s.set("required", required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsonNode items = s.get("items");
|
||||||
|
if (items != null && items.isObject()) {
|
||||||
|
markGwSchema(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
||||||
|
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
||||||
|
JsonNode resps = op.get("responses");
|
||||||
|
if (resps == null || !resps.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonNode r = resps.get(code);
|
||||||
|
if (r == null || !r.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
||||||
|
if (headers.has(headerName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectNode h = objectMapper.createObjectNode();
|
||||||
|
h.put("description", headerName);
|
||||||
|
ObjectNode sc = objectMapper.createObjectNode();
|
||||||
|
sc.put("type", "string");
|
||||||
|
h.set("schema", sc);
|
||||||
|
h.put("example", example);
|
||||||
|
headers.set(headerName, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||||
|
JsonNode node = parent.get(field);
|
||||||
|
if (node != null && node.isObject()) {
|
||||||
|
return (ObjectNode) node;
|
||||||
|
}
|
||||||
|
ObjectNode created = objectMapper.createObjectNode();
|
||||||
|
parent.set(field, created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,13 @@ package com.eactive.eai.rms.onl.transaction.apim;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.fasterxml.jackson.databind.node.TextNode;
|
|
||||||
|
|
||||||
import org.apache.commons.lang.StringEscapeUtils;
|
import org.apache.commons.lang.StringEscapeUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -60,6 +57,9 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DjbApiSpecLayoutMergeService layoutMergeService;
|
private DjbApiSpecLayoutMergeService layoutMergeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DjbApiSpecAutoGenService autoGenService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private PortalPropertyService portalPropertyService;
|
private PortalPropertyService portalPropertyService;
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
|
|||||||
spec = saved.getTestbedSpec();
|
spec = saved.getTestbedSpec();
|
||||||
source = "saved";
|
source = "saved";
|
||||||
} else {
|
} else {
|
||||||
spec = generateSpec(eaiSvcName);
|
spec = autoGenService.generateSpecJson(eaiSvcName);
|
||||||
source = force ? "regenerated" : "generated";
|
source = force ? "regenerated" : "generated";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,268 +324,6 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
|
|||||||
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 레이아웃 기반 자동 생성 + 자동생성 규칙(백엔드 단일 소스).
|
|
||||||
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
|
||||||
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
|
||||||
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
|
||||||
*/
|
|
||||||
private String generateSpec(String eaiSvcName) throws Exception {
|
|
||||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
|
||||||
if (apiInterfaceUI == null) {
|
|
||||||
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
|
||||||
}
|
|
||||||
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
|
||||||
if (inboundAdapterSpec == null) {
|
|
||||||
inboundAdapterSpec = new HashMap<>();
|
|
||||||
}
|
|
||||||
String requestLayoutName = apiInterfaceUI.getInboundRequestLayout();
|
|
||||||
String responseLayoutName = apiInterfaceUI.getInboundResponseLayout();
|
|
||||||
LayoutUI requestLayoutUI = StringUtils.isNotEmpty(requestLayoutName)
|
|
||||||
? apiSpecManService.selectLayoutUI(requestLayoutName) : null;
|
|
||||||
LayoutUI responseLayoutUI = StringUtils.isNotEmpty(responseLayoutName)
|
|
||||||
? apiSpecManService.selectLayoutUI(responseLayoutName) : null;
|
|
||||||
|
|
||||||
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
|
||||||
|
|
||||||
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
|
||||||
String reqSample = apiSpecManService.generateSampleDataFromLayout(requestLayoutUI);
|
|
||||||
String resSample = apiSpecManService.generateSampleDataFromLayout(responseLayoutUI);
|
|
||||||
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
|
||||||
return applyAutoRules(baseSpec, apiName, eaiSvcName, reqSample, resSample, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
|
||||||
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String reqSample, String resSample, String contentType) {
|
|
||||||
try {
|
|
||||||
JsonNode parsed = objectMapper.readTree(specJson);
|
|
||||||
if (!parsed.isObject()) {
|
|
||||||
return specJson;
|
|
||||||
}
|
|
||||||
ObjectNode root = (ObjectNode) parsed;
|
|
||||||
|
|
||||||
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
|
||||||
getOrCreateObject(root, "info").put("title", apiName);
|
|
||||||
|
|
||||||
String tagName = "DJBank"; // 자동생성 태그명 고정
|
|
||||||
ArrayNode tags = objectMapper.createArrayNode();
|
|
||||||
ObjectNode tag = objectMapper.createObjectNode();
|
|
||||||
tag.put("name", tagName);
|
|
||||||
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
|
||||||
tags.add(tag);
|
|
||||||
root.set("tags", tags);
|
|
||||||
|
|
||||||
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
|
||||||
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
|
||||||
String adapterPath = "";
|
|
||||||
JsonNode serversNode = root.get("servers");
|
|
||||||
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
|
||||||
adapterPath = serversNode.get(0).get("url").asText("");
|
|
||||||
}
|
|
||||||
root.remove("servers");
|
|
||||||
|
|
||||||
JsonNode pathsNode = root.get("paths");
|
|
||||||
if (pathsNode != null && pathsNode.isObject()) {
|
|
||||||
ObjectNode paths = (ObjectNode) pathsNode;
|
|
||||||
List<String> pathKeys = new ArrayList<>();
|
|
||||||
Iterator<String> pit = paths.fieldNames();
|
|
||||||
while (pit.hasNext()) {
|
|
||||||
pathKeys.add(pit.next());
|
|
||||||
}
|
|
||||||
for (String pk : pathKeys) {
|
|
||||||
JsonNode pathItem = paths.get(pk);
|
|
||||||
if (pathItem != null && pathItem.isObject()) {
|
|
||||||
List<String> methods = new ArrayList<>();
|
|
||||||
Iterator<String> mit = pathItem.fieldNames();
|
|
||||||
while (mit.hasNext()) {
|
|
||||||
methods.add(mit.next());
|
|
||||||
}
|
|
||||||
for (String mk : methods) {
|
|
||||||
JsonNode opNode = pathItem.get(mk);
|
|
||||||
if (opNode != null && opNode.isObject()) {
|
|
||||||
ObjectNode op = (ObjectNode) opNode;
|
|
||||||
op.put("operationId", eaiSvcName);
|
|
||||||
op.put("summary", apiName);
|
|
||||||
ArrayNode opTags = objectMapper.createArrayNode();
|
|
||||||
opTags.add(tagName);
|
|
||||||
op.set("tags", opTags);
|
|
||||||
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
|
||||||
markGwOnOperation(op);
|
|
||||||
addResponseHeader(op, "200", "Content-Type", contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String newKey = adapterPath.isEmpty() ? pk
|
|
||||||
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
|
||||||
if (!newKey.equals(pk)) {
|
|
||||||
paths.set(newKey, pathItem);
|
|
||||||
paths.remove(pk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return objectMapper.writeValueAsString(root);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
|
||||||
return specJson;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setContentExample(ObjectNode op, String sampleJson) {
|
|
||||||
if (StringUtils.isBlank(sampleJson)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode body = op.get("requestBody");
|
|
||||||
if (body == null || !body.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode content = body.get("content");
|
|
||||||
if (content == null || !content.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Iterator<String> mts = content.fieldNames();
|
|
||||||
if (!mts.hasNext()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode mtNode = content.get(mts.next());
|
|
||||||
if (mtNode != null && mtNode.isObject()) {
|
|
||||||
((ObjectNode) mtNode).set("example", parseJsonOrText(sampleJson));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setResponseExample(ObjectNode op, String code, String sampleJson) {
|
|
||||||
if (StringUtils.isBlank(sampleJson)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode resps = op.get("responses");
|
|
||||||
if (resps == null || !resps.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode r = resps.get(code);
|
|
||||||
if (r == null || !r.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode content = getOrCreateObject((ObjectNode) r, "content");
|
|
||||||
ObjectNode appjson = getOrCreateObject(content, "application/json");
|
|
||||||
appjson.set("example", parseJsonOrText(sampleJson));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
|
||||||
private void markGwOnOperation(ObjectNode op) {
|
|
||||||
JsonNode body = op.get("requestBody");
|
|
||||||
if (body != null && body.isObject()) {
|
|
||||||
markGwInContent(body.get("content"));
|
|
||||||
}
|
|
||||||
JsonNode resps = op.get("responses");
|
|
||||||
if (resps != null && resps.isObject()) {
|
|
||||||
List<String> codes = new ArrayList<>();
|
|
||||||
Iterator<String> it = resps.fieldNames();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
codes.add(it.next());
|
|
||||||
}
|
|
||||||
for (String c : codes) {
|
|
||||||
JsonNode r = resps.get(c);
|
|
||||||
if (r != null && r.isObject()) {
|
|
||||||
markGwInContent(r.get("content"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markGwInContent(JsonNode content) {
|
|
||||||
if (content == null || !content.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<String> mts = new ArrayList<>();
|
|
||||||
Iterator<String> it = content.fieldNames();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
mts.add(it.next());
|
|
||||||
}
|
|
||||||
for (String mt : mts) {
|
|
||||||
JsonNode m = content.get(mt);
|
|
||||||
if (m != null && m.isObject()) {
|
|
||||||
markGwSchema(m.get("schema"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markGwSchema(JsonNode schema) {
|
|
||||||
if (schema == null || !schema.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode s = (ObjectNode) schema;
|
|
||||||
JsonNode props = s.get("properties");
|
|
||||||
if (props != null && props.isObject()) {
|
|
||||||
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
|
||||||
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
|
||||||
java.util.Set<String> existing = new java.util.HashSet<>();
|
|
||||||
required.forEach(n -> existing.add(n.asText()));
|
|
||||||
List<String> keys = new ArrayList<>();
|
|
||||||
Iterator<String> it = props.fieldNames();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
keys.add(it.next());
|
|
||||||
}
|
|
||||||
for (String k : keys) {
|
|
||||||
JsonNode p = props.get(k);
|
|
||||||
if (p != null && p.isObject()) {
|
|
||||||
((ObjectNode) p).put("x-djb-gw", true);
|
|
||||||
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
|
||||||
required.add(k);
|
|
||||||
existing.add(k);
|
|
||||||
}
|
|
||||||
markGwSchema(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (required.size() > 0) {
|
|
||||||
s.set("required", required);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JsonNode items = s.get("items");
|
|
||||||
if (items != null && items.isObject()) {
|
|
||||||
markGwSchema(items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
|
||||||
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
|
||||||
JsonNode resps = op.get("responses");
|
|
||||||
if (resps == null || !resps.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode r = resps.get(code);
|
|
||||||
if (r == null || !r.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
|
||||||
if (headers.has(headerName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode h = objectMapper.createObjectNode();
|
|
||||||
h.put("description", headerName);
|
|
||||||
ObjectNode sc = objectMapper.createObjectNode();
|
|
||||||
sc.put("type", "string");
|
|
||||||
h.set("schema", sc);
|
|
||||||
h.put("example", example);
|
|
||||||
headers.set(headerName, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonNode parseJsonOrText(String s) {
|
|
||||||
try {
|
|
||||||
return objectMapper.readTree(s);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return TextNode.valueOf(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
|
||||||
JsonNode node = parent.get(field);
|
|
||||||
if (node != null && node.isObject()) {
|
|
||||||
return (ObjectNode) node;
|
|
||||||
}
|
|
||||||
ObjectNode created = objectMapper.createObjectNode();
|
|
||||||
parent.set(field, created);
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map.put(key, value);
|
map.put(key, value);
|
||||||
|
|||||||
Reference in New Issue
Block a user