summaryrefslogtreecommitdiff
path: root/lib/page-visits/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/page-visits/service.ts')
-rw-r--r--lib/page-visits/service.ts169
1 files changed, 169 insertions, 0 deletions
diff --git a/lib/page-visits/service.ts b/lib/page-visits/service.ts
new file mode 100644
index 00000000..66c57eaa
--- /dev/null
+++ b/lib/page-visits/service.ts
@@ -0,0 +1,169 @@
+import db from "@/db/db"
+import { loginSessions, pageVisits, users } from "@/db/schema"
+import { and, or, ilike, eq, desc, asc, count, sql, isNull } from "drizzle-orm"
+import { filterColumns } from "@/lib/filter-columns";
+import type { GetPageVisitsSchema, ExtendedPageVisit } from "./validation"
+
+export async function getPageVisits(input: GetPageVisitsSchema) {
+ try {
+ const offset = (input.page - 1) * input.perPage;
+ const advancedTable = true;
+
+ // 고급 필터 처리
+ const advancedWhere = filterColumns({
+ table: pageVisits,
+ filters: input.filters,
+ joinOperator: input.joinOperator,
+ });
+
+ // 전역 검색
+ let globalWhere;
+ if (input.search) {
+ const s = `%${input.search}%`;
+ globalWhere = or(
+ ilike(pageVisits.route, s),
+ ilike(pageVisits.pageTitle, s),
+ ilike(pageVisits.referrer, s),
+ ilike(pageVisits.deviceType, s),
+ ilike(pageVisits.browserName, s),
+ ilike(pageVisits.osName, s),
+ ilike(users.email, s),
+ ilike(users.name, s)
+ );
+ }
+
+ // 조건 결합
+ const conditions = [];
+ if (advancedWhere) conditions.push(advancedWhere);
+ if (globalWhere) conditions.push(globalWhere);
+
+ let finalWhere;
+ if (conditions.length > 0) {
+ finalWhere = conditions.length > 1 ? and(...conditions) : conditions[0];
+ }
+
+ // 정렬 처리
+ const orderBy =
+ input.sort.length > 0
+ ? input.sort.map((item) => {
+ // 사용자 관련 필드 정렬
+ if (item.id === 'userEmail') {
+ return item.desc ? desc(users.email) : asc(users.email);
+ } else if (item.id === 'userName') {
+ return item.desc ? desc(users.name) : asc(users.name);
+ } else {
+ // 페이지 방문 필드 정렬
+ return item.desc
+ ? desc(pageVisits[item.id as keyof typeof pageVisits.$inferSelect])
+ : asc(pageVisits[item.id as keyof typeof pageVisits.$inferSelect]);
+ }
+ })
+ : [desc(pageVisits.visitedAt)];
+
+ // 데이터 조회
+ const data = await db
+ .select({
+ id: pageVisits.id,
+ userId: pageVisits.userId,
+ sessionId: pageVisits.sessionId,
+ route: pageVisits.route,
+ pageTitle: pageVisits.pageTitle,
+ referrer: pageVisits.referrer,
+ ipAddress: pageVisits.ipAddress,
+ userAgent: pageVisits.userAgent,
+ visitedAt: pageVisits.visitedAt,
+ duration: pageVisits.duration,
+ queryParams: pageVisits.queryParams,
+ deviceType: pageVisits.deviceType,
+ browserName: pageVisits.browserName,
+ osName: pageVisits.osName,
+ userEmail: users.email,
+ userName: users.name,
+ // 지속 시간 포맷팅
+ durationFormatted: sql<string>`
+ CASE
+ WHEN ${pageVisits.duration} IS NULL THEN NULL
+ WHEN ${pageVisits.duration} < 60 THEN CONCAT(${pageVisits.duration}, '초')
+ WHEN ${pageVisits.duration} < 3600 THEN CONCAT(FLOOR(${pageVisits.duration} / 60), '분 ', ${pageVisits.duration} % 60, '초')
+ ELSE CONCAT(FLOOR(${pageVisits.duration} / 3600), '시간 ', FLOOR((${pageVisits.duration} % 3600) / 60), '분')
+ END
+ `,
+ // 장기 체류 여부 (5분 이상)
+ isLongVisit: sql<boolean>`${pageVisits.duration} >= 300`
+ })
+ .from(pageVisits)
+ .leftJoin(users, eq(pageVisits.userId, users.id))
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .limit(input.perPage)
+ .offset(offset);
+
+ // 총 개수 조회
+ const totalResult = await db
+ .select({ count: count() })
+ .from(pageVisits)
+ .leftJoin(users, eq(pageVisits.userId, users.id))
+ .where(finalWhere);
+
+ const total = totalResult[0]?.count || 0;
+ const pageCount = Math.ceil(total / input.perPage);
+
+ return { data: data as ExtendedPageVisit[], pageCount };
+ } catch (err) {
+ console.error("Failed to fetch page visits:", err);
+ return { data: [], pageCount: 0 };
+ }
+}
+
+
+export async function getUserActivitySummary(userId: string, startDate: Date, endDate: Date) {
+ try {
+ // 페이지 방문 통계
+ const pageStats = await db
+ .select({
+ route: pageVisits.route,
+ visitCount: count(),
+ totalDuration: sql<number>`SUM(${pageVisits.duration})`,
+ avgDuration: sql<number>`AVG(${pageVisits.duration})`,
+ })
+ .from(pageVisits)
+ .where(
+ and(
+ eq(pageVisits.userId, userId),
+ between(pageVisits.visitedAt, startDate, endDate)
+ )
+ )
+ .groupBy(pageVisits.route)
+ .orderBy(desc(count()));
+
+ // 세션 통계
+ const sessionStats = await db
+ .select({
+ sessionCount: count(),
+ totalSessionTime: sql<number>`
+ SUM(EXTRACT(EPOCH FROM (
+ COALESCE(${loginSessions.logoutAt}, ${loginSessions.lastActivityAt})
+ - ${loginSessions.loginAt}
+ )) / 60)
+ `,
+ })
+ .from(loginSessions)
+ .where(
+ and(
+ eq(loginSessions.userId, userId),
+ between(loginSessions.loginAt, startDate, endDate)
+ )
+ );
+
+ return {
+ pageStats,
+ sessionStats: sessionStats[0] || { sessionCount: 0, totalSessionTime: 0 },
+ };
+ } catch (error) {
+ console.error("Failed to get user activity summary:", error);
+ return {
+ pageStats: [],
+ sessionStats: { sessionCount: 0, totalSessionTime: 0 },
+ };
+ }
+} \ No newline at end of file