공지 연계 기능 추가 및 UI 개선:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- 진행 중, 과거 장애/점검 카드에 공지 제목 및 본문 추가.
- 공지 본문 2열 레이아웃 및 세로 스크롤 스타일 적용.
- CSS 반응형 수정(768px → 1024px).
This commit is contained in:
Rinjae
2026-08-05 09:33:22 +09:00
parent fbc145d145
commit d71af34950
14 changed files with 363 additions and 61 deletions
@@ -2,11 +2,18 @@ package com.eactive.apim.portal.apps.community.notice.repository;
import com.eactive.apim.portal.portalNotice.entity.PortalNotice; import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import java.util.Collection;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@EMSDataSource @EMSDataSource
public interface PortalNoticeRepository extends JpaRepository<PortalNotice, String>, JpaSpecificationExecutor<PortalNotice> { public interface PortalNoticeRepository extends JpaRepository<PortalNotice, String>, JpaSpecificationExecutor<PortalNotice> {
/**
* 게시 중인 공지만 골라 한 번에 읽는다. API Status 카드가 연결 공지 본문을 붙일 때 사용한다.
* 미게시(USE_YN='N')·삭제된 공지는 결과에서 자연히 빠진다.
*/
List<PortalNotice> findByIdInAndUseYn(Collection<String> ids, String useYn);
} }
@@ -15,6 +15,10 @@ public class ActiveIncidentDTO {
private Long incidentId; private Long incidentId;
private String noticeId; private String noticeId;
/** 연결된 공지 제목. 게시 중인 공지가 없으면 null */
private String noticeSubject;
/** 연결된 공지 본문(HTML). 게시 중인 공지가 없으면 null */
private String noticeDetail;
private String kind; private String kind;
private String state; private String state;
private String stateLabel; private String stateLabel;
@@ -15,6 +15,10 @@ public class MaintenanceCardDTO {
private Long incidentId; private Long incidentId;
private String noticeId; private String noticeId;
/** 연결된 공지 제목. 게시 중인 공지가 없으면 null */
private String noticeSubject;
/** 연결된 공지 본문(HTML). 게시 중인 공지가 없으면 null */
private String noticeDetail;
private String title; private String title;
private String summary; private String summary;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@@ -16,6 +16,10 @@ public class PastIssueCardDTO {
private Long incidentId; private Long incidentId;
private String noticeId; private String noticeId;
/** 연결된 공지 제목. 게시 중인 공지가 없으면 null */
private String noticeSubject;
/** 연결된 공지 본문(HTML). 게시 중인 공지가 없으면 null */
private String noticeDetail;
private String kind; private String kind;
private String state; private String state;
private String stateLabel; private String stateLabel;
@@ -11,6 +11,8 @@ import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusInciden
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind; import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
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.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.portalNotice.entity.PortalNotice;
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;
@@ -37,6 +39,7 @@ public class ApiStatusAssembler {
private final DjbApistatusIncidentApiRepository incidentApiRepository; private final DjbApistatusIncidentApiRepository incidentApiRepository;
private final DjbApistatusIncidentTimelineRepository timelineRepository; private final DjbApistatusIncidentTimelineRepository timelineRepository;
private final PortalNoticeRepository portalNoticeRepository;
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()) {
@@ -69,6 +72,30 @@ public class ApiStatusAssembler {
return result; return result;
} }
/**
* 이슈에 연결된 공지. 게시 중(USE_YN='Y')인 것만 담는다.
*
* <p>조회 자체의 공개 조건({@code ApiStatusIncidentQueryRepository.VISIBLE})과 같은 기준이라
* 여기서 빠지는 건은 공지가 삭제된 고아 행뿐이다.</p>
*/
public Map<String, PortalNotice> loadNotices(Collection<String> noticeIds) {
if (noticeIds == null || noticeIds.isEmpty()) {
return Collections.emptyMap();
}
List<String> ids = noticeIds.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (ids.isEmpty()) {
return Collections.emptyMap();
}
Map<String, PortalNotice> result = new HashMap<>();
for (PortalNotice notice : portalNoticeRepository.findByIdInAndUseYn(ids, "Y")) {
result.put(notice.getId(), notice);
}
return result;
}
public List<ActiveIncidentDTO> toActiveIncidents(List<DjbApistatusIncident> incidents, LocalDateTime now) { public List<ActiveIncidentDTO> toActiveIncidents(List<DjbApistatusIncident> incidents, LocalDateTime now) {
if (incidents.isEmpty()) { if (incidents.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
@@ -76,6 +103,9 @@ 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, PortalNotice> notices = loadNotices(incidents.stream()
.map(DjbApistatusIncident::getNoticeId)
.collect(Collectors.toList()));
List<ActiveIncidentDTO> result = new ArrayList<>(); List<ActiveIncidentDTO> result = new ArrayList<>();
for (DjbApistatusIncident incident : incidents) { for (DjbApistatusIncident incident : incidents) {
@@ -91,6 +121,12 @@ public class ApiStatusAssembler {
dto.setElapsedMinutes(ApiStatusSupport.minutesBetween(incident.getStartedAt(), now)); dto.setElapsedMinutes(ApiStatusSupport.minutesBetween(incident.getStartedAt(), now));
dto.setApis(apis.getOrDefault(incident.getIncidentId(), Collections.emptyList())); dto.setApis(apis.getOrDefault(incident.getIncidentId(), Collections.emptyList()));
PortalNotice notice = incident.getNoticeId() == null ? null : notices.get(incident.getNoticeId());
if (notice != null) {
dto.setNoticeSubject(notice.getNoticeSubject());
dto.setNoticeDetail(notice.getNoticeDetail());
}
List<TimelineEntryDTO> all = timelines.getOrDefault(incident.getIncidentId(), Collections.emptyList()); List<TimelineEntryDTO> all = timelines.getOrDefault(incident.getIncidentId(), Collections.emptyList());
dto.setRecentTimeline(all.size() > RECENT_TIMELINE_SIZE dto.setRecentTimeline(all.size() > RECENT_TIMELINE_SIZE
? new ArrayList<>(all.subList(0, RECENT_TIMELINE_SIZE)) : all); ? new ArrayList<>(all.subList(0, RECENT_TIMELINE_SIZE)) : all);
@@ -104,12 +140,22 @@ 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, PortalNotice> notices = loadNotices(incidents.stream()
.map(DjbApistatusIncident::getNoticeId)
.collect(Collectors.toList()));
List<MaintenanceCardDTO> result = new ArrayList<>(); List<MaintenanceCardDTO> result = new ArrayList<>();
for (DjbApistatusIncident incident : incidents) { for (DjbApistatusIncident incident : incidents) {
MaintenanceCardDTO dto = new MaintenanceCardDTO(); MaintenanceCardDTO dto = new MaintenanceCardDTO();
dto.setIncidentId(incident.getIncidentId()); dto.setIncidentId(incident.getIncidentId());
dto.setNoticeId(incident.getNoticeId()); dto.setNoticeId(incident.getNoticeId());
PortalNotice notice = incident.getNoticeId() == null ? null : notices.get(incident.getNoticeId());
if (notice != null) {
dto.setNoticeSubject(notice.getNoticeSubject());
dto.setNoticeDetail(notice.getNoticeDetail());
}
dto.setTitle(incident.getTitle()); dto.setTitle(incident.getTitle());
dto.setSummary(incident.getSummary()); dto.setSummary(incident.getSummary());
dto.setStartedAt(incident.getStartedAt()); dto.setStartedAt(incident.getStartedAt());
@@ -139,12 +185,22 @@ public class ApiStatusAssembler {
.map(DjbApistatusIncident::getIncidentId) .map(DjbApistatusIncident::getIncidentId)
.collect(Collectors.toList()); .collect(Collectors.toList());
Map<Long, List<TimelineEntryDTO>> timelines = loadTimelines(incidentKindIds); Map<Long, List<TimelineEntryDTO>> timelines = loadTimelines(incidentKindIds);
Map<String, PortalNotice> notices = loadNotices(incidents.stream()
.map(DjbApistatusIncident::getNoticeId)
.collect(Collectors.toList()));
List<PastIssueCardDTO> result = new ArrayList<>(); List<PastIssueCardDTO> result = new ArrayList<>();
for (DjbApistatusIncident incident : incidents) { for (DjbApistatusIncident incident : incidents) {
PastIssueCardDTO dto = new PastIssueCardDTO(); PastIssueCardDTO dto = new PastIssueCardDTO();
dto.setIncidentId(incident.getIncidentId()); dto.setIncidentId(incident.getIncidentId());
dto.setNoticeId(incident.getNoticeId()); dto.setNoticeId(incident.getNoticeId());
PortalNotice notice = incident.getNoticeId() == null ? null : notices.get(incident.getNoticeId());
if (notice != null) {
dto.setNoticeSubject(notice.getNoticeSubject());
dto.setNoticeDetail(notice.getNoticeDetail());
}
dto.setKind(incident.getKind() == null ? null : incident.getKind().name()); dto.setKind(incident.getKind() == null ? null : incident.getKind().name());
dto.setState(incident.getState() == null ? null : incident.getState().name()); dto.setState(incident.getState() == null ? null : incident.getState().name());
dto.setStateLabel(ApiStatusSupport.stateLabel(incident.getState())); dto.setStateLabel(ApiStatusSupport.stateLabel(incident.getState()));
+129 -50
View File
@@ -762,7 +762,7 @@ hr {
.global-header .container .header-content { .global-header .container .header-content {
height: 100%; height: 100%;
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
.global-header { .global-header {
height: clamp(44px, 11.73vw, 60px); height: clamp(44px, 11.73vw, 60px);
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
@@ -795,18 +795,26 @@ hr {
.header-left { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
} }
.header-left .logo { .header-left .logo {
height: 32px; height: 32px;
align-items: flex-end; align-items: flex-end;
flex-wrap: nowrap;
}
.header-left .logo img {
flex-shrink: 0;
} }
.header-left .logo .mobile-logo-link { .header-left .logo .mobile-logo-link {
align-items: flex-end; align-items: flex-end;
flex-shrink: 0;
} }
.header-left .logo .mobile-logo-text { .header-left .logo .mobile-logo-text {
align-items: flex-end; align-items: flex-end;
line-height: 1; line-height: 1;
padding-bottom: 2px; padding-bottom: 2px;
white-space: nowrap;
flex-shrink: 0;
} }
.logo-wrapper { .logo-wrapper {
@@ -908,7 +916,7 @@ hr {
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
.desktop-header { .desktop-header {
display: none; display: none;
} }
@@ -920,7 +928,7 @@ hr {
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
.mobile-header { .mobile-header {
display: flex; display: flex;
} }
@@ -1180,7 +1188,7 @@ hr {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.mobile-drawer .drawer-welcome .btn-drawer-login:hover { .mobile-drawer .drawer-welcome .btn-drawer-login:hover {
background: rgb(0, 65.7, 162); background: rgb(0%, 25.7647058824%, 63.5294117647%);
} }
.mobile-drawer .drawer-welcome.authenticated { .mobile-drawer .drawer-welcome.authenticated {
flex-direction: row; flex-direction: row;
@@ -1500,6 +1508,7 @@ hr {
padding: 12px; padding: 12px;
position: relative; position: relative;
transition: var(--transition-smooth); transition: var(--transition-smooth);
white-space: nowrap;
} }
.nav-link:hover { .nav-link:hover {
color: var(--primary-blue); color: var(--primary-blue);
@@ -1526,7 +1535,7 @@ hr {
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
.global-header { .global-header {
height: clamp(44px, 11.73vw, 60px); height: clamp(44px, 11.73vw, 60px);
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
@@ -1648,7 +1657,7 @@ hr {
transform: translateY(0); transform: translateY(0);
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
.header-user-info { .header-user-info {
display: none; display: none;
} }
@@ -2490,7 +2499,7 @@ hr {
color: #FFFFFF; color: #FFFFFF;
} }
.btn-success:hover { .btn-success:hover {
background: rgb(83.2897959184, 199.3102040816, 106.493877551); background: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
transform: translateY(-3px); transform: translateY(-3px);
} }
.btn-danger { .btn-danger {
@@ -2498,7 +2507,7 @@ hr {
color: #FFFFFF; color: #FFFFFF;
} }
.btn-danger:hover { .btn-danger:hover {
background: rgb(255, 70.8, 70.8); background: rgb(100%, 27.7647058824%, 27.7647058824%);
transform: translateY(-3px); transform: translateY(-3px);
} }
.btn-ghost { .btn-ghost {
@@ -2764,7 +2773,7 @@ hr {
.action-btn-delete:hover { .action-btn-delete:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1); box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
background: rgb(255, 88.9, 88.9); background: rgb(100%, 34.862745098%, 34.862745098%);
} }
.action-btn-delete:active { .action-btn-delete:active {
transform: translateY(0); transform: translateY(0);
@@ -2882,7 +2891,7 @@ hr {
background: #a4d6ea; background: #a4d6ea;
} }
.btn-input-action.btn-change:hover { .btn-input-action.btn-change:hover {
background: rgb(131.6625, 199.4303571429, 226.5375); background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1); box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
} }
@@ -2957,7 +2966,7 @@ hr {
border: none; border: none;
} }
.btn-action-primary:hover { .btn-action-primary:hover {
background: rgb(31.8731707317, 92.7219512195, 205.7268292683); background: rgb(12.4992826399%, 36.3615494978%, 80.6771879484%);
transform: translateY(-2px); transform: translateY(-2px);
color: #fff; color: #fff;
} }
@@ -3007,7 +3016,7 @@ hr {
} }
.status-badge.status-processing { .status-badge.status-processing {
background: rgba(255, 217, 61, 0.1); background: rgba(255, 217, 61, 0.1);
color: rgb(221.2, 177.8721649485, 0); color: rgb(86.7450980392%, 69.7537901759%, 0%);
} }
.status-badge.status-failed { .status-badge.status-failed {
background: rgba(255, 107, 107, 0.1); background: rgba(255, 107, 107, 0.1);
@@ -3047,7 +3056,7 @@ hr {
} }
.status-badge-header.status-processing { .status-badge-header.status-processing {
background: rgba(255, 217, 61, 0.1); background: rgba(255, 217, 61, 0.1);
color: rgb(221.2, 177.8721649485, 0); color: rgb(86.7450980392%, 69.7537901759%, 0%);
} }
.badge-sm { .badge-sm {
@@ -4235,7 +4244,7 @@ select.form-control {
.file-upload-wrapper .file-remove-btn:hover { .file-upload-wrapper .file-remove-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1); box-shadow: 0 4px 12px rgba(75, 155, 255, 0.1);
background: rgb(255, 88.9, 88.9); background: rgb(100%, 34.862745098%, 34.862745098%);
} }
.file-upload-wrapper .file-remove-btn:active { .file-upload-wrapper .file-remove-btn:active {
transform: translateY(0); transform: translateY(0);
@@ -4556,7 +4565,7 @@ select.form-control {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.form-actions--with-withdrawal .withdrawal-link:hover { .form-actions--with-withdrawal .withdrawal-link:hover {
background: rgb(210.2090909091, 232.6045454545, 242.9409090909); background: rgb(82.4349376114%, 91.2174688057%, 95.2709447415%);
} }
.form-actions--with-withdrawal .withdrawal-link img { .form-actions--with-withdrawal .withdrawal-link img {
width: 22px; width: 22px;
@@ -4711,7 +4720,7 @@ select.form-control {
text-decoration: underline; text-decoration: underline;
} }
.notice-content-box a:hover { .notice-content-box a:hover {
color: rgb(0, 65.7, 162); color: rgb(0%, 25.7647058824%, 63.5294117647%);
} }
.form-row--content .form-label-wrapper { .form-row--content .form-label-wrapper {
@@ -5420,6 +5429,7 @@ select.form-control {
padding: 10px 20px; padding: 10px 20px;
border-radius: 8px; border-radius: 8px;
transition: var(--transition-smooth); transition: var(--transition-smooth);
white-space: nowrap;
} }
.login-btn:hover { .login-btn:hover {
background-color: var(--accent-light); background-color: var(--accent-light);
@@ -5432,6 +5442,7 @@ select.form-control {
background-color: var(--primary-color); background-color: var(--primary-color);
padding: 10px 24px; padding: 10px 24px;
border-radius: 8px; border-radius: 8px;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0, 73, 180, 0.15); box-shadow: 0 4px 12px rgba(0, 73, 180, 0.15);
transition: var(--transition-smooth); transition: var(--transition-smooth);
} }
@@ -5645,7 +5656,7 @@ select.form-control {
font-size: 16px; font-size: 16px;
} }
.drawer-logout-btn:hover { .drawer-logout-btn:hover {
background: rgb(255, 70.8, 70.8); background: rgb(100%, 27.7647058824%, 27.7647058824%);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15); box-shadow: 0 8px 24px rgba(75, 155, 255, 0.15);
} }
@@ -6358,7 +6369,7 @@ select.form-control {
color: #64748b; color: #64748b;
} }
.list-table-btn--default:hover { .list-table-btn--default:hover {
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429); background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
} }
.list-table-btn--primary { .list-table-btn--primary {
background-color: #ecf0fa; background-color: #ecf0fa;
@@ -6366,7 +6377,7 @@ select.form-control {
color: #2a69de; color: #2a69de;
} }
.list-table-btn--primary:hover { .list-table-btn--primary:hover {
background-color: rgb(216.7625, 224.8125, 244.9375); background-color: rgb(85.0049019608%, 88.1617647059%, 96.0539215686%);
} }
.list-table-btn--secondary { .list-table-btn--secondary {
background-color: #f5f5f4; background-color: #f5f5f4;
@@ -6374,7 +6385,7 @@ select.form-control {
color: #64748b; color: #64748b;
} }
.list-table-btn--secondary:hover { .list-table-btn--secondary:hover {
background-color: rgb(233.3571428571, 233.3571428571, 231.1928571429); background-color: rgb(91.512605042%, 91.512605042%, 90.6638655462%);
} }
.list-table-btn--danger { .list-table-btn--danger {
background-color: #fbe7e9; background-color: #fbe7e9;
@@ -6382,7 +6393,7 @@ select.form-control {
color: #bb1026; color: #bb1026;
} }
.list-table-btn--danger:hover { .list-table-btn--danger:hover {
background-color: rgb(247.5571428571, 210.3428571429, 214.0642857143); background-color: rgb(97.081232493%, 82.487394958%, 83.9467787115%);
} }
.table-pagination { .table-pagination {
@@ -7032,7 +7043,7 @@ select.form-control {
.alert.alert-error { .alert.alert-error {
background: rgba(255, 107, 107, 0.1); background: rgba(255, 107, 107, 0.1);
border: 1px solid rgba(255, 107, 107, 0.3); border: 1px solid rgba(255, 107, 107, 0.3);
color: rgb(255, 70.8, 70.8); color: rgb(100%, 27.7647058824%, 27.7647058824%);
align-items: center; align-items: center;
} }
.alert.alert-error svg { .alert.alert-error svg {
@@ -7046,7 +7057,7 @@ select.form-control {
.alert.alert-success { .alert.alert-success {
background: rgba(107, 207, 127, 0.1); background: rgba(107, 207, 127, 0.1);
border: 1px solid rgba(107, 207, 127, 0.3); border: 1px solid rgba(107, 207, 127, 0.3);
color: rgb(61.5183673469, 189.6816326531, 87.1510204082); color: rgb(24.12484994%, 74.3849539816%, 34.1768707483%);
} }
.alert.alert-info { .alert.alert-info {
background: rgba(0, 73, 180, 0.1); background: rgba(0, 73, 180, 0.1);
@@ -11409,10 +11420,10 @@ body.index-page-body {
line-height: 20px; line-height: 20px;
} }
.login-button:hover { .login-button:hover {
background: rgb(25.65, 70.3, 173.85); background: rgb(10.0588235294%, 27.568627451%, 68.1764705882%);
} }
.login-button:active { .login-button:active {
background: rgb(24.3, 66.6, 164.7); background: rgb(9.5294117647%, 26.1176470588%, 64.5882352941%);
} }
.login-button:disabled { .login-button:disabled {
opacity: 0.6; opacity: 0.6;
@@ -11455,10 +11466,10 @@ body.index-page-body {
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
} }
.login-links-container .link-btn:hover { .login-links-container .link-btn:hover {
background: rgb(220.61, 227.85, 245.95); background: rgb(86.5137254902%, 89.3529411765%, 96.4509803922%);
} }
.login-links-container .link-btn:active { .login-links-container .link-btn:active {
background: rgb(205.22, 215.7, 241.9); background: rgb(80.4784313725%, 84.5882352941%, 94.862745098%);
} }
.login-alert { .login-alert {
@@ -11967,12 +11978,12 @@ body.index-page-body {
} }
.auth-request-button:hover, .auth-request-button:hover,
.auth-verify-button:hover { .auth-verify-button:hover {
background: rgb(37.3117757009, 153.9304672897, 235.0082242991); background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
transform: none !important; transform: none !important;
} }
.auth-request-button:active, .auth-request-button:active,
.auth-verify-button:active { .auth-verify-button:active {
background: rgb(21.411588785, 146.3125233645, 233.148411215); background: rgb(8.3967014843%, 57.3774601429%, 91.4307494961%);
} }
.auth-request-button:disabled, .auth-request-button:disabled,
.auth-verify-button:disabled { .auth-verify-button:disabled {
@@ -12021,10 +12032,10 @@ body.index-page-body {
background: #f0f2f5; background: #f0f2f5;
} }
.account-recovery-card .form-actions .cancel-button:hover { .account-recovery-card .form-actions .cancel-button:hover {
background: rgb(225.45, 229.39, 235.3); background: rgb(88.4117647059%, 89.9568627451%, 92.2745098039%);
} }
.account-recovery-card .form-actions .cancel-button:active { .account-recovery-card .form-actions .cancel-button:active {
background: rgb(210.9, 216.78, 225.6); background: rgb(82.7058823529%, 85.0117647059%, 88.4705882353%);
} }
.account-recovery-card .form-actions .submit-button { .account-recovery-card .form-actions .submit-button {
color: #FFFFFF; color: #FFFFFF;
@@ -12034,7 +12045,7 @@ body.index-page-body {
background: rgb(6, 54, 125); background: rgb(6, 54, 125);
} }
.account-recovery-card .form-actions .submit-button:active { .account-recovery-card .form-actions .submit-button:active {
background: rgb(0, 65.7, 162); background: rgb(0%, 25.7647058824%, 63.5294117647%);
} }
.account-recovery-card .form-actions .submit-button:disabled { .account-recovery-card .form-actions .submit-button:disabled {
opacity: 0.6; opacity: 0.6;
@@ -12270,7 +12281,7 @@ body.index-page-body {
transition: color 0.3s ease; transition: color 0.3s ease;
} }
.result-info-box .info-text .info-link:hover { .result-info-box .info-text .info-link:hover {
color: rgb(0, 65.7, 162); color: rgb(0%, 25.7647058824%, 63.5294117647%);
} }
@media (max-width: 576px) { @media (max-width: 576px) {
.result-info-box .info-text { .result-info-box .info-text {
@@ -17470,7 +17481,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.btn-copy-action:hover { .btn-copy-action:hover {
background: rgb(131.6625, 199.4303571429, 226.5375); background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.btn-copy-action { .btn-copy-action {
@@ -17497,7 +17508,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.btn-view-secret:hover { .btn-view-secret:hover {
background: rgb(31.8897196262, 151.4130841121, 234.5102803738); background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
} }
.btn-view-secret svg { .btn-view-secret svg {
width: 20px; width: 20px;
@@ -17682,7 +17693,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border-radius: 8px; border-radius: 8px;
} }
.btn-copy-action:hover { .btn-copy-action:hover {
background: rgb(131.6625, 199.4303571429, 226.5375); background: rgb(51.6323529412%, 78.2079831933%, 88.8382352941%);
} }
.btn-view-secret { .btn-view-secret {
width: 100% !important; width: 100% !important;
@@ -17698,7 +17709,7 @@ input[type=checkbox]:checked + .custom-checkbox {
height: 16px; height: 16px;
} }
.btn-view-secret:hover { .btn-view-secret:hover {
background: rgb(31.8897196262, 151.4130841121, 234.5102803738); background: rgb(12.5057724024%, 59.377680044%, 91.9648158329%);
} }
#revealedSecretBox { #revealedSecretBox {
width: 100%; width: 100%;
@@ -17971,7 +17982,7 @@ input[type=checkbox]:checked + .custom-checkbox {
flex-shrink: 0; flex-shrink: 0;
} }
.detail-wrap .dt-btn-copy:hover { .detail-wrap .dt-btn-copy:hover {
background: rgb(189.6, 230.0857142857, 255); background: rgb(74.3529411765%, 90.2296918768%, 100%);
} }
.detail-wrap .dt-btn-copy svg { .detail-wrap .dt-btn-copy svg {
color: #2a69de; color: #2a69de;
@@ -18148,7 +18159,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.detail-wrap .dt-btn-gray:hover { .detail-wrap .dt-btn-gray:hover {
background: rgb(170.6589473684, 183.4378947368, 193.6610526316); background: rgb(66.9250773994%, 71.9364293086%, 75.9455108359%);
} }
.detail-wrap .dt-btn-red { .detail-wrap .dt-btn-red {
width: 156px; width: 156px;
@@ -18166,7 +18177,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.detail-wrap .dt-btn-red:hover { .detail-wrap .dt-btn-red:hover {
background: rgb(255, 70.0915337423, 64.24); background: rgb(100%, 27.4868759774%, 25.1921568627%);
} }
.detail-wrap .dt-btn-blue { .detail-wrap .dt-btn-blue {
width: 156px; width: 156px;
@@ -19734,7 +19745,7 @@ input[type=checkbox]:checked + .custom-checkbox {
} }
} }
.btn-inquiry-list:hover { .btn-inquiry-list:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783); background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
} }
.btn-inquiry-list:active { .btn-inquiry-list:active {
transform: scale(0.98); transform: scale(0.98);
@@ -19767,7 +19778,7 @@ input[type=checkbox]:checked + .custom-checkbox {
} }
} }
.btn-inquiry-edit:hover { .btn-inquiry-edit:hover {
background: rgb(0, 69.35, 171); background: rgb(0%, 27.1960784314%, 67.0588235294%);
} }
.btn-inquiry-edit:active { .btn-inquiry-edit:active {
transform: scale(0.98); transform: scale(0.98);
@@ -19800,7 +19811,7 @@ input[type=checkbox]:checked + .custom-checkbox {
} }
} }
.btn-inquiry-delete:hover { .btn-inquiry-delete:hover {
background: rgb(217.9841772152, 41.3658227848, 58.2873417722); background: rgb(85.4839910648%, 16.2218912882%, 22.8577810871%);
} }
.btn-inquiry-delete:active { .btn-inquiry-delete:active {
transform: scale(0.98); transform: scale(0.98);
@@ -19872,7 +19883,7 @@ input[type=checkbox]:checked + .custom-checkbox {
margin-left: 8px; margin-left: 8px;
} }
.file-upload-inline .btn-remove-file-inline:hover { .file-upload-inline .btn-remove-file-inline:hover {
background: rgb(209.4151898734, 36.2848101266, 52.8721518987); background: rgb(82.1236038719%, 14.2293373045%, 20.7341772152%);
} }
.file-upload-inline .btn-remove-file-inline svg { .file-upload-inline .btn-remove-file-inline svg {
width: 12px; width: 12px;
@@ -19904,7 +19915,7 @@ input[type=checkbox]:checked + .custom-checkbox {
} }
} }
.file-upload-inline .btn-file-attach:hover { .file-upload-inline .btn-file-attach:hover {
background: rgb(37.3117757009, 153.9304672897, 235.0082242991); background: rgb(14.6320689023%, 60.3648891332%, 92.1600879604%);
} }
.file-upload-inline .btn-file-attach svg { .file-upload-inline .btn-file-attach svg {
width: 22px; width: 22px;
@@ -19964,7 +19975,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border: none; border: none;
} }
.inquiry-form-container .form-actions .btn-secondary:hover { .inquiry-form-container .form-actions .btn-secondary:hover {
background: rgb(215.8869565217, 218.8956521739, 224.9130434783); background: rgb(84.6615515772%, 85.8414322251%, 88.2011935209%);
} }
.inquiry-form-container .form-actions .btn-primary { .inquiry-form-container .form-actions .btn-primary {
background: #0049b4; background: #0049b4;
@@ -19972,7 +19983,7 @@ input[type=checkbox]:checked + .custom-checkbox {
border: none; border: none;
} }
.inquiry-form-container .form-actions .btn-primary:hover { .inquiry-form-container .form-actions .btn-primary:hover {
background: rgb(0, 69.35, 171); background: rgb(0%, 27.1960784314%, 67.0588235294%);
} }
.inquiry-form-container .file-upload-inline .file-input-display { .inquiry-form-container .file-upload-inline .file-input-display {
min-height: 50px; min-height: 50px;
@@ -20761,7 +20772,7 @@ input[type=checkbox]:checked + .custom-checkbox {
cursor: pointer; cursor: pointer;
} }
.djb-board-write-container .form-actions .btn-submit:hover { .djb-board-write-container .form-actions .btn-submit:hover {
background-color: rgb(33.643902439, 97.8731707317, 217.156097561); background-color: rgb(13.193687231%, 38.3816355811%, 85.1592539455%);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.djb-board-write-container .form-actions .btn-submit { .djb-board-write-container .form-actions .btn-submit {
@@ -21297,7 +21308,7 @@ input[type=checkbox]:checked + .custom-checkbox {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.org-file-remove:hover { .org-file-remove:hover {
background: rgb(255, 70.8, 70.8); background: rgb(100%, 27.7647058824%, 27.7647058824%);
} }
.org-file-notice { .org-file-notice {
@@ -22309,7 +22320,7 @@ input[type=checkbox]:checked + .custom-checkbox {
} }
.status-indicator.status-active { .status-indicator.status-active {
background-color: rgba(107, 207, 127, 0.1); background-color: rgba(107, 207, 127, 0.1);
color: rgb(83.2897959184, 199.3102040816, 106.493877551); color: rgb(32.662665066%, 78.1608643457%, 41.762304922%);
} }
.status-indicator.status-active .status-dot { .status-indicator.status-active .status-dot {
background-color: #6BCF7F; background-color: #6BCF7F;
@@ -26628,6 +26639,74 @@ input[type=checkbox]:checked + .custom-checkbox {
color: var(--as-muted); color: var(--as-muted);
cursor: default; cursor: default;
} }
.api-status .as-card-split {
margin-top: 16px;
display: grid;
gap: 16px;
align-items: start;
}
@media (min-width: 961px) {
.api-status .as-card-split {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
}
.api-status .as-card-split > .as-alert-notice {
margin-top: 0;
}
.api-status .as-alert-card .as-card-split > .as-alert-timeline {
margin-top: 0;
}
.api-status .as-maint-card .as-card-split > .as-maint-body {
margin-top: 0;
}
.api-status .as-alert-notice {
margin-top: 16px;
border: 1px solid var(--as-border);
border-radius: var(--as-radius);
background: var(--as-gray-bg);
padding: 14px 16px;
}
.api-status .as-alert-notice .as-alert-notice-head {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.api-status .as-alert-notice .as-alert-notice-head strong {
font-size: 14px;
color: var(--as-text);
}
.api-status .as-alert-notice .as-alert-notice-label {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: var(--as-pill);
background: var(--as-info-bg);
color: var(--as-info);
}
.api-status .as-alert-notice .as-alert-notice-body {
max-height: 320px;
overflow-y: auto;
font-size: 14px;
line-height: 1.7;
color: var(--as-text-2);
word-break: break-word;
}
.api-status .as-alert-notice .as-alert-notice-body img, .api-status .as-alert-notice .as-alert-notice-body table {
max-width: 100%;
}
.api-status .as-alert-notice .as-alert-notice-body p:last-child {
margin-bottom: 0;
}
.api-status .as-alert-notice .as-alert-notice-link {
display: inline-block;
margin-top: 12px;
font-size: 13px;
font-weight: 500;
color: var(--as-info);
text-decoration: none;
}
.api-status .as-alert-card { .api-status .as-alert-card {
border-left: 4px solid var(--as-err); border-left: 4px solid var(--as-err);
} }
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@
const windowDays = parseInt(page.getAttribute('data-window-days'), 10) || 90; const windowDays = parseInt(page.getAttribute('data-window-days'), 10) || 90;
const base = page.getAttribute('data-base') || '/apistatus'; const base = page.getAttribute('data-base') || '/apistatus';
const apiDetailBase = page.getAttribute('data-api-detail-base') || '/apis/detail'; const apiDetailBase = page.getAttribute('data-api-detail-base') || '/apis/detail';
const noticeDetailBase = page.getAttribute('data-notice-detail-base') || '/portalnotice/detail';
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */ /** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
const linkableApiIds = new Set(); const linkableApiIds = new Set();
@@ -191,6 +192,36 @@
return '<div class="as-api-pills"><span>영향 API:</span>' + pills + '</div>'; return '<div class="as-api-pills"><span>영향 API:</span>' + pills + '</div>';
} }
/**
* 이슈에 연결된 공지 본문. 서버가 게시 중(USE_YN='Y')인 공지만 내려주므로 여기서는 존재 여부만 본다.
* 본문은 관리자가 에디터로 작성한 HTML 이라 공지 상세(th:utext)와 같이 그대로 렌더한다.
*/
function noticeHtml(issue) {
if (!issue.noticeDetail) return '';
const link = issue.noticeId
? '<a class="as-alert-notice-link" href="' + noticeDetailBase + '?id='
+ encodeURIComponent(issue.noticeId) + '">공지 전체 보기 →</a>'
: '';
return '<div class="as-alert-notice">'
+ '<div class="as-alert-notice-head">'
+ '<span class="as-alert-notice-label">공지사항</span>'
+ (issue.noticeSubject ? '<strong>' + escapeHtml(issue.noticeSubject) + '</strong>' : '')
+ '</div>'
+ '<div class="as-alert-notice-body editor-content">' + issue.noticeDetail + '</div>'
+ link
+ '</div>';
}
/**
* 타임라인 | 공지 2열 배치. 데스크탑에서만 두 칸으로 갈라지고 좁은 화면에서는 세로로 쌓인다.
* 한쪽만 있으면 감싸지 않는다 (혼자 반쪽 폭을 차지하지 않게).
*/
function splitHtml(left, right) {
if (!left) return right;
if (!right) return left;
return '<div class="as-card-split">' + left + right + '</div>';
}
function issueCardHtml(issue) { function issueCardHtml(issue) {
const kindClass = KIND_CLASS[issue.kind] || 'is-incident'; const kindClass = KIND_CLASS[issue.kind] || 'is-incident';
@@ -222,7 +253,7 @@
+ (issue.stateLabel ? '<span>' + escapeHtml(issue.stateLabel) + '</span>' : '') + (issue.stateLabel ? '<span>' + escapeHtml(issue.stateLabel) + '</span>' : '')
+ '</div>' + '</div>'
+ '<h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>' + '<h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>'
+ inner + splitHtml(inner, noticeHtml(issue))
+ apiPillsHtml(issue.impactedApis) + apiPillsHtml(issue.impactedApis)
+ '</article>'; + '</article>';
} }
+36 -3
View File
@@ -8,6 +8,7 @@
const authenticated = page.classList.contains('is-authenticated'); const authenticated = page.classList.contains('is-authenticated');
const base = page.getAttribute('data-base') || '/apistatus'; const base = page.getAttribute('data-base') || '/apistatus';
const apiDetailBase = page.getAttribute('data-api-detail-base') || '/apis/detail'; const apiDetailBase = page.getAttribute('data-api-detail-base') || '/apis/detail';
const noticeDetailBase = page.getAttribute('data-notice-detail-base') || '/portalnotice/detail';
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */ /** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
const linkableApiIds = new Set(); const linkableApiIds = new Set();
@@ -104,6 +105,36 @@
return '<div class="as-api-pills"><span>' + escapeHtml(label) + '</span>' + pills + '</div>'; return '<div class="as-api-pills"><span>' + escapeHtml(label) + '</span>' + pills + '</div>';
} }
/**
* 이슈에 연결된 공지 본문. 서버가 게시 중(USE_YN='Y')인 공지만 내려주므로 여기서는 존재 여부만 본다.
* 본문은 관리자가 에디터로 작성한 HTML 이라 공지 상세(th:utext)와 같이 그대로 렌더한다.
*/
function noticeHtml(incident) {
if (!incident.noticeDetail) return '';
const link = incident.noticeId
? '<a class="as-alert-notice-link" href="' + noticeDetailBase + '?id='
+ encodeURIComponent(incident.noticeId) + '">공지 전체 보기 →</a>'
: '';
return '<div class="as-alert-notice">'
+ '<div class="as-alert-notice-head">'
+ '<span class="as-alert-notice-label">공지사항</span>'
+ (incident.noticeSubject ? '<strong>' + escapeHtml(incident.noticeSubject) + '</strong>' : '')
+ '</div>'
+ '<div class="as-alert-notice-body editor-content">' + incident.noticeDetail + '</div>'
+ link
+ '</div>';
}
/**
* 타임라인 | 공지 2열 배치. 데스크탑에서만 두 칸으로 갈라지고 좁은 화면에서는 세로로 쌓인다.
* 한쪽만 있으면 감싸지 않는다 (혼자 반쪽 폭을 차지하지 않게).
*/
function splitHtml(left, right) {
if (!left) return right;
if (!right) return left;
return '<div class="as-card-split">' + left + right + '</div>';
}
// ---------------- ❶ 진행 중 장애 ---------------- // ---------------- ❶ 진행 중 장애 ----------------
function renderActiveIncidents(incidents) { function renderActiveIncidents(incidents) {
const container = document.getElementById('activeIncidents'); const container = document.getElementById('activeIncidents');
@@ -140,7 +171,8 @@
+ '<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:')
+ (timeline ? '<div class="as-alert-timeline">' + timeline + '</div>' : '') + splitHtml(timeline ? '<div class="as-alert-timeline">' + timeline + '</div>' : '',
noticeHtml(incident))
+ '</section>'; + '</section>';
}).join(''); }).join('');
@@ -252,7 +284,8 @@
+ ' <h3 class="as-maint-title">' + escapeHtml(card.title) + '</h3>' + ' <h3 class="as-maint-title">' + escapeHtml(card.title) + '</h3>'
+ ' <span class="as-schedule">' + escapeHtml(phase + ' · ' + range) + '</span>' + ' <span class="as-schedule">' + escapeHtml(phase + ' · ' + range) + '</span>'
+ '</div>' + '</div>'
+ (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))
+ apiPillsHtml(card.impactedApis, '영향 API:') + apiPillsHtml(card.impactedApis, '영향 API:')
+ '<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)) + ')' : '')
@@ -300,7 +333,7 @@
+ (issue.stateLabel ? '<span>' + escapeHtml(issue.stateLabel) + '</span>' : '') + (issue.stateLabel ? '<span>' + escapeHtml(issue.stateLabel) + '</span>' : '')
+ ' </div>' + ' </div>'
+ ' <h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>' + ' <h3 class="as-issue-title">' + escapeHtml(issue.title) + '</h3>'
+ inner + splitHtml(inner, noticeHtml(issue))
+ apiPillsHtml(issue.impactedApis, '영향 API:') + apiPillsHtml(issue.impactedApis, '영향 API:')
+ '</article>' + '</article>'
+ '</div>'; + '</div>';
@@ -49,6 +49,7 @@
padding: 10px 20px; padding: 10px 20px;
border-radius: 8px; border-radius: 8px;
transition: var(--transition-smooth); transition: var(--transition-smooth);
white-space: nowrap;
&:hover { &:hover {
background-color: var(--accent-light); background-color: var(--accent-light);
@@ -63,6 +64,7 @@
background-color: var(--primary-color); background-color: var(--primary-color);
padding: 10px 24px; padding: 10px 24px;
border-radius: 8px; border-radius: 8px;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0, 73, 180, 0.15); box-shadow: 0 4px 12px rgba(0, 73, 180, 0.15);
transition: var(--transition-smooth); transition: var(--transition-smooth);
@@ -89,7 +89,7 @@
} }
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
height: clamp(44px, 11.73vw, 60px); height: clamp(44px, 11.73vw, 60px);
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
@@ -128,20 +128,30 @@
.header-left { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
// 헤더가 좁아져도 로고 영역은 줄이지 않는다 (줄임은 가운데 메뉴가 감당)
flex-shrink: 0;
.logo { .logo {
height: 32px; height: 32px;
// 로고 워드마크(DJ Bank) 밑선에 "API Portal" 글자 baseline 맞춤 // 로고 워드마크(DJ Bank) 밑선에 "API Portal" 글자 baseline 맞춤
align-items: flex-end; align-items: flex-end;
flex-wrap: nowrap;
// width 가 고정(114px)이라 flex 축소가 걸리면 가로만 눌려 비율이 깨진다
img { flex-shrink: 0; }
.mobile-logo-link { .mobile-logo-link {
align-items: flex-end; align-items: flex-end;
flex-shrink: 0;
} }
.mobile-logo-text { .mobile-logo-text {
align-items: flex-end; align-items: flex-end;
line-height: 1; line-height: 1;
padding-bottom: 2px; padding-bottom: 2px;
// "API / Portal" 로 접히지 않게
white-space: nowrap;
flex-shrink: 0;
} }
} }
} }
@@ -263,7 +273,7 @@
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
@media (max-width: 768px) { @media (max-width: 1024px) {
display: none; display: none;
} }
} }
@@ -274,7 +284,7 @@
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
@media (max-width: 768px) { @media (max-width: 1024px) {
display: flex; display: flex;
} }
} }
@@ -993,6 +1003,8 @@
padding: 12px; padding: 12px;
position: relative; position: relative;
transition: var(--transition-smooth); transition: var(--transition-smooth);
// 폭이 좁아져도 "서비스 소 / 개" 처럼 메뉴명이 잘리지 않게 한다
white-space: nowrap;
&:hover { &:hover {
color: var(--primary-blue); color: var(--primary-blue);
@@ -1107,7 +1119,7 @@
// Mobile Responsive Design // Mobile Responsive Design
// 375px 기준 반응형 - clamp() 사용하여 화면 크기에 비례 // 375px 기준 반응형 - clamp() 사용하여 화면 크기에 비례
// =========================== // ===========================
@media (max-width: 768px) { @media (max-width: 1024px) {
.global-header { .global-header {
// Figma: 44px 높이, 반투명 배경 // Figma: 44px 높이, 반투명 배경
// 375px 기준 44px → 44/375*100 ≈ 11.73vw, 범위: 44px ~ 60px // 375px 기준 44px → 44/375*100 ≈ 11.73vw, 범위: 44px ~ 60px
@@ -1258,7 +1270,7 @@
} }
// Mobile responsive // Mobile responsive
@media (max-width: 768px) { @media (max-width: 1024px) {
.header-user-info { .header-user-info {
display: none; // Hide on mobile, use drawer instead display: none; // Hide on mobile, use drawer instead
} }
@@ -255,6 +255,75 @@
} }
} }
// 타임라인 | 공지 2열. 데스크탑(961px~)에서만 갈라지고 그 아래는 세로로 쌓인다.
// minmax(0, 1fr) 은 공지 본문의 긴 URL·표가 칸을 밀어내지 못하게 하는 grid 관용구.
.as-card-split {
margin-top: 16px;
display: grid;
gap: 16px;
align-items: start;
@media (min-width: 961px) {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
}
// 감싼 두 블록의 자체 세로 margin 은 grid gap 으로 대체한다.
// 카드 컨텍스트를 함께 써서 원 규칙(.as-alert-card .as-alert-timeline 등)보다 특이도를 높인다.
.as-card-split > .as-alert-notice { margin-top: 0; }
.as-alert-card .as-card-split > .as-alert-timeline { margin-top: 0; }
.as-maint-card .as-card-split > .as-maint-body { margin-top: 0; }
// 이슈에 연결된 공지 본문 (진행 중 장애 카드 · 점검 카드 공용).
// 본문 길이가 제각각이라 높이를 제한하고 안쪽만 스크롤시킨다.
.as-alert-notice {
margin-top: 16px;
border: 1px solid var(--as-border);
border-radius: var(--as-radius);
background: var(--as-gray-bg);
padding: 14px 16px;
.as-alert-notice-head {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
strong { font-size: 14px; color: var(--as-text); }
}
.as-alert-notice-label {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: var(--as-pill);
background: var(--as-info-bg);
color: var(--as-info);
}
.as-alert-notice-body {
max-height: 320px;
overflow-y: auto;
font-size: 14px;
line-height: 1.7;
color: var(--as-text-2);
word-break: break-word;
img, table { max-width: 100%; }
p:last-child { margin-bottom: 0; }
}
.as-alert-notice-link {
display: inline-block;
margin-top: 12px;
font-size: 13px;
font-weight: 500;
color: var(--as-info);
text-decoration: none;
}
}
// ---------------- 진행 중 장애 카드 ---------------- // ---------------- 진행 중 장애 카드 ----------------
.as-alert-card { .as-alert-card {
border-left: 4px solid var(--as-err); border-left: 4px solid var(--as-err);
@@ -25,7 +25,7 @@
</div> </div>
<div class="api-status" id="apiStatusPage" <div class="api-status" id="apiStatusPage"
th:attr="data-window-days=${windowDays},data-base=@{/apistatus},data-api-detail-base=@{/apis/detail}" th:attr="data-window-days=${windowDays},data-base=@{/apistatus},data-api-detail-base=@{/apis/detail},data-notice-detail-base=@{/portalnotice/detail}"
th:classappend="${authenticated} ? 'is-authenticated' : ''"> th:classappend="${authenticated} ? 'is-authenticated' : ''">
<!-- ❶ 진행 중 장애 --> <!-- ❶ 진행 중 장애 -->
@@ -22,6 +22,7 @@
th:attr="data-window-days=${windowDays}, th:attr="data-window-days=${windowDays},
data-base=@{/apistatus}, data-base=@{/apistatus},
data-api-detail-base=@{/apis/detail}, data-api-detail-base=@{/apis/detail},
data-notice-detail-base=@{/portalnotice/detail},
data-today=${today}, data-today=${today},
data-min-date=${minDate}, data-min-date=${minDate},
data-selected-date=${selectedDate} ?: '', data-selected-date=${selectedDate} ?: '',