guoziyun 1 개월 전
부모
커밋
bfdd5c6e20

+ 14 - 6
oms/dist/src/services/dashboardService.js

@@ -149,7 +149,7 @@ class DashboardService {
                 },
                 drop: { $ne: true },
             })
-                .select({ _id: 1, name: 1, pageId: 1, publishTime: 1 })
+                .select({ _id: 1, name: 1, pageId: 1, publishTime: 1, areaCount: 1, areaCountFloor: 1 })
                 .lean();
             const dayKeys = Array.from({ length: safeDays }, (_, index) => latestDayStart.subtract(index, "day").format("YYYY-MM-DD"));
             const artsByDay = new Map();
@@ -169,6 +169,8 @@ class DashboardService {
                     resId,
                     name: art.name || "未命名作品",
                     pageId: String(art.pageId || ""),
+                    areaCount: Number(art.areaCount || 0),
+                    areaCountFloor: Number(art.areaCountFloor || 0),
                 });
             }
             const uniqueResIds = Array.from(new Set(allResIds));
@@ -186,7 +188,7 @@ class DashboardService {
         ORDER BY date
       `;
             const dauRows = await clients_1.clickhouseService.queryEvents(dauQuery);
-            const dauByDay = new Map(dauRows.map((row) => [String(row.date), row.dau]));
+            const dauByDay = new Map(dauRows.map((row) => [String(row.date), Number(row.dau || 0)]));
             const statsByDayAndRes = new Map();
             if (uniqueResIds.length > 0) {
                 const inClause = uniqueResIds.map((id) => `'${id}'`).join(",");
@@ -195,20 +197,22 @@ class DashboardService {
             toDate(time) AS date,
             res AS resId,
             countIf(event = 'color_start') AS clickCount,
-            countIf(event = 'color_done') AS completionCount
+            countIf(event = 'color_done') AS completionCount,
+            countIf(event = 'color_tip') AS tipCount
           FROM ${this.eventsTable}
           WHERE project = 1
             AND toDate(time) >= '${oldestDay}'
             AND toDate(time) <= '${newestDay}'
-            AND event IN ('color_start', 'color_done')
+            AND event IN ('color_start', 'color_done', 'color_tip')
             AND res IN (${inClause})
           GROUP BY date, res
         `;
                 const statRows = await clients_1.clickhouseService.queryEvents(artworkStatsQuery);
                 for (const row of statRows) {
                     statsByDayAndRes.set(`${row.date}|${row.resId}`, {
-                        clickCount: row.clickCount,
-                        completionCount: row.completionCount,
+                        clickCount: Number(row.clickCount || 0),
+                        completionCount: Number(row.completionCount || 0),
+                        tipCount: Number(row.tipCount || 0),
                     });
                 }
             }
@@ -218,6 +222,7 @@ class DashboardService {
                     const stat = statsByDayAndRes.get(`${dayKey}|${art.resId}`) || {
                         clickCount: 0,
                         completionCount: 0,
+                        tipCount: 0,
                     };
                     const clickRate = dau > 0 ? stat.clickCount / dau : 0;
                     const completionRate = stat.clickCount > 0 ? stat.completionCount / stat.clickCount : 0;
@@ -225,8 +230,11 @@ class DashboardService {
                         resId: art.resId,
                         name: art.name,
                         thumbnail: `http://color2.jccytech.cn/thumbs/v2/page/320/${art.resId}.png`,
+                        areaCount: art.areaCount,
+                        areaCountFloor: art.areaCountFloor,
                         clickCount: stat.clickCount,
                         completionCount: stat.completionCount,
+                        tipCount: stat.tipCount,
                         clickRate: parseFloat(clickRate.toFixed(4)),
                         completionRate: parseFloat(completionRate.toFixed(4)),
                     };

+ 1 - 1
oms/public/app/index.html

@@ -9,5 +9,5 @@
   <style>body,html{width:100%;height:100%}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles-LXBSU6DF.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-LXBSU6DF.css"></noscript></head>
   <body>
     <app-root></app-root>
-  <script src="polyfills-B6TNHZQ6.js" type="module"></script><script src="main-52DEBLCW.js" type="module"></script></body>
+  <script src="polyfills-B6TNHZQ6.js" type="module"></script><script src="main-JQECWEFL.js" type="module"></script></body>
 </html>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
oms/public/app/main-JQECWEFL.js


+ 20 - 8
oms/src/services/dashboardService.ts

@@ -28,8 +28,11 @@ interface NewArtworkTabItem {
   resId: string;
   name: string;
   thumbnail: string;
+  areaCount: number;
+  areaCountFloor: number;
   clickCount: number;
   completionCount: number;
+  tipCount: number;
   clickRate: number;
   completionRate: number;
 }
@@ -199,12 +202,12 @@ class DashboardService {
         },
         drop: { $ne: true },
       })
