# -*- coding: utf-8 -*- from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "docs" / "assets" / "security_plan" / "generated" OUT.mkdir(parents=True, exist_ok=True) FONT_CANDIDATES = [ Path("C:/Windows/Fonts/NotoSansKR-VF.ttf"), Path("C:/Windows/Fonts/malgun.ttf"), ] BOLD_CANDIDATES = [ Path("C:/Windows/Fonts/NotoSansKR-VF.ttf"), Path("C:/Windows/Fonts/malgunbd.ttf"), ] def first_existing(paths): for path in paths: if path.exists(): return path raise FileNotFoundError("No Korean font found") FONT_PATH = first_existing(FONT_CANDIDATES) BOLD_PATH = first_existing(BOLD_CANDIDATES) FONT = ImageFont.truetype(str(FONT_PATH), 22) SMALL = ImageFont.truetype(str(FONT_PATH), 18) TINY = ImageFont.truetype(str(FONT_PATH), 15) BOLD = ImageFont.truetype(str(BOLD_PATH), 25) TITLE_FONT = ImageFont.truetype(str(BOLD_PATH), 32) def wrap(draw, text, font, width): words = text.split(" ") lines = [] current = "" for word in words: test = f"{current} {word}".strip() if draw.textbbox((0, 0), test, font=font)[2] <= width or not current: current = test else: lines.append(current) current = word if current: lines.append(current) return lines def draw_title(draw, text): draw.text((40, 28), text, font=TITLE_FONT, fill="#0f172a") draw.line((40, 72, 1160, 72), fill="#cbd5e1", width=2) def draw_box(draw, xy, title, lines, fill, outline="#334155"): x1, y1, x2, y2 = xy draw.rounded_rectangle(xy, radius=14, fill=fill, outline=outline, width=3) draw.text((x1 + 18, y1 + 14), title, font=BOLD, fill="#0f172a") y = y1 + 52 for line in lines: for wrapped in wrap(draw, line, SMALL, x2 - x1 - 36): draw.text((x1 + 18, y), wrapped, font=SMALL, fill="#1f2937") y += 25 def draw_arrow(draw, p1, p2, color="#0f766e", width=5): draw.line([p1, p2], fill=color, width=width) x1, y1 = p1 x2, y2 = p2 if abs(x2 - x1) >= abs(y2 - y1): points = [(x2, y2), (x2 - 16, y2 - 9), (x2 - 16, y2 + 9)] if x2 >= x1 else [ (x2, y2), (x2 + 16, y2 - 9), (x2 + 16, y2 + 9) ] else: points = [(x2, y2), (x2 - 9, y2 - 16), (x2 + 9, y2 - 16)] if y2 >= y1 else [ (x2, y2), (x2 - 9, y2 + 16), (x2 + 9, y2 + 16) ] draw.polygon(points, fill=color) def save_target_architecture(): img = Image.new("RGB", (1200, 720), "#f8fafc") draw = ImageDraw.Draw(img) draw_title(draw, "보안대책서 보완용 목표 구성도") draw_box(draw, (55, 130, 275, 300), "전산실 센서", ["SHT30 온습도 센서", "STM32 보드", "측정 주기 5분"], "#e0f2fe") draw_box(draw, (360, 115, 610, 315), "STM32 단말 보안", ["기본 계정 변경", "환경파일 API 키", "로컬 로그", "불필요 서비스 차단"], "#dcfce7") draw_box(draw, (705, 95, 950, 330), "Cafe24 서버", ["PHP API", "MySQL DB", "임계 판정", "대시보드/보고서"], "#fef3c7") draw_box(draw, (990, 150, 1160, 280), "담당자", ["SMS 수신", "대시보드 확인", "현장 점검"], "#ede9fe") draw_box(draw, (405, 410, 905, 610), "보안 경계 및 통제", ["기관 내부 업무망과 분리", "HTTPS/TLS 전송", "기관 방화벽 목적지/포트 제한", "서버 관리자 페이지 인증/세션 보호"], "#ffe4e6") draw_arrow(draw, (275, 215), (360, 215)) draw_arrow(draw, (610, 215), (705, 215)) draw_arrow(draw, (950, 210), (990, 210)) draw_arrow(draw, (610, 340), (705, 450)) draw_arrow(draw, (405, 450), (250, 300)) draw_arrow(draw, (905, 450), (1065, 285)) draw.text((292, 184), "I2C/측정", font=TINY, fill="#334155") draw.text((626, 184), "HTTPS API", font=TINY, fill="#334155") draw.text((958, 180), "SMS/웹", font=TINY, fill="#334155") img.save(OUT / "security_plan_target_architecture.png") def save_data_flow(): img = Image.new("RGB", (1200, 720), "#ffffff") draw = ImageDraw.Draw(img) draw_title(draw, "데이터 흐름 및 보호조치 순서도") items = [ ("1. 온습도 측정", ["SHT30 측정", "온도/습도 5분 주기"]), ("2. 단말 보고", ["측정값 수집", "periodic/startup", "정상 보고"]), ("3. API 전송", ["HTTPS/TLS", "HMAC/서명 검증", "API 키 일치"]), ("4. 서버 판정/저장", ["임계 판정(METRIC_*)", "sensor_metric", "sensor_log", "sms_log"]), ("5. 알림/대응", ["임계 SMS 발송", "정상복귀 SMS", "대시보드 확인"]), ] xs = [45, 275, 505, 735, 965] colors = ["#e0f2fe", "#dcfce7", "#fef3c7", "#ede9fe", "#ffe4e6"] for index, (head, body) in enumerate(items): draw_box(draw, (xs[index], 150, xs[index] + 190, 380), head, body, colors[index]) if index < 4: draw_arrow(draw, (xs[index] + 190, 265), (xs[index + 1], 265), "#2563eb") draw_box( draw, (160, 470, 1040, 620), "문서에 추가할 보호대책 명시", ["전송 중 암호화, API 서명, 비밀값 분리, 최소 수집, 보관기간, 접근권한, 로그 위변조 방지, 백업/복구 점검을 표로 명시"], "#f1f5f9", ) for x in [140, 370, 600, 830, 1060]: draw_arrow(draw, (x, 380), (x, 470), "#64748b", 3) img.save(OUT / "security_plan_data_flow.png") def save_document_update_map(): img = Image.new("RGB", (1200, 760), "#ffffff") draw = ImageDraw.Draw(img) draw_title(draw, "보안대책서 수정·추가 반영 위치") draw_box(draw, (60, 125, 340, 300), "1. 사업개요 수정", ["서버실 온습도(SHT30) 모니터링 범위", "임계 SMS/대시보드 운영 기능 반영"], "#e0f2fe") draw_box(draw, (460, 125, 740, 300), "2. 구성도 교체/보강", ["SHT30, STM32, Cafe24, MySQL, SMS", "업무망 분리와 HTTPS 경계 표시"], "#dcfce7") draw_box(draw, (860, 125, 1140, 300), "3. 기술대책 구체화", ["API 서명, 키 관리, 임계 판정", "로그/백업/보관기간"], "#fef3c7") draw_box(draw, (260, 430, 540, 605), "4. 운영 절차 추가", ["설치 점검 화면", "월간 보고서", "임계 경보/오프라인 대응 절차"], "#ede9fe") draw_box(draw, (660, 430, 940, 605), "5. 증적 목록 추가", ["점검 스크린샷", "SMS 테스트 결과", "DB 마이그레이션 적용 확인"], "#ffe4e6") draw_arrow(draw, (340, 215), (460, 215)) draw_arrow(draw, (740, 215), (860, 215)) draw_arrow(draw, (600, 300), (400, 430)) draw_arrow(draw, (600, 300), (800, 430)) img.save(OUT / "security_plan_document_update_map.png") def main(): save_target_architecture() save_data_flow() save_document_update_map() print(f"Generated images in {OUT}") if __name__ == "__main__": main()