from PIL import Image, ImageDraw, ImageFont from pathlib import Path OUT = Path('/home/hyein/jh-mh/장헌산업/exports') PNG = OUT / 'work_data_erd_unified_lanes.png' W, H = 1900, 1980 img = Image.new('RGB', (W, H), '#151718') d = ImageDraw.Draw(img) font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 18) font_b = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 21) font_t = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 34) font_s = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 15) font_badge = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 16) BG = '#151718' PANEL = '#181b1c' CARD = '#202527' HEADER = '#293033' BORDER = '#667179' TEXT = '#eef3f5' MUTED = '#a8b1b7' PK = '#f5d76e' FK = '#87c8ff' PEOPLE = '#55d8f4' PROJECT = '#81e681' ERP = '#b99bff' CAL = '#a6b0b6' BADGE = '#263036' entities = { # left source column 'member': (80, 210, 330, ['PK MemberNo', 'korName', 'teamName', 'rankName', 'groupCode', 'rankCode', 'isRetired']), 'dallyproject': (80, 585, 360, ['PK id', 'FK MemberNo', 'WorkDate', 'EntryPCode', 'EntryTime / LeaveTime', 'OverTime', 'TotalHours', 'RegularHours', 'OvertimeHours']), 'project_alias': (80, 1035, 360, ['PK projectCode', 'shortName', 'bridge/project name']), # shared center columns 'site_worksheet_worker_cache': (640, 170, 430, ['PK projectCode + workDate', 'PK korName + jobType', 'workText', 'note', 'personCount', 'syncedAt']), 'site_worksheet_record': (640, 570, 430, ['PK projectCode + workDate', 'PK memberNo + korName', 'FK memberNo', 'jobType', 'workText', 'note', 'personCount']), 'site_worksheet_day_sync': (640, 960, 410, ['PK projectCode + workDate', 'syncedAt']), 'site_worksheet_menu_sync': (640, 1140, 410, ['PK projectCode + workDate', 'PK selMenu', '2 = normal worksheet', '3 = tension/temp works', 'syncedAt']), # right output column 'work_calendar_detail': (1350, 330, 430, ['PK id', 'source: sql / site', 'FK memberNo', 'workDate', 'projectCode', 'projectName', 'workText', 'jobType', 'hours', 'regularHours', 'overtimeHours', 'personCount']), 'work_calendar_day': (1350, 920, 430, ['PK memberNo + workDate', 'korName / teamName / rankName', 'sqlHours', 'sqlProjectCodes', 'siteCount', 'siteProjectCodes', 'siteWorkTexts', 'hasSql / hasSite']), 'project_yearly_summary': (1350, 1285, 430, ['VIEW project tab output', 'projectCode + yearMonth', 'memberNo / korName', 'intranetRegularHours', 'siteHours = personCount * 8', 'commonHours', 'total = intranet + site - common']), } heights = {k: 54 + len(v[3]) * 27 + 20 for k, v in entities.items()} relations = [ ('1', 'member -> dallyproject', PEOPLE), ('2', 'member -> site_worksheet_record', PEOPLE), ('3', 'project_alias -> dallyproject', PROJECT), ('4', 'project_alias -> worker_cache', PROJECT), ('5', 'day_sync: fetched project/date', CAL), ('6', 'menu_sync: fetched menu 2/3', CAL), ('7', 'worker_cache -> record', ERP), ('8', 'dallyproject -> calendar_detail', PEOPLE), ('9', 'record -> calendar_detail', ERP), ('10', 'project_alias -> projectName', PROJECT), ('11', 'calendar_detail -> calendar_day', CAL), ('12', 'dallyproject -> project summary', PEOPLE), ('13', 'record -> project summary', ERP), ] def panel(x1, y1, x2, y2, title, color): d.rounded_rectangle((x1, y1, x2, y2), radius=16, fill=PANEL, outline='#2e3437', width=2) d.text((x1 + 24, y1 + 20), title, font=font_b, fill=color) def card(name): x, y, w, fields = entities[name] h = heights[name] d.rounded_rectangle((x, y, x + w, y + h), radius=8, fill=CARD, outline=BORDER, width=2) d.rectangle((x, y, x + w, y + 46), fill=HEADER) d.line((x, y + 46, x + w, y + 46), fill=BORDER, width=1) d.text((x + 18, y + 12), name, font=font_b, fill=TEXT) yy = y + 64 for f in fields: color = PK if f.startswith('PK') else FK if f.startswith('FK') else TEXT d.text((x + 20, yy), f, font=font_s, fill=color) yy += 27 def anchor(name, side, off=0): x, y, w, _ = entities[name] h = heights[name] if side == 'r': return (x + w, y + h // 2 + off) if side == 'l': return (x, y + h // 2 + off) if side == 't': return (x + w // 2 + off, y) if side == 'b': return (x + w // 2 + off, y + h) def badge(num, x, y, color): d.ellipse((x - 14, y - 14, x + 14, y + 14), fill=BADGE, outline=color, width=2) tw = d.textlength(num, font=font_badge) d.text((x - tw / 2, y - 10), num, font=font_badge, fill=color) def arrow(points, color, num=None, badge_at=None): d.line(points, fill=color, width=4, joint='curve') x1, y1 = points[-2] x2, y2 = points[-1] if abs(x2 - x1) >= abs(y2 - y1): tri = [(x2, y2), (x2 - 12 if x2 >= x1 else x2 + 12, y2 - 7), (x2 - 12 if x2 >= x1 else x2 + 12, y2 + 7)] else: tri = [(x2, y2), (x2 - 7, y2 - 12 if y2 >= y1 else y2 + 12), (x2 + 7, y2 - 12 if y2 >= y1 else y2 + 12)] d.polygon(tri, fill=color) if num: bx, by = badge_at if badge_at else points[len(points)//2] badge(num, bx, by, color) # title d.text((70, 40), 'Unified Work Data ERD', font=font_t, fill=TEXT) d.text((72, 82), 'One diagram, separated into people flow and project flow. Numbered connectors avoid line-label overlap.', font=font_s, fill=MUTED) # panels panel(45, 130, 500, 1535, 'SOURCE TABLES', MUTED) panel(585, 130, 1130, 1535, 'SHARED ERP CACHE / MATCH TABLES', MUTED) panel(1300, 130, 1860, 1535, 'OUTPUT / VIEW TABLES', MUTED) # connectors without text labels arrow([anchor('member','b'), (245, 545), anchor('dallyproject','t')], PEOPLE, '1', (245, 545)) arrow([anchor('member','r',10), (555, anchor('member','r',10)[1]), (555, anchor('site_worksheet_record','l',-40)[1]), anchor('site_worksheet_record','l',-40)], PEOPLE, '2', (555, 470)) arrow([anchor('project_alias','t'), (260, 925), anchor('dallyproject','b')], PROJECT, '3', (260, 925)) arrow([anchor('project_alias','r',-20), (560, anchor('project_alias','r',-20)[1]), (560, anchor('site_worksheet_worker_cache','l',-35)[1]), anchor('site_worksheet_worker_cache','l',-35)], PROJECT, '4', (560, 825)) arrow([anchor('site_worksheet_day_sync','t'), (1110, 910), (1110, 340), anchor('site_worksheet_worker_cache','r',45)], CAL, '5', (1110, 910)) arrow([anchor('site_worksheet_menu_sync','t'), (1160, 1120), (1160, 370), anchor('site_worksheet_worker_cache','r',90)], CAL, '6', (1160, 1120)) arrow([anchor('site_worksheet_worker_cache','b'), (855, 520), anchor('site_worksheet_record','t')], ERP, '7', (855, 520)) arrow([anchor('dallyproject','r',-30), (1235, anchor('dallyproject','r',-30)[1]), (1235, anchor('work_calendar_detail','l',-80)[1]), anchor('work_calendar_detail','l',-80)], PEOPLE, '8', (1235, 610)) arrow([anchor('site_worksheet_record','r'), (1240, anchor('site_worksheet_record','r')[1]), (1240, anchor('work_calendar_detail','l',40)[1]), anchor('work_calendar_detail','l',40)], ERP, '9', (1240, 690)) arrow([anchor('project_alias','r',35), (1200, anchor('project_alias','r',35)[1]), (1200, anchor('work_calendar_detail','l',95)[1]), anchor('work_calendar_detail','l',95)], PROJECT, '10', (1200, 1030)) arrow([anchor('work_calendar_detail','b'), (1565, 880), anchor('work_calendar_day','t')], CAL, '11', (1565, 880)) arrow([anchor('dallyproject','r',35), (1280, anchor('dallyproject','r',35)[1]), (1280, anchor('project_yearly_summary','l',-45)[1]), anchor('project_yearly_summary','l',-45)], PEOPLE, '12', (1280, 800)) arrow([anchor('site_worksheet_record','r',55), (1265, anchor('site_worksheet_record','r',55)[1]), (1265, anchor('project_yearly_summary','l',30)[1]), anchor('project_yearly_summary','l',30)], ERP, '13', (1265, 1185)) # cards for name in entities: card(name) # connector legend bottom legend_x, legend_y = 70, 1590 d.rounded_rectangle((legend_x, legend_y, 1830, 1775), radius=10, fill='#1d2123', outline='#3a4247') d.text((legend_x + 20, legend_y + 16), 'CONNECTORS', font=font_b, fill=TEXT) col_w = 570 for idx, (num, text, color) in enumerate(relations): col = idx // 5 row = idx % 5 x = legend_x + 28 + col * col_w y = legend_y + 55 + row * 34 badge(num, x, y + 8, color) d.text((x + 28, y), text, font=font_s, fill=TEXT) # mini flow labels d.rounded_rectangle((70, 1810, 1830, 1850), radius=8, fill='#1d2123', outline='#3a4247') d.text((90, 1820), 'People flow: member + dallyproject + matched ERP rows -> calendar | Project flow: dallyproject + matched ERP rows -> project summary', font=font_s, fill=MUTED) img.save(PNG) print(PNG)