File size: 19,872 Bytes
b082ee3
 
 
 
e82962e
 
4298a84
b082ee3
4298a84
b082ee3
 
 
 
 
 
e82962e
b082ee3
 
 
 
 
 
 
 
e82962e
b082ee3
 
 
 
 
 
4298a84
b082ee3
 
 
55ec4cb
b082ee3
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
78f19de
b082ee3
 
 
 
 
78f19de
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78f19de
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e82962e
b082ee3
 
 
 
 
 
 
 
55ec4cb
b082ee3
 
 
 
 
 
4298a84
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4298a84
b082ee3
 
 
 
 
 
 
 
 
 
4298a84
b082ee3
4298a84
b082ee3
 
 
 
 
 
4298a84
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
4298a84
b082ee3
 
4298a84
b082ee3
 
 
7046eff
b082ee3
 
7046eff
b082ee3
 
7046eff
b082ee3
 
0228286
55ec4cb
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4298a84
b082ee3
 
 
 
 
4298a84
b082ee3
 
 
7046eff
b082ee3
7046eff
b082ee3
 
 
7046eff
b082ee3
 
 
 
 
 
 
 
 
 
7046eff
b082ee3
 
 
 
 
 
 
 
 
 
 
 
55ec4cb
b082ee3
 
55ec4cb
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55ec4cb
 
b082ee3
55ec4cb
 
b082ee3
55ec4cb
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
55ec4cb
b082ee3
 
 
 
55ec4cb
b082ee3
 
 
 
 
 
4298a84
b082ee3
 
 
 
 
 
 
 
 
 
 
 
e82962e
b082ee3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e82962e
 
b082ee3
4298a84
b082ee3
 
 
 
 
 
 
 
55ec4cb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# ──────────────────────────────── Imports ────────────────────────────────
import os, json, re, logging, requests, markdown, time
from datetime import datetime

import streamlit as st
import anthropic
from gradio_client import Client
# from bs4 import BeautifulSoup   # ν•„μš” μ‹œ 주석 ν•΄μ œ

# ──────────────────────────────── ν™˜κ²½ λ³€μˆ˜ / μƒμˆ˜ ───────────────────────────
ANTHROPIC_KEY  = os.getenv("API_KEY", "")
BRAVE_KEY      = os.getenv("SERPHOUSE_API_KEY", "")  # 이름 μœ μ§€
BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
IMAGE_API_URL  = "http://211.233.58.201:7896"
MAX_TOKENS     = 7_999

# λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ μ •μ˜
BLOG_TEMPLATES = {
    "standard": "μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€λ‘œμ„œ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό 따라 μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 κΈ€ μž‘μ„±",
    "tutorial": "단계별 νŠœν† λ¦¬μ–Ό ν˜•μ‹μœΌλ‘œ, λͺ…ν™•ν•œ κ³Όμ •κ³Ό κ²°κ³Όλ₯Ό λ³΄μ—¬μ£ΌλŠ” κ°€μ΄λ“œ μž‘μ„±",
    "review": "μ œν’ˆ/μ„œλΉ„μŠ€ 뢄석 μ€‘μ‹¬μ˜ 리뷰 ν˜•μ‹, μž₯단점 뢄석과 μΆ”μ²œ 포함",
    "storytelling": "개인 κ²½ν—˜μ΄λ‚˜ 사둀λ₯Ό μ€‘μ‹¬μœΌλ‘œ ν•œ μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹μ˜ λΈ”λ‘œκ·Έ μž‘μ„±",
    "seo_optimized": "검색엔진 μ΅œμ ν™”(SEO)λ₯Ό κ³ λ €ν•œ ν‚€μ›Œλ“œ 쀑심 λΈ”λ‘œκ·Έ μž‘μ„±"
}

