1414END_MARK = "<!-- PROGRESS_END -->"
1515TZ_OFFSET = "+0900" # Asia/Seoul (KST)
1616WEEK_START = calendar .SUNDAY # 달력 시작: 일요일
17- DOT_GREEN = "🟢" # 그날 커밋 + 목표 달성
18- DOT_ORANGE = "🟠" # 다른날 커밋 + 목표 달성
19- DOT_YELLOW = "🟡" # 커밋 있음 + 목표 미달
20- DOT_RED = "🔴" # 비봇 커밋 없음
17+ DOT_GREEN = "🟢" # 그날 3커밋 이상 + 누적 3개 이상
18+ DOT_ORANGE = "🟠" # 누적 3개 이상(레트로 달성 포함), 당일 3커밋 미만
19+ DOT_YELLOW = "🟡" # 비봇 커밋은 있으나 누적 < 3
20+ DOT_RED = "🔴" # 비봇 커밋 없음 (표시 n=0)
21+ GOAL_M = 3 # 항상 3
2122# ==============
2223
2324calendar .setfirstweekday (WEEK_START )
2627def run (cmd ):
2728 return subprocess .check_output (cmd , shell = True , text = True , encoding = "utf-8" ).strip ()
2829
29- # ---------- 커밋/메시지 판정 ----------
30+ # ---------- 커밋/메시지 조회 ----------
3031
31- def git_subjects_for_date_and_path (date_str , path ):
32- """특정 날짜(KST)의 해당 path 커밋 subject 목록 """
32+ def nonbot_commit_count_on_date (date_str , path ):
33+ """해당 날짜(KST) 동안 path를 건드린 '비봇' 커밋 수 """
3334 since = f"{ date_str } 00:00:00 { TZ_OFFSET } "
3435 until = f"{ date_str } 23:59:59 { TZ_OFFSET } "
3536 cmd = (
@@ -38,62 +39,38 @@ def git_subjects_for_date_and_path(date_str, path):
3839 )
3940 out = run (cmd )
4041 if not out :
41- return []
42- return [line .strip () for line in out .splitlines () if line .strip ()]
42+ return 0
43+ cnt = 0
44+ for s in out .splitlines ():
45+ s = s .strip ()
46+ if s and not BOT_REGEX .match (s ):
47+ cnt += 1
48+ return cnt
4349
4450def latest_nonbot_commit_for_path (path ):
4551 """
46- 해당 path에 대한 '비봇' 커밋 중 가장 최근 항목의 (날짜, 커밋해시)를 반환.
52+ path(예: YYYY-MM-DD/이름)에서 '비봇' 커밋 중 가장 최근의 (날짜, 해시) 반환.
4753 없으면 (None, None).
4854 """
49- # 날짜는 KST 로컬 포맷, 해시는 별도로 얻기 위해 %H 추가
5055 cmd = f'git log --pretty="%H%x09%ad%x09%s" --date=format-local:"%Y-%m-%d" -- "{ path } " || true'
5156 out = run (cmd )
5257 if not out :
5358 return None , None
5459 for line in out .splitlines ():
55- line = line .strip ()
56- if not line :
60+ parts = line .strip (). split ( " \t " , 3 )
61+ if len ( parts ) < 3 :
5762 continue
58- parts = line .split ("\t " , 2 )
59- if len (parts ) != 3 :
60- continue
61- commit_hash , date_str , subject = parts
62- subject = subject .strip ()
63- if BOT_REGEX .match (subject ):
64- continue
65- return date_str , commit_hash
63+ h , date_str , subj = parts [0 ], parts [1 ], parts [2 ].strip ()
64+ if not BOT_REGEX .match (subj ):
65+ return date_str , h
6666 return None , None
6767
68- def commit_flag (date_str , name ):
69- """
70- 커밋 관점 판정:
71- 'O' = 그날 비봇 커밋,
72- 'L' = 다른 날 비봇 커밋,
73- 'X' = 비봇 커밋 없음
74- """
75- path = f"{ date_str } /{ name } "
76- subjects_today = git_subjects_for_date_and_path (date_str , path )
77- for s in subjects_today :
78- if not BOT_REGEX .match (s ):
79- return "O"
80- nonbot_date , _ = latest_nonbot_commit_for_path (path )
81- if nonbot_date is not None and nonbot_date != date_str :
82- return "L"
83- return "X"
84-
85- # ---------- 스냅샷 & 파일 개수 (재귀) ----------
86-
87- def commit_at_end_of_date (date_str ):
88- """해당 날짜(KST 23:59:59)의 리포 스냅샷 커밋 해시 반환(없으면 빈 문자열)"""
89- until = f'{ date_str } 23:59:59 { TZ_OFFSET } '
90- cmd = f'git rev-list -1 --before="{ until } " HEAD || true'
91- return run (cmd ).strip ()
92-
93- def count_files_recursive_at_commit (commit , base_dir ):
68+ # ---------- 스냅샷 & 파일 개수(재귀, .gitkeep 제외) ----------
69+
70+ def files_excluding_gitkeep_at_commit (commit , base_dir ):
9471 """
95- 특정 커밋에서 base_dir/ 아래의 모든 파일(=blob) 리스트와 .gitkeep 존재 여부 반환.
96- - 출력: (파일경로목록(list[str]), has_gitkeep(bool))
72+ 특정 커밋에서 base_dir/ 아래 모든 파일 목록( .gitkeep 제외)과,
73+ .gitkeep 존재 여부를 함께 반환.
9774 """
9875 if not commit :
9976 return [], False
@@ -102,54 +79,34 @@ def count_files_recursive_at_commit(commit, base_dir):
10279 out = run (cmd )
10380 if not out :
10481 return [], False
105-
10682 files = []
10783 has_gitkeep = False
10884 for line in out .splitlines ():
10985 name = line .strip ()
11086 if not name or not name .startswith (base ):
11187 continue
112- files .append (name )
11388 if os .path .basename (name ) == ".gitkeep" :
11489 has_gitkeep = True
90+ continue # 표시/카운트에서 제외
91+ files .append (name )
11592 return files , has_gitkeep
11693
117- def snapshot_for_display_and_goal ( date_str , name , cf ):
94+ def display_total_files_and_has_nonbot ( path ):
11895 """
119- 표시/판정에 사용할 '스냅샷 커밋'을 선택하고, 그 스냅샷에서:
120- - 표시용 파일 개수(display_n): .gitkeep 제외
121- - 목표값(m): .gitkeep 있으면 4, 없으면 3
122- - 충족 여부(ok): (표시용이 아니라 실제 전체 파일수 >= m)
123- 반환: (display_n, m, ok)
96+ 표시용 누적 파일 수 n 계산:
97+ - path의 '가장 최근 비봇 커밋' 스냅샷에서 .gitkeep 제외 파일 개수
98+ 또한 비봇 커밋 존재 여부(boolean)도 함께 반환.
12499 """
125- path = f"{ date_str } /{ name } "
126-
127- if cf == "O" :
128- # 같은 날: 그날 끝 스냅샷
129- commit = commit_at_end_of_date (date_str )
130- elif cf == "L" :
131- # 레트로: 최신 비봇 커밋 스냅샷
132- _ , commit = latest_nonbot_commit_for_path (path )
133- if not commit :
134- # 안전망: 없으면 그날 스냅샷 시도
135- commit = commit_at_end_of_date (date_str )
136- else : # 'X'
137- # 비봇 커밋 없으면 그날 스냅샷으로 계산(대개 0)
138- commit = commit_at_end_of_date (date_str )
139-
140- files , has_gitkeep = count_files_recursive_at_commit (commit , path )
141- total_including_gitkeep = len (files )
142- # 표시용 개수는 .gitkeep 제외
143- display_n = total_including_gitkeep - (1 if any (os .path .basename (f ) == ".gitkeep" for f in files ) else 0 )
144- # 목표값 산정
145- m = 4 if has_gitkeep else 3
146- ok = total_including_gitkeep >= m
147- return display_n , m , ok
148-
149- # ---------- 달력 렌더링 ----------
100+ nonbot_date , nonbot_hash = latest_nonbot_commit_for_path (path )
101+ if not nonbot_hash :
102+ # 비봇 커밋이 전혀 없으면 표시 n=0
103+ return 0 , False
104+ files , _ = files_excluding_gitkeep_at_commit (nonbot_hash , path )
105+ return len (files ), True
106+
107+ # ---------- 렌더링 로직 ----------
150108
151109def find_all_date_dirs ():
152- """리포 내 YYYY-MM-DD 디렉터리들을 찾아 실제 날짜 리스트 반환."""
153110 dates = []
154111 for entry in os .listdir ("." ):
155112 if re .fullmatch (r"\d{4}-\d{2}-\d{2}" , entry ) and os .path .isdir (entry ):
@@ -161,7 +118,6 @@ def find_all_date_dirs():
161118 return sorted (dates )
162119
163120def month_iter (start_date , end_date ):
164- """start_date의 1일 ~ end_date의 1일까지 월 단위 이터레이션."""
165121 y , m = start_date .year , start_date .month
166122 while (y < end_date .year ) or (y == end_date .year and m <= end_date .month ):
167123 yield y , m
@@ -186,7 +142,6 @@ def build_month_calendar(year, month, today_kst):
186142
187143 date_obj = datetime .date (year , month , d )
188144 if date_obj >= today_kst :
189- # 오늘/미래 날짜는 빈 칸
190145 tds .append (
191146 f'<td align="center" valign="top">'
192147 f'<div align="right"><sub>{ d } </sub></div>'
@@ -197,21 +152,29 @@ def build_month_calendar(year, month, today_kst):
197152 date_str = date_obj .isoformat ()
198153 lines = []
199154 for name in NAMES :
200- cf = commit_flag (date_str , name ) # 'O','L','X'
201- display_n , m , ok = snapshot_for_display_and_goal (date_str , name , cf )
202-
203- if cf == "O" :
204- dot = DOT_GREEN if ok else DOT_YELLOW
205- elif cf == "L" :
206- dot = DOT_ORANGE if ok else DOT_YELLOW
155+ path = f"{ date_str } /{ name } "
156+
157+ # 1) 누적 파일 수 n (레트로 포함), 비봇 커밋 존재 여부
158+ n , has_nonbot = display_total_files_and_has_nonbot (path )
159+
160+ # 2) 당일 비봇 커밋 수
161+ today_nonbot_cnt = nonbot_commit_count_on_date (date_str , path )
162+
163+ # 3) 색상 결정
164+ if not has_nonbot :
165+ dot = DOT_RED # 비봇 커밋 없음 → 빨강, n=0
166+ n = 0
167+ elif n >= GOAL_M :
168+ if today_nonbot_cnt >= GOAL_M :
169+ dot = DOT_GREEN # 당일 3커밋 이상 + 누적 3개 이상
170+ else :
171+ dot = DOT_ORANGE # 누적 3개 이상, 당일 3커밋 미만(레트로 포함 달성)
207172 else :
208- dot = DOT_RED
209- # cf == 'X'일 땐 ok 의미가 없지만, 표시 일관성을 위해 (n/m) 그대로 둠.
173+ dot = DOT_YELLOW # 비봇 커밋은 있으나 누적 < 3
210174
211- # 👉 표시를 심플하게: " 이름: 🟡 (n/m)"
175+ # 4) 표시 ( 이름: 🟢 (n/3)) 단순 형식
212176 lines .append (
213- f"<div style='font-size:13px'>{ name } : { dot } "
214- f"(<code>{ display_n } /{ m } </code>)</div>"
177+ f"<div style='font-size:13px'>{ name } : { dot } (<code>{ n } /{ GOAL_M } </code>)</div>"
215178 )
216179
217180 cell_html = (
@@ -226,11 +189,12 @@ def build_month_calendar(year, month, today_kst):
226189 month_title = f"### { year } -{ month :02d} 코딩테스트 달력 (KST)"
227190 legend = (
228191 "<sub>"
229- "🟢=그날 커밋+목표달성, "
230- "🟠=다른날 커밋+목표달성, "
231- "🟡=커밋있음+목표미달, "
232- "🔴=커밋없음 · "
233- "(표시 n은 .gitkeep 제외)</sub>"
192+ "🟢=그날 3커밋 이상+누적 3개 이상, "
193+ "🟠=누적 3개 이상(레트로 포함) & 당일 3커밋 미만, "
194+ "🟡=커밋은 있으나 누적 < 3, "
195+ "🔴=비봇 커밋 없음 · "
196+ "(표시 n은 .gitkeep 제외, 목표는 항상 3)"
197+ "</sub>"
234198 )
235199 table_html = (
236200 f"{ month_title } \n \n "
0 commit comments