|
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": "서울시 공식 폰트" |
|
}, |
|
|
|
|
|
"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 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)', |
|
] |
|
|
|
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: |
|
return FONT_LICENSE_DB[cleaned_name] |
|
|
|
|
|
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 |
|
|
|
|
|
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] |
|
|
|
|
|
matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6) |
|
if matches: |
|
return FONT_LICENSE_DB[matches[0]] |
|
|
|
|
|
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"] |
|
}) |
|
|
|
|
|
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, |
|
'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 |
|
|
|
|
|
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=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", |