Skip to content

Commit 8d5a93a

Browse files
authored
Create update_readme_calendar.py
1 parent d368b0f commit 8d5a93a

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

scripts/update_readme_calendar.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import subprocess
5+
import datetime
6+
import calendar
7+
import os
8+
import re
9+
from collections import defaultdict
10+
11+
# ===== 설정 =====
12+
NAMES = ["곽태근", "김호집", "오창은", "김태민", "추창우"]
13+
READ_ME = "README.md"
14+
START_MARK = "<!-- PROGRESS_START -->"
15+
END_MARK = "<!-- PROGRESS_END -->"
16+
TZ_OFFSET = "+0900" # Asia/Seoul (KST)
17+
WEEK_START = calendar.SUNDAY # 달력 머리: 일 ~ 토
18+
DOT_O = "🟢" # O
19+
DOT_X = "🔴" # X
20+
# ==============
21+
22+
calendar.setfirstweekday(WEEK_START)
23+
24+
def run(cmd):
25+
return subprocess.check_output(cmd, shell=True, text=True, encoding="utf-8").strip()
26+
27+
def git_subjects_for_date_and_path(date_str, path):
28+
since = f"{date_str} 00:00:00 {TZ_OFFSET}"
29+
until = f"{date_str} 23:59:59 {TZ_OFFSET}"
30+
cmd = (
31+
f'git log --since="{since}" --until="{until}" '
32+
f'--pretty=%s -- "{path}" || true'
33+
)
34+
out = run(cmd)
35+
if not out:
36+
return []
37+
return [line.strip() for line in out.splitlines() if line.strip()]
38+
39+
def judge_day(date_str, name):
40+
"""
41+
반환: 'O' 또는 'X'
42+
- 해당 날짜/이름 경로에 커밋이 없거나 봇 메시지만 => 'X'
43+
- 그 외 메시지 하나라도 => 'O'
44+
"""
45+
bot_msg = f"{date_str}일자 태스크 배정완료, 화이팅!"
46+
path = f"{date_str}/{name}"
47+
subjects = git_subjects_for_date_and_path(date_str, path)
48+
if not subjects:
49+
return "X"
50+
for s in subjects:
51+
if s != bot_msg:
52+
return "O"
53+
return "X"
54+
55+
def find_all_date_dirs():
56+
"""리포 내 YYYY-MM-DD 디렉터리들을 찾아 실제 날짜 리스트 반환."""
57+
dates = []
58+
for entry in os.listdir("."):
59+
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", entry) and os.path.isdir(entry):
60+
try:
61+
y, m, d = map(int, entry.split("-"))
62+
dates.append(datetime.date(y, m, d))
63+
except ValueError:
64+
pass
65+
return sorted(dates)
66+
67+
def month_iter(start_date, end_date):
68+
"""start_date의 1일 ~ end_date의 1일까지 월 단위로 이터레이션."""
69+
y, m = start_date.year, start_date.month
70+
while (y < end_date.year) or (y == end_date.year and m <= end_date.month):
71+
yield y, m
72+
if m == 12:
73+
y += 1
74+
m = 1
75+
else:
76+
m += 1
77+
78+
def build_month_calendar(year, month, today_kst):
79+
"""
80+
월별 달력(HTML table) 생성.
81+
- 어제까지 O/X 확정
82+
- 오늘 이후 공백
83+
"""
84+
cal = calendar.monthcalendar(year, month) # 주: [월..일]이 아니라 설정된 firstweekday 기준
85+
# GitHub는 기본 마크다운 테이블보다 HTML 테이블이 칸 꾸미기 유리
86+
header_days = ["일", "월", "화", "수", "목", "금", "토"]
87+
# firstweekday를 반영해서 회전
88+
rotate = list(range(7))
89+
header_days = header_days[-calendar.firstweekday():] + header_days[:-calendar.firstweekday()]
90+
91+
rows_html = []
92+
for week in cal:
93+
tds = []
94+
for d in week:
95+
if d == 0:
96+
tds.append("<td></td>")
97+
continue
98+
99+
date_obj = datetime.date(year, month, d)
100+
if date_obj >= today_kst:
101+
# 미래/오늘은 빈 칸
102+
tds.append(f'<td align="center" valign="top"><div align="right"><sub>{d}</sub></div></td>')
103+
continue
104+
105+
date_str = date_obj.isoformat()
106+
dots = []
107+
for name in NAMES:
108+
flag = judge_day(date_str, name) # 'O' or 'X'
109+
dot = DOT_O if flag == "O" else DOT_X
110+
title = f'{name}: {flag}'
111+
dots.append(f'<span title="{title}">{dot}</span>')
112+
tds.append(
113+
'<td align="center" valign="top" style="min-width:96px">'
114+
f'<div align="right"><sub>{d}</sub></div>'
115+
f'<div style="font-size: 18px; line-height:1.2">{ "".join(dots) }</div>'
116+
"</td>"
117+
)
118+
rows_html.append("<tr>" + "".join(tds) + "</tr>")
119+
120+
# 범례(이름 순서 고정)
121+
legend_items = [f'<li><strong>{i+1}</strong>번째 점: {name}</li>' for i, name in enumerate(NAMES)]
122+
legend_html = (
123+
'<details><summary>범례 보기</summary>'
124+
'<ul style="margin-top:6px">'
125+
+ "".join(legend_items) +
126+
"</ul></details>"
127+
)
128+
129+
month_title = f"### {year}-{month:02d} 코딩테스트 달력 (KST)"
130+
table_html = (
131+
f"{month_title}\n\n"
132+
+ legend_html + "\n\n"
133+
+ '<table>'
134+
+ "<thead><tr>" + "".join([f"<th>{d}</th>" for d in header_days]) + "</tr></thead>"
135+
+ "<tbody>" + "".join(rows_html) + "</tbody>"
136+
+ "</table>\n"
137+
)
138+
return table_html
139+
140+
def build_all_months(today_kst):
141+
# 리포의 날짜 디렉터리를 스캔해서, 없으면 현재 달만 생성
142+
date_dirs = find_all_date_dirs()
143+
if date_dirs:
144+
start = datetime.date(date_dirs[0].year, date_dirs[0].month, 1)
145+
else:
146+
start = datetime.date(today_kst.year, today_kst.month, 1)
147+
148+
end = datetime.date(today_kst.year, today_kst.month, 1) # 오늘의 월까지
149+
blocks = []
150+
for y, m in month_iter(start, end):
151+
block = build_month_calendar(y, m, today_kst)
152+
# 과거 달은 접기, 이번 달은 펼침
153+
is_current = (y == today_kst.year and m == today_kst.month)
154+
summary = f"{y}-{m:02d}"
155+
details_open = " open" if is_current else ""
156+
blocks.append(f"<details{details_open}><summary><strong>{summary}</strong></summary>\n\n{block}\n</details>")
157+
return "\n\n".join(blocks)
158+
159+
def replace_block(original, new_block):
160+
if START_MARK in original and END_MARK in original:
161+
pattern = re.compile(
162+
rf"{re.escape(START_MARK)}.*?{re.escape(END_MARK)}",
163+
re.DOTALL
164+
)
165+
return pattern.sub(
166+
f"{START_MARK}\n{new_block}\n{END_MARK}", original
167+
)
168+
else:
169+
return original.rstrip() + "\n\n" + START_MARK + "\n" + new_block + "\n" + END_MARK + "\n"
170+
171+
def main():
172+
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9)))
173+
today_kst = now.date()
174+
175+
new_block = build_all_months(today_kst)
176+
177+
if os.path.exists(READ_ME):
178+
with open(READ_ME, "r", encoding="utf-8") as f:
179+
content = f.read()
180+
else:
181+
content = "# 코딩테스트 연습\n"
182+
183+
updated = replace_block(content, new_block)
184+
185+
if updated != content:
186+
with open(READ_ME, "w", encoding="utf-8") as f:
187+
f.write(updated)
188+
189+
if __name__ == "__main__":
190+
main()

0 commit comments

Comments
 (0)