Skip to content

Commit bb324ef

Browse files
author
jwham
committed
feat: ✨ active user counts
1 parent 19e45d3 commit bb324ef

File tree

3 files changed

+287
-0
lines changed

3 files changed

+287
-0
lines changed

app/src/activeUser/activeUser.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { TIMEZONE } from '#lambda/index.js';
2+
import { DAILY_LOGTIME_COLLECTION } from '#lambda/location/location.js';
3+
import { LambdaMongo } from '#lambda/mongodb/mongodb.js';
4+
import { SCALE_TEAM_COLLECTION } from '#lambda/scaleTeam/scaleTeam.js';
5+
import { DateWrapper } from '#lambda/util/date.js';
6+
import { z } from 'zod';
7+
8+
const MV_ACTIVE_USER_COLLECTION = 'mv_active_user_counts';
9+
10+
const activeUserCountSchema = z.object({
11+
date: z.date(),
12+
count: z.number(),
13+
});
14+
15+
type UserIdSetByMonth = { date: number; userIds: number[] };
16+
type ScaleTeamIdSetByMonth = {
17+
date: number;
18+
correctorIds: number[];
19+
correctedIds: number[];
20+
};
21+
22+
// eslint-disable-next-line
23+
export class ActiveUserUpdator {
24+
static async update(mongo: LambdaMongo, end: Date): Promise<void> {
25+
await ActiveUserUpdator.updateUpdated(mongo, end);
26+
}
27+
28+
private static async updateUpdated(
29+
mongo: LambdaMongo,
30+
end: Date,
31+
): Promise<void> {
32+
const userIdSetByMonth = new Map<number, Set<number>>();
33+
const twoMonthAgo = new DateWrapper(end)
34+
.startOfMonth()
35+
.moveMonth(-2)
36+
.toDate();
37+
38+
//#region scaleTeam
39+
const scaleTeamUserGroupList = await mongo
40+
.db()
41+
.collection(SCALE_TEAM_COLLECTION)
42+
.aggregate<ScaleTeamIdSetByMonth>([
43+
{
44+
$match: {
45+
beginAt: {
46+
$gte: twoMonthAgo,
47+
$lt: end,
48+
},
49+
},
50+
},
51+
{
52+
$unwind: '$correcteds',
53+
},
54+
{
55+
$group: {
56+
_id: {
57+
$dateFromParts: {
58+
year: {
59+
$year: {
60+
date: '$beginAt',
61+
timezone: TIMEZONE,
62+
},
63+
},
64+
month: {
65+
$month: {
66+
date: '$beginAt',
67+
timezone: TIMEZONE,
68+
},
69+
},
70+
timezone: TIMEZONE,
71+
},
72+
},
73+
correctorIds: {
74+
$addToSet: '$corrector.id',
75+
},
76+
correctedIds: {
77+
$addToSet: '$correcteds.id',
78+
},
79+
},
80+
},
81+
{
82+
$project: {
83+
_id: 0,
84+
date: { $toLong: '$_id' },
85+
correctorIds: 1,
86+
correctedIds: 1,
87+
},
88+
},
89+
])
90+
.toArray();
91+
92+
scaleTeamUserGroupList.forEach((scaleTeamUserGroup) => {
93+
const userIds = new Set<number>([
94+
...scaleTeamUserGroup.correctorIds,
95+
...scaleTeamUserGroup.correctedIds,
96+
]);
97+
98+
ActiveUserUpdator.addUserIdSetByMonth(userIdSetByMonth, {
99+
date: scaleTeamUserGroup.date,
100+
userIds: Array.from(userIds),
101+
});
102+
});
103+
//#endregion scaleTeam
104+
105+
//#region logtime
106+
const dailyLogtimeUserIds = await mongo
107+
.db()
108+
.collection(DAILY_LOGTIME_COLLECTION)
109+
.aggregate<UserIdSetByMonth>([
110+
{
111+
$match: {
112+
date: {
113+
$gte: twoMonthAgo,
114+
$lt: end,
115+
},
116+
},
117+
},
118+
{
119+
$group: {
120+
_id: {
121+
$dateFromParts: {
122+
year: {
123+
$year: {
124+
date: '$date',
125+
timezone: TIMEZONE,
126+
},
127+
},
128+
month: {
129+
$month: {
130+
date: '$date',
131+
timezone: TIMEZONE,
132+
},
133+
},
134+
timezone: TIMEZONE,
135+
},
136+
},
137+
userIds: {
138+
$addToSet: '$userId',
139+
},
140+
},
141+
},
142+
{
143+
$project: {
144+
_id: 0,
145+
date: { $toLong: '$_id' },
146+
userIds: 1,
147+
},
148+
},
149+
])
150+
.toArray();
151+
152+
dailyLogtimeUserIds.forEach((dailyLogtimeUserId) => {
153+
ActiveUserUpdator.addUserIdSetByMonth(
154+
userIdSetByMonth,
155+
dailyLogtimeUserId,
156+
);
157+
});
158+
//#endregion logtime
159+
160+
const userIdByMonthList = Array.from(userIdSetByMonth.entries())
161+
.sort((a, b) => a[0] - b[0])
162+
.map(([date, userIdSet]) =>
163+
activeUserCountSchema.parse({
164+
date: new DateWrapper(date).toDate(),
165+
count: userIdSet.size,
166+
}),
167+
);
168+
169+
for (const userIdByMonth of userIdByMonthList) {
170+
await mongo
171+
.db()
172+
.collection(MV_ACTIVE_USER_COLLECTION)
173+
.updateOne(userIdByMonth, { $set: userIdByMonth }, { upsert: true });
174+
}
175+
}
176+
177+
private static addUserIdSetByMonth(
178+
origin: Map<number, Set<number>>,
179+
userIdSetByMonth: UserIdSetByMonth,
180+
): void {
181+
const prev = origin.get(userIdSetByMonth.date) ?? new Set<number>();
182+
userIdSetByMonth.userIds.forEach((userId) => prev.add(userId));
183+
184+
origin.set(userIdSetByMonth.date, prev);
185+
}
186+
}

