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": "웹폰트 사용 가능" }, "나눔바른고딕": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "나눔고딕 개선 버전" }, "나눔명조": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "명조체" }, "나눔손글씨": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "손글씨 스타일" }, "나눔펜": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "네이버", "provider_url": "https://hangeul.naver.com/2017/nanum", "notes": "펜 스타일" }, # 배달의민족 폰트 "배달의민족 주아": { "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": "서울시 공식 폰트" }, "서울한강체": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "서울시", "provider_url": "https://www.seoul.go.kr/solution/font.do", "notes": "서울시 공식 폰트" }, # Google/Adobe 폰트 "Noto Sans KR": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Google", "provider_url": "https://fonts.google.com/noto", "notes": "Google Fonts 제공" }, "Noto Serif KR": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Google", "provider_url": "https://fonts.google.com/noto", "notes": "Google Fonts 명조체" }, "본고딕": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Adobe/Google", "provider_url": "https://fonts.google.com/noto", "notes": "Noto Sans CJK와 동일" }, # 오픈소스 폰트 "Pretendard": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "Kil Hyung-jin", "provider_url": "https://github.com/orioncactus/pretendard", "notes": "시스템 UI 최적화 폰트" }, "SUIT": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "SUNN", "provider_url": "https://github.com/sunn-us/SUIT", "notes": "본고딕 기반 개선" }, "Spoqa Han Sans": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "스포카", "provider_url": "https://github.com/spoqa/spoqa-han-sans", "notes": "Source Han Sans 기반" }, # IBM 폰트 "IBM Plex Sans KR": { "license": "SIL OFL 1.1", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "IBM", "provider_url": "https://fonts.google.com/specimen/IBM+Plex+Sans+KR", "notes": "IBM 공식 폰트" }, # 카카오 폰트 "카카오 Regular": { "license": "커스텀 무료", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "카카오", "provider_url": "https://kadx.co.kr/266", "notes": "카카오 공식 폰트" }, # 티몬 폰트 "TmoneyRoundWind": { "license": "커스텀 무료", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "티몬", "provider_url": "https://brunch.co.kr/@creative/32", "notes": "티몬 공식 폰트" }, # 넥슨 폰트 "NEXON Lv1 Gothic": { "license": "커스텀 무료", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "넥슨", "provider_url": "https://www.nexon.com/Home/Game/font", "notes": "넥슨 공식 폰트" }, "NEXON Lv2 Gothic": { "license": "커스텀 무료", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "넥슨", "provider_url": "https://www.nexon.com/Home/Game/font", "notes": "넥슨 공식 폰트" }, # 마켓컬리 폰트 "MarketKurly": { "license": "커스텀 무료", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "불가능", "provider": "마켓컬리", "provider_url": "https://thefaceshop.com/marketkurly-font", "notes": "마켓컬리 공식 폰트" }, # 윤디자인 폰트 (상업용) "윤고딕": { "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": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요" }, # 공공기관 폰트 "한국관광공사체": { "license": "공공누리 제1유형", "commercial_free": "✅ 가능", "attribution": "불필요", "modification": "가능", "provider": "한국관광공사", "provider_url": "https://kto.visitkorea.or.kr/kor/notice/data/storybook/font.kto", "notes": "공공누리 제1유형 (출처표시)" } } 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|ExtraBold|SemiBold|Italic)', r'(.+?)[-_\s]?\d+', # 숫자 제거 r'(.+?)[-_\s]?(Kr|KR|Korean)', # 언어 코드 제거 r'(.+?)[-_\s]?(ttf|TTF)', # ttf 제거 ] 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) # 1. 정확한 매칭 if cleaned_name in FONT_LICENSE_DB: return FONT_LICENSE_DB[cleaned_name] # 2. 부분 매칭 (포함 관계) 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(): return info # 3. 특별한 패턴 매칭 special_patterns = { r'nanum|나눔': "나눔고딕", r'baemin|배민|배달의민족': "배달의민족 주아", r'seoul|서울': "서울남산체", r'pretendard': "Pretendard", r'noto.*sans': "Noto Sans KR", r'noto.*serif': "Noto Serif KR", r'ibm.*plex': "IBM Plex Sans KR", r'spoqa': "Spoqa Han Sans", r'nexon': "NEXON Lv1 Gothic", r'yoon|윤': "윤고딕", r'sandoll|산돌': "산돌고딕" } for pattern, matched_font in special_patterns.items(): if re.search(pattern, cleaned_name.lower()): if matched_font in FONT_LICENSE_DB: return FONT_LICENSE_DB[matched_font] # 4. 유사도 매칭 matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6) if matches: return FONT_LICENSE_DB[matches[0]] # 5. 없는 경우 기본값 return { "license": "❓ 확인 필요", "commercial_free": "❓ 확인 필요", "attribution": "❓ 확인 필요", "modification": "❓ 확인 필요", "provider": "❓ 확인 필요", "provider_url": "", "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('#') 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 (len(font_list) == 1 and font_list[0].startswith("파일 파싱 오류")): return None, "파일을 올바르게 읽을 수 없습니다. UTF-8, CP949, EUC-KR 인코딩을 확인해주세요." results = [] for font_file in font_list: font_name = clean_font_name(font_file) license_info = find_font_license(font_name) results.append({ "원본 파일명": font_file, "폰트명": font_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"] }) # DataFrame으로 변환 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}%) ### 📈 라이선스 유형별 분포 """ # 라이선스 유형별 카운트 license_counts = {} for result in results: license_type = result["라이선스"] license_counts[license_type] = license_counts.get(license_type, 0) + 1 for license_type, count in sorted(license_counts.items(), key=lambda x: x[1], reverse=True): percentage = count / total_fonts * 100 summary += f"- **{license_type}**: {count}개 ({percentage:.1f}%)\n" summary += f""" ### 💡 권장사항 - **✅ 상업적 무료 폰트**: 별도 라이선스 없이 사용 가능 - **❌ 유료 라이선스 폰트**: 상업적 사용 시 라이선스 구매 필요 - **❓ 확인 필요 폰트**: 제작사 공식 사이트에서 라이선스 확인 권장 ⚠️ **주의**: 폰트 라이선스는 변경될 수 있으니 중요한 프로젝트에 사용하기 전에는 반드시 공식 사이트에서 최종 확인하시기 바랍니다. """ 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, # 제공처 URL 'I': 50 # 비고 } for col, width in column_widths.items(): worksheet.column_dimensions[col].width = width # 요약 정보 시트 추가 summary_data = { "구분": ["총 폰트 수", "상업적 무료 사용 가능", "유료 라이선스 필요", "라이선스 확인 필요"], "개수": [ len(df), len(df[df["상업적 사용"].str.contains("✅", na=False)]), len(df[df["상업적 사용"].str.contains("❌", na=False)]), len(df[df["상업적 사용"].str.contains("❓", na=False)]) ], "비율(%)": [ 100.0, len(df[df["상업적 사용"].str.contains("✅", na=False)]) / len(df) * 100, len(df[df["상업적 사용"].str.contains("❌", na=False)]) / len(df) * 100, len(df[df["상업적 사용"].str.contains("❓", na=False)]) / len(df) * 100 ] } summary_df = pd.DataFrame(summary_data) summary_df.to_excel(writer, sheet_name='요약 통계', index=False) # 라이선스별 분류 시트 license_summary = df.groupby('라이선스').size().reset_index(name='개수') license_summary['비율(%)'] = license_summary['개수'] / len(df) * 100 license_summary = license_summary.sort_values('개수', ascending=False) license_summary.to_excel(writer, sheet_name='라이선스별 통계', index=False) output.seek(0) return output.getvalue() except Exception as e: print(f"엑셀 생성 오류: {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 + "\n\n⚠️ 엑셀 파일 생성에 실패했습니다.", 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 + f"\n\n✅ 엑셀 파일이 준비되었습니다!", excel_path except Exception as e: return None, f"파일 처리 중 오류 발생: {str(e)}", None # Gradio 인터페이스 생성 def create_app(): with gr.Blocks( title="한국어 폰트 라이선스 분석기", theme=gr.themes.Soft() ) 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", size="lg" ) with gr.Accordion("💡 사용 방법", open=True): gr.Markdown(""" ### 📝 간단한 사용법 1. **폰트 목록 텍스트 파일 준비** - 메모장에서 폰트 파일명을 한 줄에 하나씩 입력 - 예시: `NanumGothic.ttf`, `Pretendard-Regular.otf` 2. **파일 업로드** - 위에서 만든 txt 파일을 업로드 3. **결과 확인 및 다운로드** - 분석 결과를 확인하고 엑셀 파일 다운로드 **📝 지원 파일 형식:** - 한 줄에 하나의 폰트 파일명 - TTF, OTF, TTC 확장자 자동 인식 - UTF-8, CP949, EUC-KR 인코딩 지원 **✨ 예시 파일 내용:** ``` NanumGothic.ttf Pretendard-Regular.otf BMDOHYEON_ttf.ttf SeoulNamsan-Medium.ttf ``` """) with gr.Column(scale=2): gr.Markdown("## 📊 분석 결과") summary_output = gr.Markdown("파일을 업로드하고 '분석 시작' 버튼을 클릭하세요.") result_table = gr.Dataframe( headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능",