CSP 관찰 요약 추가

This commit is contained in:
2026-07-29 16:07:36 +09:00
parent 23f0e9b3ef
commit c4493bb7b2
12 changed files with 245 additions and 9 deletions

View File

@@ -94,6 +94,22 @@ const {
})
})
const {
data: cspViolationSummary,
refresh: refreshCspViolationSummary
} = await useFetch('/admin/api/security/csp-violations', {
query: { days: 30, limit: 8 },
server: false,
default: () => ({
days: 30,
totalViolations: 0,
totalRows: 0,
recentViolations: 0,
latestSeenAt: null,
topViolations: []
})
})
const todayKey = getAnalyticsDayKey()
const publishedCount = computed(() => posts.value.filter((post) => post.status === 'published').length)
const draftCount = computed(() => posts.value.filter((post) => post.status === 'draft').length)
@@ -186,6 +202,8 @@ const chartPagination = computed(() => {
const adsenseCards = computed(() => adsenseSummary.value.periods || [])
const adsenseBalance = computed(() => adsenseSummary.value.balance || createEmptyAdsenseBalance())
const cspViolations = computed(() => cspViolationSummary.value?.topViolations || [])
const hasCspViolations = computed(() => Number(cspViolationSummary.value?.totalViolations || 0) > 0)
/**
* AdSense 통화 코드를 표시 가능한 값으로 보정한다.
@@ -246,6 +264,29 @@ const formatAdsenseRpm = (value, currencyCode = '') => {
}).format(Number(value || 0))
}
/**
* ISO 날짜를 관리자 대시보드용 시각으로 변환한다.
* @param {string | null | undefined} value - ISO 날짜
* @returns {string} 표시 문자열
*/
const formatDashboardDateTime = (value) => {
if (!value) {
return '-'
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return '-'
}
return new Intl.DateTimeFormat('ko-KR', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date)
}
/**
* 통계 차트 이전 기간으로 이동한다.
* @returns {void}
@@ -622,6 +663,7 @@ onMounted(() => {
refreshSummary()
refreshTrendDetail()
refreshRealtime()
refreshCspViolationSummary()
}, 30000)
})
@@ -734,6 +776,92 @@ watch(trendRows, () => {
</div>
</section>
<section
v-if="hasCspViolations"
class="admin-dashboard__csp border border-line bg-white p-4"
>
<div class="admin-dashboard__csp-header flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="admin-dashboard__section-title text-sm font-semibold text-ink">
CSP 관찰
</h2>
<p class="admin-dashboard__section-description mt-1 text-xs text-muted">
최근 {{ cspViolationSummary.days }}일 Report-Only 위반 상위 항목
</p>
</div>
<div class="admin-dashboard__csp-summary text-right text-xs text-muted">
<p>
최근 {{ formatNumber(cspViolationSummary.recentViolations) }}회
</p>
<p class="mt-1">
전체 {{ formatNumber(cspViolationSummary.totalViolations) }}회 · 마지막 {{ formatDashboardDateTime(cspViolationSummary.latestSeenAt) }}
</p>
</div>
</div>
<div
v-if="cspViolations.length"
class="admin-dashboard__csp-table mt-4 overflow-x-auto"
>
<table class="min-w-full text-left text-xs">
<thead class="border-b border-line text-[11px] uppercase text-muted">
<tr>
<th class="whitespace-nowrap px-2 py-2 font-semibold">
지시어
</th>
<th class="whitespace-nowrap px-2 py-2 font-semibold">
차단 출처
</th>
<th class="whitespace-nowrap px-2 py-2 font-semibold">
문서
</th>
<th class="whitespace-nowrap px-2 py-2 font-semibold">
브라우저
</th>
<th class="whitespace-nowrap px-2 py-2 text-right font-semibold">
횟수
</th>
<th class="whitespace-nowrap px-2 py-2 text-right font-semibold">
최근
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in cspViolations"
:key="`${row.policyVersion}-${row.effectiveDirective}-${row.blockedOrigin}-${row.documentPath}-${row.userAgentFamily}`"
class="border-b border-line last:border-b-0"
>
<td class="whitespace-nowrap px-2 py-2 font-medium text-ink">
{{ row.effectiveDirective }}
</td>
<td class="max-w-[260px] truncate px-2 py-2 text-muted">
{{ row.blockedOrigin }}
</td>
<td class="max-w-[240px] truncate px-2 py-2 text-muted">
{{ row.documentPath }}
</td>
<td class="whitespace-nowrap px-2 py-2 text-muted">
{{ row.userAgentFamily }}
</td>
<td class="whitespace-nowrap px-2 py-2 text-right font-semibold text-ink">
{{ formatNumber(row.violationCount) }}
</td>
<td class="whitespace-nowrap px-2 py-2 text-right text-muted">
{{ formatDashboardDateTime(row.lastSeenAt) }}
</td>
</tr>
</tbody>
</table>
</div>
<p
v-else
class="admin-dashboard__csp-empty mt-4 border border-line bg-paper px-3 py-2 text-xs text-muted"
>
최근 {{ cspViolationSummary.days }}일에 집계된 CSP 위반은 없습니다.
</p>
</section>
<section class="admin-dashboard__charts border border-line bg-white p-4">
<div class="admin-dashboard__charts-header flex flex-wrap items-center justify-between gap-3">
<div>