app/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { TitlesUserUpdator } from '#lambda/titlesUser/titlesUser.js';
2121
import { UserUpdator } from '#lambda/user/user.js';
2222
import { assertEnvExist } from '#lambda/util/envCheck.js';
2323
import dotenv from 'dotenv';
24+
import { ActiveUserUpdator } from './activeUser/activeUser.js';
2425

2526
dotenv.config();
2627

@@ -72,6 +73,7 @@ const main = async (): Promise<void> => {
7273
TitlesUserUpdator,
7374
SkillUpdator,
7475
UserUpdator,
76+
ActiveUserUpdator,
7577
];
7678

7779
await execUpdators(updators, mongo, end);

app/src/util/date.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
export const ASIA_SEOUL_TZ_OFFSET = -540;
2+
3+
export const MIN = 60 * 1000;
4+
export const HOUR = 60 * MIN;
5+
export const DAY = 24 * HOUR;
6+
7+
/**
8+
*
9+
* @description
10+
* 모든 함수는 원본을 변경하지 않습니다.
11+
*/
12+
export class DateWrapper {
13+
static now(): number {
14+
return new DateWrapper().getTime();
15+
}
16+
17+
private date: Date;
18+
19+
constructor(ms?: number);
20+
constructor(date: Date | string);
21+
constructor(arg?: number | Date | string) {
22+
this.date = arg !== undefined ? new Date(arg) : new Date();
23+
}
24+
25+
getTime(): number {
26+
return this.date.getTime();
27+
}
28+
29+
getUTCHours(): number {
30+
return this.date.getUTCHours();
31+
}
32+
33+
getUTCDay(): number {
34+
return this.date.getUTCDay();
35+
}
36+
37+
getUTCDate(): number {
38+
return this.date.getUTCDate();
39+
}
40+
41+
getUTCMonth(): number {
42+
return this.date.getUTCMonth();
43+
}
44+
45+
moveMs = (ms: number): DateWrapper => {
46+
return new DateWrapper(this.date.getTime() + ms);
47+
};
48+
49+
moveHour = (count: number): DateWrapper => {
50+
return new DateWrapper(this.date.getTime() + count * HOUR);
51+
};
52+
53+
moveDate = (count: number): DateWrapper => {
54+
return new DateWrapper(this.date.getTime() + count * DAY);
55+
};
56+
57+
moveMonth = (
58+
count: number,
59+
timezoneOffset = ASIA_SEOUL_TZ_OFFSET,
60+
): DateWrapper => {
61+
const timezoneFix = (timezoneOffset ?? 0) * MIN;
62+
const utcConvertDate = this.moveMs(-timezoneFix);
63+
64+
return new DateWrapper(
65+
utcConvertDate.date.setUTCMonth(
66+
utcConvertDate.date.getUTCMonth() + count,
67+
),
68+
).moveMs(timezoneFix);
69+
};
70+
71+
setUTCHours(...input: Parameters<Date['setUTCHours']>): number {
72+
return this.date.setUTCHours(...input);
73+
}
74+
75+
startOfDate = (timezoneOffset = ASIA_SEOUL_TZ_OFFSET): DateWrapper => {
76+
const copy = new DateWrapper(this.date);
77+
copy.date.setUTCHours(0, timezoneOffset, 0, 0);
78+
79+
return copy;
80+
};
81+
82+
startOfHour = (timezoneOffset = ASIA_SEOUL_TZ_OFFSET): DateWrapper => {
83+
const copy = new DateWrapper(this.date);
84+
copy.date.setUTCMinutes(timezoneOffset % 60, 0, 0);
85+
86+
return copy;
87+
};
88+
89+
startOfMonth = (timezoneOffset = ASIA_SEOUL_TZ_OFFSET): DateWrapper => {
90+
const copy = new Date(this.date.getTime() - timezoneOffset * MIN);
91+
copy.setUTCDate(1);
92+
93+
return new DateWrapper(copy).startOfDate(timezoneOffset);
94+
};
95+
96+
toDate = (): Date => {
97+
return new Date(this.date);
98+
};
99+
}

0 commit comments

Comments
 (0)