Skip to content

Commit

Permalink
Feat: add draft for data module
Browse files Browse the repository at this point in the history
  • Loading branch information
HC-kang committed Oct 22, 2024
1 parent d150036 commit c5210c0
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/api/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const GITHUB_API_BASE_URL = 'https://api.github.com';
export const TOTAL_PROBLEMS = 75;

export const ALTERNATIVE_IDS: Record<string, string> = {
// 1기
meoooh: 'han',
koreas9408: 'seunghyun-lim',
leokim0922: 'leo',

// 2기
obzva: 'flynn',
'kim-young': 'kimyoung',
kjb512: 'kayden',
lymchgmk: 'egon',
jeonghwanmin: 'hwanmini',
};
42 changes: 42 additions & 0 deletions src/api/getMembersByCohort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ALTERNATIVE_IDS, GITHUB_API_BASE_URL } from './const';
import { Cohort, CohortInfo } from './types';
import { fetchWithCache } from './utils';

const GITHUB_TOKEN = 'process.env.GITHUB_TOKEN'; // TODO: Github Token을 환경변수로 설정

export async function getMembersByCohort(cohort: number): Promise<CohortInfo> {
return fetchWithCache(`members_cohort_${cohort}`, async () => {
const teamName = ['', 'leetcode01', 'leetcode']; // TODO: getTeams 에서 가져와 주입 받도록 수정
const url = `/orgs/DaleStudy/teams/${teamName[cohort]}/members`;
const headers = {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${GITHUB_TOKEN}`,
};

try {
const res = await fetch(GITHUB_API_BASE_URL + url, { headers });

if (!res.ok)
throw new Error(`Failed to fetch members: ${res.statusText}`);

const data = await res.json();
return {
cohort: cohort as Cohort,
totalMembers: data.length,
members: data.map((member: { login: string }) => ({
id:
ALTERNATIVE_IDS[member.login.toLowerCase()] || // 파일명이 Github 계정과 다른 경우
member.login.toLowerCase(),
name: member.login,
})),
};
} catch (err) {
if (err instanceof Error) {
console.error(`Error fetching cohort members: ${err.message}`);
} else {
console.error('Error fetching cohort members:', err);
}
return { cohort: cohort as Cohort, totalMembers: 0, members: [] };
}
});
}
31 changes: 31 additions & 0 deletions src/api/getRepoDirectoryData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { GITHUB_API_BASE_URL } from './const';
import { RepositoryTree } from './types';
import { fetchWithCache } from './utils';

export async function getRepositoryDirectoryData(): Promise<RepositoryTree[]> {
return fetchWithCache('repository_directory', async () => {
const url = `/repos/DaleStudy/leetcode-study/git/trees/main?recursive=1`; // recursive=1로 충분하다.
const headers = {
Accept: 'application/vnd.github+json',
};

try {
const res = await fetch(GITHUB_API_BASE_URL + url, { headers });

if (!res.ok)
throw new Error(
`Failed to fetch repository directory: ${res.statusText}`
);

const data = await res.json();
return data.tree;
} catch (err) {
if (err instanceof Error) {
console.error(`Error fetching repository directory: ${err.message}`);
} else {
console.error('Error fetching repository directory:', err);
}
return [];
}
});
}
117 changes: 117 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { RepositoryTree, SubmissionOfMember, SubmissionPath } from './types';
import { getMembersByCohort } from './getMembersByCohort';
import { getRepositoryDirectoryData } from './getRepoDirectoryData';
import { TOTAL_PROBLEMS } from './const';

/**
* 전체 트리에서 필요한 데이터만 추출
* 블롭이면서 /를 포함하는 데이터
*/
function extractRelevantData(data: RepositoryTree[]): SubmissionPath[] {
return data
.filter((item) => item.type === 'blob')
.filter((item) => item.path.includes('/'))
.map((item) => item.path.toLowerCase() as SubmissionPath);
}

/**
* 제출 경로를 분석하여 필요한 정보를 추출
*/
function parseSubmissionPath(path: SubmissionPath): {
memberId: string;
problemTitle: string;
language: string;
} {
const regex = /^([^/]+)\/([^.]+)\.([a-zA-Z0-9]+)$/;
const match = path.match(regex);

if (match) {
const problemTitle = match[1];
const memberId = match[2];
const language = match[3];
return { memberId, problemTitle, language };
}
return { memberId: '', problemTitle: '', language: '' };
}

/**
* 각 멤버별 제출 현황 디스플레이
*/
function displayProgress(
memberMap: Record<string, SubmissionOfMember>
): string[] {
const progressDisplay: string[] = [];
const maxMemberIdLength = Math.max(
...Object.values(memberMap).map((member) => member.memberId.length)
);
Object.values(memberMap).forEach((member) => {
const progressPercentage = (member.totalSubmissions / TOTAL_PROBLEMS) * 100;
const progressBarLength = Math.ceil(progressPercentage / 2);
const progressBar = '█'.repeat(progressBarLength).padEnd(50, ' ');

progressDisplay.push(
`${member.memberId.padEnd(maxMemberIdLength)} | ${progressBar} | ${
member.totalSubmissions
}/${TOTAL_PROBLEMS} (${progressPercentage.toFixed(2)}%)`
);
});
return progressDisplay;
}

/**
* 주어진 기수별 제출 현황을 출력한다.
*/
export async function printProcess(cohort: number) {
try {
// TODO: getTeams 추가
// 필요한 데이터 조회
const [membersOfCohort, repositoryDirectory] = await Promise.all([
getMembersByCohort(cohort),
getRepositoryDirectoryData(),
]);

// 멤버별 제출 정보를 담을 객체 생성
const memberMap: Record<string, SubmissionOfMember> = {};
membersOfCohort.members.forEach((member) => {
memberMap[member.id] = {
memberId: member.id,
totalSubmissions: 0,
submissions: [],
};
});

// 제출 정보를 memberMap에 저장
const relevantData = extractRelevantData(repositoryDirectory);
relevantData.forEach((path) => {
const { memberId, problemTitle, language } = parseSubmissionPath(path);
if (memberMap[memberId]) {
if ( // 다수의 언어로 제출한 경우 중복 제출로 간주하지 않음
!memberMap[memberId].submissions.some(
(s) => s.problemTitle === problemTitle
)
) {
memberMap[memberId].totalSubmissions += 1;
}
memberMap[memberId].submissions.push({
memberId,
problemTitle,
language,
});
}
});

const progressArray = displayProgress(memberMap);
const progressString = progressArray.join('\n'); // 문자열로 변환

console.log(progressString);
} catch (error) {
if (error instanceof Error) {
console.error(`An error occurred: ${error.message}`);
} else {
console.error('An error occurred:', error);
}
}
}

// 전체 함수 호출
await printProcess(2);
35 changes: 35 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type Cohort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

export type CohortInfo = {
cohort: Cohort;
totalMembers: number;
members: Member[];
};

export type Member = {
id: string;
name: string;
};

export type RepositoryTree = {
path: string;
mode: string;
type: string;
sha: string;
url: string;
size: number;
};

export type SubmissionPath = string;

export type Submission = {
memberId: string;
problemTitle: string;
language: string;
};

export type SubmissionOfMember = {
memberId: string;
totalSubmissions: number;
submissions: Submission[];
};
66 changes: 66 additions & 0 deletions src/api/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 캐시를 저장할 객체
const cache: Record<string, { data: unknown; timestamp: number }> = {};
const CACHE_DURATION = 10 * 60 * 1000; // 10분 (밀리초 단위)

// 캐시를 이용한 데이터 요청 함수
export async function fetchWithCache<T>(
key: string,
fetchFunction: () => Promise<T>
): Promise<T> {
const currentTime = Date.now();
const cached = cache[key];

// 캐시가 존재하고 만료되지 않았다면 데이터 반환
if (cached) {
if (currentTime - cached.timestamp < CACHE_DURATION) {
console.log(
`${new Date(
currentTime
).toISOString()} - Returning cached data for key: ${key}`
);
return cached.data as T;
} else {
// 만료된 캐시는 삭제
console.log(
`${new Date(
currentTime
).toISOString()} - Cache expired for key: ${key}, removing from cache`
);
delete cache[key];
}
}

// API 호출 후 새로 캐시에 저장
const data = await fetchFunction();
cache[key] = { data, timestamp: currentTime };
console.log(
`${new Date(currentTime).toISOString()} - Fetched new data for key: ${key}`
);
return data;
}

// 요청을 추적하기 위한 캐시
const rateLimitCache: Record<string, { count: number; timestamp: number }> = {};
const RATE_LIMIT_DURATION = 60 * 1000; // 1분 (밀리초 단위)
const MAX_REQUESTS_PER_MINUTE = 60; // 1분당 최대 60회 요청

// 레이트 리미트 체크 함수
export function isRateLimited(ip: string): boolean {
const currentTime = Date.now();
const cacheEntry = rateLimitCache[ip];

// 캐시에 IP가 없거나, 제한 시간이 지난 경우 초기화
if (!cacheEntry || currentTime - cacheEntry.timestamp > RATE_LIMIT_DURATION) {
rateLimitCache[ip] = { count: 1, timestamp: currentTime };
return false;
}

// 요청 횟수를 초과한 경우
if (cacheEntry.count >= MAX_REQUESTS_PER_MINUTE) {
return true;
}

// 요청 횟수를 증가시킴
cacheEntry.count += 1;
return false;
}

0 comments on commit c5210c0

Please sign in to comment.