BLOG_TONES = {
    "professional": "전문적이고 곡식적인 μ–΄μ‘°λ‘œ μž‘μ„±",
    "casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”μ²΄ μ€‘μ‹¬μ˜ νŽΈμ•ˆν•œ ν†€μœΌλ‘œ μž‘μ„±",
    "humorous": "μœ λ¨Έμ™€ 재치λ₯Ό κ°€λ―Έν•œ κ°€λ²Όμš΄ μ–΄μ‘°λ‘œ μž‘μ„±",
    "storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― 감성적이고 λͺ°μž…감 μžˆλŠ” ν†€μœΌλ‘œ μž‘μ„±"
}

# ──────────────────────────────── λ‘œκΉ… ──────────────────────────────────────
logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s - %(levelname)s - %(message)s")

# ──────────────────────────────── Anthropic Client ─────────────────────────
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)

# ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
def get_system_prompt(template="standard", tone="professional", word_count=1750) -> str:
    base_prompt = """
당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:

λ…μž μ—°κ²° 단계
1.1. κ³΅κ°λŒ€ ν˜•μ„±μ„ μœ„ν•œ μΉœκ·Όν•œ 인사
1.2. λ…μžμ˜ μ‹€μ œ 고민을 λ°˜μ˜ν•œ λ„μž… 질문
1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적 관심 μœ λ„

문제 μ •μ˜ 단계
2.1. λ…μžμ˜ 페인포인트 ꡬ체화
2.2. 문제의 μ‹œκΈ‰μ„±κ³Ό 영ν–₯도 뢄석
2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±

μ „λ¬Έμ„± μž…μ¦ 단계
3.1. 객관적 데이터 기반 뢄석
3.2. μ „λ¬Έκ°€ 견해와 연ꡬ κ²°κ³Ό 인용
3.3. μ‹€μ œ 사둀λ₯Ό ν†΅ν•œ 문제 ꡬ체화

μ†”λ£¨μ…˜ 제곡 단계
4.1. 단계별 μ‹€μ²œ κ°€μ΄λ“œλΌμΈ μ œμ‹œ
4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ ꡬ체적 팁
4.3. μ˜ˆμƒ μž₯μ• λ¬Όκ³Ό 극볡 λ°©μ•ˆ 포함

신뒰도 κ°•ν™” 단계
5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
5.2. ꡬ체적 μ‚¬μš©μž ν›„κΈ° 인용
5.3. 객관적 λ°μ΄ν„°λ‘œ 효과 μž…μ¦

행동 μœ λ„ 단계
6.1. λͺ…ν™•ν•œ 첫 μ‹€μ²œ 단계 μ œμ‹œ
6.2. μ‹œκΈ‰μ„±μ„ κ°•μ‘°ν•œ 행동 촉ꡬ
6.3. μ‹€μ²œ 동기 λΆ€μ—¬ μš”μ†Œ 포함

μ§„μ •μ„± κ°•ν™” 단계
7.1. μ†”λ£¨μ…˜μ˜ ν•œκ³„ 투λͺ…ν•˜κ²Œ 곡개
7.2. κ°œμΈλ³„ 차이 쑴재 인정
7.3. ν•„μš” 쑰건과 μ£Όμ˜μ‚¬ν•­ λͺ…μ‹œ

관계 지속 단계
8.1. μ§„μ •μ„± μžˆλŠ” 감사 인사
8.2. λ‹€μŒ 컨텐츠 예고둜 κΈ°λŒ€κ° μ‘°μ„±
8.3. μ†Œν†΅ 채널 μ•ˆλ‚΄
"""

    # ν…œν”Œλ¦Ώλ³„ μΆ”κ°€ μ§€μΉ¨
    template_guides = {
        "tutorial": """
이 λΈ”λ‘œκ·ΈλŠ” νŠœν† λ¦¬μ–Ό ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- λͺ…ν™•ν•œ λͺ©ν‘œμ™€ μ΅œμ’… κ²°κ³Όλ¬Ό λ¨Όμ € μ œμ‹œ
- λ‹¨κ³„λ³„λ‘œ λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„λœ κ³Όμ • μ„€λͺ…
- 각 λ‹¨κ³„λ§ˆλ‹€ 이미지λ₯Ό μ‚½μž…ν•  μœ„μΉ˜ ν‘œμ‹œ
- μ˜ˆμƒ μ†Œμš” μ‹œκ°„κ³Ό λ‚œμ΄λ„ λͺ…μ‹œ
- ν•„μš”ν•œ λ„κ΅¬λ‚˜ 사전 지식 μ•ˆλ‚΄
- λ¬Έμ œν•΄κ²° 팁과 자주 λ°œμƒν•˜λŠ” μ‹€μˆ˜ 포함
- μ™„λ£Œ ν›„ λ‹€μŒ λ‹¨κ³„λ‚˜ μ‘μš©λ²• μ œμ•ˆ
""",
        
        "review": """
이 λΈ”λ‘œκ·ΈλŠ” 리뷰 ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- 객관적 사싀과 주관적 평가 ꡬ뢄
- λͺ…ν™•ν•œ 평가 κΈ°μ€€ μ œμ‹œ
- μž₯점과 단점 κ· ν˜•μžˆκ²Œ μ„œμˆ 
- μœ μ‚¬ μ œν’ˆ/μ„œλΉ„μŠ€μ™€ 비ꡐ
- λˆ„κ΅¬μ—κ²Œ μ ν•©ν•œμ§€ νƒ€κ²Ÿ μ„€λͺ…
- ꡬ체적인 μ‚¬μš© κ²½ν—˜κ³Ό κ²°κ³Ό 포함
- μ΅œμ’… μΆ”μ²œ 여뢀와 λŒ€μ•ˆ μ œμ‹œ
""",
        
        "storytelling": """
이 λΈ”λ‘œκ·ΈλŠ” μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- μ‹€μ œ μΈλ¬Όμ΄λ‚˜ μ‚¬λ‘€λ‘œ μ‹œμž‘
- 문제 상황과 감정적 μ—°κ²° κ°•ν™”
- κ°ˆλ“±κ³Ό ν•΄κ²°κ³Όμ • μ€‘μ‹¬μ˜ λ‚΄λŸ¬ν‹°λΈŒ
- κ΅ν›ˆκ³Ό 배움을 μžμ—°μŠ€λŸ½κ²Œ 포함
- λ…μžκ°€ 곡감할 수 μžˆλŠ” 감정선 μœ μ§€
- 이야기와 μœ μš©ν•œ μ •λ³΄μ˜ κ· ν˜• μœ μ§€
- λ…μžμ—κ²Œ μžμ‹ μ˜ 이야기λ₯Ό μƒκ°ν•΄λ³΄κ²Œ μœ λ„
""",
        
        "seo_optimized": """
이 λΈ”λ‘œκ·ΈλŠ” SEO μ΅œμ ν™” ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- 핡심 ν‚€μ›Œλ“œλ₯Ό 제λͺ©, μ†Œμ œλͺ©, 첫 단락에 배치
- κ΄€λ ¨ ν‚€μ›Œλ“œλ₯Ό μžμ—°μŠ€λŸ½κ²Œ 본문에 λΆ„μ‚°
- 300-500자 λΆ„λŸ‰μ˜ λͺ…ν™•ν•œ 단락 ꡬ성
- 질문 ν˜•μ‹μ˜ μ†Œμ œλͺ© ν™œμš©
- λͺ©λ‘, ν‘œ, κ°•μ‘° ν…μŠ€νŠΈ λ“± λ‹€μ–‘ν•œ μ„œμ‹ ν™œμš©
- λ‚΄λΆ€ 링크 μ‚½μž… μœ„μΉ˜ ν‘œμ‹œ
- 2000-3000자 μ΄μƒμ˜ μΆ©λΆ„ν•œ μ½˜ν…μΈ  제곡
"""
    }
    
    # 톀별 μΆ”κ°€ μ§€μΉ¨
    tone_guides = {
        "professional": "전문적이고 κΆŒμœ„μžˆλŠ” μ–΄μ‘°λ‘œ μž‘μ„±ν•˜λ˜, μ „λ¬Έ μš©μ–΄λŠ” 적절히 μ„€λͺ…ν•΄ μ£Όμ„Έμš”. 데이터와 연ꡬ κ²°κ³Όλ₯Ό μ€‘μ‹¬μœΌλ‘œ 논리적 흐름을 μœ μ§€ν•˜μ„Έμš”.",
        "casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”ν•˜λ“― νŽΈμ•ˆν•œ μ–΄μ‘°λ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. '~λ„€μš”', '~ν•΄μš”' 같은 λŒ€ν™”μ²΄λ₯Ό μ‚¬μš©ν•˜κ³ , 개인적 κ²½ν—˜κ³Ό λΉ„μœ λ₯Ό 톡해 λ‚΄μš©μ„ μ „λ‹¬ν•˜μ„Έμš”.",
        "humorous": "μœ λ¨Έμ™€ μž¬μΉ˜μžˆλŠ” ν‘œν˜„μ„ 적절히 ν™œμš©ν•΄ μ£Όμ„Έμš”. μž¬λ―ΈμžˆλŠ” λΉ„μœ λ‚˜ μ˜ˆμ‹œ, κ°€λ²Όμš΄ 농담을 ν¬ν•¨ν•˜λ˜, μ •λ³΄μ˜ μ •ν™•μ„±κ³Ό μœ μš©μ„±μ€ μœ μ§€ν•˜μ„Έμš”.",
        "storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― 감성적이고 λͺ°μž…감 μžˆλŠ” ν†€μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. 인물, λ°°κ²½, κ°ˆλ“±, 해결과정이 λ‹΄κΈ΄ λ‚΄λŸ¬ν‹°λΈŒ ꡬ쑰λ₯Ό ν™œμš©ν•˜μ„Έμš”."
    }
    
    # μ΅œμ’… ν”„λ‘¬ν”„νŠΈ μ‘°ν•©
    final_prompt = base_prompt
    
    # μ„ νƒλœ ν…œν”Œλ¦Ώ μ§€μΉ¨ μΆ”κ°€
    if template in template_guides:
        final_prompt += "\n" + template_guides[template]
    
    # μ„ νƒλœ 톀 μ§€μΉ¨ μΆ”κ°€
    if tone in tone_guides:
        final_prompt += f"\n\nν†€μ•€λ§€λ„ˆ: {tone_guides[tone]}"
    
    # κΈ€μž 수 μ§€μΉ¨ μΆ”κ°€
    final_prompt += f"\n\nμž‘μ„± μ‹œ μ€€μˆ˜μ‚¬ν•­\n9.1. κΈ€μž 수: {word_count-250}-{word_count+250}자 λ‚΄μ™Έ\n9.2. 문단 길이: 3-4λ¬Έμž₯ 이내\n9.3. μ‹œκ°μ  ꡬ뢄: μ†Œμ œλͺ©, ꡬ뢄선, 번호 λͺ©λ‘ ν™œμš©\n9.4. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ\n9.5. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©"
    
    return final_prompt

