DB_JOB/작업파일/_gen_status_xlsx.py
hehihoho3 df16c98366 백업: DB수집 전체 스냅샷 (공공기관2 정리 전)
공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:15:40 +09:00

240 lines
11 KiB
Python

# -*- 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()