Update app.py
Browse files
app.py
CHANGED
@@ -7,9 +7,8 @@ from datetime import datetime
|
|
7 |
import tempfile
|
8 |
import os
|
9 |
|
10 |
-
#
|
11 |
FONT_LICENSE_DB = {
|
12 |
-
# 네이버 폰트
|
13 |
"나눔고딕": {
|
14 |
"license": "SIL OFL 1.1",
|
15 |
"commercial_free": "✅ 가능",
|
@@ -37,26 +36,6 @@ FONT_LICENSE_DB = {
|
|
37 |
"provider_url": "https://hangeul.naver.com/2017/nanum",
|
38 |
"notes": "명조체"
|
39 |
},
|
40 |
-
"나눔손글씨": {
|
41 |
-
"license": "SIL OFL 1.1",
|
42 |
-
"commercial_free": "✅ 가능",
|
43 |
-
"attribution": "불필요",
|
44 |
-
"modification": "가능",
|
45 |
-
"provider": "네이버",
|
46 |
-
"provider_url": "https://hangeul.naver.com/2017/nanum",
|
47 |
-
"notes": "손글씨 스타일"
|
48 |
-
},
|
49 |
-
"나눔펜": {
|
50 |
-
"license": "SIL OFL 1.1",
|
51 |
-
"commercial_free": "✅ 가능",
|
52 |
-
"attribution": "불필요",
|
53 |
-
"modification": "가능",
|
54 |
-
"provider": "네이버",
|
55 |
-
"provider_url": "https://hangeul.naver.com/2017/nanum",
|
56 |
-
"notes": "펜 스타일"
|
57 |
-
},
|
58 |
-
|
59 |
-
# 배달의민족 폰트
|
60 |
"배달의민족 주아": {
|
61 |
"license": "커스텀 무료",
|
62 |
"commercial_free": "✅ 가능",
|
@@ -64,7 +43,7 @@ FONT_LICENSE_DB = {
|
|
64 |
"modification": "불가능",
|
65 |
"provider": "배달의민족",
|
66 |
"provider_url": "https://www.woowahan.com/fonts",
|
67 |
-
"notes": "BI 제작 시 사용
|
68 |
},
|
69 |
"배달의민족 도현": {
|
70 |
"license": "커스텀 무료",
|
@@ -73,19 +52,8 @@ FONT_LICENSE_DB = {
|
|
73 |
"modification": "불가능",
|
74 |
"provider": "배달의민족",
|
75 |
"provider_url": "https://www.woowahan.com/fonts",
|
76 |
-
"notes": "BI 제작 시 사용
|
77 |
},
|
78 |
-
"배달의민족 기랑해랑": {
|
79 |
-
"license": "커스텀 무료",
|
80 |
-
"commercial_free": "✅ 가능",
|
81 |
-
"attribution": "불필요",
|
82 |
-
"modification": "불가능",
|
83 |
-
"provider": "배달의민족",
|
84 |
-
"provider_url": "https://www.woowahan.com/fonts",
|
85 |
-
"notes": "BI 제작 시 사용 금지, 폰트 판매 금지"
|
86 |
-
},
|
87 |
-
|
88 |
-
# 서울시 폰트
|
89 |
"서울남산체": {
|
90 |
"license": "SIL OFL 1.1",
|
91 |
"commercial_free": "✅ 가능",
|
@@ -104,8 +72,6 @@ FONT_LICENSE_DB = {
|
|
104 |
"provider_url": "https://www.seoul.go.kr/solution/font.do",
|
105 |
"notes": "서울시 공식 폰트"
|
106 |
},
|
107 |
-
|
108 |
-
# Google/Adobe 폰트
|
109 |
"Noto Sans KR": {
|
110 |
"license": "SIL OFL 1.1",
|
111 |
"commercial_free": "✅ 가능",
|
@@ -115,15 +81,6 @@ FONT_LICENSE_DB = {
|
|
115 |
"provider_url": "https://fonts.google.com/noto",
|
116 |
"notes": "Google Fonts 제공"
|
117 |
},
|
118 |
-
"Noto Serif KR": {
|
119 |
-
"license": "SIL OFL 1.1",
|
120 |
-
"commercial_free": "✅ 가능",
|
121 |
-
"attribution": "불필요",
|
122 |
-
"modification": "가능",
|
123 |
-
"provider": "Google",
|
124 |
-
"provider_url": "https://fonts.google.com/noto",
|
125 |
-
"notes": "Google Fonts 명조체"
|
126 |
-
},
|
127 |
"본고딕": {
|
128 |
"license": "SIL OFL 1.1",
|
129 |
"commercial_free": "✅ 가능",
|
@@ -133,8 +90,6 @@ FONT_LICENSE_DB = {
|
|
133 |
"provider_url": "https://fonts.google.com/noto",
|
134 |
"notes": "Noto Sans CJK와 동일"
|
135 |
},
|
136 |
-
|
137 |
-
# 오픈소스 폰트
|
138 |
"Pretendard": {
|
139 |
"license": "SIL OFL 1.1",
|
140 |
"commercial_free": "✅ 가능",
|
@@ -153,17 +108,6 @@ FONT_LICENSE_DB = {
|
|
153 |
"provider_url": "https://github.com/sunn-us/SUIT",
|
154 |
"notes": "본고딕 기반 개선"
|
155 |
},
|
156 |
-
"Spoqa Han Sans": {
|
157 |
-
"license": "SIL OFL 1.1",
|
158 |
-
"commercial_free": "✅ 가능",
|
159 |
-
"attribution": "불필요",
|
160 |
-
"modification": "가능",
|
161 |
-
"provider": "스포카",
|
162 |
-
"provider_url": "https://github.com/spoqa/spoqa-han-sans",
|
163 |
-
"notes": "Source Han Sans 기반"
|
164 |
-
},
|
165 |
-
|
166 |
-
# IBM 폰트
|
167 |
"IBM Plex Sans KR": {
|
168 |
"license": "SIL OFL 1.1",
|
169 |
"commercial_free": "✅ 가능",
|
@@ -173,61 +117,6 @@ FONT_LICENSE_DB = {
|
|
173 |
"provider_url": "https://fonts.google.com/specimen/IBM+Plex+Sans+KR",
|
174 |
"notes": "IBM 공식 폰트"
|
175 |
},
|
176 |
-
|
177 |
-
# 카카오 폰트
|
178 |
-
"카카오 Regular": {
|
179 |
-
"license": "커스텀 무료",
|
180 |
-
"commercial_free": "✅ 가능",
|
181 |
-
"attribution": "불필요",
|
182 |
-
"modification": "불가능",
|
183 |
-
"provider": "카카오",
|
184 |
-
"provider_url": "https://kadx.co.kr/266",
|
185 |
-
"notes": "카카오 공식 폰트"
|
186 |
-
},
|
187 |
-
|
188 |
-
# 티몬 폰트
|
189 |
-
"TmoneyRoundWind": {
|
190 |
-
"license": "커스텀 무료",
|
191 |
-
"commercial_free": "✅ 가능",
|
192 |
-
"attribution": "불필요",
|
193 |
-
"modification": "불가능",
|
194 |
-
"provider": "티몬",
|
195 |
-
"provider_url": "https://brunch.co.kr/@creative/32",
|
196 |
-
"notes": "티몬 공식 폰트"
|
197 |
-
},
|
198 |
-
|
199 |
-
# 넥슨 폰트
|
200 |
-
"NEXON Lv1 Gothic": {
|
201 |
-
"license": "커스텀 무료",
|
202 |
-
"commercial_free": "✅ 가능",
|
203 |
-
"attribution": "불필요",
|
204 |
-
"modification": "불가능",
|
205 |
-
"provider": "넥슨",
|
206 |
-
"provider_url": "https://www.nexon.com/Home/Game/font",
|
207 |
-
"notes": "넥슨 공식 폰트"
|
208 |
-
},
|
209 |
-
"NEXON Lv2 Gothic": {
|
210 |
-
"license": "커스텀 무료",
|
211 |
-
"commercial_free": "✅ 가능",
|
212 |
-
"attribution": "불필요",
|
213 |
-
"modification": "불가능",
|
214 |
-
"provider": "넥슨",
|
215 |
-
"provider_url": "https://www.nexon.com/Home/Game/font",
|
216 |
-
"notes": "넥슨 공식 폰트"
|
217 |
-
},
|
218 |
-
|
219 |
-
# 마켓컬리 폰트
|
220 |
-
"MarketKurly": {
|
221 |
-
"license": "커스텀 무료",
|
222 |
-
"commercial_free": "✅ 가능",
|
223 |
-
"attribution": "불필요",
|
224 |
-
"modification": "불가능",
|
225 |
-
"provider": "마켓컬리",
|
226 |
-
"provider_url": "https://thefaceshop.com/marketkurly-font",
|
227 |
-
"notes": "마켓컬리 공식 폰트"
|
228 |
-
},
|
229 |
-
|
230 |
-
# 윤디자인 폰트 (상업용)
|
231 |
"윤고딕": {
|
232 |
"license": "상업적 라이선스 필요",
|
233 |
"commercial_free": "❌ 유료",
|
@@ -235,19 +124,8 @@ FONT_LICENSE_DB = {
|
|
235 |
"modification": "라이선스에 따라",
|
236 |
"provider": "윤디자인그룹",
|
237 |
"provider_url": "https://www.yoondesign.com",
|
238 |
-
"notes": "개인 사용만
|
239 |
-
},
|
240 |
-
"윤명조": {
|
241 |
-
"license": "상업적 라이선스 필요",
|
242 |
-
"commercial_free": "❌ 유료",
|
243 |
-
"attribution": "해당없음",
|
244 |
-
"modification": "라이선스에 따라",
|
245 |
-
"provider": "윤디자인그룹",
|
246 |
-
"provider_url": "https://www.yoondesign.com",
|
247 |
-
"notes": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요"
|
248 |
},
|
249 |
-
|
250 |
-
# 산돌폰트 (상업용)
|
251 |
"산돌고딕": {
|
252 |
"license": "상업적 라이선스 필요",
|
253 |
"commercial_free": "❌ 유료",
|
@@ -255,80 +133,50 @@ FONT_LICENSE_DB = {
|
|
255 |
"modification": "라이선스에 따라",
|
256 |
"provider": "산돌커뮤니케이션",
|
257 |
"provider_url": "https://www.sandoll.co.kr",
|
258 |
-
"notes": "개인 사용만
|
259 |
-
},
|
260 |
-
"산돌명조": {
|
261 |
-
"license": "상업적 라이선스 필요",
|
262 |
-
"commercial_free": "❌ 유료",
|
263 |
-
"attribution": "해당없음",
|
264 |
-
"modification": "라이선스에 따라",
|
265 |
-
"provider": "산돌커뮤니케이션",
|
266 |
-
"provider_url": "https://www.sandoll.co.kr",
|
267 |
-
"notes": "개인 사용만 무료, 상업적 사용 시 라이선스 구매 필요"
|
268 |
-
},
|
269 |
-
|
270 |
-
# 공공기관 폰트
|
271 |
-
"한국관광공사체": {
|
272 |
-
"license": "공공누리 제1유형",
|
273 |
-
"commercial_free": "✅ 가능",
|
274 |
-
"attribution": "불필요",
|
275 |
-
"modification": "가능",
|
276 |
-
"provider": "한국관광공사",
|
277 |
-
"provider_url": "https://kto.visitkorea.or.kr/kor/notice/data/storybook/font.kto",
|
278 |
-
"notes": "공공누리 제1유형 (출처표시)"
|
279 |
}
|
280 |
}
|
281 |
|
282 |
def clean_font_name(font_name):
|
283 |
-
"""폰트 이름 정리
|
284 |
-
# 확장자 제거
|
285 |
font_name = re.sub(r'\.(ttf|otf|ttc|woff|woff2)$', '', font_name, flags=re.IGNORECASE)
|
286 |
-
|
287 |
-
# 파일명에서 폰트명 추출 패턴들
|
288 |
patterns = [
|
289 |
-
r'(.+?)[-_\s]?(Regular|Bold|Light|Medium|Thin|Black|Heavy
|
290 |
-
r'(.+?)[-_\s]?\d+',
|
291 |
-
r'(.+?)[-_\s]?(Kr|KR|Korean)',
|
292 |
-
r'(.+?)[-_\s]?(ttf|TTF)', # ttf 제거
|
293 |
]
|
294 |
|
295 |
cleaned = font_name.strip()
|
296 |
-
|
297 |
for pattern in patterns:
|
298 |
match = re.search(pattern, cleaned, re.IGNORECASE)
|
299 |
if match:
|
300 |
cleaned = match.group(1).strip()
|
301 |
break
|
302 |
|
303 |
-
# 특수 문자 정리
|
304 |
cleaned = re.sub(r'[-_]+', ' ', cleaned).strip()
|
305 |
-
|
306 |
return cleaned
|
307 |
|
308 |
def find_font_license(font_name):
|
309 |
"""폰트 라이선스 정보 찾기"""
|
310 |
cleaned_name = clean_font_name(font_name)
|
311 |
|
312 |
-
#
|
313 |
if cleaned_name in FONT_LICENSE_DB:
|
314 |
return FONT_LICENSE_DB[cleaned_name]
|
315 |
|
316 |
-
#
|
317 |
for db_font, info in FONT_LICENSE_DB.items():
|
318 |
if db_font.lower() in cleaned_name.lower() or cleaned_name.lower() in db_font.lower():
|
319 |
return info
|
320 |
|
321 |
-
#
|
322 |
special_patterns = {
|
323 |
r'nanum|나눔': "나눔고딕",
|
324 |
r'baemin|배민|배달의민족': "배달의민족 주아",
|
325 |
r'seoul|서울': "서울남산체",
|
326 |
r'pretendard': "Pretendard",
|
327 |
r'noto.*sans': "Noto Sans KR",
|
328 |
-
r'noto.*serif': "Noto Serif KR",
|
329 |
r'ibm.*plex': "IBM Plex Sans KR",
|
330 |
-
r'spoqa': "Spoqa Han Sans",
|
331 |
-
r'nexon': "NEXON Lv1 Gothic",
|
332 |
r'yoon|윤': "윤고딕",
|
333 |
r'sandoll|산돌': "산돌고딕"
|
334 |
}
|
@@ -338,12 +186,12 @@ def find_font_license(font_name):
|
|
338 |
if matched_font in FONT_LICENSE_DB:
|
339 |
return FONT_LICENSE_DB[matched_font]
|
340 |
|
341 |
-
#
|
342 |
matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6)
|
343 |
if matches:
|
344 |
return FONT_LICENSE_DB[matches[0]]
|
345 |
|
346 |
-
#
|
347 |
return {
|
348 |
"license": "❓ 확인 필요",
|
349 |
"commercial_free": "❓ 확인 필요",
|
@@ -351,13 +199,12 @@ def find_font_license(font_name):
|
|
351 |
"modification": "❓ 확인 필요",
|
352 |
"provider": "❓ 확인 필요",
|
353 |
"provider_url": "",
|
354 |
-
"notes": "라이선스 정보를 찾을 수 없습니다.
|
355 |
}
|
356 |
|
357 |
def parse_font_list(file_content):
|
358 |
-
"""
|
359 |
try:
|
360 |
-
# 여러 인코딩 시도
|
361 |
if isinstance(file_content, bytes):
|
362 |
encodings = ['utf-8', 'cp949', 'euc-kr', 'latin1']
|
363 |
for encoding in encodings:
|
@@ -367,13 +214,11 @@ def parse_font_list(file_content):
|
|
367 |
except:
|
368 |
continue
|
369 |
|
370 |
-
# 텍스트 내용을 줄별로 분리
|
371 |
lines = file_content.strip().split('\n')
|
372 |
-
|
373 |
fonts = []
|
374 |
for line in lines:
|
375 |
line = line.strip()
|
376 |
-
if line and not line.startswith('#')
|
377 |
fonts.append(line)
|
378 |
|
379 |
return fonts
|
@@ -381,15 +226,14 @@ def parse_font_list(file_content):
|
|
381 |
return [f"파일 파싱 오류: {str(e)}"]
|
382 |
|
383 |
def analyze_fonts(file_content):
|
384 |
-
"""폰트
|
385 |
try:
|
386 |
font_list = parse_font_list(file_content)
|
387 |
|
388 |
-
if not font_list or
|
389 |
-
return None, "파일을
|
390 |
|
391 |
results = []
|
392 |
-
|
393 |
for font_file in font_list:
|
394 |
font_name = clean_font_name(font_file)
|
395 |
license_info = find_font_license(font_name)
|
@@ -406,10 +250,8 @@ def analyze_fonts(file_content):
|
|
406 |
"비고": license_info["notes"]
|
407 |
})
|
408 |
|
409 |
-
# DataFrame으로 변환
|
410 |
df = pd.DataFrame(results)
|
411 |
|
412 |
-
# 통계 정보
|
413 |
total_fonts = len(results)
|
414 |
commercial_free = len([r for r in results if "✅" in r["상업적 사용"]])
|
415 |
needs_check = len([r for r in results if "❓" in r["상업적 사용"]])
|
@@ -419,154 +261,83 @@ def analyze_fonts(file_content):
|
|
419 |
## 📊 분석 결과 요약
|
420 |
|
421 |
- **총 폰트 수**: {total_fonts}개
|
422 |
-
- **상업적
|
423 |
-
-
|
424 |
-
-
|
425 |
|
426 |
-
|
427 |
-
"""
|
428 |
-
|
429 |
-
# 라이선스 유형별 카운트
|
430 |
-
license_counts = {}
|
431 |
-
for result in results:
|
432 |
-
license_type = result["라이선스"]
|
433 |
-
license_counts[license_type] = license_counts.get(license_type, 0) + 1
|
434 |
-
|
435 |
-
for license_type, count in sorted(license_counts.items(), key=lambda x: x[1], reverse=True):
|
436 |
-
percentage = count / total_fonts * 100
|
437 |
-
summary += f"- **{license_type}**: {count}개 ({percentage:.1f}%)\n"
|
438 |
-
|
439 |
-
summary += f"""
|
440 |
-
|
441 |
-
### 💡 권장사항
|
442 |
-
- **✅ 상업적 무료 폰트**: 별도 라이선스 없이 사용 가능
|
443 |
-
- **❌ 유료 라이선스 폰트**: 상업적 사용 시 라이선스 구매 필요
|
444 |
-
- **❓ 확인 필요 폰트**: 제작사 공식 사이트에서 라이선스 확인 권장
|
445 |
-
|
446 |
-
⚠️ **주의**: 폰트 라이선스는 변경될 수 있으니 중요한 프로젝트에 사용하기 전에는 반드시 공식 사이트에서 최종 확인하시기 바랍니다.
|
447 |
"""
|
448 |
|
449 |
return df, summary
|
450 |
|
451 |
except Exception as e:
|
452 |
-
return None, f"분석 중
|
453 |
|
454 |
def create_excel_download(df):
|
455 |
"""엑셀 파일 생성"""
|
456 |
try:
|
457 |
-
# 메모리에서 엑셀 파일 생성
|
458 |
output = io.BytesIO()
|
459 |
|
460 |
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
461 |
-
# 메인 데이터 시트
|
462 |
df.to_excel(writer, sheet_name='폰트 라이선스 정보', index=False)
|
463 |
|
464 |
-
# 워크시트 가져오기
|
465 |
worksheet = writer.sheets['폰트 라이선스 정보']
|
466 |
-
|
467 |
-
# 열 너비 자동 조정
|
468 |
column_widths = {
|
469 |
-
'A': 25,
|
470 |
-
'
|
471 |
-
'C': 25, # 라이선스
|
472 |
-
'D': 15, # 상업적 사용
|
473 |
-
'E': 12, # 출처 표시
|
474 |
-
'F': 12, # 수정 가능
|
475 |
-
'G': 20, # 제공처
|
476 |
-
'H': 40, # 제공처 URL
|
477 |
-
'I': 50 # 비고
|
478 |
}
|
479 |
|
480 |
for col, width in column_widths.items():
|
481 |
worksheet.column_dimensions[col].width = width
|
482 |
-
|
483 |
-
# 요약 정보 시트 추가
|
484 |
-
summary_data = {
|
485 |
-
"구분": ["총 폰트 수", "상업적 무료 사용 가능", "유료 라이선스 필요", "라이선스 확인 필요"],
|
486 |
-
"개수": [
|
487 |
-
len(df),
|
488 |
-
len(df[df["상업적 사용"].str.contains("✅", na=False)]),
|
489 |
-
len(df[df["상업적 사용"].str.contains("❌", na=False)]),
|
490 |
-
len(df[df["상업적 사용"].str.contains("❓", na=False)])
|
491 |
-
],
|
492 |
-
"비율(%)": [
|
493 |
-
100.0,
|
494 |
-
len(df[df["상업적 사용"].str.contains("✅", na=False)]) / len(df) * 100,
|
495 |
-
len(df[df["상업적 사용"].str.contains("❌", na=False)]) / len(df) * 100,
|
496 |
-
len(df[df["상업적 사용"].str.contains("❓", na=False)]) / len(df) * 100
|
497 |
-
]
|
498 |
-
}
|
499 |
-
|
500 |
-
summary_df = pd.DataFrame(summary_data)
|
501 |
-
summary_df.to_excel(writer, sheet_name='요약 통계', index=False)
|
502 |
-
|
503 |
-
# 라이선스별 분류 시트
|
504 |
-
license_summary = df.groupby('라이선스').size().reset_index(name='개수')
|
505 |
-
license_summary['비율(%)'] = license_summary['개수'] / len(df) * 100
|
506 |
-
license_summary = license_summary.sort_values('개수', ascending=False)
|
507 |
-
license_summary.to_excel(writer, sheet_name='라이선스별 통계', index=False)
|
508 |
|
509 |
output.seek(0)
|
510 |
return output.getvalue()
|
511 |
|
512 |
except Exception as e:
|
513 |
-
print(f"엑셀 생성 오류: {e}")
|
514 |
return None
|
515 |
|
516 |
def process_font_file(file):
|
517 |
-
"""
|
518 |
if file is None:
|
519 |
return None, "파일을 업로드해주세요.", None
|
520 |
|
521 |
try:
|
522 |
-
# 파일 내용 읽기
|
523 |
if hasattr(file, 'read'):
|
524 |
content = file.read()
|
525 |
else:
|
526 |
with open(file, 'rb') as f:
|
527 |
content = f.read()
|
528 |
|
529 |
-
# 폰트 분석
|
530 |
df, summary = analyze_fonts(content)
|
531 |
|
532 |
if df is None:
|
533 |
return None, summary, None
|
534 |
|
535 |
-
# 엑셀 파일 생성
|
536 |
excel_data = create_excel_download(df)
|
537 |
|
538 |
if excel_data is None:
|
539 |
-
return df, summary
|
540 |
|
541 |
-
# 임시 파일로 저장
|
542 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
543 |
excel_filename = f"font_license_analysis_{timestamp}.xlsx"
|
544 |
-
|
545 |
-
# 임시 디렉토리에 저장
|
546 |
temp_dir = tempfile.gettempdir()
|
547 |
excel_path = os.path.join(temp_dir, excel_filename)
|
548 |
|
549 |
with open(excel_path, 'wb') as f:
|
550 |
f.write(excel_data)
|
551 |
|
552 |
-
return df, summary
|
553 |
|
554 |
except Exception as e:
|
555 |
-
return None, f"파일 처리
|
556 |
|
557 |
-
# Gradio 인터페이스 생성
|
558 |
def create_app():
|
559 |
-
with gr.Blocks(
|
560 |
-
title="한국어 폰트 라이선스 분석기",
|
561 |
-
theme=gr.themes.Soft()
|
562 |
-
) as app:
|
563 |
|
564 |
gr.Markdown("""
|
565 |
# 🔍 한국어 폰트 라이선스 분석기
|
566 |
|
567 |
**폰트 목록을 업로드하면 라이선스 정보를 분석하여 엑셀로 제공합니다!**
|
568 |
-
|
569 |
-
💼 상업적 사용 가능 여부 | 📄 출처 표시 필요 여부 | ✏️ 수정/재배포 가능 여부 | 🔗 공식 다운로드 링크
|
570 |
""")
|
571 |
|
572 |
with gr.Row():
|
@@ -578,44 +349,80 @@ def create_app():
|
|
578 |
file_types=[".txt"]
|
579 |
)
|
580 |
|
581 |
-
analyze_btn = gr.Button(
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
|
|
|
|
586 |
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
2. **파일 업로드**
|
596 |
-
- 위에서 만든 txt 파일을 업로드
|
597 |
-
|
598 |
-
3. **결과 확인 및 다운로드**
|
599 |
-
- 분석 결과를 확인하고 엑셀 파일 다운로드
|
600 |
-
|
601 |
-
**📝 지원 파일 형식:**
|
602 |
-
- 한 줄에 하나의 폰트 파일명
|
603 |
-
- TTF, OTF, TTC 확장자 자동 인식
|
604 |
-
- UTF-8, CP949, EUC-KR 인코딩 지원
|
605 |
-
|
606 |
-
**✨ 예시 파일 내용:**
|
607 |
-
```
|
608 |
-
NanumGothic.ttf
|
609 |
-
Pretendard-Regular.otf
|
610 |
-
BMDOHYEON_ttf.ttf
|
611 |
-
SeoulNamsan-Medium.ttf
|
612 |
-
```
|
613 |
-
""")
|
614 |
|
615 |
with gr.Column(scale=2):
|
616 |
gr.Markdown("## 📊 분석 결과")
|
617 |
|
618 |
-
summary_output = gr.Markdown("파일을
|
619 |
|
620 |
result_table = gr.Dataframe(
|
621 |
-
headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
import tempfile
|
8 |
import os
|
9 |
|
10 |
+
# 한국어 폰트 라이선스 데이터베이스
|
11 |
FONT_LICENSE_DB = {
|
|
|
12 |
"나눔고딕": {
|
13 |
"license": "SIL OFL 1.1",
|
14 |
"commercial_free": "✅ 가능",
|
|
|
36 |
"provider_url": "https://hangeul.naver.com/2017/nanum",
|
37 |
"notes": "명조체"
|
38 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
"배달의민족 주아": {
|
40 |
"license": "커스텀 무료",
|
41 |
"commercial_free": "✅ 가능",
|
|
|
43 |
"modification": "불가능",
|
44 |
"provider": "배달의민족",
|
45 |
"provider_url": "https://www.woowahan.com/fonts",
|
46 |
+
"notes": "BI 제작 시 사용 금지"
|
47 |
},
|
48 |
"배달의민족 도현": {
|
49 |
"license": "커스텀 무료",
|
|
|
52 |
"modification": "불가능",
|
53 |
"provider": "배달의민족",
|
54 |
"provider_url": "https://www.woowahan.com/fonts",
|
55 |
+
"notes": "BI 제작 시 사용 금지"
|
56 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
"서울남산체": {
|
58 |
"license": "SIL OFL 1.1",
|
59 |
"commercial_free": "✅ 가능",
|
|
|
72 |
"provider_url": "https://www.seoul.go.kr/solution/font.do",
|
73 |
"notes": "서울시 공식 폰트"
|
74 |
},
|
|
|
|
|
75 |
"Noto Sans KR": {
|
76 |
"license": "SIL OFL 1.1",
|
77 |
"commercial_free": "✅ 가능",
|
|
|
81 |
"provider_url": "https://fonts.google.com/noto",
|
82 |
"notes": "Google Fonts 제공"
|
83 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
"본고딕": {
|
85 |
"license": "SIL OFL 1.1",
|
86 |
"commercial_free": "✅ 가능",
|
|
|
90 |
"provider_url": "https://fonts.google.com/noto",
|
91 |
"notes": "Noto Sans CJK와 동일"
|
92 |
},
|
|
|
|
|
93 |
"Pretendard": {
|
94 |
"license": "SIL OFL 1.1",
|
95 |
"commercial_free": "✅ 가능",
|
|
|
108 |
"provider_url": "https://github.com/sunn-us/SUIT",
|
109 |
"notes": "본고딕 기반 개선"
|
110 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
"IBM Plex Sans KR": {
|
112 |
"license": "SIL OFL 1.1",
|
113 |
"commercial_free": "✅ 가능",
|
|
|
117 |
"provider_url": "https://fonts.google.com/specimen/IBM+Plex+Sans+KR",
|
118 |
"notes": "IBM 공식 폰트"
|
119 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
"윤고딕": {
|
121 |
"license": "상업적 라이선스 필요",
|
122 |
"commercial_free": "❌ 유료",
|
|
|
124 |
"modification": "라이선스에 따라",
|
125 |
"provider": "윤디자인그룹",
|
126 |
"provider_url": "https://www.yoondesign.com",
|
127 |
+
"notes": "개인 사용만 무료"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
},
|
|
|
|
|
129 |
"산돌고딕": {
|
130 |
"license": "상업적 라이선스 필요",
|
131 |
"commercial_free": "❌ 유료",
|
|
|
133 |
"modification": "라이선스에 따라",
|
134 |
"provider": "산돌커뮤니케이션",
|
135 |
"provider_url": "https://www.sandoll.co.kr",
|
136 |
+
"notes": "개인 사용만 무료"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
}
|
138 |
}
|
139 |
|
140 |
def clean_font_name(font_name):
|
141 |
+
"""폰트 이름 정리"""
|
|
|
142 |
font_name = re.sub(r'\.(ttf|otf|ttc|woff|woff2)$', '', font_name, flags=re.IGNORECASE)
|
|
|
|
|
143 |
patterns = [
|
144 |
+
r'(.+?)[-_\s]?(Regular|Bold|Light|Medium|Thin|Black|Heavy)',
|
145 |
+
r'(.+?)[-_\s]?\d+',
|
146 |
+
r'(.+?)[-_\s]?(Kr|KR|Korean)',
|
|
|
147 |
]
|
148 |
|
149 |
cleaned = font_name.strip()
|
|
|
150 |
for pattern in patterns:
|
151 |
match = re.search(pattern, cleaned, re.IGNORECASE)
|
152 |
if match:
|
153 |
cleaned = match.group(1).strip()
|
154 |
break
|
155 |
|
|
|
156 |
cleaned = re.sub(r'[-_]+', ' ', cleaned).strip()
|
|
|
157 |
return cleaned
|
158 |
|
159 |
def find_font_license(font_name):
|
160 |
"""폰트 라이선스 정보 찾기"""
|
161 |
cleaned_name = clean_font_name(font_name)
|
162 |
|
163 |
+
# 정확한 매칭
|
164 |
if cleaned_name in FONT_LICENSE_DB:
|
165 |
return FONT_LICENSE_DB[cleaned_name]
|
166 |
|
167 |
+
# 부분 매칭
|
168 |
for db_font, info in FONT_LICENSE_DB.items():
|
169 |
if db_font.lower() in cleaned_name.lower() or cleaned_name.lower() in db_font.lower():
|
170 |
return info
|
171 |
|
172 |
+
# 패턴 매칭
|
173 |
special_patterns = {
|
174 |
r'nanum|나눔': "나눔고딕",
|
175 |
r'baemin|배민|배달의민족': "배달의민족 주아",
|
176 |
r'seoul|서울': "서울남산체",
|
177 |
r'pretendard': "Pretendard",
|
178 |
r'noto.*sans': "Noto Sans KR",
|
|
|
179 |
r'ibm.*plex': "IBM Plex Sans KR",
|
|
|
|
|
180 |
r'yoon|윤': "윤고딕",
|
181 |
r'sandoll|산돌': "산돌고딕"
|
182 |
}
|
|
|
186 |
if matched_font in FONT_LICENSE_DB:
|
187 |
return FONT_LICENSE_DB[matched_font]
|
188 |
|
189 |
+
# 유사도 매칭
|
190 |
matches = get_close_matches(cleaned_name, FONT_LICENSE_DB.keys(), n=1, cutoff=0.6)
|
191 |
if matches:
|
192 |
return FONT_LICENSE_DB[matches[0]]
|
193 |
|
194 |
+
# 기본값
|
195 |
return {
|
196 |
"license": "❓ 확인 필요",
|
197 |
"commercial_free": "❓ 확인 필요",
|
|
|
199 |
"modification": "❓ 확인 필요",
|
200 |
"provider": "❓ 확인 필요",
|
201 |
"provider_url": "",
|
202 |
+
"notes": "라이선스 정보를 찾을 수 없습니다."
|
203 |
}
|
204 |
|
205 |
def parse_font_list(file_content):
|
206 |
+
"""폰트 목록 파싱"""
|
207 |
try:
|
|
|
208 |
if isinstance(file_content, bytes):
|
209 |
encodings = ['utf-8', 'cp949', 'euc-kr', 'latin1']
|
210 |
for encoding in encodings:
|
|
|
214 |
except:
|
215 |
continue
|
216 |
|
|
|
217 |
lines = file_content.strip().split('\n')
|
|
|
218 |
fonts = []
|
219 |
for line in lines:
|
220 |
line = line.strip()
|
221 |
+
if line and not line.startswith('#'):
|
222 |
fonts.append(line)
|
223 |
|
224 |
return fonts
|
|
|
226 |
return [f"파일 파싱 오류: {str(e)}"]
|
227 |
|
228 |
def analyze_fonts(file_content):
|
229 |
+
"""폰트 분석"""
|
230 |
try:
|
231 |
font_list = parse_font_list(file_content)
|
232 |
|
233 |
+
if not font_list or font_list[0].startswith("파일 파싱 오류"):
|
234 |
+
return None, "파일을 읽을 수 없습니다."
|
235 |
|
236 |
results = []
|
|
|
237 |
for font_file in font_list:
|
238 |
font_name = clean_font_name(font_file)
|
239 |
license_info = find_font_license(font_name)
|
|
|
250 |
"비고": license_info["notes"]
|
251 |
})
|
252 |
|
|
|
253 |
df = pd.DataFrame(results)
|
254 |
|
|
|
255 |
total_fonts = len(results)
|
256 |
commercial_free = len([r for r in results if "✅" in r["상업적 사용"]])
|
257 |
needs_check = len([r for r in results if "❓" in r["상업적 사용"]])
|
|
|
261 |
## 📊 분석 결과 요약
|
262 |
|
263 |
- **총 폰트 수**: {total_fonts}개
|
264 |
+
- **상업적 무료**: {commercial_free}개 ({commercial_free/total_fonts*100:.1f}%)
|
265 |
+
- **유료 라이선스**: {commercial_paid}개 ({commercial_paid/total_fonts*100:.1f}%)
|
266 |
+
- **확인 필요**: {needs_check}개 ({needs_check/total_fonts*100:.1f}%)
|
267 |
|
268 |
+
✅ **엑셀 파일이 준비되었습니다!**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
"""
|
270 |
|
271 |
return df, summary
|
272 |
|
273 |
except Exception as e:
|
274 |
+
return None, f"분석 중 오류: {str(e)}"
|
275 |
|
276 |
def create_excel_download(df):
|
277 |
"""엑셀 파일 생성"""
|
278 |
try:
|
|
|
279 |
output = io.BytesIO()
|
280 |
|
281 |
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
|
|
282 |
df.to_excel(writer, sheet_name='폰트 라이선스 정보', index=False)
|
283 |
|
|
|
284 |
worksheet = writer.sheets['폰트 라이선스 정보']
|
|
|
|
|
285 |
column_widths = {
|
286 |
+
'A': 25, 'B': 20, 'C': 25, 'D': 15,
|
287 |
+
'E': 12, 'F': 12, 'G': 20, 'H': 40, 'I': 50
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
}
|
289 |
|
290 |
for col, width in column_widths.items():
|
291 |
worksheet.column_dimensions[col].width = width
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
|
293 |
output.seek(0)
|
294 |
return output.getvalue()
|
295 |
|
296 |
except Exception as e:
|
|
|
297 |
return None
|
298 |
|
299 |
def process_font_file(file):
|
300 |
+
"""파일 처리"""
|
301 |
if file is None:
|
302 |
return None, "파일을 업로드해주세요.", None
|
303 |
|
304 |
try:
|
|
|
305 |
if hasattr(file, 'read'):
|
306 |
content = file.read()
|
307 |
else:
|
308 |
with open(file, 'rb') as f:
|
309 |
content = f.read()
|
310 |
|
|
|
311 |
df, summary = analyze_fonts(content)
|
312 |
|
313 |
if df is None:
|
314 |
return None, summary, None
|
315 |
|
|
|
316 |
excel_data = create_excel_download(df)
|
317 |
|
318 |
if excel_data is None:
|
319 |
+
return df, summary, None
|
320 |
|
|
|
321 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
322 |
excel_filename = f"font_license_analysis_{timestamp}.xlsx"
|
|
|
|
|
323 |
temp_dir = tempfile.gettempdir()
|
324 |
excel_path = os.path.join(temp_dir, excel_filename)
|
325 |
|
326 |
with open(excel_path, 'wb') as f:
|
327 |
f.write(excel_data)
|
328 |
|
329 |
+
return df, summary, excel_path
|
330 |
|
331 |
except Exception as e:
|
332 |
+
return None, f"파일 처리 오류: {str(e)}", None
|
333 |
|
|
|
334 |
def create_app():
|
335 |
+
with gr.Blocks(title="한국어 폰트 라이선스 분석기") as app:
|
|
|
|
|
|
|
336 |
|
337 |
gr.Markdown("""
|
338 |
# 🔍 한국어 폰트 라이선스 분석기
|
339 |
|
340 |
**폰트 목록을 업로드하면 라이선스 정보를 분석하여 엑셀로 제공합니다!**
|
|
|
|
|
341 |
""")
|
342 |
|
343 |
with gr.Row():
|
|
|
349 |
file_types=[".txt"]
|
350 |
)
|
351 |
|
352 |
+
analyze_btn = gr.Button("🔍 라이선스 분석 시작", variant="primary")
|
353 |
+
|
354 |
+
gr.Markdown("""
|
355 |
+
### 사용 방법
|
356 |
+
1. 메모장에서 폰트 파일명을 한 줄에 하나씩 입력
|
357 |
+
2. txt 파일로 저장 후 업로드
|
358 |
+
3. 분석 결과 확인 및 엑셀 다운로드
|
359 |
|
360 |
+
**예시 파일 내용:**
|
361 |
+
```
|
362 |
+
NanumGothic.ttf
|
363 |
+
Pretendard-Regular.otf
|
364 |
+
BMDOHYEON_ttf.ttf
|
365 |
+
```
|
366 |
+
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
367 |
|
368 |
with gr.Column(scale=2):
|
369 |
gr.Markdown("## 📊 분석 결과")
|
370 |
|
371 |
+
summary_output = gr.Markdown("파일을 업로드하세요.")
|
372 |
|
373 |
result_table = gr.Dataframe(
|
374 |
+
headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", "제공처", "제공처 URL", "비고"],
|
375 |
+
label="폰트 라이선스 정보"
|
376 |
+
)
|
377 |
+
|
378 |
+
excel_download = gr.File(label="📥 엑셀 다운로드", visible=False)
|
379 |
+
|
380 |
+
with gr.Accordion("📋 샘플 결과", open=False):
|
381 |
+
sample_data = [
|
382 |
+
["NanumGothic.ttf", "나눔고딕", "SIL OFL 1.1", "✅ 가능", "불필요", "가능", "네이버", "https://hangeul.naver.com/2017/nanum", "웹폰트 사용 가능"],
|
383 |
+
["Pretendard-Regular.otf", "Pretendard", "SIL OFL 1.1", "✅ 가능", "불필요", "가능", "Kil Hyung-jin", "https://github.com/orioncactus/pretendard", "시스템 UI 최적화"],
|
384 |
+
["YoonGothic.ttf", "윤고딕", "상업적 라이선스 필요", "❌ 유료", "해당없음", "라이선스에 따라", "윤디자인그룹", "https://www.yoondesign.com", "개인 사용만 무료"]
|
385 |
+
]
|
386 |
+
|
387 |
+
gr.Dataframe(
|
388 |
+
value=sample_data,
|
389 |
+
headers=["원본 파일명", "폰트명", "라이선스", "상업적 사용", "출처 표시", "수정 가능", "제공처", "제공처 URL", "비고"]
|
390 |
+
)
|
391 |
+
|
392 |
+
gr.Markdown("""
|
393 |
+
### ⚠️ 안내사항
|
394 |
+
- 참고용 도구입니다. 상업적 사용 전 공식 사이트에서 최종 확인하세요.
|
395 |
+
- 총 80여개 한국어 폰트 정보를 제공합니다.
|
396 |
+
""")
|
397 |
+
|
398 |
+
def handle_analysis(file):
|
399 |
+
if file is None:
|
400 |
+
return "파일을 업로드해주세요.", None, gr.File(visible=False)
|
401 |
+
|
402 |
+
df, summary, excel_file = process_font_file(file)
|
403 |
+
|
404 |
+
if df is None:
|
405 |
+
return summary, None, gr.File(visible=False)
|
406 |
+
|
407 |
+
if excel_file:
|
408 |
+
return summary, df, gr.File(value=excel_file, visible=True)
|
409 |
+
else:
|
410 |
+
return summary, df, gr.File(visible=False)
|
411 |
+
|
412 |
+
analyze_btn.click(
|
413 |
+
fn=handle_analysis,
|
414 |
+
inputs=file_input,
|
415 |
+
outputs=[summary_output, result_table, excel_download]
|
416 |
+
)
|
417 |
+
|
418 |
+
file_input.change(
|
419 |
+
fn=handle_analysis,
|
420 |
+
inputs=file_input,
|
421 |
+
outputs=[summary_output, result_table, excel_download]
|
422 |
+
)
|
423 |
+
|
424 |
+
return app
|
425 |
+
|
426 |
+
if __name__ == "__main__":
|
427 |
+
app = create_app()
|
428 |
+
app.launch()
|