-        .select({ _id: 1, name: 1, pageId: 1, publishTime: 1 })
+        .select({ _id: 1, name: 1, pageId: 1, publishTime: 1, areaCount: 1, areaCountFloor: 1 })
         .lean();
 
       const dayKeys = Array.from({ length: safeDays }, (_, index) => latestDayStart.subtract(index, "day").format("YYYY-MM-DD"));
 
-      const artsByDay = new Map<string, Array<{ resId: string; name: string; pageId: string }>>();
+      const artsByDay = new Map<string, Array<{ resId: string; name: string; pageId: string; areaCount: number; areaCountFloor: number }>>();
       for (const dayKey of dayKeys) {
         artsByDay.set(dayKey, []);
       }
@@ -220,6 +223,8 @@ class DashboardService {
           resId,
           name: art.name || "未命名作品",
           pageId: String(art.pageId || ""),
+          areaCount: Number(art.areaCount || 0),
+          areaCountFloor: Number(art.areaCountFloor || 0),
         });
       }
 
@@ -240,9 +245,9 @@ class DashboardService {
       `;
 
       const dauRows = await clickhouseService.queryEvents<{ date: string; dau: number }>(dauQuery);
-      const dauByDay = new Map<string, number>(dauRows.map((row) => [String(row.date), row.dau]));
+      const dauByDay = new Map<string, number>(dauRows.map((row) => [String(row.date), Number(row.dau || 0)]));
 
-      const statsByDayAndRes = new Map<string, { clickCount: number; completionCount: number }>();
+      const statsByDayAndRes = new Map<string, { clickCount: number; completionCount: number; tipCount: number }>();
       if (uniqueResIds.length > 0) {
         const inClause = uniqueResIds.map((id) => `'${id}'`).join(",");
         const artworkStatsQuery = `
@@ -250,12 +255,13 @@ class DashboardService {
             toDate(time) AS date,
             res AS resId,
             countIf(event = 'color_start') AS clickCount,
-            countIf(event = 'color_done') AS completionCount
+            countIf(event = 'color_done') AS completionCount,
+            countIf(event = 'color_tip') AS tipCount
           FROM ${this.eventsTable}
           WHERE project = 1
             AND toDate(time) >= '${oldestDay}'
             AND toDate(time) <= '${newestDay}'
-            AND event IN ('color_start', 'color_done')
+            AND event IN ('color_start', 'color_done', 'color_tip')
             AND res IN (${inClause})
           GROUP BY date, res
         `;
@@ -265,12 +271,14 @@ class DashboardService {
           resId: string;
           clickCount: number;
           completionCount: number;
+          tipCount: number;
         }>(artworkStatsQuery);
 
         for (const row of statRows) {
           statsByDayAndRes.set(`${row.date}|${row.resId}`, {
-            clickCount: row.clickCount,
-            completionCount: row.completionCount,
+            clickCount: Number(row.clickCount || 0),
+            completionCount: Number(row.completionCount || 0),
+            tipCount: Number(row.tipCount || 0),
           });
         }
       }
@@ -281,6 +289,7 @@ class DashboardService {
           const stat = statsByDayAndRes.get(`${dayKey}|${art.resId}`) || {
             clickCount: 0,
             completionCount: 0,
+            tipCount: 0,
           };
 
           const clickRate = dau > 0 ? stat.clickCount / dau : 0;
@@ -290,8 +299,11 @@ class DashboardService {
             resId: art.resId,
             name: art.name,
             thumbnail: `http://color2.jccytech.cn/thumbs/v2/page/320/${art.resId}.png`,
+            areaCount: art.areaCount,
+            areaCountFloor: art.areaCountFloor,
             clickCount: stat.clickCount,
             completionCount: stat.completionCount,
+            tipCount: stat.tipCount,
             clickRate: parseFloat(clickRate.toFixed(4)),
             completionRate: parseFloat(completionRate.toFixed(4)),
           };

+ 107 - 0
omsapp/src/app/pages/art-done-rate.component.ts

@@ -0,0 +1,107 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+type ArtDoneRateData = {
+  completionRate?: number;
+  areaCountFloor?: number;
+  totalStartCount?: number;
+  totalDoneCount?: number;
+};
+
+@Component({
+  selector: 'art-done-rate',
+  standalone: true,
+  imports: [CommonModule],
+  template: `
+    <div class="done-rate" [ngStyle]="{ color: color }">
+      <span class="rate">{{ format(absRatePercent) }}%</span>
+      <span *ngIf="diff > 0" class="trend">↑ {{ format(diff) }}%</span>
+      <span *ngIf="diff < 0" class="trend">↓ {{ format(diff) }}%</span>
+      <span *ngIf="!simple" class="meta">({{ doneCount }}/{{ startCount }})</span>
+    </div>
+  `,
+  styles: [
+    `
+      .done-rate {
+        display: inline-flex;
+        align-items: center;
+        gap: 6px;
+        font-size: 12px;
+      }
+
+      .rate {
+        font-weight: 700;
+      }
+
+      .trend {
+        font-weight: 600;
+      }
+
+      .meta {
+        color: #595959;
+      }
+    `,
+  ],
+})
+export class ArtDoneRateComponent {
+  @Input('data') item: ArtDoneRateData = {};
+  @Input() simple = false;
+
+  get startCount(): number {
+    return Number(this.item?.totalStartCount || 0);
+  }
+
+  get doneCount(): number {
+    return Number(this.item?.totalDoneCount || 0);
+  }
+
+  private get normalizedRatePercent(): number {
+    const value = Number(this.item?.completionRate || 0);
+    if (!Number.isFinite(value) || value <= 0) return 0;
+    // 兼容 0~1 和 0~100 两种输入
+    return value <= 1 ? value * 100 : value;
+  }
+
+  get absRatePercent(): number {
+    return Math.max(0, this.normalizedRatePercent);
+  }
+
+  get diff(): number {
+    const floor = Number(this.item?.areaCountFloor || 0);
+    if (!Number.isFinite(floor) || floor < 0 || floor >= averageDoneRates.length) return 0;
+    return this.absRatePercent - averageDoneRates[floor];
+  }
+
+  get color(): string {
+    if (this.startCount < 1000) return '#8c8c8c';
+    if (this.diff > 0) return '#cf1322';
+    if (this.diff < 0) return '#389e0d';
+    return '#262626';
+  }
+
+  format(num: number): string {
+    if (!Number.isFinite(num)) return '0.00';
+    return Math.abs(num).toFixed(2);
+  }
+}
+
+const averageDoneRates: number[] = [87.78, 84.69, 77.44, 74.39, 73.31, 73.01, 71.45, 68.00, 66.75, 64.99, 63.64, 64.12, 63.93, 62.26, 57.40];
+
+/*
+Loading file: area_count_done_rate.js
+区块数区间: 0-100, 完成率: 87.78%
+区块数区间: 100-200, 完成率: 84.69%
+区块数区间: 200-300, 完成率: 77.44%
+区块数区间: 300-400, 完成率: 74.39%
+区块数区间: 400-500, 完成率: 73.31%
+区块数区间: 500-600, 完成率: 73.01%
+区块数区间: 600-700, 完成率: 71.45%
+区块数区间: 700-800, 完成率: 68.00%
+区块数区间: 800-900, 完成率: 66.75%
+区块数区间: 900-1000, 完成率: 64.99%
+区块数区间: 1000-1100, 完成率: 63.64%
+区块数区间: 1100-1200, 完成率: 64.12%
+区块数区间: 1200-1300, 完成率: 63.93%
+区块数区间: 1300-1400, 完成率: 62.26%
+区块数区间: 1400-1500, 完成率: 57.40%
+*/

+ 119 - 181
omsapp/src/app/pages/dashboard.component.ts

@@ -1,17 +1,15 @@
 import { Component, OnInit } from '@angular/core';
 import { CommonModule, NgFor, NgIf, DatePipe } from '@angular/common';
 import { DashboardService, NewArtworkTab } from '../services/dashboard.service';
+import { ArtDoneRateComponent } from './art-done-rate.component';
 
 import { NzCardModule } from 'ng-zorro-antd/card';
 import { NzIconModule } from 'ng-zorro-antd/icon';
-import { NzGridModule } from 'ng-zorro-antd/grid';
 import { NzStatisticModule } from 'ng-zorro-antd/statistic';
 import { NzSpinModule } from 'ng-zorro-antd/spin';
 import { NzTableModule } from 'ng-zorro-antd/table';
-import { NzProgressModule } from 'ng-zorro-antd/progress';
 import { NzPageHeaderModule } from 'ng-zorro-antd/page-header';
 import { NzTabsModule } from 'ng-zorro-antd/tabs';
-import { NzModalService } from 'ng-zorro-antd/modal';
 import { NzMessageService } from 'ng-zorro-antd/message';
 
 @Component({
@@ -22,173 +20,122 @@ import { NzMessageService } from 'ng-zorro-antd/message';
     NgFor,
     NgIf,
     DatePipe,
+    ArtDoneRateComponent,
     NzCardModule,
     NzIconModule,
-    NzGridModule,
     NzStatisticModule,
     NzSpinModule,
     NzTableModule,
-    NzProgressModule,
     NzPageHeaderModule,
     NzTabsModule,
   ],
-  providers: [NzModalService, NzMessageService],
+  providers: [NzMessageService],
   template: `
     <div class="dashboard-container">
       <nz-page-header [nzGhost]="false">
         <nz-page-header-title>数据看板</nz-page-header-title>
         <nz-page-header-extra>
-          <span
-            nz-icon
-            nzType="sync"
-            nzTheme="outline"
-            (click)="refreshData()"
-          ></span>
+          <span nz-icon nzType="sync" nzTheme="outline" (click)="refreshData()"></span>
         </nz-page-header-extra>
         <nz-page-header-content>
-          <p>
-            最后更新时间:{{ lastUpdateTime | date: 'yyyy-MM-dd HH:mm:ss' }}
-          </p>
+          <p>最后更新时间:{{ lastUpdateTime | date: 'yyyy-MM-dd HH:mm:ss' }}</p>
         </nz-page-header-content>
       </nz-page-header>
 
       <nz-spin [nzSpinning]="isLoading" nzTip="数据加载中...">
-        <nz-row [nzGutter]="16">
-          <nz-col [nzXs]="24" [nzLg]="10">
-            <nz-card nzTitle="用户活跃" nzHoverable>
-              <nz-statistic
-                [nzTitle]="'当日日活(DAU)'"
-                [nzValue]="activeUsersToday"
-                [nzPrefix]="userIcon"
-                [nzSuffix]="'人'"
-                [nzValueStyle]="{ color: '#1677ff' }"
-              ></nz-statistic>
-              <ng-template #userIcon>
-                <span nz-icon nzType="user" nzTheme="outline"></span>
-              </ng-template>
-
-              <div class="range-buttons">
-                <button
-                  type="button"
-                  class="range-btn"
-                  [class.active]="selectedDauRange === 7"
-                  (click)="changeDauRange(7)"
-                >
-                  7天
-                </button>
-                <button
-                  type="button"
-                  class="range-btn"
-                  [class.active]="selectedDauRange === 14"
-                  (click)="changeDauRange(14)"
-                >
-                  14天
-                </button>
-                <button
-                  type="button"
-                  class="range-btn"
-                  [class.active]="selectedDauRange === 30"
-                  (click)="changeDauRange(30)"
-                >
-                  30天
-                </button>
-              </div>
-
-              <div class="chart-wrapper">
-                <svg
-                  viewBox="0 0 600 180"
-                  class="line-chart"
-                  preserveAspectRatio="none"
-                >
-                  <polyline
-                    *ngIf="dauChartPoints"
-                    [attr.points]="dauChartPoints"
-                    fill="none"
-                    stroke="#1677ff"
-                    stroke-width="3"
-                    stroke-linecap="round"
-                    stroke-linejoin="round"
-                  ></polyline>
-                </svg>
-              </div>
-
-              <div class="chart-meta">
-                <span>{{ dauTrendStartDate }}</span>
-                <span>Max: {{ dauMax }}</span>
-                <span>Min: {{ dauMin }}</span>
-                <span>{{ dauTrendEndDate }}</span>
-              </div>
-            </nz-card>
-          </nz-col>
-
-          <nz-col [nzXs]="24" [nzLg]="14">
-            <nz-card nzTitle="最近7天上新作品表现" nzHoverable>
-              <nz-tabset
-                [nzSelectedIndex]="activeArtworkTabIndex"
-                (nzSelectedIndexChange)="activeArtworkTabIndex = $event"
+        <nz-card nzTitle="用户卡片:当日日活 + 日活曲线" nzHoverable class="stack-card">
+          <nz-statistic
+            [nzTitle]="'当日日活(DAU)'"
+            [nzValue]="activeUsersToday"
+            [nzPrefix]="userIcon"
+            [nzSuffix]="'人'"
+            [nzValueStyle]="{ color: '#1677ff' }"
+          ></nz-statistic>
+          <ng-template #userIcon>
+            <span nz-icon nzType="user" nzTheme="outline"></span>
+          </ng-template>
+
+          <div class="range-buttons">
+            <button type="button" class="range-btn" [class.active]="selectedDauRange === 7" (click)="changeDauRange(7)">7天</button>
+            <button type="button" class="range-btn" [class.active]="selectedDauRange === 14" (click)="changeDauRange(14)">14天</button>
+            <button type="button" class="range-btn" [class.active]="selectedDauRange === 30" (click)="changeDauRange(30)">30天</button>
+          </div>
+
+          <div class="chart-wrapper">
+            <svg viewBox="0 0 600 180" class="line-chart" preserveAspectRatio="none">
+              <polyline
+                *ngIf="dauChartPoints"
+                [attr.points]="dauChartPoints"
+                fill="none"
+                stroke="#1677ff"
+                stroke-width="3"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              ></polyline>
+            </svg>
+          </div>
+
+          <div class="chart-meta">
+            <span>{{ dauTrendStartDate }}</span>
+            <span>Max: {{ dauMax }}</span>
+            <span>Min: {{ dauMin }}</span>
+            <span>{{ dauTrendEndDate }}</span>
+          </div>
+        </nz-card>
+
+        <nz-card nzTitle="作品卡片:最近7天上新作品表现" nzHoverable class="stack-card">
+          <nz-tabset [nzSelectedIndex]="activeArtworkTabIndex" (nzSelectedIndexChange)="activeArtworkTabIndex = $event">
+            <nz-tab *ngFor="let tab of artworkTabs" [nzTitle]="tab.label">
+              <div class="tab-subtitle">{{ tab.date }} · 当日DAU {{ tab.dau }}</div>
+
+              <nz-table
+                #worksTable
+                [nzData]="tab.artworks"
+                [nzFrontPagination]="false"
+                [nzBordered]="true"
+                [nzSize]="'small'"
               >
-                <nz-tab *ngFor="let tab of artworkTabs" [nzTitle]="tab.label">
-                  <div class="tab-subtitle">
-                    {{ tab.date }} · 当日DAU {{ tab.dau }}
-                  </div>
-
-                  <nz-table
-                    #worksTable
-                    [nzData]="tab.artworks"
-                    [nzFrontPagination]="false"
-                    [nzBordered]="true"
-                    [nzSize]="'small'"
-                  >
-                    <thead>
-                      <tr>
-                        <th>作品</th>
-                        <th>名称</th>
-                        <th>点击率</th>
-                        <th>完成率</th>
-                        <th>点击数</th>
-                        <th>完成数</th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      <tr *ngFor="let work of worksTable.data">
-                        <td>
-                          <div
-                            class="work-thumbnail"
-                            [style.backgroundImage]="
-                              'url(' + work.thumbnail + ')'
-                            "
-                          ></div>
-                        </td>
-                        <td>{{ work.name }}</td>
-                        <td>{{ work.clickRate * 100 | number: '1.1-2' }}%</td>
-                        <td>
-                          <nz-progress
-                            [nzPercent]="work.completionRate * 100"
-                            [nzShowInfo]="true"
-                            [nzStrokeWidth]="6"
-                            [nzStrokeColor]="
-                              work.completionRate >= 0.6
-                                ? '#52c41a'
-                                : work.completionRate >= 0.35
-                                  ? '#faad14'
-                                  : '#ff4d4f'
-                            "
-                          ></nz-progress>
-                        </td>
-                        <td>{{ work.clickCount }}</td>
-                        <td>{{ work.completionCount }}</td>
-                      </tr>
-                    </tbody>
-                  </nz-table>
-
-                  <div class="empty-tip" *ngIf="tab.artworks.length === 0">
-                    当日无上新作品
-                  </div>
-                </nz-tab>
-              </nz-tabset>
-            </nz-card>
-          </nz-col>
-        </nz-row>
+                <thead>
+                  <tr>
+                    <th>作品</th>
+                    <th>名称</th>
+                    <th>区块数</th>
+                    <th>点击率</th>
+                    <th>完成率</th>
+                    <th>点击数</th>
+                    <th>完成数</th>
+                    <th>道具使用数</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr *ngFor="let work of worksTable.data">
+                    <td>
+                      <div class="work-thumbnail" [style.backgroundImage]="'url(' + work.thumbnail + ')'" ></div>
+                    </td>
+                    <td>{{ work.name }}</td>
+                    <td>{{ work.areaCount || 0 }}</td>
+                    <td>{{ work.clickRate * 100 | number: '1.1-2' }}%</td>
+                    <td>
+                      <art-done-rate
+                        [data]="{
+                          completionRate: work.completionRate,
+                          areaCountFloor: work.areaCountFloor,
+                          totalStartCount: work.clickCount,
+                          totalDoneCount: work.completionCount
+                        }"
+                      ></art-done-rate>
+                    </td>
+                    <td>{{ work.clickCount }}</td>
+                    <td>{{ work.completionCount }}</td>
+                    <td>{{ work.tipCount || 0 }}</td>
+                  </tr>
+                </tbody>
+              </nz-table>
+
+              <div class="empty-tip" *ngIf="tab.artworks.length === 0">当日无上新作品</div>
+            </nz-tab>
+          </nz-tabset>
+        </nz-card>
       </nz-spin>
     </div>
   `,
@@ -200,6 +147,10 @@ import { NzMessageService } from 'ng-zorro-antd/message';
         min-height: 100%;
       }
 
+      .stack-card {
+        margin-bottom: 16px;
+      }
+
       .range-buttons {
         margin: 12px 0 8px;
         display: flex;
@@ -282,7 +233,6 @@ export class DashboardComponent implements OnInit {
   activeArtworkTabIndex = 0;
 
   constructor(
-    private modalService: NzModalService,
     private message: NzMessageService,
     private dashboardService: DashboardService,
   ) {}
@@ -323,6 +273,22 @@ export class DashboardComponent implements OnInit {
       .join(' ');
   }
 
+  private applyDauData(dau: { today: number; trend: Array<{ date: string; dau: number }> }): void {
+    this.activeUsersToday = dau.today;
+    this.dauTrendLabels = dau.trend.map((item) => item.date);
+    this.dauTrendData = dau.trend.map((item) => item.dau);
+    this.dauMin = this.dauTrendData.length
+      ? Math.min(...this.dauTrendData)
+      : 0;
+    this.dauMax = this.dauTrendData.length
+      ? Math.max(...this.dauTrendData)
+      : 0;
+    this.dauTrendStartDate = this.dauTrendLabels[0] || '';
+    this.dauTrendEndDate =
+      this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
+    this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
+  }
+
   private loadUserCardData(): void {
     this.isLoading = true;
     this.dashboardService.getKpi(this.selectedDauRange).subscribe({
@@ -333,21 +299,7 @@ export class DashboardComponent implements OnInit {
           return;
         }
 
-        const { dau } = response.data;
-        this.activeUsersToday = dau.today;
-        this.dauTrendLabels = dau.trend.map((item) => item.date);
-        this.dauTrendData = dau.trend.map((item) => item.dau);
-        this.dauMin = this.dauTrendData.length
-          ? Math.min(...this.dauTrendData)
-          : 0;
-        this.dauMax = this.dauTrendData.length
-          ? Math.max(...this.dauTrendData)
-          : 0;
-        this.dauTrendStartDate = this.dauTrendLabels[0] || '';
-        this.dauTrendEndDate =
-          this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
-        this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
-
+        this.applyDauData(response.data.dau);
         this.lastUpdateTime = new Date();
         this.isLoading = false;
       },
@@ -390,21 +342,7 @@ export class DashboardComponent implements OnInit {
           return;
         }
 
-        const { dau } = kpiResponse.data;
-        this.activeUsersToday = dau.today;
-        this.dauTrendLabels = dau.trend.map((item) => item.date);
-        this.dauTrendData = dau.trend.map((item) => item.dau);
-        this.dauMin = this.dauTrendData.length
-          ? Math.min(...this.dauTrendData)
-          : 0;
-        this.dauMax = this.dauTrendData.length
-          ? Math.max(...this.dauTrendData)
-          : 0;
-        this.dauTrendStartDate = this.dauTrendLabels[0] || '';
-        this.dauTrendEndDate =
-          this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
-        this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
-
+        this.applyDauData(kpiResponse.data.dau);
         await this.loadArtworkTabsData();
 
         this.lastUpdateTime = new Date();

+ 3 - 0
omsapp/src/app/services/dashboard.service.ts

@@ -38,8 +38,11 @@ export interface NewArtworkTabItem {
   resId: string;
   name: string;
   thumbnail: string;
+  areaCount: number;
+  areaCountFloor: number;
   clickCount: number;
   completionCount: number;
+  tipCount: number;
   clickRate: number;
   completionRate: number;
 }

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.