개발자포탈 미게시 GW 인터페이스 표시 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- hiddenApiCount 필드로 미게시 인터페이스 집계 노출
- API Pills 및 관련 CSS/JS 수정
- 영향 인터페이스 노출 로직 개선 및 VisibleApis 클래스 적용
This commit is contained in:
Rinjae
2026-08-05 10:44:50 +09:00
parent d71af34950
commit 2c63fa1557
14 changed files with 176 additions and 40 deletions
@@ -54,6 +54,8 @@ public class PortalNoticeDTO {
private String state; private String state;
private String previousState; private String previousState;
private List<IncidentAffectedApiDTO> affectedApis = Collections.emptyList(); private List<IncidentAffectedApiDTO> affectedApis = Collections.emptyList();
/** 개발자포탈에 게시되지 않아 개별 노출하지 않는 GW 인터페이스 건수 */
private int hiddenApiCount;
/** 장애 처리 타임라인. 최신순(내림차순), 공개(visibleYn='Y') 항목만 담는다. */ /** 장애 처리 타임라인. 최신순(내림차순), 공개(visibleYn='Y') 항목만 담는다. */
private List<TimelineEntryDTO> timeline = Collections.emptyList(); private List<TimelineEntryDTO> timeline = Collections.emptyList();
@@ -5,7 +5,9 @@ import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeDTO;
import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeSearch; import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeSearch;
import com.eactive.apim.portal.apps.community.notice.mapper.PortalNoticeMapper; import com.eactive.apim.portal.apps.community.notice.mapper.PortalNoticeMapper;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident; import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
import com.eactive.apim.portal.djb.apistatus.service.ApiStatusAssembler; import com.eactive.apim.portal.djb.apistatus.service.ApiStatusAssembler;
import com.eactive.apim.portal.djb.apistatus.service.ApiStatusCatalogService;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository; import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository; import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
import com.eactive.apim.portal.portalNotice.entity.PortalNotice; import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
@@ -17,8 +19,10 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -31,6 +35,7 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
private final DjbApistatusIncidentRepository incidentRepository; private final DjbApistatusIncidentRepository incidentRepository;
private final DjbApistatusIncidentApiRepository incidentApiRepository; private final DjbApistatusIncidentApiRepository incidentApiRepository;
private final ApiStatusAssembler apiStatusAssembler; private final ApiStatusAssembler apiStatusAssembler;
private final ApiStatusCatalogService apiStatusCatalogService;
@Override @Override
public List<PortalNoticeDTO> getLatestNotices() { public List<PortalNoticeDTO> getLatestNotices() {
@@ -85,11 +90,22 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
dto.setState(incident.getState() == null ? null : incident.getState().name()); dto.setState(incident.getState() == null ? null : incident.getState().name());
dto.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name()); dto.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name());
List<IncidentAffectedApiDTO> apis = incidentApiRepository // 영향 인터페이스 중 개발자포탈에 게시된 API 만 개별 노출한다.
.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream() // 나머지 GW 인터페이스는 이름·ID 를 감추고 건수로만 알린다.
.map(api -> new IncidentAffectedApiDTO(api.getApiId(), api.getApiName())) Map<String, String> visibleNames = apiStatusCatalogService.getVisibleApiNames();
.collect(Collectors.toList()); List<IncidentAffectedApiDTO> apis = new ArrayList<>();
int hiddenCount = 0;
for (DjbApistatusIncidentApi api :
incidentApiRepository.findByIncidentIdOrderByApiId(incident.getIncidentId())) {
String publishedName = visibleNames.get(api.getApiId());
if (publishedName == null) {
hiddenCount++;
continue;
}
apis.add(new IncidentAffectedApiDTO(api.getApiId(), publishedName));
}
dto.setAffectedApis(apis); dto.setAffectedApis(apis);
dto.setHiddenApiCount(hiddenCount);
// 장애·지연만 타임라인을 붙인다 (점검은 타임라인을 쌓지 않음 — ADR-F15) // 장애·지연만 타임라인을 붙인다 (점검은 타임라인을 쌓지 않음 — ADR-F15)
boolean degrading = incident.getKind() != null && incident.getKind().isDegrading(); boolean degrading = incident.getKind() != null && incident.getKind().isDegrading();
@@ -28,5 +28,7 @@ public class ActiveIncidentDTO {
private LocalDateTime startedAt; private LocalDateTime startedAt;
private long elapsedMinutes; private long elapsedMinutes;
private List<AffectedApiDTO> apis = new ArrayList<>(); private List<AffectedApiDTO> apis = new ArrayList<>();
/** 개발자포탈에 게시되지 않아 개별 노출하지 않는 GW 인터페이스 건수 */
private int hiddenApiCount;
private List<TimelineEntryDTO> recentTimeline = new ArrayList<>(); private List<TimelineEntryDTO> recentTimeline = new ArrayList<>();
} }
@@ -27,6 +27,8 @@ public class MaintenanceCardDTO {
private LocalDateTime endAt; private LocalDateTime endAt;
private Long durationMinutes; private Long durationMinutes;
private List<AffectedApiDTO> impactedApis = new ArrayList<>(); private List<AffectedApiDTO> impactedApis = new ArrayList<>();
/** 개발자포탈에 게시되지 않아 개별 노출하지 않는 GW 인터페이스 건수 */
private int hiddenApiCount;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime registeredAt; private LocalDateTime registeredAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@@ -33,5 +33,7 @@ public class PastIssueCardDTO {
private LocalDate dateGroup; private LocalDate dateGroup;
private Long durationMinutes; private Long durationMinutes;
private List<AffectedApiDTO> impactedApis = new ArrayList<>(); private List<AffectedApiDTO> impactedApis = new ArrayList<>();
/** 개발자포탈에 게시되지 않아 개별 노출하지 않는 GW 인터페이스 건수 */
private int hiddenApiCount;
private List<TimelineEntryDTO> timeline = new ArrayList<>(); private List<TimelineEntryDTO> timeline = new ArrayList<>();
} }
@@ -13,6 +13,7 @@ import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusInc
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository; import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository;
import com.eactive.apim.portal.apps.community.notice.repository.PortalNoticeRepository; import com.eactive.apim.portal.apps.community.notice.repository.PortalNoticeRepository;
import com.eactive.apim.portal.portalNotice.entity.PortalNotice; import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -40,6 +41,41 @@ public class ApiStatusAssembler {
private final DjbApistatusIncidentApiRepository incidentApiRepository; private final DjbApistatusIncidentApiRepository incidentApiRepository;
private final DjbApistatusIncidentTimelineRepository timelineRepository; private final DjbApistatusIncidentTimelineRepository timelineRepository;
private final PortalNoticeRepository portalNoticeRepository; private final PortalNoticeRepository portalNoticeRepository;
private final ApiStatusCatalogService catalogService;
/**
* 영향 인터페이스를 "개발자포탈에 게시된 API" 와 그 외 GW 인터페이스로 가른 결과.
* 게시된 것만 개별 노출하고 나머지는 건수로만 알린다.
*/
@Getter
@RequiredArgsConstructor
public static class VisibleApis {
private final List<AffectedApiDTO> visible;
private final int hiddenCount;
}
/**
* 이슈 영향 인터페이스에서 현재 사용자에게 공개된 API 만 남긴다.
*
* <p>이름은 이슈에 캐시된 EAI 서비스명이 아니라 포털 노출명(PTL_API_SPEC_INFO)을 쓴다 -
* 같은 API 가 화면마다 다른 이름으로 보이지 않게 한다.</p>
*/
private VisibleApis splitByVisibility(List<AffectedApiDTO> apis, Map<String, String> visibleNames) {
if (apis == null || apis.isEmpty()) {
return new VisibleApis(Collections.emptyList(), 0);
}
List<AffectedApiDTO> visible = new ArrayList<>();
int hidden = 0;
for (AffectedApiDTO api : apis) {
String publishedName = visibleNames.get(api.getApiId());
if (publishedName == null) {
hidden++;
continue;
}
visible.add(new AffectedApiDTO(api.getApiId(), publishedName, api.getRecoveredAt()));
}
return new VisibleApis(visible, hidden);
}
public Map<Long, List<AffectedApiDTO>> loadApis(Collection<Long> incidentIds) { public Map<Long, List<AffectedApiDTO>> loadApis(Collection<Long> incidentIds) {
if (incidentIds == null || incidentIds.isEmpty()) { if (incidentIds == null || incidentIds.isEmpty()) {
@@ -103,6 +139,7 @@ public class ApiStatusAssembler {
List<Long> ids = incidentIds(incidents); List<Long> ids = incidentIds(incidents);
Map<Long, List<AffectedApiDTO>> apis = loadApis(ids); Map<Long, List<AffectedApiDTO>> apis = loadApis(ids);
Map<Long, List<TimelineEntryDTO>> timelines = loadTimelines(ids); Map<Long, List<TimelineEntryDTO>> timelines = loadTimelines(ids);
Map<String, String> visibleNames = catalogService.getVisibleApiNames();
Map<String, PortalNotice> notices = loadNotices(incidents.stream() Map<String, PortalNotice> notices = loadNotices(incidents.stream()
.map(DjbApistatusIncident::getNoticeId) .map(DjbApistatusIncident::getNoticeId)
.collect(Collectors.toList())); .collect(Collectors.toList()));
@@ -119,7 +156,10 @@ public class ApiStatusAssembler {
dto.setSummary(incident.getSummary()); dto.setSummary(incident.getSummary());
dto.setStartedAt(incident.getStartedAt()); dto.setStartedAt(incident.getStartedAt());
dto.setElapsedMinutes(ApiStatusSupport.minutesBetween(incident.getStartedAt(), now)); dto.setElapsedMinutes(ApiStatusSupport.minutesBetween(incident.getStartedAt(), now));
dto.setApis(apis.getOrDefault(incident.getIncidentId(), Collections.emptyList())); VisibleApis affected = splitByVisibility(
apis.getOrDefault(incident.getIncidentId(), Collections.emptyList()), visibleNames);
dto.setApis(affected.getVisible());
dto.setHiddenApiCount(affected.getHiddenCount());
PortalNotice notice = incident.getNoticeId() == null ? null : notices.get(incident.getNoticeId()); PortalNotice notice = incident.getNoticeId() == null ? null : notices.get(incident.getNoticeId());
if (notice != null) { if (notice != null) {
@@ -140,6 +180,7 @@ public class ApiStatusAssembler {
return Collections.emptyList(); return Collections.emptyList();
} }
Map<Long, List<AffectedApiDTO>> apis = loadApis(incidentIds(incidents)); Map<Long, List<AffectedApiDTO>> apis = loadApis(incidentIds(incidents));
Map<String, String> visibleNames = catalogService.getVisibleApiNames();
Map<String, PortalNotice> notices = loadNotices(incidents.stream() Map<String, PortalNotice> notices = loadNotices(incidents.stream()
.map(DjbApistatusIncident::getNoticeId) .map(DjbApistatusIncident::getNoticeId)
.collect(Collectors.toList())); .collect(Collectors.toList()));
@@ -162,7 +203,10 @@ public class ApiStatusAssembler {
dto.setEndAt(incident.getEndAt()); dto.setEndAt(incident.getEndAt());
dto.setDurationMinutes(incident.getEndAt() == null ? null dto.setDurationMinutes(incident.getEndAt() == null ? null
: ApiStatusSupport.minutesBetween(incident.getStartedAt(), incident.getEndAt())); : ApiStatusSupport.minutesBetween(incident.getStartedAt(), incident.getEndAt()));
dto.setImpactedApis(apis.getOrDefault(incident.getIncidentId(), Collections.emptyList())); VisibleApis affected = splitByVisibility(
apis.getOrDefault(incident.getIncidentId(), Collections.emptyList()), visibleNames);
dto.setImpactedApis(affected.getVisible());
dto.setHiddenApiCount(affected.getHiddenCount());
dto.setRegisteredAt(incident.getCreatedDate()); dto.setRegisteredAt(incident.getCreatedDate());
dto.setLastModifiedAt(incident.getLastModifiedDate()); dto.setLastModifiedAt(incident.getLastModifiedDate());
result.add(dto); result.add(dto);
@@ -179,6 +223,7 @@ public class ApiStatusAssembler {
} }
List<Long> ids = incidentIds(incidents); List<Long> ids = incidentIds(incidents);
Map<Long, List<AffectedApiDTO>> apis = loadApis(ids); Map<Long, List<AffectedApiDTO>> apis = loadApis(ids);
Map<String, String> visibleNames = catalogService.getVisibleApiNames();
List<Long> incidentKindIds = incidents.stream() List<Long> incidentKindIds = incidents.stream()
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading()) .filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
@@ -211,7 +256,10 @@ public class ApiStatusAssembler {
dto.setDateGroup(incident.getStartedAt() == null ? null : incident.getStartedAt().toLocalDate()); dto.setDateGroup(incident.getStartedAt() == null ? null : incident.getStartedAt().toLocalDate());
dto.setDurationMinutes(incident.getEndAt() == null ? null dto.setDurationMinutes(incident.getEndAt() == null ? null
: ApiStatusSupport.minutesBetween(incident.getStartedAt(), incident.getEndAt())); : ApiStatusSupport.minutesBetween(incident.getStartedAt(), incident.getEndAt()));
dto.setImpactedApis(apis.getOrDefault(incident.getIncidentId(), Collections.emptyList())); VisibleApis affected = splitByVisibility(
apis.getOrDefault(incident.getIncidentId(), Collections.emptyList()), visibleNames);
dto.setImpactedApis(affected.getVisible());
dto.setHiddenApiCount(affected.getHiddenCount());
dto.setTimeline(timelines.getOrDefault(incident.getIncidentId(), Collections.emptyList())); dto.setTimeline(timelines.getOrDefault(incident.getIncidentId(), Collections.emptyList()));
result.add(dto); result.add(dto);
} }
@@ -18,7 +18,9 @@ import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -80,6 +82,23 @@ public class ApiStatusCatalogService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/**
* 현재 사용자에게 공개된 API 의 {API ID → 노출명} 맵.
*
* <p>GW 인터페이스는 전부 이슈 데이터(DJB_APISTATUS_INCIDENT_API)에 들어오지만
* 개발자포탈에 게시되는 것은 그 일부(= API)뿐이다. 화면은 이 맵에 있는 것만
* 개별 이름으로 노출하고 나머지는 건수로 묶는다.</p>
*
* <p>{@link #getSelectableApis()} 와 같은 경로라 역할·소속에 따른 공개 범위가 그대로 반영된다.</p>
*/
public Map<String, String> getVisibleApiNames() {
Map<String, String> names = new LinkedHashMap<>();
for (ApiOptionDTO api : getSelectableApis()) {
names.put(api.getApiId(), api.getApiName());
}
return names;
}
/** /**
* API 상태 모니터링 Job(eapim-admin Quartz)의 마지막 실행 시각. * API 상태 모니터링 Job(eapim-admin Quartz)의 마지막 실행 시각.
* *
+12
View File
@@ -19008,6 +19008,12 @@ input[type=checkbox]:checked + .custom-checkbox {
font-weight: 700; font-weight: 700;
flex-shrink: 0; flex-shrink: 0;
} }
.affected-api-badge.is-gw {
background-color: #ffffff;
border-style: dashed;
border-color: #cbd5e1;
color: #6b7280;
}
.btn-affected-toggle { .btn-affected-toggle {
font-size: 13px; font-size: 13px;
@@ -26639,6 +26645,12 @@ input[type=checkbox]:checked + .custom-checkbox {
color: var(--as-muted); color: var(--as-muted);
cursor: default; cursor: default;
} }
.api-status .as-api-pills .as-api-pill.is-gw {
background: #fff;
border: 1px dashed var(--as-border-strong);
color: var(--as-muted);
cursor: default;
}
.api-status .as-card-split { .api-status .as-card-split {
margin-top: 16px; margin-top: 16px;
display: grid; display: grid;
File diff suppressed because one or more lines are too long
@@ -175,21 +175,31 @@
// ---------------- 이슈 카드 ---------------- // ---------------- 이슈 카드 ----------------
/** /**
* 영향 API 태그. 오픈 API 목록에 있는 API 만 상세 화면으로 링크하고, * 영향 API 태그.
* 목록에 없는 API(비공개·미편성)는 링크 없이 흐리게 표시한다. *
* 서버가 이미 현재 사용자에게 게시된 API 만 내려주므로 여기 오는 항목은 전부 링크 대상이다.
* 게시되지 않은 GW 인터페이스는 개별 노출하지 않고 hiddenApiCount 로만 받아 건수로 묶는다.
*/ */
function apiPillsHtml(apis) { function apiPillsHtml(apis, hiddenCount) {
if (!apis || !apis.length) return ''; const hidden = hiddenCount || 0;
const pills = apis.map(function (api) { if ((!apis || !apis.length) && hidden === 0) return '';
const pills = (apis || []).map(function (api) {
const text = escapeHtml(api.apiName || api.apiId); const text = escapeHtml(api.apiName || api.apiId);
if (!linkableApiIds.has(api.apiId)) { if (!linkableApiIds.has(api.apiId)) {
return '<span class="as-api-pill is-unlinked" title="공개된 API 목록에 없는 API 입니다">' return '<span class="as-api-pill is-unlinked">' + text + '</span>';
+ text + '</span>';
} }
return '<a class="as-api-pill" href="' + apiDetailBase + '?id=' + encodeURIComponent(api.apiId) + '">' return '<a class="as-api-pill" href="' + apiDetailBase + '?id=' + encodeURIComponent(api.apiId) + '">'
+ text + '</a>'; + text + '</a>';
}).join(''); }).join('');
return '<div class="as-api-pills"><span>영향 API:</span>' + pills + '</div>';
// 개발자포탈에 게시되지 않은 GW 인터페이스는 개별 노출 없이 건수로만 알린다
const hiddenPill = hidden > 0
? '<span class="as-api-pill is-gw" title="개발자포탈에 게시되지 않은 게이트웨이 인터페이스입니다">'
+ 'GW 인터페이스 ' + hidden + '건</span>'
: '';
return '<div class="as-api-pills"><span>영향 API:</span>' + pills + hiddenPill + '</div>';
} }
/** /**
@@ -254,7 +264,7 @@
+ '</div>' + '</div>'
+ '<h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>' + '<h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>'
+ splitHtml(inner, noticeHtml(issue)) + splitHtml(inner, noticeHtml(issue))
+ apiPillsHtml(issue.impactedApis) + apiPillsHtml(issue.impactedApis, issue.hiddenApiCount)
+ '</article>'; + '</article>';
} }
+20 -11
View File
@@ -88,21 +88,30 @@
} }
/** /**
* 영향 API 태그. 오픈 API 목록에 있는 API 만 상세 화면으로 링크하고, * 영향 API 태그.
* 목록에 없는 API(비공개·미편성)는 링크 없이 흐리게 표시한다. *
* 서버가 이미 현재 사용자에게 게시된 API 만 내려주므로 여기 오는 항목은 전부 링크 대상이다.
* 게시되지 않은 GW 인터페이스는 개별 노출하지 않고 hiddenApiCount 로만 받아 건수로 묶는다.
*/ */
function apiPillsHtml(apis, label) { function apiPillsHtml(apis, label, hiddenCount) {
if (!apis || !apis.length) return ''; const hidden = hiddenCount || 0;
const pills = apis.map(function (api) { if ((!apis || !apis.length) && hidden === 0) return '';
const pills = (apis || []).map(function (api) {
const text = escapeHtml(api.apiName || api.apiId); const text = escapeHtml(api.apiName || api.apiId);
if (!linkableApiIds.has(api.apiId)) { if (!linkableApiIds.has(api.apiId)) {
return '<span class="as-api-pill is-unlinked" title="공개된 API 목록에 없는 API 입니다">' return '<span class="as-api-pill is-unlinked">' + text + '</span>';
+ text + '</span>';
} }
return '<a class="as-api-pill" href="' + apiDetailBase + '?id=' + encodeURIComponent(api.apiId) + '">' return '<a class="as-api-pill" href="' + apiDetailBase + '?id=' + encodeURIComponent(api.apiId) + '">'
+ text + '</a>'; + text + '</a>';
}).join(''); }).join('');
return '<div class="as-api-pills"><span>' + escapeHtml(label) + '</span>' + pills + '</div>';
const hiddenPill = hidden > 0
? '<span class="as-api-pill is-gw" title="개발자포탈에 게시되지 않은 게이트웨이 인터페이스입니다">'
+ 'GW 인터페이스 ' + hidden + '건</span>'
: '';
return '<div class="as-api-pills"><span>' + escapeHtml(label) + '</span>' + pills + hiddenPill + '</div>';
} }
/** /**
@@ -170,7 +179,7 @@
+ '</div>' + '</div>'
+ '<h2 class="as-alert-title">' + escapeHtml(incident.title) + '</h2>' + '<h2 class="as-alert-title">' + escapeHtml(incident.title) + '</h2>'
+ (incident.summary ? '<p class="as-meta">' + escapeHtml(incident.summary) + '</p>' : '') + (incident.summary ? '<p class="as-meta">' + escapeHtml(incident.summary) + '</p>' : '')
+ apiPillsHtml(incident.apis, '영향 API:') + apiPillsHtml(incident.apis, '영향 API:', incident.hiddenApiCount)
+ splitHtml(timeline ? '<div class="as-alert-timeline">' + timeline + '</div>' : '', + splitHtml(timeline ? '<div class="as-alert-timeline">' + timeline + '</div>' : '',
noticeHtml(incident)) noticeHtml(incident))
+ '</section>'; + '</section>';
@@ -286,7 +295,7 @@
+ '</div>' + '</div>'
+ splitHtml(card.summary ? '<div class="as-maint-body">' + escapeHtml(card.summary) + '</div>' : '', + splitHtml(card.summary ? '<div class="as-maint-body">' + escapeHtml(card.summary) + '</div>' : '',
noticeHtml(card)) noticeHtml(card))
+ apiPillsHtml(card.impactedApis, '영향 API:') + apiPillsHtml(card.impactedApis, '영향 API:', card.hiddenApiCount)
+ '<div class="as-maint-meta">등록 ' + escapeHtml(formatDateTime(card.registeredAt)) + '<div class="as-maint-meta">등록 ' + escapeHtml(formatDateTime(card.registeredAt))
+ (card.lastModifiedAt ? ' (최종 수정 ' + escapeHtml(formatDateTime(card.lastModifiedAt)) + ')' : '') + (card.lastModifiedAt ? ' (최종 수정 ' + escapeHtml(formatDateTime(card.lastModifiedAt)) + ')' : '')
+ '</div>' + '</div>'
@@ -334,7 +343,7 @@
+ ' </div>' + ' </div>'
+ ' <h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>' + ' <h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>'
+ splitHtml(inner, noticeHtml(issue)) + splitHtml(inner, noticeHtml(issue))
+ apiPillsHtml(issue.impactedApis, '영향 API:') + apiPillsHtml(issue.impactedApis, '영향 API:', issue.hiddenApiCount)
+ '</article>' + '</article>'
+ '</div>'; + '</div>';
} }
@@ -253,6 +253,14 @@
color: var(--as-muted); color: var(--as-muted);
cursor: default; cursor: default;
} }
// 개발자포탈에 게시되지 않은 GW 인터페이스 묶음 ("GW 인터페이스 3건")
.as-api-pill.is-gw {
background: #fff;
border: 1px dashed var(--as-border-strong);
color: var(--as-muted);
cursor: default;
}
} }
// 타임라인 | 공지 2열. 데스크탑(961px~)에서만 갈라지고 그 아래는 세로로 쌓인다. // 타임라인 | 공지 2열. 데스크탑(961px~)에서만 갈라지고 그 아래는 세로로 쌓인다.
@@ -358,6 +358,14 @@
font-weight: 700; font-weight: 700;
flex-shrink: 0; flex-shrink: 0;
} }
// 개발자포탈에 게시되지 않은 GW 인터페이스 묶음 ("GW 인터페이스 3건")
&.is-gw {
background-color: #ffffff;
border-style: dashed;
border-color: #cbd5e1;
color: #6b7280;
}
} }
.btn-affected-toggle { .btn-affected-toggle {
@@ -78,29 +78,27 @@
<th>상태</th> <th>상태</th>
<td colspan="3" th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td> <td colspan="3" th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
</tr> </tr>
<tr th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}"> <tr
th:if="${(portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()) or portalNotice.hiddenApiCount > 0}">
<th>영향 API</th> <th>영향 API</th>
<td colspan="3"> <td colspan="3">
<div class="affected-apis-wrapper"> <div class="affected-apis-wrapper">
<!-- First 3 APIs (Always visible) --> <!-- 개발자포탈에 게시된 API 만 개별 노출한다 (인터페이스 ID 는 표시하지 않음) -->
<div class="affected-apis-list"> <div class="affected-apis-list">
<span class="affected-api-badge" th:each="api, iterStat : ${portalNotice.affectedApis}" <span class="affected-api-badge" th:each="api, iterStat : ${portalNotice.affectedApis}"
th:if="${iterStat.index < 3}"> th:if="${iterStat.index < 3}" th:text="${api.apiName}">API 명</span>
<strong th:text="${api.apiId}">API_ID</strong><span
th:if="${api.apiName != null and !api.apiName.isEmpty()}"
th:text="| - ${api.apiName}|"></span>
</span>
<!-- Rest of APIs (Hidden by default) --> <!-- Rest of APIs (Hidden by default) -->
<th:block th:if="${portalNotice.affectedApis.size() > 3}"> <th:block th:if="${portalNotice.affectedApis.size() > 3}">
<span class="affected-api-badge extra-api" <span class="affected-api-badge extra-api"
th:each="api, iterStat : ${portalNotice.affectedApis}" th:if="${iterStat.index >= 3}" th:each="api, iterStat : ${portalNotice.affectedApis}" th:if="${iterStat.index >= 3}"
style="display: none;"> style="display: none;" th:text="${api.apiName}">API 명</span>
<strong th:text="${api.apiId}">API_ID</strong><span
th:if="${api.apiName != null and !api.apiName.isEmpty()}"
th:text="| - ${api.apiName}|"></span>
</span>
</th:block> </th:block>
<!-- 게시되지 않은 GW 인터페이스는 건수로만 묶어서 표기 -->
<span class="affected-api-badge is-gw" th:if="${portalNotice.hiddenApiCount > 0}"
th:text="|GW 인터페이스 ${portalNotice.hiddenApiCount}건|"
title="개발자포탈에 게시되지 않은 게이트웨이 인터페이스입니다">GW 인터페이스 0건</span>
</div> </div>
<!-- Toggle Button --> <!-- Toggle Button -->