# ──────────────────────────────── Brave Search API ─────────────────────────
def brave_search(query: str, count: int = 5):
    """
    Brave Web Search API 호좜 β†’ list[dict]
    λ°˜ν™˜ ν•„λ“œ: index, title, link, snippet, displayed_link
    """
    if not BRAVE_KEY:
        raise RuntimeError("⚠️  SERPHOUSE_API_KEY (Brave API Key) ν™˜κ²½λ³€μˆ˜κ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")

    headers = {
        "Accept": "application/json",
        "Accept-Encoding": "gzip",
        "X-Subscription-Token": BRAVE_KEY
    }
    params = {"q": query, "count": str(count)}
    
    for attempt in range(3):  # μ΅œλŒ€ 3번 μž¬μ‹œλ„
        try:
            r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
            r.raise_for_status()
            data = r.json()
            
            # κ²°κ³Ό ν˜•μ‹ 확인 및 λ‘œκΉ…
            logging.info(f"Brave 검색 κ²°κ³Ό 데이터 ꡬ쑰: {list(data.keys())}")
            
            raw = data.get("web", {}).get("results") or data.get("results", [])
            if not raw:
                logging.warning(f"Brave 검색 κ²°κ³Ό μ—†μŒ. 응닡: {data}")
                raise ValueError("검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€")
                
            arts = []
            for i, res in enumerate(raw[:count], 1):
                url   = res.get("url", res.get("link", ""))
                host  = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
                arts.append({
                    "index": i,
                    "title": res.get("title", "제λͺ© μ—†μŒ"),
                    "link": url,
                    "snippet": res.get("description", res.get("text", "λ‚΄μš© μ—†μŒ")),
                    "displayed_link": host
                })
            
            logging.info(f"Brave 검색 성곡: {len(arts)}개 κ²°κ³Ό")
            return arts
            
        except Exception as e:
            logging.error(f"Brave 검색 μ‹€νŒ¨ (μ‹œλ„ {attempt+1}/3): {e}")
            if attempt < 2:  # λ§ˆμ§€λ§‰ μ‹œλ„κ°€ μ•„λ‹ˆλ©΄ λŒ€κΈ° ν›„ μž¬μ‹œλ„
                time.sleep(2)
            
    return []  # λͺ¨λ“  μ‹œλ„ μ‹€νŒ¨ μ‹œ 빈 λͺ©λ‘ λ°˜ν™˜

