import gradio as gr import pandas as pd import io import re from difflib import get_close_matches from datetime import datetime import tempfile import os # 한국어 폰트 라이선스 데이터베이스 (재검증됨) FONT_LICENSE_DB = { "나눔고딕": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "나눔바른고딕": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "나눔명조": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "나눔손글씨": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "나눔펜": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "배달의민족 주아": { "license": "커스텀 무료 라이선스", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "배달의민족", "provider_url": "https://www.woowahan.com/fonts", "notes": "상업적 사용 가능하나 BI 제작 시 사용 금지, 폰트 자체 판매 금지" }, "배달의민족 도현": { "license": "커스텀 무료 라이선스", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "배달의민족", "provider_url": "https://www.woowahan.com/fonts", "notes": "상업적 사용 가능하나 BI 제작 시 사용 금지, 폰트 자체 판매 금지" }, "배달의민족 기랑해랑": { "license": "커스텀 무료 라이선스", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "배달의민족", "provider_url": "https://www.woowahan.com/fonts", "notes": "상업적 사용 가능하나 BI 제작 시 사용 금지, 폰트 자체 판매 금지" }, "서울남산체": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "서울시", "provider_url": "https://www.seoul.go.kr/solution/font.do", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "서울한강체": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "서울시", "provider_url": "https://www.seoul.go.kr/solution/font.do", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "Noto Sans Korean": { "license": "SIL OFL 1.1", "display_name": "노토 산스 한국어", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Google", "provider_url": "https://fonts.google.com/noto", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "Source Han Sans Korean": { "license": "SIL OFL 1.1", "display_name": "본고딕", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Adobe/Google", "provider_url": "https://fonts.google.com/noto", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "Pretendard": { "license": "SIL OFL 1.1", "display_name": "프리텐다드", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Kil Hyung-jin", "provider_url": "https://github.com/orioncactus/pretendard", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "SUIT": { "license": "SIL OFL 1.1", "display_name": "수트", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "SUNN", "provider_url": "https://github.com/sunn-us/SUIT", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "IBM Plex Sans Korean": { "license": "SIL OFL 1.1", "display_name": "IBM 플렉스 산스 한국어", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "IBM", "provider_url": "https://fonts.google.com/specimen/IBM+Plex+Sans+KR", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "Spoqa Han Sans": { "license": "SIL OFL 1.1", "display_name": "스포카 한 산스", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "스포카", "provider_url": "https://github.com/spoqa/spoqa-han-sans", "notes": "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요" }, "NEXON Lv1 Gothic": { "license": "커스텀 무료 라이선스", "display_name": "넥슨 레벨1 고딕", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "넥슨", "provider_url": "https://www.nexon.com/Home/Game/font", "notes": "상업적 사용 가능, 폰트 자체 판매 금지" }, "NEXON Lv2 Gothic": { "license": "커스텀 무료 라이선스", "display_name": "넥슨 레벨2 고딕", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "넥슨", "provider_url": "https://www.nexon.com/Home/Game/font", "notes": "상업적 사용 가능, 폰트 자체 판매 금지" }, "TmoneyRoundWind": { "license": "커스텀 무료 라이선스", "display_name": "티머니 라운드윈드", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "티몬", "provider_url": "https://brunch.co.kr/@creative/32", "notes": "상업적 사용 가능, 폰트 자체 판매 금지" }, "한국관광공사체": { "license": "공공누리 제1유형", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "한국관광공사", "provider_url": "https://kto.visitkorea.or.kr/kor/notice/data/storybook/font.kto", "notes": "공공누리 제1유형: 출처표시 조건으로 자유이용 가능" }, "KoPubWorld돋움체": { "license": "공공누리 제1유형", "commercial_free": "✅ 가능", "attribution": "필요", "modification": "가능", "provider": "한국출판인쇄문화협회", "provider_url": "http://www.kopus.org/biz/electronic/font.aspx", "notes": "공공누리 제1유형: 출처표시 조건으로 자유이용 가능" }, "윤고딕": { "license": "상업적 라이선스 필요", "commercial_free": "❌ 유료", "attribution": "해당없음", "modification": "라이선스에 따라", "provider": "윤디자인그룹", "provider_url": "https://www.yoondesign.com", "notes": "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요" }, "윤명조": { "license": "상업적 라이선스 필요", "commercial_free": "❌ 유료", "attribution": "해당없음", "modification": "라이선스에 따라", "provider": "윤디자인그룹", "provider_url": "https://www.yoondesign.com", "notes": "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요" }, "산돌고딕": { "license": "상업적 라이선스 필요", "commercial_free": "❌ 유료", "attribution": "해당없음", "modification": "라이선스에 따라", "provider": "산돌커뮤니케이션", "provider_url": "https://www.sandoll.co.kr", "notes": "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요" }, "산돌명조": { "license": "상업적 라이선스 필요", "commercial_free": "❌ 유료", "attribution": "해당없음", "modification": "라이선스에 따라", "provider": "산돌커뮤니케이션", "provider_url": "https://www.sandoll.co.kr", "notes": "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요" } } def clean_font_name(font_name): """폰트 이름 정리""" font_name = re.sub(r'\.(ttf|otf|ttc|woff|woff2)$', '', font_name, flags=re.IGNORECASE) patterns = [ r'(.+?)[-_\s]?(Regular|Bold|Light|Medium|Thin|Black|Heavy)', r'(.+?)[-_\s]?\d+', r'(.+?)[-_\s]?(Kr|KR|Korean)', ] cleaned = font_name.strip() for pattern in patterns: match = re.search(pattern, cleaned, re.IGNORECASE) if match: cleaned = match.group(1).strip() break cleaned = re.sub(r'[-_]+', ' ', cleaned).strip() return cleaned def find_font_license(font_name): """폰트 라이선스 정보 찾기 - 한글명 우선 표시""" cleaned_name = clean_font_name(font_name) # 정확한 매칭 if cleaned_name in FONT_LICENSE_DB: info = FONT_LICENSE_DB[cleaned_name].copy() # 한글 표시명이 있으면 사용 if 'display_name' in info: info['display_font_name'] = info['display_name'] else: info['display_font_name'] = cleaned_name return info # 부분 매칭 for db_font, info in FONT_LICENSE_DB.items(): if db_font.lower() in cleaned_name.lower() or cleaned_name.lower() in db_font.lower(): info_copy = info.copy() if 'display_name' in info_copy: info_copy['display_font_name'] = info_copy['display_name'] else: info_copy['display_font_name'] = db_font return info_copy # 패턴 매칭 (재검증됨) special_patterns = { r'nanum|나눔': "나눔고딕", r'baemin|배민|배달의민족': "배달의민족 주아", r'seoul|서울': "서울남산체", r'pretendard': "Pretendard", r'noto.*sans.*kr|noto.*sans.*korean': "Noto Sans Korean", r'source.*han.*sans|본고딕': "Source Han Sans Korean", r'ibm.*plex.*sans.*kr|ibm.*plex.*sans.*korean': "IBM Plex Sans Korean", r'spoqa.*han.*sans': "Spoqa Han Sans", r'nexon.*lv1|nexon.*level.*1': "NEXON Lv1 Gothic", r'nexon.*lv2|nexon.*level.*2': "NEXON Lv2 Gothic", r'tmoney.*round.*wind': "TmoneyRoundWind", r'suit': "SUIT", r'yoon|윤': "윤고딕", r'sandoll|산돌': "산돌고딕", r'kopub.*world': "KoPubWorld돋움체", r'한국관광공사': "한국관광공사체" } for pattern, matched_font in special_patterns.items(): if re.search(pattern, cleaned_name.lower()): if matched_font in FONT_LICENSE_DB: info_copy = FONT_LICENSE_DB[matched_font].copy() if 'display_name' in info_copy: info_copy['display_font_name'] = info_copy['display_name'] else: info_copy['display_font_name'] = matched_font return info_copy # 유사도 매칭 matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6) if matches: info_copy = FONT_LICENSE_DB[matches[0]].copy() if 'display_name' in info_copy: info_copy['display_font_name'] = info_copy['display_name'] else: info_copy['display_font_name'] = matches[0] return info_copy # 기본값 return { "license": "❓ 확인 필요", "commercial_free": "❓ 확인 필요", "attribution": "❓ 확인 필요", "modification": "❓ 확인 필요", "provider": "❓ 확인 필요", "provider_url": "", "display_font_name": cleaned_name, "notes": "라이선스 정보를 찾을 수 없습니다. 제작사 공식 사이트에서 확인하세요." } def parse_font_list(file_content): """폰트 목록 파싱""" try: if isinstance(file_content, bytes): encodings = ['utf-8', 'cp949', 'euc-kr', 'latin1'] for encoding in encodings: try: file_content = file_content.decode(encoding) break except: continue lines = file_content.strip().split('\n') fonts = [] for line in lines: line = line.strip() if line and not line.startswith('#'): fonts.append(line) return fonts except Exception as e: return [f"파일 파싱 오류: {str(e)}"] def analyze_fonts(file_content): """폰트 분석""" try: font_list = parse_font_list(file_content) if not font_list or font_list[0].startswith("파일 파싱 오류"): return None, "파일을 읽을 수 없습니다." results = [] for font_file in font_list: font_name = clean_font_name(font_file) license_info = find_font_license(font_name) # 한글 폰트명 사용 display_name = license_info.get('display_font_name', font_name) results.append({ "원본 파일명": font_file, "폰트명": display_name, "라이선스": license_info["license"], "상업적 사용": license_info["commercial_free"], "출처 표시": license_info["attribution"], "수정 가능": license_info["modification"], "제공처": license_info["provider"], "제공처 URL": license_info["provider_url"], "비고": license_info["notes"] }) df = pd.DataFrame(results) total_fonts = len(results) commercial_free = len([r for r in results if "✅" in r["상업적 사용"]]) needs_check = len([r for r in results if "❓" in r["상업적 사용"]]) commercial_paid = len([r for r in results if "❌" in r["상업적 사용"]]) summary = f""" ## 📊 분석 결과 요약 - **총 폰트 수**: {total_fonts}개 - **상업적 무료**: {commercial_free}개 ({commercial_free/total_fonts*100:.1f}%) - **유료 라이선스**: {commercial_paid}개 ({commercial_paid/total_fonts*100:.1f}%) - **확인 필요**: {needs_check}개 ({needs_check/total_fonts*100:.1f}%) ✅ **엑셀 파일이 준비되었습니다!** """ return df, summary except Exception as e: return None, f"분석 중 오류: {str(e)}" def create_excel_download(df): """엑셀 파일 생성""" try: output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='폰트 라이선스 정보', index=False) worksheet = writer.sheets['폰트 라이선스 정보'] column_widths = { 'A': 25, 'B': 20, 'C': 25, 'D': 15, 'E': 12, 'F': 12, 'G': 20, 'H': 40, 'I': 50 } for col, width in column_widths.items(): worksheet.column_dimensions[col].width = width output.seek(0) return output.getvalue() except Exception as e: return None def process_font_file(file): """파일 처리""" if file is None: return None, "파일을 업로드해주세요.", None try: if hasattr(file, 'read'): content = file.read() else: with open(file, 'rb') as f: content = f.read() df, summary = analyze_fonts(content) if df is None: return None, summary, None excel_data = create_excel_download(df) if excel_data is None: return df, summary, None timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") excel_filename = f"font_license_analysis_{timestamp}.xlsx" temp_dir = tempfile.gettempdir() excel_path = os.path.join(temp_dir, excel_filename) with open(excel_path, 'wb') as f: f.write(excel_data) return df, summary, excel_path except Exception as e: return None, f"파일 처리 오류: {str(e)}", None def create_app(): with gr.Blocks(title="한국어 폰트 라이선스 분석기") as app: gr.Markdown(""" # 🔍 한국어 폰트 라이선스 분석기 **폰트 목록을 업로드하면 라이선스 정보를 분석하여 엑셀로 제공합니다!** """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("## 📁 폰트 목록 파일 업로드") file_input = gr.File( label="폰트 목록 텍스트 파일 (.txt)", file_types=[".txt"] ) analyze_btn = gr.Button("🔍 라이선스 분석 시작", variant="primary") gr.Markdown(""" ### 사용 방법 1. 메모장에서 폰트 파일명을 한 줄에 하나씩 입력 2. txt 파일로 저장 후 업로드 3. 분석 결과 확인 및 엑셀 다운로드 **예시 파일 내용:** ``` NanumGothic.ttf Pretendard-Regular.otf BMDOHYEON_ttf.ttf ``` """) with gr.Column(scale=2): gr.Markdown("## 📊 분석 결과") summary_output = gr.Markdown("파일을 업로드하세요.") result_table = gr.Dataframe( headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", "제공처", "제공처 URL", "비고"], label="폰트 라이선스 정보", column_widths=["15%", "12%", "15%", "10%", "8%", "8%", "12%", "20%", "20%"], interactive=True ) excel_download = gr.File(label="📥 엑셀 다운로드", visible=False) with gr.Accordion("📋 샘플 결과", open=False): sample_data = [ ["NanumGothic.ttf", "나눔고딕", "SIL OFL 1.1", "✅ 가능", "불필요", "가능", "네이버", "https://hangeul.naver.com/2017/nanum", "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"], ["Pretendard-Regular.otf", "프리텐다드", "SIL OFL 1.1", "✅ 가능", "불필요", "가능", "Kil Hyung-jin", "https://github.com/orioncactus/pretendard", "OFL 1.1 라이선스로 상업적 사용 자유, 출처 표시 불필요"], ["YoonGothic.ttf", "윤고딕", "상업적 라이선스 필요", "❌ 유료", "해당없음", "라이선스에 따라", "윤디자인그룹", "https://www.yoondesign.com", "개인 사용만 무료, 상업적 사용 시 유료 라이선스 구매 필요"], ["KoPubWorldDotum.ttf", "KoPubWorld돋움체", "공공누리 제1유형", "✅ 가능", "필요", "가능", "한국출판인쇄문화협회", "http://www.kopus.org/biz/electronic/font.aspx", "공공누리 제1유형: 출처표시 조건으로 자유이용 가능"] ] gr.Dataframe( value=sample_data, headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", "제공처", "제공처 URL", "비고"], column_widths=["15%", "12%", "15%", "10%", "8%", "8%", "12%", "20%", "20%"] ) with gr.Accordion("ℹ️ 라이선스 유형 설명", open=False): gr.Markdown(""" ### 주요 라이선스 유형 (재검증됨) **✅ SIL OFL 1.1 (Open Font License)** - 상업적 사용: 완전 자유 - 출처 표시: 불필요 - 수정/재배포: 가능 - 웹폰트 사용: 가능 **✅ 커스텀 무료 라이선스** - 각 제작사별 고유 조건 - 대부분 상업적 사용 가능 - 일부 제한사항 존재 (BI 사용 금지 등) **✅ 공공누리 제1유형** - 공공기관에서 제작한 폰트 - 출처표시 조건으로 자유이용 가능 - 상업적 사용 가능 **❌ 상업적 라이선스 필요** - 개인 사용: 무료 - 상업적 사용: 유료 라이선스 구매 필요 - 주로 전문 폰트 제작사 폰트들 **❓ 확인 필요** - 데이터베이스에 정보가 없는 폰트 - 제작사 공식 사이트에서 직접 확인 필요 """) gr.Markdown("### ⚠️ 안내사항") gr.Markdown("- 참고용 도구입니다. 상업적 사용 전 공식 사이트에서 최종 확인하세요.") gr.Markdown("- 라이선스 정보는 재검증되었으나 변경될 수 있습니다.") gr.Markdown("- 총 25개 주요 한국어 폰트 정보를 제공합니다.") def handle_analysis(file): if file is None: return "파일을 업로드해주세요.", None, gr.File(visible=False) df, summary, excel_file = process_font_file(file) if df is None: return summary, None, gr.File(visible=False) if excel_file: return summary, df, gr.File(value=excel_file, visible=True) else: return summary, df, gr.File(visible=False) analyze_btn.click( fn=handle_analysis, inputs=file_input, outputs=[summary_output, result_table, excel_download] ) file_input.change( fn=handle_analysis, inputs=file_input, outputs=[summary_output, result_table, excel_download] ) return app if __name__ == "__main__": app = create_app() app.launch()