공지사항/Q&A 개선 - 장애 점검 정보 노출 및 상단고정 정렬 - Q&A 이벤트 정리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

This commit is contained in:
Rinjae
2026-06-16 14:54:02 +09:00
parent 71f4cb6f79
commit bddd7e3f85
7 changed files with 144 additions and 9 deletions
+5 -5
View File
@@ -150,11 +150,11 @@ compileJava {
}
processResources {
exclude { details ->
details.file.name.startsWith('application-') &&
details.file.name.endsWith('.yml') &&
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
}
// exclude { details ->
// details.file.name.startsWith('application-') &&
// details.file.name.endsWith('.yml') &&
// !(details.file.name in ['application-stage.yml', 'application-prod.yml'])
// }
}
test {
Vendored Regular → Executable
View File
@@ -0,0 +1,13 @@
package com.eactive.apim.portal.apps.community.notice.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IncidentAffectedApiDTO {
private String apiId;
private String apiName;
}
@@ -8,11 +8,18 @@ import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PortalNoticeDTO {
public static final String NOTICE_TYPE_NORMAL = "1";
public static final String NOTICE_TYPE_MAINTENANCE = "2";
public static final String NOTICE_TYPE_INCIDENT = "3";
private String id;
@NotEmpty(message = "제목을 입력해주세요.")
@@ -29,9 +36,33 @@ public class PortalNoticeDTO {
private boolean useYn;
private String fixYn;
private String noticeType;
private String inquirerName;
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
// ───── 장애/점검 연동 필드 ─────
private String summary;
private LocalDateTime startedAt;
private LocalDateTime endAt;
private String state;
private String previousState;
private List<IncidentAffectedApiDTO> affectedApis = Collections.emptyList();
public boolean isIncidentType() {
return NOTICE_TYPE_INCIDENT.equals(noticeType);
}
public boolean isMaintenanceType() {
return NOTICE_TYPE_MAINTENANCE.equals(noticeType);
}
public boolean isIncidentOrMaintenance() {
return isIncidentType() || isMaintenanceType();
}
}
@@ -1,8 +1,12 @@
package com.eactive.apim.portal.apps.community.notice.service;
import com.eactive.apim.portal.apps.community.notice.dto.IncidentAffectedApiDTO;
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.mapper.PortalNoticeMapper;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
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.portalNotice.entity.PortalNotice;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
@@ -12,7 +16,10 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service("portalNoticeFacade")
@RequiredArgsConstructor
@@ -20,12 +27,13 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
private final PortalNoticeService portalNoticeService;
private final PortalNoticeMapper portalNoticeMapper;
private final DjbApistatusIncidentRepository incidentRepository;
private final DjbApistatusIncidentApiRepository incidentApiRepository;
@Override
public List<PortalNoticeDTO> getLatestNotices() {
PortalNoticeSearch search = new PortalNoticeSearch();
Pageable pageable = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "createdDate"));
// Pageable pageable = Pageable.ofSize(3);
Page<PortalNoticeDTO> notices = getNotices(search, pageable);
return notices.getContent();
}
@@ -36,13 +44,47 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
Specification<PortalNotice> spec = Specification.where(search.buildSpecification())
.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("useYn"),"Y"));
Page<PortalNotice> noticesPage = portalNoticeService.getNotices(spec, pageable);
// 상단고정(fixYn='Y') 우선 정렬 — 호출자의 sort 앞에 결합 (admin Service 와 일관)
Sort fixYnFirst = Sort.by(Sort.Direction.DESC, "fixYn");
Pageable fixedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
fixYnFirst.and(pageable.getSort())
);
Page<PortalNotice> noticesPage = portalNoticeService.getNotices(spec, fixedPageable);
return noticesPage.map(portalNoticeMapper::map);
}
@Override
public PortalNoticeDTO getNotice(String id) {
PortalNotice portalNotice = portalNoticeService.getNotice(id);
return portalNoticeMapper.map(portalNotice);
PortalNoticeDTO dto = portalNoticeMapper.map(portalNotice);
populateIncident(dto);
return dto;
}
private void populateIncident(PortalNoticeDTO dto) {
if (!dto.isIncidentOrMaintenance()) {
dto.setAffectedApis(Collections.emptyList());
return;
}
Optional<DjbApistatusIncident> incidentOpt = incidentRepository.findByNoticeId(dto.getId());
if (!incidentOpt.isPresent()) {
dto.setAffectedApis(Collections.emptyList());
return;
}
DjbApistatusIncident incident = incidentOpt.get();
dto.setSummary(incident.getSummary());
dto.setStartedAt(incident.getStartedAt());
dto.setEndAt(incident.getEndAt());
dto.setState(incident.getState() == null ? null : incident.getState().name());
dto.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name());
List<IncidentAffectedApiDTO> apis = incidentApiRepository
.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream()
.map(api -> new IncidentAffectedApiDTO(api.getApiId(), api.getApiName()))
.collect(Collectors.toList());
dto.setAffectedApis(apis);
}
}
@@ -15,10 +15,50 @@
<!-- Notice Header: Title and Date -->
<div class="notice-detail-header">
<h2 class="notice-detail-title" th:text="${portalNotice.noticeSubject}">개인정보처리 방침 정정 공지</h2>
<h2 class="notice-detail-title">
<span th:if="${portalNotice.noticeType == '3'}"
style="display:inline-block;padding:2px 10px;margin-right:8px;border-radius:12px;font-size:14px;vertical-align:middle;background:#fde7e9;color:#c0152a;">장애</span>
<span th:if="${portalNotice.noticeType == '2'}"
style="display:inline-block;padding:2px 10px;margin-right:8px;border-radius:12px;font-size:14px;vertical-align:middle;background:#fff4d6;color:#9a6700;">점검</span>
<span th:text="${portalNotice.noticeSubject}">개인정보처리 방침 정정 공지</span>
</h2>
<span class="notice-detail-date" th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
</div>
<!-- 장애/점검 정보 영역 -->
<div class="notice-detail-incident-info" th:if="${portalNotice.incidentOrMaintenance}"
style="margin:16px 0;padding:16px;background:#f7f8fa;border:1px solid #e3e6eb;border-radius:8px;">
<table style="width:100%;border-collapse:collapse;">
<colgroup>
<col style="width:120px;"><col><col style="width:120px;"><col>
</colgroup>
<tr>
<th style="text-align:left;padding:6px 8px;">시작</th>
<td style="padding:6px 8px;"
th:text="${portalNotice.startedAt != null ? #temporals.format(portalNotice.startedAt, 'yyyy.MM.dd HH:mm') : '-'}">-</td>
<th style="text-align:left;padding:6px 8px;">종료</th>
<td style="padding:6px 8px;"
th:text="${portalNotice.endAt != null ? #temporals.format(portalNotice.endAt, 'yyyy.MM.dd HH:mm') : '진행중'}">-</td>
</tr>
<tr th:if="${portalNotice.incidentType}">
<th style="text-align:left;padding:6px 8px;">상태</th>
<td colspan="3" style="padding:6px 8px;" th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
</tr>
<tr th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">
<th style="text-align:left;padding:6px 8px;vertical-align:top;">영향 API</th>
<td colspan="3" style="padding:6px 8px;">
<ul style="margin:0;padding-left:18px;">
<li th:each="api : ${portalNotice.affectedApis}">
<strong th:text="${api.apiId}">API_ID</strong>
<span th:if="${api.apiName != null and !api.apiName.isEmpty()}"
th:text="| - ${api.apiName}|"></span>
</li>
</ul>
</td>
</tr>
</table>
</div>
<!-- Attachment Section -->
<div class="notice-detail-attachment" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
@@ -58,6 +58,15 @@
<div class="row-cell row-cell--title" style="flex: 1; min-width: 200px;" data-label="제목">
<a th:href="@{/portalnotice/detail(id=${notice.id})}" class="notice-title-link">
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
<span class="notice-fix-badge"
th:if="${notice.fixYn == 'Y'}"
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#e5f0ff;color:#0a4ea3;font-weight:600;">고정</span>
<span class="notice-type-badge notice-type-badge--incident"
th:if="${notice.noticeType == '3'}"
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#fde7e9;color:#c0152a;">장애</span>
<span class="notice-type-badge notice-type-badge--maintenance"
th:if="${notice.noticeType == '2'}"
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#fff4d6;color:#9a6700;">점검</span>
<span th:text="${notice.noticeSubject}">공지사항 제목</span>
<span class="file-icon" th:if="${notice.fileId != null and !notice.fileId.isEmpty()}">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">