def mock_results(query: str) -> str:
    """검색 API μ‹€νŒ¨ μ‹œ 가상 검색 κ²°κ³Ό 제곡"""
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return (f"# 검색 κ²°κ³Ό λŒ€μ²΄ λ‚΄μš© (생성: {ts})\n\n"
            f"검색 API 호좜이 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. 주제 '{query}'에 λŒ€ν•΄ κΈ°μ‘΄ 지식을 ν™œμš©ν•΄ λ‹΅λ³€ν•΄ μ£Όμ„Έμš”.\n\n"
            f"λ‹€μŒ λ‚΄μš©μ΄ 도움이 될 수 μžˆμŠ΅λ‹ˆλ‹€:\n\n"
            f"- {query}의 κΈ°λ³Έ κ°œλ…κ³Ό μ€‘μš”μ„±\n"
            f"- 일반적으둜 μ•Œλ €μ§„ κ΄€λ ¨ 톡계와 νŠΈλ Œλ“œ\n"
            f"- ν•΄λ‹Ή μ£Όμ œμ— λŒ€ν•œ μ „λ¬Έκ°€λ“€μ˜ 일반적인 견해\n"
            f"- λ…μžλ“€μ΄ μ‹€μ œλ‘œ κΆκΈˆν•΄ν•  λ§Œν•œ μ§ˆλ¬Έλ“€\n\n"
            f"μ°Έκ³ : 이 λ‚΄μš©μ€ μ‹€μ‹œκ°„ 검색 κ²°κ³Όκ°€ μ•„λ‹Œ λŒ€μ²΄ μ•ˆλ‚΄μž…λ‹ˆλ‹€.\n\n")

