-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
307 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: [] }; | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 []; | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |