관리자 통계 UI 개선 v1.9.7
기간 필터 hover 스타일 충돌을 수정하고, 통계 추이 기본값을 일간으로 변경한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -119,6 +119,15 @@ const isBarDimmed = (row) => {
|
||||
return Boolean(hoveredBarKey.value && !isBarEmphasized(row))
|
||||
}
|
||||
|
||||
/**
|
||||
* 축 라벨이 강조 상태인지 확인한다.
|
||||
* @param {Object} row - 추이 행
|
||||
* @returns {boolean} 강조 여부
|
||||
*/
|
||||
const isAxisHighlighted = (row) => {
|
||||
return isBarEmphasized(row) || (isAnalyticsCurrentTrendRow(props.period, row, todayKey) && !hoveredBarKey.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 막대 높이 비율을 반환한다.
|
||||
* @param {Object} row - 추이 행
|
||||
@@ -247,10 +256,7 @@ const getRowDelta = (row) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="admin-analytics-trend-chart"
|
||||
@mouseleave="handleBarLeave"
|
||||
>
|
||||
<section class="admin-analytics-trend-chart">
|
||||
<header
|
||||
v-if="showHeader && chartHeaderLabel"
|
||||
class="admin-analytics-trend-chart__header mb-3 flex items-center justify-between gap-3"
|
||||
@@ -272,13 +278,14 @@ const getRowDelta = (row) => {
|
||||
:key="`${period}-${getBarRowKey(row)}`"
|
||||
class="admin-analytics-trend-chart__column flex min-w-0 flex-1 flex-col transition-opacity duration-150"
|
||||
:class="isBarDimmed(row) ? 'opacity-40' : 'opacity-100'"
|
||||
@mouseenter="handleBarEnter(row)"
|
||||
@mouseleave="handleBarLeave"
|
||||
>
|
||||
<component
|
||||
:is="clickable ? 'button' : 'div'"
|
||||
class="admin-analytics-trend-chart__bar-wrap group relative flex w-full items-end border-b border-line"
|
||||
:class="[barHeightClass, clickable ? 'cursor-pointer border-0 bg-transparent p-0' : '']"
|
||||
:type="clickable ? 'button' : undefined"
|
||||
@mouseenter="handleBarEnter(row)"
|
||||
@focus="handleBarEnter(row)"
|
||||
@click="handleBarClick(row)"
|
||||
>
|
||||
@@ -341,10 +348,10 @@ const getRowDelta = (row) => {
|
||||
/>
|
||||
</component>
|
||||
|
||||
<div class="admin-analytics-trend-chart__axis border-t border-line pt-2 text-center">
|
||||
<div class="admin-analytics-trend-chart__axis overflow-visible border-t border-line px-0.5 pt-2 text-center">
|
||||
<span
|
||||
class="admin-analytics-trend-chart__axis-primary inline-block text-[11px] font-medium transition-colors duration-150"
|
||||
:class="isBarEmphasized(row) || (isAnalyticsCurrentTrendRow(period, row, todayKey) && !hoveredBarKey) ? 'rounded-full bg-[#15171a] px-2 py-0.5 text-[10px] text-white' : 'text-muted'"
|
||||
class="admin-analytics-trend-chart__axis-primary inline-flex max-w-full items-center justify-center whitespace-nowrap leading-none transition-colors duration-150"
|
||||
:class="isAxisHighlighted(row) ? 'rounded-full bg-[#15171a] px-1.5 py-0.5 text-[9px] font-semibold !text-white' : 'text-[11px] font-medium text-muted'"
|
||||
>
|
||||
{{ getAnalyticsTrendAxisPrimaryLabel(period, row, index === visibleRows.length - 1, todayKey) }}
|
||||
</span>
|
||||
|
||||
@@ -502,7 +502,7 @@ components/content/
|
||||
- 게시물 `reads`는 클라이언트에서 15초 이상 체류·50% 이상 스크롤 후 별도 전송.
|
||||
- `POST /api/analytics/heartbeat`는 20초 간격으로 체류시간·스크롤·현재 경로를 전송한다. 로그인 사용자는 서버 세션으로 `user_id`를 연결한다.
|
||||
- 관리자 대시보드는 `GET /admin/api/analytics/realtime`으로 현재 접속자 목록(닉네임·아바타·게시물 제목·접속 유지시간)을 조회한다.
|
||||
- 관리자 대시보드 **통계 추이**는 `trends` 데이터를 3개 막대 차트(방문자수·평균 체류시간·50% 스크롤 도달)로 표시한다. 선택지는 일일·주간·월간·년간이며, 집계는 `ANALYTICS_COLLECTED_SINCE` 환경 변수(기본 `2026-06-01`) 이후 데이터만 합산하고 그 이전 구간은 0으로 표시한다. **방문자수** 막대는 검색(빨강)·SNS(청록)·기타(회색) 유입 비율을 스택으로 표시하며, hover 시 해당 막대만 색상을 강조한다. 안내 문구는 `통계는 2026년 6월 1일부터 집계한 데이터를 표시합니다.` 형식이다. 막대 hover/focus 시 기간과 정확한 값을 툴팁으로 표시하며, 막대 클릭 시 해당 날짜 또는 묶음 구간의 유입·디바이스·유입 키워드·인기 게시물·인기 페이지 상세 모달을 표시한다. 표(table)나 외부 차트 라이브러리는 사용하지 않는다.
|
||||
- 관리자 대시보드 **통계 추이**는 `trends` 데이터를 3개 막대 차트(방문자수·평균 체류시간·50% 스크롤 도달)로 표시한다. 선택지는 일간·주간·월간·년간이며 기본값은 일간이다. 집계는 `ANALYTICS_COLLECTED_SINCE` 환경 변수(기본 `2026-06-01`) 이후 데이터만 합산하고 그 이전 구간은 0으로 표시한다. **방문자수** 막대는 검색(빨강)·SNS(청록)·기타(회색) 유입 비율을 스택으로 표시하며, hover 시 해당 막대만 색상을 강조한다. 안내 문구는 `통계는 2026년 6월 1일부터 집계한 데이터를 표시합니다.` 형식이다. 막대 hover/focus 시 기간과 정확한 값을 툴팁으로 표시하며, 막대 클릭 시 해당 날짜 또는 묶음 구간의 유입·디바이스·유입 키워드·인기 게시물·인기 페이지 상세 모달을 표시한다. 표(table)나 외부 차트 라이브러리는 사용하지 않는다.
|
||||
- 관리자 대시보드 **방문자 유입 정보**는 선택 기간 기준 검색·SNS·기타(직접 포함) 유입원, 디바이스/OS, 유입 키워드를 표시한다.
|
||||
- 관리자 대시보드 **인기 게시물**은 선택 기간 조회수와 함께 최근 30일 월간 조회수, 작성일을 표시한다.
|
||||
- 관리자 글 목록은 게시물별 `post_analytics_daily.views` 누적 합계를 별도 조회수 열에 `1,000`처럼 숫자만 표시하고, 같은 셀 아래 작은 회색 통계 버튼으로 게시물별 통계 모달을 연다. 통계 버튼은 hover 시에만 파란색으로 표시한다.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.9.7
|
||||
|
||||
- 관리자 통계 차트: 현재 구간 축 라벨(이번 주 등) 줄바꿈·hover 시 글자 안 보임 문제 수정.
|
||||
- 관리자 대시보드 통계 추이 기본 선택을 주간에서 일간으로 변경, `일일` 버튼명을 `일간`으로 통일.
|
||||
- 관리자 대시보드 기간 필터: 선택된 버튼 hover 시 `hover:bg-paper`와 `text-white`가 겹치던 스타일 충돌 수정.
|
||||
|
||||
## v1.9.6
|
||||
|
||||
- 관리자 대시보드 방문자 추이: 검색(빨강)·SNS(청록)·기타(회색) 유입 스택 막대 적용.
|
||||
|
||||
@@ -205,11 +205,11 @@ export const getAnalyticsCurrentPeriodLabel = (period) => {
|
||||
}
|
||||
|
||||
if (period === 'weekly') {
|
||||
return '이번주'
|
||||
return '이번\u00A0주'
|
||||
}
|
||||
|
||||
if (period === 'monthly') {
|
||||
return '이번달'
|
||||
return '이번\u00A0달'
|
||||
}
|
||||
|
||||
if (period === 'yearly') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.9.6",
|
||||
"version": "1.9.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -11,15 +11,15 @@ const { data: posts } = await useFetch('/admin/api/posts', {
|
||||
})
|
||||
|
||||
const analyticsRangeOptions = [
|
||||
{ label: '일일', period: 'daily', days: 7 },
|
||||
{ label: '일간', period: 'daily', days: 7 },
|
||||
{ label: '주간', period: 'weekly', days: 56 },
|
||||
{ label: '월간', period: 'monthly', days: 365 },
|
||||
{ label: '년간', period: 'yearly', days: 365 * 8 }
|
||||
]
|
||||
const selectedAnalyticsPeriod = ref('weekly')
|
||||
const selectedAnalyticsPeriod = ref('daily')
|
||||
const analyticsChartPageOffset = ref(0)
|
||||
const selectedAnalyticsRange = computed(() => {
|
||||
return analyticsRangeOptions.find((option) => option.period === selectedAnalyticsPeriod.value) || analyticsRangeOptions[1]
|
||||
return analyticsRangeOptions.find((option) => option.period === selectedAnalyticsPeriod.value) || analyticsRangeOptions[0]
|
||||
})
|
||||
const selectedAnalyticsDays = computed(() => selectedAnalyticsRange.value.days)
|
||||
const analyticsQuery = computed(() => ({
|
||||
@@ -422,8 +422,10 @@ watch(selectedAnalyticsPeriod, () => {
|
||||
v-for="option in analyticsRangeOptions"
|
||||
:key="option.period"
|
||||
type="button"
|
||||
class="admin-dashboard__range-button border border-line px-3 py-1 text-xs font-medium text-muted hover:bg-paper hover:text-ink"
|
||||
:class="option.period === selectedAnalyticsPeriod ? 'bg-[#15171a] text-white hover:bg-[#15171a] hover:text-white' : 'bg-white'"
|
||||
class="admin-dashboard__range-button border border-line px-3 py-1 text-xs font-medium transition-colors"
|
||||
:class="option.period === selectedAnalyticsPeriod
|
||||
? 'bg-[#15171a] !text-white hover:!bg-[#15171a] hover:!text-white'
|
||||
: 'bg-white text-muted hover:bg-paper hover:text-ink'"
|
||||
@click="selectedAnalyticsPeriod = option.period"
|
||||
>
|
||||
{{ option.label }}
|
||||
|
||||
@@ -456,7 +456,7 @@ const deletePage = async (page) => {
|
||||
v-for="metric in analyticsMetricOptions"
|
||||
:key="metric.value"
|
||||
class="h-8 border-r border-line px-3 text-xs font-semibold last:border-r-0"
|
||||
:class="analyticsModalMetric === metric.value ? 'bg-[#15171a] text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
:class="analyticsModalMetric === metric.value ? 'bg-[#15171a] !text-white hover:!bg-[#15171a] hover:!text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
type="button"
|
||||
@click="analyticsModalMetric = metric.value"
|
||||
>
|
||||
@@ -468,7 +468,7 @@ const deletePage = async (page) => {
|
||||
v-for="option in analyticsPeriodOptions"
|
||||
:key="option.value"
|
||||
class="h-8 border-r border-line px-3 text-xs font-semibold last:border-r-0"
|
||||
:class="analyticsModalPeriod === option.value ? 'bg-[#15171a] text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
:class="analyticsModalPeriod === option.value ? 'bg-[#15171a] !text-white hover:!bg-[#15171a] hover:!text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
type="button"
|
||||
@click="selectAnalyticsModalPeriod(option.value)"
|
||||
>
|
||||
|
||||
@@ -1041,7 +1041,7 @@ watch(openPostMenuId, async (postId) => {
|
||||
v-for="metric in analyticsMetricOptions"
|
||||
:key="metric.value"
|
||||
class="h-8 border-r border-line px-3 text-xs font-semibold last:border-r-0"
|
||||
:class="analyticsModalMetric === metric.value ? 'bg-[#15171a] text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
:class="analyticsModalMetric === metric.value ? 'bg-[#15171a] !text-white hover:!bg-[#15171a] hover:!text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
type="button"
|
||||
@click="analyticsModalMetric = metric.value"
|
||||
>
|
||||
@@ -1053,7 +1053,7 @@ watch(openPostMenuId, async (postId) => {
|
||||
v-for="option in analyticsPeriodOptions"
|
||||
:key="option.value"
|
||||
class="h-8 border-r border-line px-3 text-xs font-semibold last:border-r-0"
|
||||
:class="analyticsModalPeriod === option.value ? 'bg-[#15171a] text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
:class="analyticsModalPeriod === option.value ? 'bg-[#15171a] !text-white hover:!bg-[#15171a] hover:!text-white' : 'text-muted hover:bg-paper hover:text-ink'"
|
||||
type="button"
|
||||
@click="selectAnalyticsModalPeriod(option.value)"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user