Skip to content

Commit 45f841a

Browse files
committed
Merge branch 'sujin' of https://github.com/planet-devo-k/solveit into sujin
2 parents e8d3c4a + c47f9da commit 45f841a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1701
-1147
lines changed

.github/INFRASTRUCTURE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Infrastructure
2+
3+
## Commit Convention
4+
5+
이 레포의 인프라/자동화 작업에 사용하는 커밋 타입입니다.
6+
7+
| Type | Description | Example |
8+
| ---------- | ---------------------------------------------- | --------------------------------------------- |
9+
| `feat` | 새로운 스크립트, 워크플로우, 자동화 기능 추가 | `feat: add goal track automation` |
10+
| `refactor` | 기존 스크립트/워크플로우 구조 변경 (동작 동일) | `refactor: move milestone_id to session data` |
11+
| `fix` | 스크립트/워크플로우 버그 수정 | `fix: correct template placeholder mismatch` |
12+
| `chore` | 설정, 의존성, 환경 변수 등 단순 변경 | `chore: update secrets config` |
13+
| `docs` | 문서 추가/수정 (README, 템플릿 등) | `docs: add infrastructure guide` |

.github/ISSUE_TEMPLATE/goal_track_session.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: "goal track"
2+
name: "Goal Track"
33
about: "Use this template for using issues as goal tracking items."
44
duration: "10 weeks"
55
title: "`Session: Week ~ Week` Programmers level0 / level1"
@@ -15,9 +15,7 @@ labels:
1515
# Goal
1616

1717
**Duration**: {{duration}} weeks {{start_date}} ~ {{end_date}}
18-
1918
**Description**: Solve 3 coding challenges from Programmers Level {{levels}} every week.
20-
2119
**Deadline**: {{end_date}} SUN
2220

2321
## This Session Challenges

.github/ISSUE_TEMPLATE/goal_track_week.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: "goal track week"
2+
name: "Goal Track Week"
33
about: "Use this template for using issues as weekly goal tracking items."
44
title: "`Week`"
55
assignees: "sgoldenbird"
@@ -12,10 +12,8 @@ labels:
1212

1313
# Goal
1414

15-
**Week**: {{start_date}} ~ {{end_date}}
16-
15+
**Period**: {{start_date}} ~ {{end_date}}
1716
**Description**: Solve 3 coding challenges from Programmers Level {{levels}}.
18-
1917
**Deadline**: {{end_date}}
2018

2119
## This Week Challenges

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,41 @@
1-
### Source Link
1+
## Source
22