def do_web_search(query: str) -> str:
    """μ›Ή 검색 μˆ˜ν–‰ 및 κ²°κ³Ό ν¬λ§·νŒ…"""
    try:
        arts = brave_search(query, 5)
        if not arts:
            logging.warning("검색 κ²°κ³Ό μ—†μŒ, λŒ€μ²΄ μ½˜ν…μΈ  μ‚¬μš©")
            return mock_results(query)
        
        hdr = "# μ›Ή 검색 κ²°κ³Ό\nμ•„λž˜ 정보λ₯Ό μ°Έκ³ ν•΄μ„œ λ‹΅λ³€ν•˜μ„Έμš”.\n\n"
        body = "\n".join(
            f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
            f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
            for a in arts
        )
        return hdr + body
    except Exception as e:
        logging.error(f"μ›Ή 검색 전체 ν”„λ‘œμ„ΈμŠ€ μ‹€νŒ¨: {str(e)}")
        return mock_results(query)

# ──────────────────────────────── 이미지 Β· λ³€ν™˜ μœ ν‹Έ ────────────────────────
def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
    if not prompt: return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
    try:
        res = Client(IMAGE_API_URL).predict(
            prompt=prompt, width=w, height=h, guidance=g,
            inference_steps=steps, seed=seed,
            do_img2img=False, init_image=None,
            image2image_strength=0.8, resize_img=True,
            api_name="/generate_image")
        return res[0], f"Seed: {res[1]}"
    except Exception as e:
        logging.error(e); return None, str(e)

