v1.4.3: 관리자 UI·홈·미디어 개선

- 관리자 라이트 테마 격리, 대시보드 활성 링크, 로그인 우측 정렬
- 대시보드 통계 추이 차트·툴팁, 홈 Latest/Featured 보정
- 미디어 종류·미사용 필터, 비디오 프레임 썸네일
- NAS 운영 업데이트 절차 문서 추가
This commit is contained in:
2026-05-21 18:30:50 +09:00
parent 6919669330
commit 10c5a099fc
15 changed files with 523 additions and 84 deletions

View File

@@ -111,7 +111,22 @@ const formatTrendValue = (key, value) => {
return formatEngagedDuration(value)
}
return `${Number(value || 0)}`
if (key === 'visitors') {
return `${Number(value || 0)}`
}
return `${Number(value || 0)}`
}
/**
* 추세 막대 툴팁 문구를 반환한다.
* @param {{ key: 'visitors' | 'avgEngagedSeconds' | 'scroll50Reach', title: string }} metric - 지표 정보
* @param {Object} row - 추세 행
* @returns {string} 툴팁 문구
*/
const getTrendTooltipLabel = (metric, row) => {
const dayLabel = row.label || formatTrendDayLabel(row.day)
return `${dayLabel} · ${metric.title} ${formatTrendValue(metric.key, row[metric.key])}`
}
const chartMetrics = computed(() => [
@@ -139,7 +154,7 @@ const chartMetrics = computed(() => [
* @returns {number} 높이 %
*/
const getTrendBarHeight = (key, row) => {
const rows = trendRows.value
const rows = chartTrendRows.value
const maxValue = Math.max(...rows.map((item) => Number(item[key] || 0)), 0)
if (maxValue <= 0) {
@@ -164,6 +179,67 @@ const formatTrendDayLabel = (day) => {
return `${month}.${date}`
}
/**
* 선택 기간에 맞는 차트 표시 집계 단위를 반환한다.
* @param {number} days - 선택 기간
* @returns {number} 묶을 일수
*/
const getTrendChartBucketSize = (days) => {
if (days <= 14) {
return 1
}
if (days <= 60) {
return 7
}
if (days <= 180) {
return 14
}
return 30
}
/**
* 차트 표시용 추세 묶음 행을 만든다.
* @param {Array<Object>} rows - 일자별 추세 행
* @param {number} bucketSize - 묶을 일수
* @returns {Array<Object>} 차트 표시 행
*/
const buildTrendChartRows = (rows, bucketSize) => {
if (bucketSize <= 1) {
return rows.map((row) => ({
...row,
label: formatTrendDayLabel(row.day)
}))
}
const buckets = []
for (let index = 0; index < rows.length; index += bucketSize) {
const bucketRows = rows.slice(index, index + bucketSize)
const firstRow = bucketRows[0]
const lastRow = bucketRows[bucketRows.length - 1]
const engagedRows = bucketRows.filter((row) => Number(row.avgEngagedSeconds || 0) > 0)
const totalEngagedSeconds = engagedRows.reduce((sum, row) => sum + Number(row.avgEngagedSeconds || 0), 0)
const startLabel = formatTrendDayLabel(firstRow?.day)
const endLabel = formatTrendDayLabel(lastRow?.day)
buckets.push({
day: firstRow?.day || '',
label: startLabel === endLabel ? startLabel : `${startLabel}-${endLabel}`,
visitors: bucketRows.reduce((sum, row) => sum + Number(row.visitors || 0), 0),
avgEngagedSeconds: engagedRows.length ? Math.round(totalEngagedSeconds / engagedRows.length) : 0,
scroll50Reach: bucketRows.reduce((sum, row) => sum + Number(row.scroll50Reach || 0), 0)
})
}
return buckets
}
const chartTrendRows = computed(() => {
return buildTrendChartRows(trendRows.value, getTrendChartBucketSize(selectedAnalyticsDays.value))
})
/**
* 현재 접속자가 보고 있는 화면명을 반환한다.
* @param {Object} session - 접속 세션
@@ -268,19 +344,35 @@ watch(selectedAnalyticsDays, () => {
{{ metric.label }}
</strong>
</div>
<div class="admin-dashboard__chart-bars mt-4 flex h-32 items-end gap-1 border-b border-line">
<div
class="admin-dashboard__chart-bars mt-4 flex h-32 items-end gap-1 border-b border-line"
role="img"
:aria-label="`${metric.title} ${analyticsRangeLabel} 추이`"
>
<div
v-for="row in trendRows"
v-for="row in chartTrendRows"
:key="`${metric.key}-${row.day}`"
class="admin-dashboard__chart-bar-wrap flex min-w-[3px] flex-1 items-end"
:title="`${row.day} · ${formatTrendValue(metric.key, row[metric.key])}`"
class="admin-dashboard__chart-bar-wrap group relative flex h-full min-w-[3px] flex-1 items-end"
tabindex="0"
:aria-label="getTrendTooltipLabel(metric, row)"
>
<div
class="admin-dashboard__chart-bar w-full bg-[#15171a]/80"
class="admin-dashboard__chart-tooltip pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 whitespace-nowrap rounded bg-[#15171a] px-2 py-1 text-[11px] font-medium text-white shadow-lg group-hover:block group-focus-visible:block"
>
{{ getTrendTooltipLabel(metric, row) }}
</div>
<div
class="admin-dashboard__chart-bar w-full rounded-t-sm bg-[#15171a] transition-colors group-hover:bg-[#2f6feb] group-focus-visible:bg-[#2f6feb]"
:style="{ height: `${getTrendBarHeight(metric.key, row)}%` }"
/>
</div>
</div>
<p
v-if="!chartTrendRows.length"
class="admin-dashboard__chart-empty mt-4 text-center text-xs text-muted"
>
선택한 기간에 표시할 추이 데이터가 없습니다.
</p>
<div class="mt-2 flex justify-between text-[11px] text-muted">
<span>{{ formatTrendDayLabel(trendStartDay) }}</span>
<span>{{ analyticsRangeLabel }}</span>