ssboost commited on
Commit
bc17107
·
verified ·
1 Parent(s): 81be558

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +265 -188
app.py CHANGED
@@ -1,30 +1,110 @@
 
1
  import gradio as gr
2
  import os
3
  import tempfile
4
  from datetime import datetime
5
  from PIL import Image
 
6
 
7
- # 환경변수에서 API 엔드포인트 가져오기 (코드에서는 절대 노출 안됨)
8
- API_ENDPOINT = os.getenv('API_ENDPOINT')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- if not API_ENDPOINT:
11
- print("❌ API_ENDPOINT 환경변수가 설정되지 않았습니다.")
12
- exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- # 로그 억제된 클라이언트 임포트
15
  try:
16
- from gradio_client import Client, handle_file
17
- print("✅ Gradio Client 임포트 성공")
 
 
 
 
 
18
  except ImportError as e:
19
- print(f"❌ Gradio Client 임포트 오류: {e}")
20
- exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- # 커스텀 CSS 스타일 (기존과 동일)
23
  custom_css = """
24
  /* ============================================
25
  다크모드 자동 변경 템플릿 CSS
26
  ============================================ */
27
-
28
  /* 1. CSS 변수 정의 (라이트모드 - 기본값) */
29
  :root {
30
  /* 메인 컬러 */
@@ -56,7 +136,6 @@ custom_css = """
56
  /* 기타 */
57
  --border-radius: 18px;
58
  }
59
-
60
  /* 2. 다크모드 색상 변수 (자동 감지) */