def extract_image_prompt(blog: str, topic: str):
    sys = f"λ‹€μŒ κΈ€λ‘œλΆ€ν„° μ˜μ–΄ 1쀄 이미지 ν”„λ‘¬ν”„νŠΈ 생성:\n{topic}"
    try:
        res = client.messages.create(
            model="claude-3-7-sonnet-20250219",
            max_tokens=80, system=sys,
            messages=[{"role": "user", "content": blog}]
        )
        return res.content[0].text.strip()
    except Exception:
        return f"A professional photo related to {topic}, high quality"

def md_to_html(md: str, title="Ginigen Blog"):
    return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"

def keywords(text: str, top=5):
    return " ".join(re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text).split()[:top])

# ──────────────────────────────── Streamlit UI ────────────────────────────
def ginigen_app():
    st.title("Ginigen Blog")

    # μ„Έμ…˜ κΈ°λ³Έκ°’
    defaults = dict(
        ai_model="claude-3-7-sonnet-20250219",
        messages=[],
        auto_save=True,
        generate_image=False,
        use_web_search=False,
        blog_template="standard",
        blog_tone="professional",
        word_count=1750
    )
    for k, v in defaults.items():
        st.session_state.setdefault(k, v)

    # ── μ‚¬μ΄λ“œλ°” 컨트둀
    sb = st.sidebar
    sb.title("λΈ”λ‘œκ·Έ μ„€μ •")
    
    # λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ 선택
    sb.subheader("λΈ”λ‘œκ·Έ μŠ€νƒ€μΌ μ„€μ •")
    sb.selectbox("λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ", options=list(BLOG_TEMPLATES.keys()), 
                format_func=lambda x: x.replace("_", " ").title(),
                key="blog_template")
    
    sb.selectbox("λΈ”λ‘œκ·Έ 톀", options=list(BLOG_TONES.keys()),
                format_func=lambda x: x.replace("_", " ").title(),
                key="blog_tone")
    
    sb.slider("λΈ”λ‘œκ·Έ 길이 (단어 수)", 800, 3000, 1750, key="word_count")
    
    sb.subheader("기타 μ„€μ •")
    sb.toggle("μžλ™ μ €μž₯",   key="auto_save")
    sb.toggle("이미지 μžλ™ 생성", key="generate_image")
    
    # μ›Ή 검색 ν† κΈ€ (λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•΄ μœ μ§€ν•˜λ˜ 기본값은 False)
    search_enabled = sb.toggle("μ›Ή 검색 μ‚¬μš©", value=False, key="use_web_search")
    if search_enabled:
        st.warning("⚠️ μ›Ή 검색 κΈ°λŠ₯은 ν˜„μž¬ λΆˆμ•ˆμ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 검색 κ²°κ³Όκ°€ μ—†μœΌλ©΄ κΈ°λ³Έ μ§€μ‹μœΌλ‘œ λŒ€μ²΄λ©λ‹ˆλ‹€.")

    # ── 졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (λ§ˆν¬λ‹€μš΄ / HTML)
    latest_blog = next(
        (m["content"] for m in reversed(st.session_state.messages)
         if m["role"] == "assistant" and m["content"].strip()), None)

    if latest_blog:
        title = re.search(r"# (.*?)(\n|$)", latest_blog)
        title = title.group(1).strip() if title else "blog"
        sb.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
        c1, c2 = sb.columns(2)
        c1.download_button("Markdown", latest_blog,
                           file_name=f"{title}.md", mime="text/markdown")
        c2.download_button("HTML", md_to_html(latest_blog, title),
                           file_name=f"{title}.html", mime="text/html")

    # ── JSON λŒ€ν™” 기둝 μ—…λ‘œλ“œ
    up = sb.file_uploader("λŒ€ν™” 기둝 뢈러였기 (.json)", type=["json"])
    if up:
        try:
            st.session_state.messages = json.load(up)
            sb.success("λŒ€ν™” 기둝 뢈러였기 μ™„λ£Œ")
        except Exception as e:
            sb.error(f"뢈러였기 μ‹€νŒ¨: {e}")

    # ── JSON λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
    if sb.button("λŒ€ν™” 기둝 JSON λ‹€μš΄λ‘œλ“œ"):
        sb.download_button("μ €μž₯", json.dumps(st.session_state.messages,
                                              ensure_ascii=False, indent=2),
                           file_name="chat_history.json",
                           mime="application/json")

    # ── κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
    for m in st.session_state.messages:
        with st.chat_message(m["role"]):
            st.markdown(m["content"])
            if "image" in m:
                st.image(m["image"], caption=m.get("image_caption", ""))

    # ── μ‚¬μš©μž μž…λ ₯
    if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
        st.session_state.messages.append({"role": "user", "content": prompt})
        with st.chat_message("user"): st.markdown(prompt)

        with st.chat_message("assistant"):
            placeholder = st.empty(); answer = ""
            
            # μ„ νƒλœ ν…œν”Œλ¦Ώ, 톀, 단어 수둜 μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ 생성
            sys_prompt = get_system_prompt(
                template=st.session_state.blog_template,
                tone=st.session_state.blog_tone,
                word_count=st.session_state.word_count
            )

            if st.session_state.use_web_search:
                with st.spinner("μ›Ή 검색 쀑…"):
                    search_md = do_web_search(keywords(prompt))
                    sys_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_md}\n"

            # Claude 슀트리밍
            with client.messages.stream(
                model=st.session_state.ai_model, max_tokens=MAX_TOKENS,
                system=sys_prompt,
                messages=[{"role": m["role"], "content": m["content"]}
                          for m in st.session_state.messages]
            ) as stream:
                for t in stream.text_stream:
                    answer += t or ""
                    placeholder.markdown(answer + "β–Œ")
            placeholder.markdown(answer)

            # 이미지 μ˜΅μ…˜
            if st.session_state.generate_image:
                with st.spinner("이미지 생성 쀑…"):
                    ip = extract_image_prompt(answer, prompt)
                    img, cap = generate_image(ip)
                    if img:
                        st.image(img, caption=cap)
                        st.session_state.messages.append(
                            {"role": "assistant", "content": answer,
                             "image": img, "image_caption": cap})
                        answer_entry_saved = True
            if not st.session_state.generate_image:
                st.session_state.messages.append(
                    {"role": "assistant", "content": answer})

            # λ³Έλ¬Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ (MD / HTML)
            st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
            b1, b2 = st.columns(2)
            b1.download_button("Markdown", answer,
                               file_name=f"{prompt[:30]}.md", mime="text/markdown")
            b2.download_button("HTML", md_to_html(answer, prompt[:30]),
                               file_name=f"{prompt[:30]}.html", mime="text/html")

    # ── μžλ™ λ°±μ—… μ €μž₯
    if st.session_state.auto_save and st.session_state.messages:
        try:
            fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
            with open(fn, "w", encoding="utf-8") as fp:
                json.dump(st.session_state.messages, fp,
                          ensure_ascii=False, indent=2)
        except Exception as e:
            logging.error(f"μžλ™ μ €μž₯ μ‹€νŒ¨: {e}")

# ──────────────────────────────── main / requirements ──────────────────────
def main(): ginigen_app()

if __name__ == "__main__":
    # requirements.txt 동적 생성
    with open("requirements.txt", "w") as f:
        f.write("\n".join([
            "streamlit>=1.31.0",
            "anthropic>=0.18.1",
            "gradio-client>=1.8.0",
            "requests>=2.32.3",
            "markdown>=3.5.1",
            "pillow>=10.1.0"
        ]))
    main()