# -*- coding: utf-8 -*- """_작업현황.md → 작업현황.xlsx 대시보드. 컬럼: 광역 | 번호 | 기관 | 담당자 | 상태 | 검수 | 행수 | 비고 - 담당자: 충남/충북=프리랜서1, 전북=프리랜서2, 제주=(미지정) ← 드롭다운 - 상태: 작업상태(수집완료 등) ← 드롭다운+조건부서식(색) - 검수: 완료/미검수 (md의 ☑️검수완료 → 완료) ← 드롭다운+조건부서식 - 행수: 데이터바 - 요약 시트: 광역별·담당자별 집계(수식 COUNTIFS/SUMIF) md를 원장으로 두고 여기서 자동생성. """ import re import sys import openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter from openpyxl.formatting.rule import FormulaRule, DataBarRule from openpyxl.worksheet.datavalidation import DataValidation MD = '_작업현황.md' OUT = sys.argv[1] if len(sys.argv) > 1 else '작업현황.xlsx' REGIONS = ['충청남도', '충청북도', '전북특별자치도', '제주특별자치도'] ASSIGNEE = {'충청남도': '프리랜서1', '충청북도': '프리랜서1', '전북특별자치도': '프리랜서2', '제주특별자치도': '프리랜서2'} STATUS_OPTIONS = ['⬜ 미착수', '🔄 수집 중', '✅ 수집완료', '⚠️ 보류·이슈'] INSPECT_OPTIONS = ['완료', '미검수'] ASSIGNEE_OPTIONS = ['프리랜서1', '프리랜서2'] SUBMIT_OPTIONS = ['완료', '미완료'] def parse_submissions(): """## 📤 제출 이력 표에서 {기관: 제출일} 추출 (제출 컬럼 원장). 제출분이 늘어도 md 제출 이력 표만 갱신하면 자동 반영됨(하드코딩 금지).""" s = open(MD, encoding='utf-8').read() i = s.find('## 📤 제출 이력') if i < 0: return {} j = s.find('\n## ', i + 5) sec = s[i:j if j > 0 else i + 3000] out = {} for ln in sec.splitlines(): m = re.match(r'\|\s*(2026-\d{2}-\d{2})\s*\|(.+?)\|', ln) if not m: continue date, insts = m.group(1), m.group(2) for nm in re.findall(r'([가-힣]+[시군])\(', insts): out[nm] = date return out SUBMITTED = parse_submissions() # {기관: 제출일} def parse(): lines = open(MD, encoding='utf-8').read().splitlines() rows, cur = [], None for ln in lines: m = re.match(r'^##\s+(충청남도|충청북도|전북특별자치도|제주특별자치도)', ln) if m: cur = m.group(1); continue if cur and ln.startswith('|'): cells = [c.strip() for c in ln.strip().strip('|').split('|')] if len(cells) < 5 or not cells[0].isdigit(): continue gm = re.search(r'\[([^\]]+)\]', cells[1]) inst = gm.group(1) if gm else cells[1] st = cells[2] cntm = re.search(r'(\d[\d,]*)', cells[3]) cnt = int(cntm.group(1).replace(',', '')) if cntm else None # md 상태 → (작업상태, 검수) if '검수완료' in st: work, insp = '✅ 수집완료', '완료' elif '수집완료' in st: work, insp = '✅ 수집완료', '미검수' elif '보류' in st or '⚠' in st: work, insp = '⚠️ 보류·이슈', '미검수' elif '수집' in st and '중' in st: work, insp = '🔄 수집 중', '미검수' else: work, insp = '⬜ 미착수', '미검수' bigo = cells[4] # 비고 앞 [검수 YYYY-MM-DD] 마커 → 검수완료일 컬럼으로 추출하고 비고에서 제거 dm = re.search(r'\[검수\s*(\d{4}-\d{2}-\d{2})\]', bigo) idate = dm.group(1) if dm else '' bigo = re.sub(r'\[검수\s*\d{4}-\d{2}-\d{2}\]\s*·?\s*', '', bigo).strip() # 제출: 제출 이력 표(SUBMITTED 원장)에 있으면 제출완료(제출일=표의 날짜) if inst in SUBMITTED: submit, sdate = '완료', SUBMITTED[inst] else: submit, sdate = '미완료', '' rows.append({'region': cur, 'num': int(cells[0]), 'inst': inst, 'assignee': ASSIGNEE.get(cur, ''), 'work': work, 'inspect': insp, 'idate': idate, 'count': cnt, 'submit': submit, 'sdate': sdate, 'bigo': bigo}) return rows def main(): rows = parse() wb = openpyxl.Workbook(); ws = wb.active; ws.title = '현황' thin = Side(style='thin', color='BFBFBF') border = Border(left=thin, right=thin, top=thin, bottom=thin) hf = PatternFill('solid', fgColor='4472C4'); hfont = Font(bold=True, color='FFFFFF') headers = ['광역', '번호', '기관', '담당자', '상태', '검수', '행수', '검수완료일', '제출완료', '제출일', '비고'] widths = [15, 6, 12, 11, 12, 9, 10, 13, 10, 12, 100] for c, (h, w) in enumerate(zip(headers, widths), 1): cell = ws.cell(1, c, h) cell.fill = hf; cell.font = hfont; cell.border = border cell.alignment = Alignment(horizontal='center', vertical='center') ws.column_dimensions[get_column_letter(c)].width = w ws.freeze_panes = 'A2' r = 2 for row in rows: ws.cell(r, 1, row['region']); ws.cell(r, 2, row['num']); ws.cell(r, 3, row['inst']) ws.cell(r, 4, row['assignee']); ws.cell(r, 5, row['work']); ws.cell(r, 6, row['inspect']) ws.cell(r, 7, row['count']); ws.cell(r, 8, row['idate']) ws.cell(r, 9, row['submit']); ws.cell(r, 10, row['sdate']); ws.cell(r, 11, row['bigo']) for c in range(1, 12): cell = ws.cell(r, c); cell.border = border cell.alignment = Alignment(horizontal='left' if c == 11 else 'center', vertical='center', wrap_text=(c == 11)) ws.cell(r, 7).number_format = '#,##0' ws.row_dimensions[r].height = 30 r += 1 last = r - 1 ws.auto_filter.ref = f'A1:K{last}' # 드롭다운(셀렉트박스) def add_dv(col, options, title): dv = DataValidation(type='list', formula1='"' + ','.join(options) + '"', allow_blank=True, showDropDown=False) dv.promptTitle = title; dv.prompt = f'{title} 선택' ws.add_data_validation(dv); dv.add(f'{col}2:{col}{last}') add_dv('D', ASSIGNEE_OPTIONS, '담당자') add_dv('E', STATUS_OPTIONS, '작업상태') add_dv('F', INSPECT_OPTIONS, '검수') add_dv('I', SUBMIT_OPTIONS, '제출완료') # 조건부서식 (값 바뀌면 색 자동). priority 낮을수록 우선. _pri = [0] def cf(rng, kw, fill, font, formula=None): _pri[0] += 1 rule = FormulaRule( formula=[formula or f'ISNUMBER(SEARCH("{kw}",{rng.split(":")[0]}))'], # ★ CF 채우기는 Excel이 bgColor로 렌더 → fg/bg 둘 다 지정해야 색이 보임 fill=PatternFill('solid', fgColor=fill, bgColor=fill), font=(Font(bold=True, color=font) if font else None), stopIfTrue=False) rule.priority = _pri[0] ws.conditional_formatting.add(rng, rule) # ★ 검수완료 & 제출미완료 → 행 전체 노랑 (초록보다 우선: 먼저 등록해 priority↑) cf(f'A2:K{last}', None, 'FFEB9C', None, formula=f'AND($F2="완료",$I2="미완료")') # ★ 검수=완료 → 행 전체 초록 cf(f'A2:K{last}', None, 'C6EFCE', None, formula=f'$F2="완료"') # 제출완료: 완료=파랑 / 미완료=회색 cf(f'I2:I{last}', '완료', 'BDD7EE', '1F4E78', formula=f'$I2="완료"') cf(f'I2:I{last}', '미완료', 'F2F2F2', '808080', formula=f'$I2="미완료"') # 담당자 cf(f'D2:D{last}', '프리랜서1', 'DDEBF7', '1F4E78') cf(f'D2:D{last}', '프리랜서2', 'FCE4D6', '974706') # 상태 cf(f'E2:E{last}', '수집완료', 'E2EFDA', '375623') cf(f'E2:E{last}', '보류', 'FFEB9C', '9C5700') cf(f'E2:E{last}', '수집 중', 'FFF2CC', '7F6000') cf(f'E2:E{last}', '미착수', 'F2F2F2', '808080') # 검수 (미검수만 — 완료는 행 규칙이 처리) cf(f'F2:F{last}', '미검수', 'F2F2F2', '808080') # 행수 데이터바 (초록 행 위에도 막대 표시됨) db = DataBarRule(start_type='min', end_type='max', color='8EAADB', showValue=True) _pri[0] += 1; db.priority = _pri[0] ws.conditional_formatting.add(f'G2:G{last}', db) # ===== 요약 시트 ===== s = wb.create_sheet('요약') A = f'현황!$A$2:$A${last}'; DD = f'현황!$D$2:$D${last}' FF = f'현황!$F$2:$F${last}'; GG = f'현황!$G$2:$G${last}' II = f'현황!$I$2:$I${last}' # 제출완료 def hdr(rowi, titles, fill='4472C4'): for c, t in enumerate(titles, 1): cell = s.cell(rowi, c, t) cell.fill = PatternFill('solid', fgColor=fill) cell.font = Font(bold=True, color='FFFFFF') cell.border = border cell.alignment = Alignment(horizontal='center', vertical='center') for c, w in enumerate([16, 9, 11, 11, 11, 11], 1): s.column_dimensions[get_column_letter(c)].width = w # 광역별 s.cell(1, 1, '◆ 광역별').font = Font(bold=True, size=12) hdr(2, ['광역', '기관수', '검수완료', '미검수', '제출완료', '총 행수']) rr = 3 for region in REGIONS: a = f'A{rr}' s.cell(rr, 1, region) s.cell(rr, 2, f'=COUNTIF({A},{a})') s.cell(rr, 3, f'=COUNTIFS({A},{a},{FF},"완료")') s.cell(rr, 4, f'=COUNTIFS({A},{a},{FF},"미검수")') s.cell(rr, 5, f'=COUNTIFS({A},{a},{II},"완료")') s.cell(rr, 6, f'=SUMIF({A},{a},{GG})') for c in range(1, 7): s.cell(rr, c).border = border s.cell(rr, c).alignment = Alignment(horizontal='center' if c > 1 else 'left', vertical='center') s.cell(rr, 6).number_format = '#,##0' rr += 1 s.cell(rr, 1, '합계').font = Font(bold=True) for c in range(2, 7): col = get_column_letter(c) cc = s.cell(rr, c, f'=SUM({col}3:{col}{rr-1})'); cc.font = Font(bold=True) cc.border = border; cc.alignment = Alignment(horizontal='center', vertical='center') cc.fill = PatternFill('solid', fgColor='FCE4D6') if c == 6: cc.number_format = '#,##0' s.cell(rr, 1).fill = PatternFill('solid', fgColor='FCE4D6'); s.cell(rr, 1).border = border # 담당자별 base = rr + 2 s.cell(base - 1, 1, '◆ 담당자별').font = Font(bold=True, size=12) hdr(base, ['담당자', '기관수', '검수완료', '미검수', '제출완료', '총 행수'], fill='ED7D31') rr2 = base + 1 for who in ['프리랜서1', '프리랜서2']: a = f'A{rr2}' s.cell(rr2, 1, who) s.cell(rr2, 2, f'=COUNTIF({DD},{a})') s.cell(rr2, 3, f'=COUNTIFS({DD},{a},{FF},"완료")') s.cell(rr2, 4, f'=COUNTIFS({DD},{a},{FF},"미검수")') s.cell(rr2, 5, f'=COUNTIFS({DD},{a},{II},"완료")') s.cell(rr2, 6, f'=SUMIFS({GG},{DD},{a})') for c in range(1, 7): s.cell(rr2, c).border = border s.cell(rr2, c).alignment = Alignment(horizontal='center' if c > 1 else 'left', vertical='center') s.cell(rr2, 6).number_format = '#,##0' rr2 += 1 s.freeze_panes = 'A3' wb.save(OUT) print(f'생성: {OUT} | 현황 {len(rows)}기관 (담당자/상태/검수 분리, 드롭다운·조건부서식·수식)') if __name__ == '__main__': main()