61
  @media (prefers-color-scheme: dark) {
62
  :root {
@@ -82,7 +161,6 @@ custom_css = """
82
  --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
83
  }
84
  }
85
-
86
  /* 3. 수동 다크모드 클래스 (Gradio 토글용) */
87
  [data-theme="dark"],
88
  .dark,
@@ -108,7 +186,6 @@ custom_css = """
108
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
109
  --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
110
  }
111
-
112
  /* 4. 기본 요소 다크모드 적용 */
113
  body {
114
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
@@ -120,7 +197,6 @@ body {
120
  font-size: 16px;
121
  transition: background-color 0.3s ease, color 0.3s ease;
122
  }
123
-
124
  /* 5. Gradio 컨테이너 강제 적용 */
125
  .gradio-container,
126
  .gradio-container *,
@@ -130,19 +206,16 @@ body {
130
  background-color: var(--background-color) !important;
131
  color: var(--text-color) !important;
132
  }
133
-
134
  /* 푸터 숨김 설정 추가 */
135
  footer {
136
  visibility: hidden;
137
  }
138
-
139
  .gradio-container {
140
  width: 100%;
141
  margin: 0 auto;
142
  padding: 20px;
143
  background-color: var(--background-color);
144
  }
145
-
146
  /* ── 그룹 래퍼 배경 완전 제거 ── */
147
  .custom-section-group,
148
  .gr-block.gr-group {
@@ -156,13 +229,11 @@ footer {
156
  display: none !important;
157
  content: none !important;
158
  }
159
-
160
  /* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */
161
  .custom-section-group {
162
  background-color: var(--background-color) !important;
163
  box-shadow: none !important;
164
  }
165
-
166
  /* 6. 카드 및 패널 스타일 */
167
  .custom-frame,
168
  .gr-form,
@@ -179,7 +250,6 @@ footer {
179
  box-shadow: var(--shadow) !important;
180
  color: var(--text-color) !important;
181
  }
182
-
183
  /* 섹션 그룹 스타일 - 회색 배경 완전 제거 */
184
  .custom-section-group {
185
  margin-top: 20px;
@@ -189,7 +259,6 @@ footer {
189
  background-color: var(--background-color);
190
  box-shadow: none !important;
191
  }
192
-
193
  /* 버튼 스타일 - 글자 크기 18px */
194
  .custom-button {
195
  border-radius: 30px !important;
@@ -205,7 +274,6 @@ footer {
205
  transform: translateY(-2px);
206
  box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
207
  }
208
-
209
  /* 제목 스타일 (모든 항목명이 동일하게 custom-title 클래스로) */
210
  .custom-title {
211
  font-size: 28px;
@@ -215,7 +283,6 @@ footer {
215
  border-bottom: 2px solid var(--primary-color);
216
  padding-bottom: 5px;
217
  }
218
-
219
  /* 이미지 컨테이너 - 크기 고정 */
220
  .image-container {
221
  border-radius: var(--border-radius);
@@ -224,7 +291,6 @@ footer {
224
  transition: all 0.3s ease;
225
  background-color: var(--card-bg);
226
  }
227
-
228
  /* 이미지 업로드 영역 개선 */
229
  .gradio-container .gr-image {
230
  border: 2px dashed var(--border-color) !important;
@@ -232,12 +298,10 @@ footer {
232
  background-color: var(--card-bg) !important;
233
  transition: all 0.3s ease !important;
234
  }
235
-
236
  .gradio-container .gr-image:hover {
237
  border-color: var(--primary-color) !important;
238
  box-shadow: 0 4px 12px rgba(251, 127, 13, 0.15) !important;
239
  }
240
-
241
  /* 업로드 영역 내부 텍스트 */
242
  .gradio-container .gr-image .upload-container,
243
  .gradio-container .gr-image [data-testid="upload-container"] {
@@ -245,14 +309,12 @@ footer {
245
  color: var(--text-color) !important;
246
  border: none !important;
247
  }
248
-
249
  /* 업로드 영역 드래그 안내 텍스트 */
250
  .gradio-container .gr-image .upload-container p,
251
  .gradio-container .gr-image [data-testid="upload-container"] p {
252
  color: var(--text-color) !important;
253
  font-size: 14px !important;
254
  }
255
-
256
  /* 업로드 버튼 스타일 개선 */
257
  .gradio-container .gr-image .upload-container button,
258
  .gradio-container .gr-image [data-testid="upload-container"] button {
@@ -265,13 +327,11 @@ footer {
265
  cursor: pointer !important;
266
  transition: all 0.3s ease !important;
267
  }
268
-
269
  .gradio-container .gr-image .upload-container button:hover,
270
  .gradio-container .gr-image [data-testid="upload-container"] button:hover {
271
  background-color: var(--secondary-color) !important;
272
  transform: translateY(-1px) !important;
273
  }
274
-
275
  /* 업로드 영역 아이콘 */
276
  .gradio-container .gr-image .upload-container svg,
277
  .gradio-container .gr-image [data-testid="upload-container"] svg {
@@ -279,13 +339,11 @@ footer {
279
  width: 32px !important;
280
  height: 32px !important;
281
  }
282
-
283
  /* 이미지가 업로드된 후 표시 영역 */
284
  .gradio-container .gr-image img {
285
  background-color: var(--card-bg) !important;
286
  border-radius: var(--border-radius) !important;
287
  }
288
-
289
  /* 이미지 제거 버튼 */
290
  .gradio-container .gr-image .image-container button,
291
  .gradio-container .gr-image [data-testid="image"] button {
@@ -301,13 +359,11 @@ footer {
301
  cursor: pointer !important;
302
  transition: all 0.3s ease !important;
303
  }
304
-
305
  .gradio-container .gr-image .image-container button:hover,
306
  .gradio-container .gr-image [data-testid="image"] button:hover {
307
  background-color: rgba(255, 255, 255, 1) !important;
308
  transform: scale(1.1) !important;
309
  }
310
-
311
  /* 업로드 이미지 컨테이너 (600x600) */
312
  .upload-image-container {
313
  width: 600px !important;
@@ -317,7 +373,6 @@ footer {
317
  max-width: 600px !important;
318
  max-height: 600px !important;
319
  }
320
-
321
  /* 출력 이미지 컨테이너 (700x600) */
322
  .output-image-container {
323
  width: 700px !important;
@@ -327,17 +382,14 @@ footer {
327
  max-width: 700px !important;
328
  max-height: 600px !important;
329
  }
330
-
331
  .image-container:hover {
332
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
333
  }
334
-
335
  .image-container img {
336
  width: 100% !important;
337
  height: 100% !important;
338
  object-fit: contain !important;
339
  }
340
-
341
  /* Gradio 업로드 이미지 컴포넌트 크기 고정 (600x600) */
342
  .gradio-container .gr-image.upload-image {
343
  width: 600px !important;
@@ -347,7 +399,6 @@ footer {
347
  max-width: 600px !important;
348
  max-height: 600px !important;
349
  }
350
-
351
  /* Gradio 출력 이미지 컴포넌트 크기 고정 (700x600) */
352
  .gradio-container .gr-image.output-image {
353
  width: 700px !important;
@@ -357,7 +408,6 @@ footer {
357
  max-width: 700px !important;
358
  max-height: 600px !important;
359
  }
360
-
361
  /* 이미지 업로드 영역 크기 고정 */
362
  .gradio-container .gr-image.upload-image > div {
363
  width: 600px !important;
@@ -367,7 +417,6 @@ footer {
367
  max-width: 600px !important;
368
  max-height: 600px !important;
369
  }
370
-
371
  /* 이미지 출력 영역 크기 고정 */
372
  .gradio-container .gr-image.output-image > div {
373
  width: 700px !important;
@@ -377,7 +426,6 @@ footer {
377
  max-width: 700px !important;
378
  max-height: 600px !important;
379
  }
380
-
381
  /* 이미지 업로드 드래그 영역 크기 고정 */
382
  .gradio-container .gr-image.upload-image .image-container,
383
  .gradio-container .gr-image.upload-image [data-testid="image"],
@@ -389,7 +437,6 @@ footer {
389
  max-width: 600px !important;
390
  max-height: 600px !important;
391
  }
392
-
393
  /* 이미지 출력 드래그 영역 크기 고정 */
394
  .gradio-container .gr-image.output-image .image-container,
395
  .gradio-container .gr-image.output-image [data-testid="image"],
@@ -401,7 +448,6 @@ footer {
401
  max-width: 700px !important;
402
  max-height: 600px !important;
403
  }
404
-
405
  /* 7. 입력 필드 스타일 */
406
  .gr-input, .gr-text-input, .gr-sample-inputs,
407
  input[type="text"],
@@ -421,7 +467,6 @@ select,
421
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
422
  transition: all 0.3s ease !important;
423
  }
424
-
425
  .gr-input:focus, .gr-text-input:focus,
426
  input[type="text"]:focus,
427
  input[type="number"]:focus,
@@ -435,7 +480,6 @@ select:focus,
435
  outline: none !important;
436
  box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
437
  }
438
-
439
  /* 8. 라벨 및 텍스트 요소 */
440
  .gradio-container label,
441
  label,
@@ -448,12 +492,10 @@ p, span, div {
448
  font-weight: 600 !important;
449
  margin-bottom: 8px !important;
450
  }
451
-
452
  /* 드롭다운 및 라디오 버튼 폰트 크기 */
453
  .gr-radio label, .gr-dropdown label, .gr-checkbox label {
454
  font-size: 15px !important;
455
  }
456
-
457
  /* 라디오 버튼 선택지 볼드 처리 제거 */
458
  .gr-radio .gr-radio-option label,
459
  .gr-radio input[type="radio"] + label,
@@ -461,26 +503,22 @@ p, span, div {
461
  font-weight: normal !important;
462
  font-size: 15px !important;
463
  }
464
-
465
  /* 라디오 버튼 그룹 내 모든 라벨 일반 폰트로 설정 */
466
  .gr-radio fieldset label {
467
  font-weight: normal !important;
468
  }
469
-
470
  /* 마크다운 텍스트 크기 증가 */
471
  .gradio-container .gr-markdown {
472
  font-size: 15px !important;
473
  line-height: 1.6 !important;
474
  color: var(--text-color) !important;
475
  }
476
-
477
  /* 텍스트박스 내용 폰트 크기 */
478
  .gr-textbox textarea, .gr-textbox input {
479
  font-size: 15px !important;
480
  background-color: var(--input-bg) !important;
481
  color: var(--text-color) !important;
482
  }
483
-
484
  /* 아코디언 제목 폰트 크기 */
485
  .gr-accordion summary {
486
  font-size: 17px !important;
@@ -488,44 +526,36 @@ p, span, div {
488
  background-color: var(--card-bg) !important;
489
  color: var(--text-color) !important;
490
  }
491
-
492
  /* 메인 컨텐츠 스크롤바 */
493
  ::-webkit-scrollbar {
494
  width: 8px;
495
  height: 8px;
496
  }
497
-
498
  ::-webkit-scrollbar-track {
499
  background: var(--card-bg);
500
  border-radius: 10px;
501
  }
502
-
503
  ::-webkit-scrollbar-thumb {
504
  background: var(--primary-color);
505
  border-radius: 10px;
506
  }
507
-
508
  ::-webkit-scrollbar-thumb:hover {
509
  background: var(--secondary-color);
510
  }
511
-
512
  /* 애니메이션 스타일 */
513
  @keyframes fadeIn {
514
  from { opacity: 0; transform: translateY(10px); }
515
  to { opacity: 1; transform: translateY(0); }
516
  }
517
-
518
  .fade-in {
519
  animation: fadeIn 0.5s ease-out;
520
  }
521
-
522
  /* 반응형 */
523
  @media (max-width: 768px) {
524
  .button-grid {
525
  grid-template-columns: repeat(2, 1fr);
526
  }
527
  }
528
-
529
  /* 섹션 제목 스타일 */
530
  .section-title {
531
  display: flex;
@@ -538,7 +568,6 @@ p, span, div {
538
  border-bottom: 2px solid var(--primary-color);
539
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
540
  }
541
-
542
  .section-title img {
543
  margin-right: 12px;
544
  width: 28px;
@@ -546,21 +575,18 @@ p, span, div {
546
  /* 다크모드에서 아이콘 필터 적용 */
547
  filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
548
  }
549
-
550
  /* 라이트모드에서는 원래 아이��� 색상 유지 */
551
  @media (prefers-color-scheme: light) {
552
  .section-title img {
553
  filter: none;
554
  }
555
  }
556
-
557
  /* 수동 다크모드 클래스에서도 아이콘 색상 적용 */
558
  [data-theme="dark"] .section-title img,
559
  .dark .section-title img,
560
  .gr-theme-dark .section-title img {
561
  filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
562
  }
563
-
564
  /* 10. 아코디언 및 드롭다운 - 수동설정 영역 회색 배경 제거 */
565
  details,
566
  .gr-accordion,
@@ -571,7 +597,6 @@ details,
571
  border-radius: var(--border-radius) !important;
572
  margin: 10px 0 !important;
573
  }
574
-
575
  details summary,
576
  .gr-accordion summary,
577
  .gr-accordion details summary {
@@ -584,20 +609,17 @@ details summary,
584
  font-weight: 600 !important;
585
  transition: all 0.3s ease !important;
586
  }
587
-
588
  details summary:hover,
589
  .gr-accordion summary:hover,
590
  .gr-accordion details summary:hover {
591
  background-color: var(--table-hover-bg) !important;
592
  }
593
-
594
  /* 아코디언 내부 콘텐츠 */
595
  details[open],
596
  .gr-accordion[open],
597
  .gr-accordion details[open] {
598
  background-color: var(--background-color) !important;
599
  }
600
-
601
  details[open] > *:not(summary),
602
  .gr-accordion[open] > *:not(summary),
603
  .gr-accordion details[open] > *:not(summary) {
@@ -606,7 +628,6 @@ details[open] > *:not(summary),
606
  padding: 16px !important;
607
  border-top: 1px solid var(--border-color) !important;
608
  }
609
-
610
  /* 그룹 내부 스타일 - 수동설정 내부 그룹들 */
611
  .gr-group,
612
  details .gr-group,
@@ -617,7 +638,6 @@ details .gr-group,
617
  margin: 8px 0 !important;
618
  border-radius: var(--border-radius) !important;
619
  }
620
-
621
  /* 그룹 내부 제목 */
622
  .gr-group .gr-markdown h3,
623
  .gr-group h3 {
@@ -628,7 +648,6 @@ details .gr-group,
628
  padding-bottom: 6px !important;
629
  border-bottom: 1px solid var(--border-color) !important;
630
  }
631
-
632
  /* 11. 추가 Gradio 컴포넌트들 */
633
  .gr-block,
634
  .gr-group,
@@ -637,23 +656,12 @@ details .gr-group,
637
  background-color: var(--background-color) !important;
638
  color: var(--text-color) !important;
639
  }
640
-
641
  /* 12. 버튼은 기존 스타일 유지 (primary-color 사용) */
642
  button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) {
643
  background-color: var(--card-bg) !important;
644
  color: var(--text-color) !important;
645
  border-color: var(--border-color) !important;
646
  }
647
-
648
- /* 13. 코드 블록 및 pre 태그 */
649
- code,
650
- pre,
651
- .code-block {
652
- background-color: var(--table-even-bg) !important;
653
- color: var(--text-color) !important;
654
- border-color: var(--border-color) !important;
655
- }
656
-
657
  /* 14. 전환 애니메이션 */
658
  * {
659
  transition: background-color 0.3s ease,
@@ -719,22 +727,13 @@ def prepare_download_file(image, keyword):
719
  image_to_save.save(file_path, 'JPEG', quality=95)
720
 
721
  print(f"✅ 다운로드 파일 준비: {filename}")
 
722
  return file_path
723
 
724
  except Exception as e:
725
  print(f"❌ 다운로드 파일 준비 실패: {e}")
726
  return None
727
 
728
- # 🎯 핵심: 클라이언트 연결 함수 (로그 최소화)
729
- def get_client():
730
- """환경변수 기반 클라이언트 연결"""
731
- try:
732
- client = Client(API_ENDPOINT)
733
- return client
734
- except Exception as e:
735
- print(f"❌ 연결 실패: {str(e)[:50]}...")
736
- return None
737
-
738
  def main():
739
  with gr.Blocks(
740
  css=custom_css,
@@ -927,116 +926,194 @@ def main():
927
  interactive=False
928
  )
929
 
930
- # 🎯 핵심: 엔드포인트 기반 이벤트 핸들러들
931
-
932
- def update_copy_type_description(selected_type):
933
- """원본과 동일한 카피 타입 선택시 설명 업데이트"""
934
- try:
935
- client = get_client()
936
- if not client:
937
- return "❌ 서버 연결 실패"
938
-
939
- result = client.predict(
940
- selected_type=selected_type,
941
- api_name="/update_copy_type_description"
942
- )
943
- return result
944
- except Exception as e:
945
- print(f"❌ 카피 타입 설명 업데이트 실패: {str(e)[:50]}...")
946
- return f"❌ 오류: 설명 업데이트 실패"
947
 
948
  def handle_copy_generation(keyword, selected_type):
949
- """원본과 동일한 카피 생성 처리"""
950
- try:
951
- client = get_client()
952
- if not client:
953
- return ("❌ 서버 연결 실패", {}, "", "", "", "", "", "", "", keyword.strip() if keyword else "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
 
955
- result = client.predict(
956
- keyword=keyword,
957
- selected_type=selected_type,
958
- api_name="/handle_copy_generation"
959
- )
960
 
961
- # 원본과 동일한 튜플 반환 (10개 요소)
962
- # [0] 상태메시지, [1] copy_suggestions_state, [2-6] 카피1-5, [7-8] 메인/서브, [9] current_keyword_state
963
- if len(result) >= 8:
964
- # API에서 8개를 받으면 추가 상태값들을 로컬에서 처리
965
- return (result[0], {}, result[1], result[2], result[3], result[4], result[5], result[6], result[7], keyword.strip() if keyword else "")
966
- else:
967
- return result
 
968
 
969
- except Exception as e:
970
- print(f"❌ 카피 생성 실패: {str(e)[:50]}...")
971
- error_msg = "❌ 카피 생성 중 오류가 발생했습니다"
972
- return (error_msg, {}, "", "", "", "", "", "", "", keyword.strip() if keyword else "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
 
974
- def handle_copy_selection(copy_suggestions_state, selected_type, selected_copy):
975
- """원본과 동일한 카피 선택시 메인/서브 텍스트박스 업데이트"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
  try:
977
- client = get_client()
978
- if not client:
979
- return ("", "")
980
-
981
- result = client.predict(
982
- selected_type=selected_type,
983
- selected_copy=selected_copy,
984
- api_name="/handle_copy_selection"
985
- )
 
 
 
 
 
 
986
 
987
- # 튜플 반환 (2개 요소)
988
- return result
989
 
990
  except Exception as e:
991
- print(f"❌ 카피 선택 실패: {str(e)[:50]}...")
992
- return ("", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
 
994
  def handle_image_generation(input_image, main_text, sub_text, color_mode,
995
  main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color,
996
  manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
997
  top_bottom_margin, text_gap, current_keyword):
998
- """원본과 동일한 이미지 생성 처리 (여백 조정 기능 포함)"""
999
- try:
1000
- client = get_client()
1001
- if not client:
1002
- return (None, color_mode, manual_bg_color, manual_main_text_color,
1003
- manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
1004
- None, "❌ 서버 연결 실패")
1005
-
1006
- # 이미지 파일 핸들링
1007
- image_file = None
1008
- if input_image:
1009
- image_file = handle_file(input_image)
1010
-
1011
- result = client.predict(
1012
- input_image=image_file,
1013
- main_text=main_text,
1014
- sub_text=sub_text,
1015
- color_mode=color_mode,
1016
- main_font_choice=main_font_choice,
1017
- sub_font_choice=sub_font_choice,
1018
- manual_bg_color=manual_bg_color,
1019
- manual_main_text_color=manual_main_text_color,
1020
- manual_sub_text_color=manual_sub_text_color,
1021
- manual_main_font_size=manual_main_font_size,
1022
- manual_sub_font_size=manual_sub_font_size,
1023
- top_bottom_margin=top_bottom_margin,
1024
- text_gap=text_gap,
1025
- api_name="/handle_image_generation"
1026
- )
1027
-
1028
- # 원본과 동일한 튜플 반환 (9개 요소)
1029
- # [0] 이미지, [1] 색상모드, [2] 배경색, [3] 메인텍스트색, [4] 서브텍스트색,
1030
- # [5] 메인폰트크기, [6] 서브폰트크기, [7] 다운로드파일, [8] 여백정보
1031
- return result
1032
-
1033
- except Exception as e:
1034
- print(f"❌ 이미지 생성 실패: {str(e)[:50]}...")
1035
- return (None, color_mode, manual_bg_color, manual_main_text_color,
1036
- manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
1037
- None, "❌ 이미지 생성 중 오류가 발생했습니다")
1038
 
1039
- # 원본과 동일한 이벤트 연결
1040
  copy_type_selection.change(
1041
  fn=update_copy_type_description,
1042
  inputs=[copy_type_selection],
 
1
+ #app.py
2
  import gradio as gr
3
  import os
4
  import tempfile
5
  from datetime import datetime
6
  from PIL import Image
7
+ import random
8
 
9
+ # 🔑 허깅페이스 Secrets에서 API 키들 가져와서 랜덤 선택
10
+ def get_random_gemini_api_key():
11
+ """허깅페이스 Secrets에서 API 키 리스트를 가져와 랜덤 선택 (디버깅 강화)"""
12
+ try:
13
+ print("🔍 API 키 로딩 시작...")
14
+
15
+ # Secrets에서 API 키 리스트 가져오기 (콤마로 구분된 문자열)
16
+ api_keys_string = os.environ.get('GEMINI_API_KEYS')
17
+ print(f"🔍 GEMINI_API_KEYS 원본: {api_keys_string[:50] if api_keys_string else 'None'}...")
18
+
19
+ if api_keys_string:
20
+ # 콤마로 분리하고 공백 제거
21
+ api_keys = [key.strip() for key in api_keys_string.split(',') if key.strip()]
22
+ print(f"🔍 파싱된 키 개수: {len(api_keys)}")
23
+
24
+ for i, key in enumerate(api_keys):
25
+ print(f" 키 {i+1}: {key[:8]}***{key[-4:] if len(key) > 12 else '***'} (길이: {len(key)})")
26
+
27
+ if api_keys:
28
+ # 랜덤하게 하나 선택
29
+ selected_key = random.choice(api_keys)
30
+ print(f"🎲 랜덤 API 키 선택: {selected_key[:8]}***{selected_key[-4:]}")
31
+ return selected_key
32
+ else:
33
+ print("❌ 파싱된 키가 없음")
34
+ else:
35
+ print("❌ GEMINI_API_KEYS 환경변수가 없음")
36
+
37
+ # 폴백: 단일 키
38
+ fallback_key = os.environ.get('GEMINI_API_KEY')
39
+ if fallback_key:
40
+ print(f"🔑 폴백 API 키 사용: {fallback_key[:8]}***{fallback_key[-4:]}")
41
+ return fallback_key
42
+
43
+ print("❌ 모든 API 키를 찾을 수 없습니다")
44
+ return None
45
+
46
+ except Exception as e:
47
+ print(f"❌ API 키 선택 실패: {e}")
48
+ return os.environ.get('GEMINI_API_KEY')
49
 
50
+ # 허깅페이스 Secrets에서 모듈 코드 불러와서 실행
51
+ def load_module_from_env(env_var_name):
52
+ """환경변수에서 코드를 불러와서 모듈로 실행"""
53
+ try:
54
+ code = os.environ.get(env_var_name)
55
+ if not code:
56
+ raise ImportError(f"Environment variable '{env_var_name}' not found")
57
+
58
+ # 모듈 namespace 생성하고 코드 실행
59
+ module_globals = {}
60
+ exec(code, module_globals)
61
+
62
+ print(f"✅ {env_var_name} 모듈 로드 성공")
63
+ return module_globals
64
+
65
+ except Exception as e:
66
+ print(f"❌ {env_var_name} 모듈 로드 실패: {e}")
67
+ return None
68
 
69
+ # 이미지 처리 모듈 임포트
70
  try:
71
+ image_processor_module = load_module_from_env('IMAGE_PROCESSOR_CODE')
72
+ if image_processor_module:
73
+ create_uhp_image = image_processor_module['create_uhp_image']
74
+ save_image_to_downloads = image_processor_module['save_image_to_downloads']
75
+ print("✅ image_processor 모듈 임포트 성공")
76
+ else:
77
+ raise ImportError("IMAGE_PROCESSOR_CODE not found")
78
  except ImportError as e:
79
+ print(f"❌ image_processor 임포트 오류: {e}")
80
+
81
+ def create_uhp_image(*args, **kwargs):
82
+ return None, "추천배경", "#FFFFFF", "#000000", "#000000", 100, 55, {}
83
+
84
+ def save_image_to_downloads(*args, **kwargs):
85
+ return None, "image_processor 모듈을 찾을 수 없습니다."
86
+
87
+ # 카피 생성 모듈 임포트
88
+ try:
89
+ copy_generator_module = load_module_from_env('COPY_GENERATOR_CODE')
90
+ if copy_generator_module:
91
+ generate_copy_suggestions = copy_generator_module['generate_copy_suggestions']
92
+ print("✅ copy_generator 모듈 임포트 성공")
93
+ else:
94
+ raise ImportError("COPY_GENERATOR_CODE not found")
95
+ except ImportError as e:
96
+ print(f"⚠️ copy_generator 임포트 오류: {e}")
97
+
98
+ def generate_copy_suggestions(keyword, api_key, selected_type):
99
+ return {
100
+ "error": "copy_generator 모듈을 찾을 수 없습니다. 카피 생성 기능이 비활성화됩니다."
101
+ }, ""
102
 
103
+ # 커스텀 CSS 스타일 (다크모드 적용 버전)
104
  custom_css = """
105
  /* ============================================
106
  다크모드 자동 변경 템플릿 CSS
107
  ============================================ */
 
108
  /* 1. CSS 변수 정의 (라이트모드 - 기본값) */
109
  :root {
110
  /* 메인 컬러 */
 
136
  /* 기타 */
137
  --border-radius: 18px;
138
  }
 
139
  /* 2. 다크모드 색상 변수 (자동 감지) */
140
  @media (prefers-color-scheme: dark) {
141
  :root {
 
161
  --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
162
  }
163
  }
 
164
  /* 3. 수동 다크모드 클래스 (Gradio 토글용) */
165
  [data-theme="dark"],
166
  .dark,
 
186
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
187
  --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
188
  }
 
189
  /* 4. 기본 요소 다크모드 적용 */
190
  body {
191
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
 
197
  font-size: 16px;
198
  transition: background-color 0.3s ease, color 0.3s ease;
199
  }
 
200
  /* 5. Gradio 컨테이너 강제 적용 */
201
  .gradio-container,
202
  .gradio-container *,
 
206
  background-color: var(--background-color) !important;
207
  color: var(--text-color) !important;
208
  }
 
209
  /* 푸터 숨김 설정 추가 */
210
  footer {
211
  visibility: hidden;
212
  }
 
213
  .gradio-container {
214
  width: 100%;
215
  margin: 0 auto;
216
  padding: 20px;
217
  background-color: var(--background-color);
218
  }
 
219
  /* ── 그룹 래퍼 배경 완전 제거 ── */
220
  .custom-section-group,
221
  .gr-block.gr-group {
 
229
  display: none !important;
230
  content: none !important;
231
  }
 
232
  /* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */
233
  .custom-section-group {
234
  background-color: var(--background-color) !important;
235
  box-shadow: none !important;
236
  }
 
237
  /* 6. 카드 및 패널 스타일 */
238
  .custom-frame,
239
  .gr-form,
 
250
  box-shadow: var(--shadow) !important;
251
  color: var(--text-color) !important;
252
  }
 
253
  /* 섹션 그룹 스타일 - 회색 배경 완전 제거 */
254
  .custom-section-group {
255
  margin-top: 20px;
 
259
  background-color: var(--background-color);
260
  box-shadow: none !important;
261
  }
 
262
  /* 버튼 스타일 - 글자 크기 18px */
263
  .custom-button {
264
  border-radius: 30px !important;
 
274
  transform: translateY(-2px);
275
  box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
276
  }
 
277
  /* 제목 스타일 (모든 항목명이 동일하게 custom-title 클래스로) */
278
  .custom-title {
279
  font-size: 28px;
 
283
  border-bottom: 2px solid var(--primary-color);
284
  padding-bottom: 5px;
285
  }
 
286
  /* 이미지 컨테이너 - 크기 고정 */
287
  .image-container {
288
  border-radius: var(--border-radius);
 
291
  transition: all 0.3s ease;
292
  background-color: var(--card-bg);
293
  }
 
294
  /* 이미지 업로드 영역 개선 */
295
  .gradio-container .gr-image {
296
  border: 2px dashed var(--border-color) !important;
 
298
  background-color: var(--card-bg) !important;
299
  transition: all 0.3s ease !important;
300
  }
 
301
  .gradio-container .gr-image:hover {
302
  border-color: var(--primary-color) !important;
303
  box-shadow: 0 4px 12px rgba(251, 127, 13, 0.15) !important;
304
  }
 
305
  /* 업로드 영역 내부 텍스트 */
306
  .gradio-container .gr-image .upload-container,
307
  .gradio-container .gr-image [data-testid="upload-container"] {
 
309
  color: var(--text-color) !important;
310
  border: none !important;
311
  }
 
312
  /* 업로드 영역 드래그 안내 텍스트 */
313
  .gradio-container .gr-image .upload-container p,
314
  .gradio-container .gr-image [data-testid="upload-container"] p {
315
  color: var(--text-color) !important;
316
  font-size: 14px !important;
317
  }
 
318
  /* 업로드 버튼 스타일 개선 */
319
  .gradio-container .gr-image .upload-container button,
320
  .gradio-container .gr-image [data-testid="upload-container"] button {
 
327
  cursor: pointer !important;
328
  transition: all 0.3s ease !important;
329
  }
 
330
  .gradio-container .gr-image .upload-container button:hover,
331
  .gradio-container .gr-image [data-testid="upload-container"] button:hover {
332
  background-color: var(--secondary-color) !important;
333
  transform: translateY(-1px) !important;
334
  }
 
335
  /* 업로드 영역 아이콘 */
336
  .gradio-container .gr-image .upload-container svg,
337
  .gradio-container .gr-image [data-testid="upload-container"] svg {
 
339
  width: 32px !important;
340
  height: 32px !important;
341
  }
 
342
  /* 이미지가 업로드된 후 표시 영역 */
343
  .gradio-container .gr-image img {
344
  background-color: var(--card-bg) !important;
345
  border-radius: var(--border-radius) !important;
346
  }
 
347
  /* 이미지 제거 버튼 */
348
  .gradio-container .gr-image .image-container button,
349
  .gradio-container .gr-image [data-testid="image"] button {
 
359
  cursor: pointer !important;
360
  transition: all 0.3s ease !important;
361
  }
 
362
  .gradio-container .gr-image .image-container button:hover,
363
  .gradio-container .gr-image [data-testid="image"] button:hover {
364
  background-color: rgba(255, 255, 255, 1) !important;
365
  transform: scale(1.1) !important;
366
  }
 
367
  /* 업로드 이미지 컨테이너 (600x600) */
368
  .upload-image-container {
369
  width: 600px !important;
 
373
  max-width: 600px !important;
374
  max-height: 600px !important;
375
  }
 
376
  /* 출력 이미지 컨테이너 (700x600) */
377
  .output-image-container {
378
  width: 700px !important;
 
382
  max-width: 700px !important;
383
  max-height: 600px !important;
384
  }
 
385
  .image-container:hover {
386
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
387
  }
 
388
  .image-container img {
389
  width: 100% !important;
390
  height: 100% !important;
391
  object-fit: contain !important;
392
  }
 
393
  /* Gradio 업로드 이미지 컴포넌트 크기 고정 (600x600) */
394
  .gradio-container .gr-image.upload-image {
395
  width: 600px !important;
 
399
  max-width: 600px !important;
400
  max-height: 600px !important;
401
  }
 
402
  /* Gradio 출력 이미지 컴포넌트 크기 고정 (700x600) */
403
  .gradio-container .gr-image.output-image {
404
  width: 700px !important;
 
408
  max-width: 700px !important;
409
  max-height: 600px !important;
410
  }
 
411
  /* 이미지 업로드 영역 크기 고정 */
412
  .gradio-container .gr-image.upload-image > div {
413
  width: 600px !important;
 
417
  max-width: 600px !important;
418
  max-height: 600px !important;
419
  }
 
420
  /* 이미지 출력 영역 크기 고정 */
421
  .gradio-container .gr-image.output-image > div {
422
  width: 700px !important;
 
426
  max-width: 700px !important;
427
  max-height: 600px !important;
428
  }
 
429
  /* 이미지 업로드 드래그 영역 크기 고정 */
430
  .gradio-container .gr-image.upload-image .image-container,
431
  .gradio-container .gr-image.upload-image [data-testid="image"],
 
437
  max-width: 600px !important;
438
  max-height: 600px !important;
439
  }
 
440
  /* 이미지 출력 드래그 영역 크기 고정 */
441
  .gradio-container .gr-image.output-image .image-container,
442
  .gradio-container .gr-image.output-image [data-testid="image"],
 
448
  max-width: 700px !important;
449
  max-height: 600px !important;
450
  }
 
451
  /* 7. 입력 필드 스타일 */
452
  .gr-input, .gr-text-input, .gr-sample-inputs,
453
  input[type="text"],
 
467
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important;
468
  transition: all 0.3s ease !important;
469
  }
 
470
  .gr-input:focus, .gr-text-input:focus,
471
  input[type="text"]:focus,
472
  input[type="number"]:focus,
 
480
  outline: none !important;
481
  box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
482
  }
 
483
  /* 8. 라벨 및 텍스트 요소 */
484
  .gradio-container label,
485
  label,
 
492
  font-weight: 600 !important;
493
  margin-bottom: 8px !important;
494
  }
 
495
  /* 드롭다운 및 라디오 버튼 폰트 크기 */
496
  .gr-radio label, .gr-dropdown label, .gr-checkbox label {
497
  font-size: 15px !important;
498
  }
 
499
  /* 라디오 버튼 선택지 볼드 처리 제거 */
500
  .gr-radio .gr-radio-option label,
501
  .gr-radio input[type="radio"] + label,
 
503
  font-weight: normal !important;
504
  font-size: 15px !important;
505
  }
 
506
  /* 라디오 버튼 그룹 내 모든 라벨 일반 폰트로 설정 */
507
  .gr-radio fieldset label {
508
  font-weight: normal !important;
509
  }
 
510
  /* 마크다운 텍스트 크기 증가 */
511
  .gradio-container .gr-markdown {
512
  font-size: 15px !important;
513
  line-height: 1.6 !important;
514
  color: var(--text-color) !important;
515
  }
 
516
  /* 텍스트박스 내용 폰트 크기 */
517
  .gr-textbox textarea, .gr-textbox input {
518
  font-size: 15px !important;
519
  background-color: var(--input-bg) !important;
520
  color: var(--text-color) !important;
521
  }
 
522
  /* 아코디언 제목 폰트 크기 */
523
  .gr-accordion summary {
524
  font-size: 17px !important;
 
526
  background-color: var(--card-bg) !important;
527
  color: var(--text-color) !important;
528
  }
 
529
  /* 메인 컨텐츠 스크롤바 */
530
  ::-webkit-scrollbar {
531
  width: 8px;
532
  height: 8px;
533
  }
 
534
  ::-webkit-scrollbar-track {
535
  background: var(--card-bg);
536
  border-radius: 10px;
537
  }
 
538
  ::-webkit-scrollbar-thumb {
539
  background: var(--primary-color);
540
  border-radius: 10px;
541
  }
 
542
  ::-webkit-scrollbar-thumb:hover {
543
  background: var(--secondary-color);
544
  }
 
545
  /* 애니메이션 스타일 */
546
  @keyframes fadeIn {
547
  from { opacity: 0; transform: translateY(10px); }
548
  to { opacity: 1; transform: translateY(0); }
549
  }
 
550
  .fade-in {
551
  animation: fadeIn 0.5s ease-out;
552
  }
 
553
  /* 반응형 */
554
  @media (max-width: 768px) {
555
  .button-grid {
556
  grid-template-columns: repeat(2, 1fr);
557
  }
558
  }
 
559
  /* 섹션 제목 스타일 */
560
  .section-title {
561
  display: flex;
 
568
  border-bottom: 2px solid var(--primary-color);
569
  font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
570
  }
 
571
  .section-title img {
572
  margin-right: 12px;
573
  width: 28px;
 
575
  /* 다크모드에서 아이콘 필터 적용 */
576
  filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
577
  }
 
578
  /* 라이트모드에서는 원래 아이��� 색상 유지 */
579
  @media (prefers-color-scheme: light) {
580
  .section-title img {
581
  filter: none;
582
  }
583
  }
 
584
  /* 수동 다크모드 클래스에서도 아이콘 색상 적용 */
585
  [data-theme="dark"] .section-title img,
586
  .dark .section-title img,
587
  .gr-theme-dark .section-title img {
588
  filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
589
  }
 
590
  /* 10. 아코디언 및 드롭다운 - 수동설정 영역 회색 배경 제거 */
591
  details,
592
  .gr-accordion,
 
597
  border-radius: var(--border-radius) !important;
598
  margin: 10px 0 !important;
599
  }
 
600
  details summary,
601
  .gr-accordion summary,
602
  .gr-accordion details summary {
 
609
  font-weight: 600 !important;
610
  transition: all 0.3s ease !important;
611
  }
 
612
  details summary:hover,
613
  .gr-accordion summary:hover,
614
  .gr-accordion details summary:hover {
615
  background-color: var(--table-hover-bg) !important;
616
  }
 
617
  /* 아코디언 내부 콘텐츠 */
618
  details[open],
619
  .gr-accordion[open],
620
  .gr-accordion details[open] {
621
  background-color: var(--background-color) !important;
622
  }
 
623
  details[open] > *:not(summary),
624
  .gr-accordion[open] > *:not(summary),
625
  .gr-accordion details[open] > *:not(summary) {
 
628
  padding: 16px !important;
629
  border-top: 1px solid var(--border-color) !important;
630
  }
 
631
  /* 그룹 내부 스타일 - 수동설정 내부 그룹들 */
632
  .gr-group,
633
  details .gr-group,
 
638
  margin: 8px 0 !important;
639
  border-radius: var(--border-radius) !important;
640
  }
 
641
  /* 그룹 내부 제목 */
642
  .gr-group .gr-markdown h3,
643
  .gr-group h3 {
 
648
  padding-bottom: 6px !important;
649
  border-bottom: 1px solid var(--border-color) !important;
650
  }
 
651
  /* 11. 추가 Gradio 컴포넌트들 */
652
  .gr-block,
653
  .gr-group,
 
656
  background-color: var(--background-color) !important;
657
  color: var(--text-color) !important;
658
  }
 
659
  /* 12. 버튼은 기존 스타일 유지 (primary-color 사용) */
660
  button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) {
661
  background-color: var(--card-bg) !important;
662
  color: var(--text-color) !important;
663
  border-color: var(--border-color) !important;
664
  }
 
 
 
 
 
 
 
 
 
 
665
  /* 14. 전환 애니메이션 */
666
  * {
667
  transition: background-color 0.3s ease,
 
727
  image_to_save.save(file_path, 'JPEG', quality=95)
728
 
729
  print(f"✅ 다운로드 파일 준비: {filename}")
730
+ print(f"📁 파일 경로: {file_path}")
731
  return file_path
732
 
733
  except Exception as e:
734
  print(f"❌ 다운로드 파일 준비 실패: {e}")
735
  return None
736
 
 
 
 
 
 
 
 
 
 
 
737
  def main():
738
  with gr.Blocks(
739
  css=custom_css,
 
926
  interactive=False
927
  )
928
 
929
+ # 이벤트 핸들러들
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
 
931
  def handle_copy_generation(keyword, selected_type):
932
+ """카피 생성 처리 (랜덤 API 키 사용)"""
933
+ # 🎲 랜덤 API 키 선택
934
+ api_key = get_random_gemini_api_key()
935
+ print(f"🔑 카피 생성용 API 키: {api_key[:8] if api_key else 'None'}***")
936
+
937
+ if not keyword.strip():
938
+ return ("⚠️ 상품 키워드를 입력해주세���.", {}, "", "", "", "", "", "", "", keyword.strip())
939
+
940
+ if not api_key:
941
+ return ("⚠️ API 키가 설정되지 않았습니다.", {}, "", "", "", "", "", "", "", keyword.strip())
942
+
943
+ if not selected_type:
944
+ return ("⚠️ 카피 타입을 먼저 선택해주세요.", {}, "", "", "", "", "", "", "", keyword.strip())
945
+
946
+ print(f"🚀 카피 생성 프로세스 시작: {keyword} - {selected_type}")
947
+ suggestions, analysis = generate_copy_suggestions(keyword, api_key, selected_type)
948
+
949
+ print(f"🔍 AI 응답 타입: {type(suggestions)}")
950
+ print(f"🔍 AI 응답 키들: {list(suggestions.keys()) if isinstance(suggestions, dict) else 'Not dict'}")
951
+
952
+ if "error" not in suggestions:
953
+ print("✅ 카피 생성 성공!")
954
 
955
+ # 선택된 타입의 카피 리스트 가져오기
956
+ copy_list = suggestions.get(selected_type, [])
957
+ print(f"📋 {selected_type} 카피 개수: {len(copy_list)}")
 
 
958
 
959
+ # 카피 아이템 검증
960
+ for i, item in enumerate(copy_list):
961
+ if isinstance(item, dict):
962
+ main = item.get("main", "")
963
+ sub = item.get("sub", "")
964
+ print(f" 추천{i+1}: 메인='{main}', 서브='{sub}'")
965
+ else:
966
+ print(f" 추천{i+1}: 잘못된 형식 {type(item)}")
967
 
968
+ # 표시용 카피 문자열 생성 (추천1~5 텍스트박스용)
969
+ copy_values = []
970
+ for i in range(5):
971
+ if i < len(copy_list) and isinstance(copy_list[i], dict):
972
+ main = copy_list[i].get("main", "")
973
+ sub = copy_list[i].get("sub", "")
974
+ combined = f"{main} / {sub}"
975
+ copy_values.append(combined)
976
+ print(f"📝 추천{i+1} 표시: {combined}")
977
+ else:
978
+ copy_values.append("")
979
+ print(f"📝 추천{i+1} 표시: (비어있음)")
980
+
981
+ # 첫 번째 카피를 메인/서브 텍스트박스에 자동 입력
982
+ first_main = ""
983
+ first_sub = ""
984
+ if copy_list and isinstance(copy_list[0], dict):
985
+ first_main = copy_list[0].get("main", "")
986
+ first_sub = copy_list[0].get("sub", "")
987
+ print(f"🎯 자동 선택: 메인='{first_main}', 서브='{first_sub}'")
988
+
989
+ return (f"✅ {selected_type} 카피 생성 완료!", suggestions, *copy_values, first_main, first_sub, keyword.strip())
990
+ else:
991
+ print(f"❌ 카피 생성 실패: {suggestions['error']}")
992
+ error_msg = f"❌ 오류: {suggestions['error']}"
993
+ return (error_msg, {}, "", "", "", "", "", "", "", keyword.strip())
994
+
995
+ def update_copy_type_description(selected_type):
996
+ """카피 타입 선택시 설명 업데이트"""
997
+ descriptions = {
998
+ "장점요약형": "**장점요약형**: 제품의 장점을 한눈에 강조 - 핵심 기능과 혜택을 간결하게 요약하여 제시",
999
+ "문제제시형": "**문제제시형**: 문제를 제시 후 해결책 제안 - 고객의 불편함을 먼저 언급하고 해결방안 제시",
1000
+ "사회적증거형": "**사회적증거형**: 신뢰와 인기를 강조 - 타인의 사용후기와 검증된 실적으로 신뢰성 어필",
1001
+ "긴급성유도형": "**긴급성유도형**: 즉시 구매를 유도 - 한정수량, 시간제한 등으로 긴급감 조성",
1002
+ "가격경쟁력형": "**가격경쟁력형**: 합리적 가격을 강조해 성취 - 가성비, 할인��택 등 경제적 이득 강조",
1003
+ "매인변화형": "**매인변화형**: 강한 비교를 시선 집중 - 사용 전후 변화나 극적인 개선 효과 부각",
1004
+ "충동구매유도형": "**충동구매유도형**: 특정 구매욕구를 자극 - 특별함과 프리미엄 가치로 소유욕 자극",
1005
+ "공포소구형": "**공포소구형**: 불안 위험을 강조해 감정 유발 - 놓치면 후회할 기회나 위험성을 경고"
1006
+ }
1007
+
1008
+ if selected_type and selected_type in descriptions:
1009
+ return descriptions[selected_type]
1010
+ else:
1011
+ return "### 카피 타입을 선택하면 설명이 표시됩니다."
1012
 
1013
+ def handle_copy_selection(suggestions, selected_type, selected_copy):
1014
+ """카피 선택시 메인/서브 텍스트박스 업데이트 (개선된 버전)"""
1015
+ print(f"🔘 라디오 선택: {selected_copy}, 타입: {selected_type}")
1016
+ print(f"🔍 suggestions 상태: {type(suggestions)}, 키: {list(suggestions.keys()) if suggestions else 'None'}")
1017
+
1018
+ if not suggestions or "error" in suggestions:
1019
+ print("❌ suggestions 데이터가 없거나 에러 상태")
1020
+ return "", ""
1021
+
1022
+ if not selected_type or not selected_copy:
1023
+ print("❌ 타입이나 선택된 카피가 없음")
1024
+ return "", ""
1025
+
1026
+ if selected_type not in suggestions:
1027
+ print(f"❌ {selected_type} 타입을 suggestions에서 찾을 수 없음")
1028
+ print(f"📋 사용 가능한 타입들: {list(suggestions.keys())}")
1029
+ return "", ""
1030
+
1031
+ copy_list = suggestions[selected_type]
1032
+ if not isinstance(copy_list, list):
1033
+ print(f"❌ {selected_type} 데이터가 리스트가 아님: {type(copy_list)}")
1034
+ return "", ""
1035
+
1036
+ print(f"📝 {selected_type} 카피 리스트: {len(copy_list)}개")
1037
+
1038
+ # 라디오 버튼 선택값에서 번호 추출 (더 안전한 방법)
1039
  try:
1040
+ if "추천 1" in selected_copy:
1041
+ option_number = 0
1042
+ elif "추천 2" in selected_copy:
1043
+ option_number = 1
1044
+ elif "추천 3" in selected_copy:
1045
+ option_number = 2
1046
+ elif "추천 4" in selected_copy:
1047
+ option_number = 3
1048
+ elif "추천 5" in selected_copy:
1049
+ option_number = 4
1050
+ else:
1051
+ # 백업 방법: 숫자 추출
1052
+ import re
1053
+ numbers = re.findall(r'\d+', selected_copy)
1054
+ option_number = int(numbers[0]) - 1 if numbers else 0
1055
 
1056
+ print(f"🔢 추출된 옵션 번호: {option_number}")
 
1057
 
1058
  except Exception as e:
1059
+ print(f"❌ 옵션 번호 추출 실패: {e}, 기본값 0 사용")
1060
+ option_number = 0
1061
+
1062
+ # 범위 체크
1063
+ if 0 <= option_number < len(copy_list):
1064
+ selected_copy_item = copy_list[option_number]
1065
+
1066
+ if not isinstance(selected_copy_item, dict):
1067
+ print(f"❌ 선택된 카피 아이템이 딕셔너리가 아님: {type(selected_copy_item)}")
1068
+ return "", ""
1069
+
1070
+ main_text = selected_copy_item.get("main", "")
1071
+ sub_text = selected_copy_item.get("sub", "")
1072
+
1073
+ print(f"✅ 선택된 카피 - 메인: '{main_text}', 서브: '{sub_text}'")
1074
+
1075
+ # 빈 문자열 체크
1076
+ if not main_text and not sub_text:
1077
+ print("⚠️ 메인카피와 서브카피가 모두 비어있음")
1078
+
1079
+ return main_text, sub_text
1080
+ else:
1081
+ print(f"❌ 잘못된 옵션 번호: {option_number} (범위: 0~{len(copy_list)-1})")
1082
+ return "", ""
1083
 
1084
  def handle_image_generation(input_image, main_text, sub_text, color_mode,
1085
  main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color,
1086
  manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
1087
  top_bottom_margin, text_gap, current_keyword):
1088
+ """이미지 생성 처리 (여백 조정 기능 포함)"""
1089
+ # 🎲 랜덤 API 키 선택
1090
+ api_key = get_random_gemini_api_key()
1091
+ print(f"🔑 이미지 생성용 API 키: {api_key[:8] if api_key else 'None'}***")
1092
+
1093
+ # 🎯 NEW: 여백 설정을 create_uhp_image 함수에 전달
1094
+ image_result, color_mode_result, new_bg_color, new_main_text_color, new_sub_text_color, new_main_font_size, new_sub_font_size, applied_margin_info = create_uhp_image(
1095
+ input_image, main_text, sub_text, color_mode,
1096
+ main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color, manual_sub_text_color,
1097
+ manual_main_font_size, manual_sub_font_size, api_key,
1098
+ top_bottom_margin, text_gap # 새로운 여백 파라미터 추가
1099
+ )
1100
+
1101
+ # 🎯 핵심: 다운로드 파일 준비
1102
+ download_file_path = None
1103
+ if image_result is not None:
1104
+ download_file_path = prepare_download_file(image_result, current_keyword)
1105
+
1106
+ # 🎯 NEW: 적용된 여백 정보 생성
1107
+ margin_info_text = f"""📏 **적용된 여백 정보:**
1108
+ • 상하 여백: {applied_margin_info.get('top_bottom_margin', 0)}px
1109
+ • 메인↔서브 간격: {applied_margin_info.get('text_gap', 0)}px
1110
+ • 이미지 크기: {applied_margin_info.get('canvas_width', 0)} × {applied_margin_info.get('canvas_height', 0)}px
1111
+ • 원본 크기: {applied_margin_info.get('original_width', 0)} × {applied_margin_info.get('original_height', 0)}px"""
1112
+
1113
+ return (image_result, color_mode_result, new_bg_color, new_main_text_color, new_sub_text_color,
1114
+ new_main_font_size, new_sub_font_size, download_file_path, margin_info_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
1115
 
1116
+ # 이벤트 연결
1117
  copy_type_selection.change(
1118
  fn=update_copy_type_description,
1119
  inputs=[copy_type_selection],