33
[Programmers](https://school.programmers.co.kr/learn/challenges?order=recent&levels=0&languages=javascript&page=1)
44

5-
### Progress
6-
7-
Check off the problems you solved this week.
5+
## Progress
86

97
- [x] 문자열 출력하기
108
- [x]
119
- [x]
1210

13-
### Related Issues
11+
## Related Issues & Questions
1412

1513
<!--
16-
- [관련 이슈 번호, e.g., question #123]
14+
- [관련 이슈 번호, e.g. #123]
1715
- 없으면 none
1816
-->
1917

2018
- none
2119

22-
### Checklist before creating a PR
20+
## Checklist
2321

24-
- [ ] 제목이 `week1` 형식을 따르고 있나요?
25-
- [ ] 본인을 Assignee로 지정했나요?
26-
- [ ] Reviewers를 직접 지정하지 **마세요**. 커스텀 로직으로 자동 배정됩니다.
27-
- [ ] 관련 라벨(source, difficulty, category 등)을 모두 추가했나요?
22+
- [ ] PR 제목을 `week1` 형식에 맞춰 작성했습니다.
23+
- [ ] 본인을 Assignee로 지정했습니다.
24+
- [ ] Reviewers를 직접 지정하지 **마세요.** 커스텀 로직으로 자동 배정됩니다.
25+
- [ ] 관련 라벨(source, difficulty, category 등)을 모두 추가했습니다.
2826
- [ ] Projects에 연결하지 **마세요**.
2927
- [ ] Milestone에 연결하지 **마세요**.
30-
- [ ] Development의 해당 week(issue)에 연결했나요?
28+
- [ ] Development 항목에 해당 주차(week) 이슈를 연결했습니다.
29+
3130

3231
<!-- 영어
33-
- [ ] Is the PR title following the `week1` format?
34-
- [ ] Have you assigned yourself as the Assignee?
35-
- [ ] Have all relevant labels (source, difficulty, category, etc.) been added?
36-
- [ ] Do **NOT** link the PR to Projects.
37-
- [ ] Do **NOT** link the PR to a Milestone.
38-
- [ ] Is the appropriate week linked as a Milestone?
39-
- [ ] Is the appropriate week linked under Development?
32+
- [ ] The PR title follows the `week1` format.
33+
- [ ] I have assigned myself as the Assignee.
34+
- [ ] I did not manually assign Reviewers. (They will be auto-assigned with custom logic)
35+
- [ ] All relevant labels (source, difficulty, category, etc.) have been added.
36+
- [ ] I did **NOT** link the PR to Projects.
37+
- [ ] I did **NOT** link the PR to a Milestone.
38+
- [ ] I did not link the PR to Projects.
39+
- [ ] I did not link the PR to a Milestone.
40+
- [ ] The appropriate week is linked under the Development section.
4041
-->

.github/data/session/session_6.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
{
22
"id": 6,
33
"levels": [0, 1],
4+
"milestone_id": 1,
5+
"parent_issue_number": 143,
6+
"duration": 10,
47
"weeks": [50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
58
"date": {
69
"start": "2026-03-15",
710
"end": "2026-05-29"
811
},
12+
"absentees": [
13+
{ "name": "홍길동", "status": "absent", "type": "medical" },
14+
{ "name": "김철수", "status": "late", "type": "authorized" }
15+
],
916
"challenges": [
1017
{
1118
"week": 50,
1219
"date": {
13-
"start": "2026-03-15",
20+
"start": "2026-04-02",
1421
"end": "2026-04-19"
1522
},
16-
"absentees": [],
1723
"list": [
1824
{
1925
"name": "두 수의 합",
@@ -35,7 +41,6 @@
3541
"start": "2026-04-20",
3642
"end": "2026-04-26"
3743
},
38-
"absentees": [],
3944
"list": [
4045
{
4146
"name": "문제A",
@@ -57,7 +62,6 @@
5762
"start": "2026-04-20",
5863
"end": "2026-04-26"
5964
},
60-
"absentees": [],
6165
"list": [
6266
{
6367
"name": "문제A",

.github/data/warnings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"1483775665307389983": {
3+
"count": 0,
4+
"types": [],
5+
"messages": [],
6+
"isConfirmed": []
7+
}
8+
}

.github/scripts/alert-deadline.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { MEMBERS, STUDY_CONFIG } from "./utils/constants.js";
2+
import { getKSTDateString } from "./utils/date.js";
3+
import { getLatestSessionData } from "./utils/session.js";
4+
import { createDiscordTable } from "./utils/formatter.js";
5+
import { getThisWeekPRs } from "./utils/github.js";
6+
7+
export default async ({ github, context, core, test }) => {
8+
const { RULES } = STUDY_CONFIG;
9+
const { MIN_REVIEWS_REQUIRED } = RULES;
10+
11+
try {
12+
const sessionData = getLatestSessionData();
13+
14+
let currentWeekInfo;
15+
16+
if (test !== null) {
17+
currentWeekInfo = sessionData.challenges.find((c) => c.week === test);
18+
if (!currentWeekInfo) {
19+
console.warn(`테스트 주차(${test})를 찾을 수 없습니다.`);
20+
return;
21+
}
22+
console.log(`[테스트 모드] ${test}주차 강제 지정`);
23+
} else {
24+
const nowStr = getKSTDateString(new Date());
25+
currentWeekInfo = sessionData.challenges.find(
26+
(c) => nowStr >= c.date.start && nowStr <= c.date.end,
27+
);
28+
if (!currentWeekInfo) {
29+
console.log(`(${nowStr})는 현재 스터디 진행 기간이 아닙니다.`);
30+
return;
31+
}
32+
}
33+
34+
console.log(`${currentWeekInfo.week}주차 PR 마감 사전 경고 시작`);
35+
36+
const thisMonday = new Date(currentWeekInfo.date.start);
37+
const thisSunday = new Date(currentWeekInfo.date.end);
38+
const reviewDeadline = new Date(thisSunday.getTime() + 20 * 60 * 60 * 1000);
39+
40+
const thisWeekPRs = await getThisWeekPRs({
41+
github,
42+
context,
43+
startDate: thisMonday,
44+
endDate: thisSunday,
45+
});
46+
47+
const memberStatus = {};
48+
MEMBERS.forEach((member) => {
49+
memberStatus[member.githubId] = {
50+
name: member.name,
51+
githubId: member.githubId,
52+
discordId: member.discordId,
53+
submitted: false,
54+
reviewPrCount: 0,
55+
hasMetReviewQuota: false,
56+
};
57+
});
58+
59+
// ─── PR 제출 + 리뷰 집계 ───
60+
await Promise.all(
61+
thisWeekPRs.map(async (pr) => {
62+
const author = pr.user.login;
63+
64+
if (memberStatus[author]) {
65+
memberStatus[author].submitted = true;
66+
}
67+
68+
const { data: reviews } = await github.rest.pulls.listReviews({
69+
owner: context.repo.owner,
70+
repo: context.repo.repo,
71+
pull_number: pr.number,
72+
});
73+
74+
const validReviews = reviews.filter((r) => {
75+
const submittedAt = new Date(r.submitted_at);
76+
return submittedAt >= thisMonday && submittedAt <= reviewDeadline;
77+
});
78+
79+
const uniqueReviewersOnPr = new Set(
80+
validReviews.map((r) => r.user.login),
81+
);
82+
83+
uniqueReviewersOnPr.forEach((reviewerId) => {
84+
const isStudyMember = memberStatus[reviewerId];
85+
const isNotOwnPr = reviewerId !== author;
86+
if (isStudyMember && isNotOwnPr) {
87+
memberStatus[reviewerId].reviewPrCount++;
88+
}
89+
});
90+
}),
91+
);
92+
93+
const memberIds = MEMBERS.map((m) => m.githubId);
94+
memberIds.forEach((id) => {
95+
const status = memberStatus[id];
96+
if (status.reviewPrCount >= MIN_REVIEWS_REQUIRED) {
97+
status.hasMetReviewQuota = true;
98+
}
99+
});
100+
101+
// ─── 미수행자 필터링 (PR 미제출 또는 리뷰 미달) ───
102+
const incompleteMembers = Object.values(memberStatus).filter(
103+
(m) => !(m.submitted && m.hasMetReviewQuota),
104+
);
105+
106+
const tableConfig = {
107+
headers: ["이름", "PR 제출", "리뷰(PR 기준)"],
108+
paddings: [6, 9, 6],
109+
renderRow: (id) => {
110+
const s = memberStatus[id];
111+
return {
112+
name: s.name || id,
113+
prStatus: s.submitted ? `✅ [PR](${s.prUrl})` : "❌",
114+
reviewStatus: `${s.reviewPrCount}/${MIN_REVIEWS_REQUIRED}`,
115+
};
116+
},
117+
};
118+
119+
const incompleteTable =
120+
incompleteMembers.length > 0
121+
? createDiscordTable(
122+
incompleteMembers.map((m) => m.githubId),
123+
tableConfig,
124+
)
125+
: null;
126+
127+
if (incompleteMembers.length === 0) {
128+
console.log("모든 멤버가 과제를 완료했습니다.");
129+
} else {
130+
console.log(
131+
`미수행자: ${incompleteMembers.map((m) => m.name).join(", ")}`,
132+
);
133+
}
134+
135+
return {
136+
incompleteTable,
137+
incompleteMembers,
138+
};
139+
} catch (error) {
140+
console.error("PR 마감 경고 집계 실패:", error.message);
141+
core.setFailed(error.message);
142+
throw error;
143+
}
144+
};
Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import sessionData from "../data/session/session_6.json" with { type: "json" };
21
import { MEMBERS, STUDY_CONFIG } from "./utils/constants.js";
32
import { getThisWeekPRs, requestReviewers } from "./utils/github.js";
43
import { shuffleArray } from "./utils/math.js";
54
import { getKSTDateString } from "./utils/date.js";
5+
import { getLatestSessionData } from "./utils/session.js";
66

77
export default async ({ github, context, core }) => {
8+
const sessionData = getLatestSessionData();
89
const { RULES } = STUDY_CONFIG;
910
const { MIN_REVIEWS_REQUIRED } = RULES;
1011
const repo = context.repo.repo;
@@ -14,14 +15,17 @@ export default async ({ github, context, core }) => {
1415
const prNumber = currentPR.number;
1516

1617
try {
17-
if (currentPR.requested_reviewers?.length > 0) {
18-
const existingReviewers = currentPR.requested_reviewers
19-
.map((r) => {
20-
const member = MEMBERS.find((m) => m.githubId === r.login);
21-
return member ? member.name : r.login;
22-
})
23-
.join(", ");
18+
const { data: existingReviewers } = await github.rest.pulls.listReviews({
19+
owner: repoOwner,
20+
repo,
21+
pull_number: prNumber,
22+
});
23+
24+
const hasExternalReviewers = existingReviewers.some(
25+
(r) => r.user.login !== prOwner,
26+
);
2427

28+
if (currentPR.requested_reviewers?.length > 0 || hasExternalReviewers) {
2529
console.log("이미 리뷰어가 배정되어 있어 기존 목록을 유지합니다.");
2630
return null;
2731
}
@@ -39,8 +43,7 @@ export default async ({ github, context, core }) => {
3943
const thisWeekPRs = await getThisWeekPRs({
4044
github,
4145
context,
42-
startDate: new Date(currentWeekInfo.date.start),
43-
endDate: new Date(currentWeekInfo.date.end),
46+
currentWeek: currentWeekInfo.week,
4447
});
4548

4649
const reviewCounts = {};
@@ -81,12 +84,25 @@ export default async ({ github, context, core }) => {
8184

8285
let candidates = MEMBERS.filter((m) => m.githubId !== prOwner);
8386

84-
candidates = shuffleArray(candidates);
85-
candidates.sort(
86-
(a, b) => reviewCounts[a.githubId] - reviewCounts[b.githubId],
87-
);
87+
const grouped = {};
88+
candidates.forEach((c) => {
89+
const count = reviewCounts[c.githubId];
90+
if (!grouped[count]) grouped[count] = [];
91+
grouped[count].push(c);
92+
});
93+
94+
const sorted = Object.keys(grouped)
95+
.sort((a, b) => a - b)
96+
.flatMap((count) => {
97+
const s = shuffleArray(grouped[count]);
98+
console.log(
99+
"shuffled:",
100+
s.map((m) => m.name),
101+
);
102+
return s;
103+
});
88104

89-
const selectedReviewers = candidates.slice(0, 2);
105+
const selectedReviewers = sorted.slice(0, MIN_REVIEWS_REQUIRED);
90106

91107
const selectedReviewersGithubId = selectedReviewers.map((m) => m.githubId);
92108

0 commit comments

Comments
 (0)