Skip to content

Commit

Permalink
Merge pull request #338 from boostcampwm-2022/develop
Browse files Browse the repository at this point in the history
v0.2.6 배포
  • Loading branch information
tunggary authored Dec 15, 2022
2 parents fce0ffa + 97bc703 commit 4d4d3a7
Show file tree
Hide file tree
Showing 48 changed files with 559 additions and 427 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# <img src="https://user-images.githubusercontent.com/79798443/206173852-a4baafe3-5de7-4afa-b494-f630d5170d54.svg"/> Devrank <img src="https://user-images.githubusercontent.com/79798443/206173852-a4baafe3-5de7-4afa-b494-f630d5170d54.svg"/>
<div align="center">
<h1> <img src="https://user-images.githubusercontent.com/79798443/206173852-a4baafe3-5de7-4afa-b494-f630d5170d54.svg"/> Devrank <img src="https://user-images.githubusercontent.com/79798443/206173852-a4baafe3-5de7-4afa-b494-f630d5170d54.svg"/></h1>

[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fboostcampwm-2022%2Fweb21-devrank&count_bg=%23C455F9&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)

Expand All @@ -7,6 +8,11 @@
![redis](https://img.shields.io/badge/redis-6.2.5-%23DC382D?style=flat&logo=Redis)
![mongoDB](https://img.shields.io/badge/mongoDB-5.0.14-%2347A248?style=flat&logo=MongoDB)
![docker](https://img.shields.io/badge/docker-20.10.21-%232496ED?style=flat&logo=Docker)
![React](https://img.shields.io/badge/react-18.2.0-%2361DAFB?style=flat&logo=React)
![Next.js](https://img.shields.io/badge/next-13.0.3-%23000000?style=flat&logo=Next.js)

</div>


![Frame 12023](https://user-images.githubusercontent.com/79798443/206137429-5cb1d269-4bec-4aaa-85ad-053ae564c625.png)

Expand Down
2 changes: 2 additions & 0 deletions backend/libs/consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export const PAGE_UNIT_COUNT = 10;
export const RANK_CACHE_DELAY = 12 * HOUR;

export const GITHUB_API_DELAY = 1500; // ms

export const KR_TIME_DIFF = 9 * HOUR * 1000; // ms
2 changes: 1 addition & 1 deletion backend/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export class AppController {

@Get()
getHello(): string {
return this.appService.getHello();
return this.appService.healthCheck();
}
}
22 changes: 18 additions & 4 deletions backend/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { Injectable } from '@nestjs/common';
import { logger } from '@libs/utils';
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { UserRepository } from './user/user.repository';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
export class AppService implements OnApplicationBootstrap {
constructor(private readonly userRepository: UserRepository) {}

async onApplicationBootstrap() {
const allUsers = await this.userRepository.findAll({}, false, ['lowerUsername', 'score', 'tier', 'history']);
await Promise.all(
allUsers
.filter((user) => user.history)
.map(({ lowerUsername, score, tier }) => this.userRepository.updateCachedUserRank(tier, score, lowerUsername)),
);
logger.log('all user score loaded in Redis.');
}

healthCheck(): string {
return 'devrank server is listening!';
}
}
10 changes: 0 additions & 10 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,6 @@ export class AuthService {
await this.authRepository.create(id, refreshToken);
}

// async replaceRefreshToken(id: string, refreshToken: string, githubToken: string): Promise<string> {
// const storedRefreshToken = await this.authRepository.findRefreshTokenById(id);
// if (refreshToken !== storedRefreshToken) {
// throw new UnauthorizedException('invalid token.');
// }
// const newRefreshToken = this.issueRefreshToken(id, githubToken);
// await this.authRepository.create(id, newRefreshToken);
// return newRefreshToken;
// }

async deleteRefreshToken(id: string): Promise<void> {
await this.authRepository.delete(id);
}
Expand Down
10 changes: 9 additions & 1 deletion backend/src/ranking/dto/ranking-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,26 @@ export class RankingUserDto {
@ApiProperty()
tier: string;

@ApiProperty()
totalRank?: number;

@ApiProperty()
tierRank?: number;

@ApiProperty()
primaryLanguages: string[];

of(user: UserDto): RankingUserDto {
this.id = user.id;
this.username = user.username;
this.avatarUrl = user.avatarUrl;
this.score = user.commitsScore + user.followersScore + user.issuesScore;
this.score = user.score;
this.tier = user.tier;
this.dailyViews = user.dailyViews;
this.scoreDifference = user.scoreDifference;
this.primaryLanguages = user.primaryLanguages;
this.totalRank = user.totalRank;
this.tierRank = user.tierRank;
return this;
}
}
6 changes: 6 additions & 0 deletions backend/src/ranking/ranking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export class RankingService {
if (!paginationResult?.metadata || paginationResult?.users.length === 0) {
throw new NotFoundException('user not found.');
}
for (const user of paginationResult.users) {
const [totalRank, tierRank] = await this.userRepository.findCachedUserRank(user.tier, user.lowerUsername);
user.totalRank = totalRank;
user.tierRank = tierRank;
}

const lastPage = Math.ceil(paginationResult.metadata.total / limit);
const lastPageGroupNumber = Math.ceil(lastPage / PAGE_UNIT_COUNT);
const pageGroupNumber = Math.ceil(page / PAGE_UNIT_COUNT);
Expand Down
4 changes: 0 additions & 4 deletions backend/src/user/dto/rank.dto.ts

This file was deleted.

6 changes: 6 additions & 0 deletions backend/src/user/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export class UserDto {
@ApiProperty({ type: History })
history?: History;

@ApiProperty()
totalRank?: number;

@ApiProperty()
tierRank?: number;

@ApiProperty({ isArray: true, type: PinnedRepositoryDto })
pinnedRepositories?: PinnedRepositoryDto[];

Expand Down
1 change: 0 additions & 1 deletion backend/src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Controller,
DefaultValuePipe,
Get,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import { UserService } from './user.service';
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserScheme }])],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
exports: [UserService, UserRepository],
})
export class UserModule {}
30 changes: 18 additions & 12 deletions backend/src/user/user.repository.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { RankingPaginationDto } from '@apps/ranking/dto/ranking-pagination.dto';
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import { RANK_CACHE_DELAY } from '@libs/consts';
import { KR_TIME_DIFF, RANK_CACHE_DELAY } from '@libs/consts';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import Redis from 'ioredis';
import { Model } from 'mongoose';
import { Rank } from './dto/rank.dto';
import { UserDto } from './dto/user.dto';
import { User } from './user.schema';

Expand Down Expand Up @@ -74,7 +73,12 @@ export class UserRepository {

async setDuplicatedRequestIp(ip: string, lowerUsername: string): Promise<void> {
this.redis.sadd(ip, lowerUsername);
const timeToMidnight = Math.floor((new Date().setHours(23, 59, 59) - Date.now()) / 1000);
const now = +new Date() + KR_TIME_DIFF;

const midNight = new Date(new Date().setHours(23, 59, 59));
midNight.setHours(midNight.getHours() + 9);

const timeToMidnight = Math.floor((+midNight - +now) / 1000);
this.redis.expire(ip, timeToMidnight);
}

Expand All @@ -95,11 +99,11 @@ export class UserRepository {
): Promise<Pick<RankingPaginationDto, 'metadata'> & { users: UserDto[] }> {
const tierOption = tier === 'all' ? {} : { tier: tier };
const usernameOption = lowerUsername ? { lowerUsername: { $regex: `^${lowerUsername}` } } : {};

const result = (
await this.userModel.aggregate([
{ $match: { ...tierOption, ...usernameOption } },
{ $sort: { score: -1 } },
{ $project: { id: 1, username: 1, lowerUsername: 1, avatarUrl: 1, tier: 1, score: 1, primaryLanguages: 1 } },
{ $sort: { score: -1, lowerUsername: -1 } },
{
$facet: {
metadata: [
Expand All @@ -115,16 +119,18 @@ export class UserRepository {
return { metadata: result.metadata[0], users: result.users };
}

async findCachedUserRank(scoreKey: string): Promise<Rank> {
return this.redis.hgetall(scoreKey) as unknown as Rank;
async findCachedUserRank(tier: string, lowerUsername: string): Promise<[number, number]> {
return Promise.all([
this.redis.zrevrank('all&', lowerUsername).then((num) => (Number.isInteger(num) ? num + 1 : null)),
this.redis.zrevrank(`${tier}&`, lowerUsername).then((num) => (Number.isInteger(num) ? num + 1 : null)),
]);
}

async setCachedUserRank(scoreKey: string, scores: Rank): Promise<void> {
this.redis.hset(scoreKey, scores);
this.redis.expire(scoreKey, RANK_CACHE_DELAY);
async updateCachedUserRank(tier: string, score: number, lowerUsername: string): Promise<void> {
Promise.all([this.redis.zadd('all&', score, lowerUsername), this.redis.zadd(`${tier}&`, score, lowerUsername)]);
}

async deleteCachedUserRank(scoreKey: string): Promise<void> {
this.redis.del(scoreKey);
async deleteCachedUserRank(tier: string, lowerUsername: string): Promise<void> {
this.redis.zrem(`${tier}&`, lowerUsername);
}
}
5 changes: 3 additions & 2 deletions backend/src/user/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ export const UserScheme = SchemaFactory.createForClass(User);
UserScheme.index({ score: -1 });
UserScheme.index({ scoreDifference: -1 });
UserScheme.index({ dailyViews: -1 });
UserScheme.index({ lowerUsername: 1, score: 1 });
UserScheme.index({ tier: 1, lowerUsername: 1, score: 1 });
UserScheme.index({ lowerUsername: 1, score: -1 });
UserScheme.index({ score: -1, lowerUsername: 1 });
UserScheme.index({ tier: 1, lowerUsername: 1, score: -1 });
49 changes: 11 additions & 38 deletions backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { GITHUB_API_DELAY } from '@libs/consts';
import { GITHUB_API_DELAY, KR_TIME_DIFF } from '@libs/consts';
import { getNeedExp, getStartExp, getTier, logger } from '@libs/utils';
import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
import { Octokit } from '@octokit/core';
import { AutoCompleteDto } from './dto/auto-complete.dto';
import { History } from './dto/history.dto';
import { OrganizationDto } from './dto/organization.dto';
import { PinnedRepositoryDto } from './dto/pinned-repository.dto';
import { Rank } from './dto/rank.dto';
import { UserDto } from './dto/user.dto';
import { UserProfileDto } from './dto/user.profile.dto';
import {
Expand Down Expand Up @@ -34,14 +33,10 @@ export class UserService {
async findOneByUsername(githubToken: string, ip: string, lowerUsername: string): Promise<UserProfileDto> {
let user = await this.userRepository.findOneByLowerUsername(lowerUsername);
if (!user) {
try {
user = await this.updateUser(lowerUsername, githubToken);
} catch {
throw new HttpException(`can't update user ${lowerUsername}.`, HttpStatus.SERVICE_UNAVAILABLE);
}
user = await this.updateUser(lowerUsername, githubToken);
}
const { totalRank, tierRank } =
(await this.getUserRelativeRanking(user)) || (await this.setUserRelativeRanking(user));

const [totalRank, tierRank] = await this.userRepository.findCachedUserRank(user.tier, lowerUsername);
if (!(await this.userRepository.isDuplicatedRequestIp(ip, lowerUsername)) && user.history) {
user.dailyViews += 1;
await this.userRepository.createOrUpdate(user);
Expand All @@ -59,6 +54,7 @@ export class UserService {
async updateUser(lowerUsername: string, githubToken: string): Promise<UserProfileDto> {
let user = await this.getUserInfo(githubToken, lowerUsername);
user = await this.userRepository.createOrUpdate(user);
const { tier: prevTier } = user;
const octokit = new Octokit({
auth: githubToken,
});
Expand All @@ -78,7 +74,7 @@ export class UserService {
if (!updatedUser?.scoreHistory?.length) {
updatedUser.scoreHistory = [{ date: new Date(), score: updatedUser.score }];
}
const KR_TIME_DIFF = 9 * 60 * 60 * 1000;

const utc = updatedUser.scoreHistory[updatedUser.scoreHistory.length - 1].date.getTime();
if (new Date(utc + KR_TIME_DIFF).getDate() === new Date(new Date().getTime() + KR_TIME_DIFF).getDate()) {
updatedUser.scoreHistory.pop();
Expand All @@ -94,7 +90,11 @@ export class UserService {
updatedUser.scoreDifference = 0;
}
user = await this.userRepository.createOrUpdate(updatedUser);
const { totalRank, tierRank } = await this.setUserRelativeRanking(user);
if (prevTier !== user.tier) {
await this.userRepository.deleteCachedUserRank(prevTier, lowerUsername);
}
await this.userRepository.updateCachedUserRank(user.tier, user.score, lowerUsername);
const [totalRank, tierRank] = await this.userRepository.findCachedUserRank(user.tier, lowerUsername);
const userWithRank: UserProfileDto = {
...user,
totalRank,
Expand All @@ -113,7 +113,6 @@ export class UserService {
try {
const updateUser = await this.updateUser(user.lowerUsername, githubToken);
this.userRepository.createOrUpdate(updateUser);
this.userRepository.deleteCachedUserRank(updateUser.lowerUsername + '&');
} catch {
logger.error(`can't update user ${user.lowerUsername}`);
}
Expand Down Expand Up @@ -336,30 +335,4 @@ export class UserService {
}),
};
}

async getUserRelativeRanking(user: UserDto): Promise<Rank | false> {
const cachedRanks = await this.userRepository.findCachedUserRank(user.id + '&');
if (Object.keys(cachedRanks).length) {
return cachedRanks;
}
return false;
}

async setUserRelativeRanking(user: UserDto): Promise<Rank> {
const users = await this.userRepository.findAll({}, true, ['lowerUsername', 'tier', 'score']);
let tierRank = 0;
for (let rank = 0; rank < users.length; rank++) {
if (users[rank].lowerUsername === user.lowerUsername) {
const rankInfo = {
totalRank: rank + 1,
tierRank: tierRank + 1,
};
await this.userRepository.setCachedUserRank(user.id + '&', rankInfo);
return rankInfo;
}
if (users[rank].tier === user.tier) {
tierRank += 1;
}
}
}
}
2 changes: 1 addition & 1 deletion frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const nextConfig = {
// 외부 이미지 허용
{
protocol: 'https',
hostname: 'cdn.jsdelivr.net',
hostname: 'raw.githubusercontent.com',
},
{
protocol: 'https',
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.6.7",
"next-sitemap": "^3.1.32",
"prettier": "^2.7.1",
"storybook-addon-next": "^1.6.10",
"storybook-addon-styled-component-theme": "^2.0.0"
Expand Down
3 changes: 3 additions & 0 deletions frontend/public/locales/en/500.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"404-information": "An unexpected error has occurred. Please try again later."
}
4 changes: 3 additions & 1 deletion frontend/public/locales/en/about.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@
"score-calculate-method-11": "· y = (1 / 1.0019) ^ x is an exponential function whose time weight becomes 0.5 after one year",
"score-calculate-method-12": "· To exclude unusual weights of contributions to non-code document reports document oriented repo has 0 repo score",
"score-calculate-method-13": "· Organizational repos not registered as collaborators are not aggregated",
"score-calculate-method-14": "· External repos are not sorted in order of stars"
"score-calculate-method-14": "· External repos are not sorted in order of stars",
"role": "Role: {{role}}",
"email": "Email: {{email}}"
}
6 changes: 4 additions & 2 deletions frontend/public/locales/en/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"about-title": "Introduction | Devrank",
"ranking-title": "Ranking | Devrank",
"ranking-description": "Check the ranking of users using Devrank service!",
"profile-title": " | Devrank",
"not-found-title": "Page Not Found | Devrank"
"profile-title": "{{username}} | Devrank",
"profile-description": "Rank: {{tier}} / Current score: {{score}} / Total: {{totalRank}} / $t(tier:{{tier}}): {{tierRank}} / {{languages}}",
"not-found-title": "Page Not Found | Devrank",
"server-error-title": "Internal Server Error | Devrank"
}
14 changes: 9 additions & 5 deletions frontend/public/locales/en/profile.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
{
"current-score": "Current score",
"register-wakatime-api-key": "Register Wakatime API Key",
"register-wakatime-button-label": "Register",
"maximum-continuous-commit-history": "Maximum continuous commit history",
"user-update-delay": "You can update it in",
"user-update-delay": "You can update it in <count>{{count}}</count> seconds",
"day": "day",
"total": "Total",
"rank": "Rank",
"total-rank_one": "Total: {{count}}st",
"total-rank_two": "Total: {{count}}nd",
"total-rank_few": "Total: {{count}}rd",
"total-rank_other": "Total: {{count}}th",
"tier-rank_one": "<tier>{{tier}}</tier>: {{count}}st",
"tier-rank_two": "<tier>$t(tier:{{tier}})</tier>: {{count}}nd",
"tier-rank_few": "<tier>$t(tier:{{tier}})</tier>: {{count}}rd",
"tier-rank_other": "<tier>$t(tier:{{tier}})</tier>: {{count}}th",
"invalid-user": "This user cannot get information",
"invalid-rank": "Unable to get the rank"
}
Loading

0 comments on commit 4d4d3a7

Please sign in to comment.