Kims12 commited on
Commit
fe6aa7a
ยท
verified ยท
1 Parent(s): f2dc2e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +547 -195
app.py CHANGED
@@ -3,6 +3,14 @@ import google.generativeai as genai
3
  from PIL import Image
4
  import os
5
  import json
 
 
 
 
 
 
 
 
6
 
7
  # Gemini API ํ‚ค ์„ค์ • (ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์ง์ ‘ ์ž…๋ ฅ)
8
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
@@ -149,7 +157,7 @@ def generate_prompt_with_gemini(product_name, background_info, additional_info="
149
  )
150
  response = model.generate_content(
151
  prompt_request,
152
- generation_config=genai.types.GenerationConfig(
153
  temperature=0.7,
154
  top_p=0.95,
155
  top_k=64,
@@ -163,210 +171,554 @@ def generate_prompt_with_gemini(product_name, background_info, additional_info="
163
  except Exception as e:
164
  return f"ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
165
 
166
- def create_app():
167
- with gr.Blocks(title="๊ณ ๊ธ‰ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋ฐฐ๊ฒฝ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ๊ธฐ") as demo:
168
- gr.Markdown("# ๊ณ ๊ธ‰ ์ƒํ’ˆ ์ด๋ฏธ์ง€ ๋ฐฐ๊ฒฝ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ๊ธฐ")
169
- gr.Markdown("์ƒํ’ˆ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๊ณ  ์˜ต์…˜์„ ์„ ํƒํ•˜๋ฉด ๊ณ ํ’ˆ์งˆ ์ƒ์—…์šฉ ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- # ์ƒํ’ˆ๋ช… ์„น์…˜
172
- with gr.Row():
173
- product_name = gr.Textbox(label="์ƒํ’ˆ๋ช… (ํ•œ๊ตญ์–ด ์ž…๋ ฅ)", placeholder="์˜ˆ: ์Šคํ‚จ์ผ€์–ด ํŠœ๋ธŒ, ํ…€๋ธ”๋Ÿฌ ๋“ฑ", interactive=True)
174
 
175
- # ๋ฐฐ๊ฒฝ ์œ ํ˜•์„ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ๋ผ๋””์˜ค ๋ฒ„ํŠผ
176
- background_type = gr.Radio(
177
- choices=["์‹ฌํ”Œ ๋ฐฐ๊ฒฝ", "์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ", "์ž์—ฐ ํ™˜๊ฒฝ", "์‹ค๋‚ด ํ™˜๊ฒฝ", "์ถ”์ƒ/ํŠน์ˆ˜ ๋ฐฐ๊ฒฝ"],
178
- label="๋ฐฐ๊ฒฝ ์œ ํ˜•",
179
- value="์‹ฌํ”Œ ๋ฐฐ๊ฒฝ"
 
 
 
 
 
 
 
 
180
  )
181
 
182
- # ๊ฐ ๋ฐฐ๊ฒฝ ์œ ํ˜•์— ๋งž๋Š” ๋“œ๋กญ๋‹ค์šด ์ปดํฌ๋„ŒํŠธ๋“ค (์ฒ˜์Œ์—๋Š” ์‹ฌํ”Œ ๋ฐฐ๊ฒฝ๋งŒ ํ‘œ์‹œ)
183
- with gr.Row():
184
- # ์ขŒ์ธก ์ปฌ๋Ÿผ - ์ž…๋ ฅ ์ปจํŠธ๋กค
185
- with gr.Column(scale=1):
186
- image_input = gr.Image(label="์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ", type="pil")
187
-
188
- # ์‹ฌํ”Œ ๋ฐฐ๊ฒฝ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด
189
- simple_dropdown = gr.Dropdown(
190
- choices=list(SIMPLE_BACKGROUNDS.keys()),
191
- value=list(SIMPLE_BACKGROUNDS.keys())[0] if SIMPLE_BACKGROUNDS else None,
192
- label="์‹ฌํ”Œ ๋ฐฐ๊ฒฝ ์„ ํƒ",
193
- visible=True,
194
- interactive=True
195
- )
196
-
197
- # ์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด
198
- studio_dropdown = gr.Dropdown(
199
- choices=list(STUDIO_BACKGROUNDS.keys()),
200
- value=list(STUDIO_BACKGROUNDS.keys())[0] if STUDIO_BACKGROUNDS else None,
201
- label="์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ ์„ ํƒ",
202
- visible=False,
203
- interactive=True
204
- )
205
-
206
- # ์ž์—ฐ ํ™˜๊ฒฝ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด
207
- nature_dropdown = gr.Dropdown(
208
- choices=list(NATURE_BACKGROUNDS.keys()),
209
- value=list(NATURE_BACKGROUNDS.keys())[0] if NATURE_BACKGROUNDS else None,
210
- label="์ž์—ฐ ํ™˜๊ฒฝ ์„ ํƒ",
211
- visible=False,
212
- interactive=True
213
- )
214
-
215
- # ์‹ค๋‚ด ํ™˜๊ฒฝ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด
216
- indoor_dropdown = gr.Dropdown(
217
- choices=list(INDOOR_BACKGROUNDS.keys()),
218
- value=list(INDOOR_BACKGROUNDS.keys())[0] if INDOOR_BACKGROUNDS else None,
219
- label="์‹ค๋‚ด ํ™˜๊ฒฝ ์„ ํƒ",
220
- visible=False,
221
- interactive=True
222
- )
223
-
224
- # ์ถ”์ƒ/ํŠน์ˆ˜ ๋ฐฐ๊ฒฝ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด
225
- abstract_dropdown = gr.Dropdown(
226
- choices=list(ABSTRACT_BACKGROUNDS.keys()),
227
- value=list(ABSTRACT_BACKGROUNDS.keys())[0] if ABSTRACT_BACKGROUNDS else None,
228
- label="์ถ”์ƒ/ํŠน์ˆ˜ ๋ฐฐ๊ฒฝ ์„ ํƒ",
229
- visible=False,
230
- interactive=True
231
- )
232
-
233
- # ์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ
234
- additional_info = gr.Textbox(
235
- label="์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ (์„ ํƒ์‚ฌํ•ญ)",
236
- placeholder="์˜ˆ: ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๋А๋‚Œ, ๋ฐ์€ ์กฐ๋ช…, ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณด์กฐ์ ์ธ ๊ฐ์ฒด๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š” ๋“ฑ",
237
- lines=3,
238
- interactive=True
239
- )
240
-
241
- submit_btn = gr.Button("ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ", variant="primary")
242
-
243
- # ์šฐ์ธก ์ปฌ๋Ÿผ - ์ถœ๋ ฅ ๊ฒฐ๊ณผ
244
- with gr.Column(scale=1):
245
- prompt_output = gr.Textbox(label="์ƒ์„ฑ๋œ ํ”„๋กฌํ”„ํŠธ", lines=10)
246
- image_preview = gr.Image(label="์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ (#1)", type="pil")
247
- preview_html = gr.HTML("ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹œ ์—ฌ๊ธฐ์— ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.")
248
 
249
- # ๋ฐฐ๊ฒฝ ์œ ํ˜•์— ๋”ฐ๋ผ ๋“œ๋กญ๋‹ค์šด ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
250
- def update_dropdowns(bg_type):
251
- return {
252
- simple_dropdown: gr.update(visible=(bg_type == "์‹ฌํ”Œ ๋ฐฐ๊ฒฝ")),
253
- studio_dropdown: gr.update(visible=(bg_type == "์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ")),
254
- nature_dropdown: gr.update(visible=(bg_type == "์ž์—ฐ ํ™˜๊ฒฝ")),
255
- indoor_dropdown: gr.update(visible=(bg_type == "์‹ค๋‚ด ํ™˜๊ฒฝ")),
256
- abstract_dropdown: gr.update(visible=(bg_type == "์ถ”์ƒ/ํŠน์ˆ˜ ๋ฐฐ๊ฒฝ"))
257
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
- # ๋ฐฐ๊ฒฝ ์œ ํ˜• ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
260
- background_type.change(
261
- fn=update_dropdowns,
262
- inputs=[background_type],
263
- outputs=[simple_dropdown, studio_dropdown, nature_dropdown, indoor_dropdown, abstract_dropdown]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  )
265
-
266
- # ์„ ํƒ๋œ ๋ฐฐ๊ฒฝ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
267
- def get_selected_background_info(bg_type, simple, studio, nature, indoor, abstract):
268
- if bg_type == "์‹ฌํ”Œ ๋ฐฐ๊ฒฝ":
269
- return {
270
- "category": "์‹ฌํ”Œ ๋ฐฐ๊ฒฝ",
271
- "name": simple,
272
- "english": SIMPLE_BACKGROUNDS.get(simple, "white background")
273
- }
274
- elif bg_type == "์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ":
275
- return {
276
- "category": "์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ",
277
- "name": studio,
278
- "english": STUDIO_BACKGROUNDS.get(studio, "product photography studio")
279
- }
280
- elif bg_type == "์ž์—ฐ ํ™˜๊ฒฝ":
281
- return {
282
- "category": "์ž์—ฐ ํ™˜๊ฒฝ",
283
- "name": nature,
284
- "english": NATURE_BACKGROUNDS.get(nature, "natural environment")
285
- }
286
- elif bg_type == "์‹ค๋‚ด ํ™˜๊ฒฝ":
287
- return {
288
- "category": "์‹ค๋‚ด ํ™˜๊ฒฝ",
289
- "name": indoor,
290
- "english": INDOOR_BACKGROUNDS.get(indoor, "indoor environment")
291
- }
292
- elif bg_type == "์ถ”์ƒ/ํŠน์ˆ˜ ๋ฐฐ๊ฒฝ":
293
- return {
294
- "category": "์ถ”์ƒ/ํŠน์ˆ˜ ๋ฐฐ๊ฒฝ",
295
- "name": abstract,
296
- "english": ABSTRACT_BACKGROUNDS.get(abstract, "abstract background")
297
- }
298
- else:
299
- return {
300
- "category": "๊ธฐ๋ณธ ๋ฐฐ๊ฒฝ",
301
- "name": "ํ™”์ดํŠธ ๋ฐฐ๊ฒฝ",
302
- "english": "white background"
303
- }
304
-
305
- # ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ํ•จ์ˆ˜
306
- def generate_output(image, bg_type, simple, studio, nature, indoor, abstract, product_text, additional_text):
307
- if image is None:
308
- gr.Warning("์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.")
309
- return "์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.", None, "์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ›„ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”."
310
 
311
- product_text = product_text.strip() or "์ œํ’ˆ"
 
 
 
 
 
 
 
 
 
 
312
 
313
- # ๋ฐฐ๊ฒฝ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
314
- background_info = get_selected_background_info(bg_type, simple, studio, nature, indoor, abstract)
315
 
316
- try:
317
- prompt = generate_prompt_with_gemini(product_text, background_info, additional_text)
318
-
319
- if not GEMINI_API_KEY:
320
- gr.Warning("Gemini API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์•„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.")
321
- prompt = """
322
- [Gemini API ํ‚ค ๋ˆ„๋ฝ]
323
- API ํ‚ค ์„ค์ • ๋ฐฉ๋ฒ•:
324
- 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜: export GEMINI_API_KEY="your-api-key"
325
- 2. ์ฝ”๋“œ ๋‚ด ์ง์ ‘ ์ž…๋ ฅ: GEMINI_API_KEY = "your-api-key"
326
- ํ‚ค ๋ฐœ๊ธ‰: https://makersuite.google.com/
327
- """
328
- return prompt, image, "API ํ‚ค๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."
329
-
330
- # ํ”„๋กฌํ”„ํŠธ ์š”์•ฝ HTML ์ƒ์„ฑ
331
- preview = f"""
332
- <div style="padding:10px; border:1px solid #ddd; border-radius:8px; margin-top:10px;">
333
- <h3>ํ”„๋กฌํ”„ํŠธ ์š”์•ฝ</h3>
334
- <p><strong>์ด ๊ธธ์ด:</strong> {len(prompt)} ๊ธ€์ž</p>
335
- <p><strong>๋ฐฐ๊ฒฝ:</strong> {background_info['category']} &gt; {background_info['name']}</p>
336
- <p><strong>์ฃผ์š” ์š”์†Œ:</strong> {", ".join([kw for kw in ["commercial photography", "product", "square format", "centered", "detailed"] if kw.lower() in prompt.lower()])}</p>
337
- <p><strong>๋ฏธ๋“œ์ €๋‹ˆ ํŒŒ๋ผ๋ฏธํ„ฐ:</strong> {" ".join([param for param in ["--ar 1:1", "--s 750", "--q 2"] if param in prompt])}</p>
338
- </div>
339
- """
340
-
341
- return prompt, image, preview
342
- except Exception as e:
343
- error_msg = f"ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
344
- gr.Error(error_msg)
345
- return error_msg, image, "์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
- # ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
348
- submit_btn.click(
349
- fn=generate_output,
350
- inputs=[
351
- image_input,
352
- background_type,
353
- simple_dropdown,
354
- studio_dropdown,
355
- nature_dropdown,
356
- indoor_dropdown,
357
- abstract_dropdown,
358
- product_name,
359
- additional_info
360
- ],
361
- outputs=[
362
- prompt_output,
363
- image_preview,
364
- preview_html
365
- ]
366
- )
367
 
368
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- if __name__ == "__main__":
371
- app = create_app()
372
- app.launch()
 
 
3
  from PIL import Image
4
  import os
5
  import json
6
+ import tempfile
7
+ import re
8
+ import time
9
+ import logging
10
+
11
+ # ๋กœ๊น… ์„ค์ •
12
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
13
+ logger = logging.getLogger(__name__)
14
 
15
  # Gemini API ํ‚ค ์„ค์ • (ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์ง์ ‘ ์ž…๋ ฅ)
16
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
 
157
  )
158
  response = model.generate_content(
159
  prompt_request,
160
+ generation_config=genai.GenerationConfig(
161
  temperature=0.7,
162
  top_p=0.95,
163
  top_k=64,
 
171
  except Exception as e:
172
  return f"ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
173
 
174
+ # ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ํ•จ์ˆ˜๋“ค
175
+ def save_binary_file(file_name, data):
176
+ with open(file_name, "wb") as f:
177
+ f.write(data)
178
+
179
+ def translate_prompt_to_english(prompt):
180
+ if not re.search("[๊ฐ€-ํžฃ]", prompt):
181
+ return prompt
182
+
183
+ prompt = prompt.replace("#1", "IMAGE_TAG_ONE")
184
+ prompt = prompt.replace("#2", "IMAGE_TAG_TWO")
185
+ prompt = prompt.replace("#3", "IMAGE_TAG_THREE")
186
+
187
+ try:
188
+ if not GEMINI_API_KEY:
189
+ logger.error("Gemini API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
190
+ prompt = prompt.replace("IMAGE_TAG_ONE", "#1")
191
+ prompt = prompt.replace("IMAGE_TAG_TWO", "#2")
192
+ prompt = prompt.replace("IMAGE_TAG_THREE", "#3")
193
+ return prompt
194
+
195
+ model = genai.GenerativeModel('gemini-2.0-flash')
196
+ translation_prompt = f"""
197
+ Translate the following Korean text to English:
198
 
199
+ {prompt}
 
 
200
 
201
+ IMPORTANT: The tokens IMAGE_TAG_ONE, IMAGE_TAG_TWO, and IMAGE_TAG_THREE are special tags
202
+ and must be preserved exactly as is in your translation. Do not translate these tokens.
203
+ """
204
+
205
+ logger.info(f"Translation prompt: {translation_prompt}")
206
+ response = model.generate_content(
207
+ translation_prompt,
208
+ generation_config=genai.GenerationConfig(
209
+ temperature=0.2,
210
+ top_p=0.95,
211
+ top_k=40,
212
+ max_output_tokens=512
213
+ )
214
  )
215
 
216
+ translated_text = response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ if translated_text.strip():
219
+ translated_text = translated_text.replace("IMAGE_TAG_ONE", "#1")
220
+ translated_text = translated_text.replace("IMAGE_TAG_TWO", "#2")
221
+ translated_text = translated_text.replace("IMAGE_TAG_THREE", "#3")
222
+ logger.info(f"Translated text: {translated_text.strip()}")
223
+ return translated_text.strip()
224
+ else:
225
+ logger.warning("๋ฒˆ์—ญ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์›๋ณธ ํ”„๋กฌํ”„ํŠธ ์‚ฌ์šฉ")
226
+ prompt = prompt.replace("IMAGE_TAG_ONE", "#1")
227
+ prompt = prompt.replace("IMAGE_TAG_TWO", "#2")
228
+ prompt = prompt.replace("IMAGE_TAG_THREE", "#3")
229
+ return prompt
230
+ except Exception as e:
231
+ logger.exception("๋ฒˆ์—ญ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:")
232
+ prompt = prompt.replace("IMAGE_TAG_ONE", "#1")
233
+ prompt = prompt.replace("IMAGE_TAG_TWO", "#2")
234
+ prompt = prompt.replace("IMAGE_TAG_THREE", "#3")
235
+ return prompt
236
+
237
+ def preprocess_prompt(prompt, image1, image2, image3):
238
+ has_img1 = image1 is not None
239
+ has_img2 = image2 is not None
240
+ has_img3 = image3 is not None
241
+
242
+ if "#1" in prompt and not has_img1:
243
+ prompt = prompt.replace("#1", "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€(์—†์Œ)")
244
+ else:
245
+ prompt = prompt.replace("#1", "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€")
246
+
247
+ if "#2" in prompt and not has_img2:
248
+ prompt = prompt.replace("#2", "๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€(์—†์Œ)")
249
+ else:
250
+ prompt = prompt.replace("#2", "๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€")
251
+
252
+ if "#3" in prompt and not has_img3:
253
+ prompt = prompt.replace("#3", "์„ธ ๋ฒˆ์งธ ์ด๋ฏธ์ง€(์—†์Œ)")
254
+ else:
255
+ prompt = prompt.replace("#3", "์„ธ ๋ฒˆ์งธ ์ด๋ฏธ์ง€")
256
+
257
+ if "1. ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ" in prompt:
258
+ desc_match = re.search(r'#1์„ "(.*?)"์œผ๋กœ ๋ฐ”๊ฟ”๋ผ', prompt)
259
+ if desc_match:
260
+ description = desc_match.group(1)
261
+ prompt = f"์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ {description}์œผ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ์„ธ์š”. ์›๋ณธ ์ด๋ฏธ์ง€์˜ ์ฃผ์š” ๋‚ด์šฉ์€ ์œ ์ง€ํ•˜๋˜ ์ƒˆ๋กœ์šด ์Šคํƒ€์ผ๊ณผ ๋ถ„์œ„๊ธฐ๋กœ ์žฌํ•ด์„ํ•ด์ฃผ์„ธ์š”."
262
+ else:
263
+ prompt = "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ ์ฐฝ์˜์ ์œผ๋กœ ๋ณ€ํ˜•ํ•ด์ฃผ์„ธ์š”. ๋” ์ƒ์ƒํ•˜๊ณ  ์˜ˆ์ˆ ์ ์ธ ๋ฒ„์ „์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”."
264
+
265
+ elif "2. ๊ธ€์ž์ง€์šฐ๊ธฐ" in prompt:
266
+ text_match = re.search(r'#1์—์„œ "(.*?)"๋ฅผ ์ง€์›Œ๋ผ', prompt)
267
+ if text_match:
268
+ text_to_remove = text_match.group(1)
269
+ prompt = f"์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์—์„œ '{text_to_remove}' ํ…์ŠคํŠธ๋ฅผ ์ฐพ์•„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ œ๊ฑฐํ•ด์ฃผ์„ธ์š”. ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋˜ ๋ถ€๋ถ„์„ ๋ฐฐ๊ฒฝ๊ณผ ์กฐํ™”๋กญ๊ฒŒ ์ฑ„์›Œ์ฃผ์„ธ์š”."
270
+ else:
271
+ prompt = "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์—์„œ ๋ชจ๋“  ํ…์ŠคํŠธ๋ฅผ ์ฐพ์•„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ œ๊ฑฐํ•ด์ฃผ์„ธ์š”. ๊น”๋”ํ•œ ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”."
272
+
273
+ elif "4. ์˜ท๋ฐ”๊พธ๊ธฐ" in prompt:
274
+ prompt = "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์ธ๋ฌผ ์˜์ƒ์„ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์˜์ƒ์œผ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ์„ธ์š”. ์˜์ƒ์˜ ์Šคํƒ€์ผ๊ณผ ์ƒ‰์ƒ์€ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ ๋”ฐ๋ฅด๋˜, ์‹ ์ฒด ๋น„์œจ๊ณผ ํฌ์ฆˆ๋Š” ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๋ฅผ ์œ ์ง€ํ•ด์ฃผ์„ธ์š”."
275
+
276
+ elif "5. ๋ฐฐ๊ฒฝ๋ฐ”๊พธ๊ธฐ" in prompt:
277
+ prompt = "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ๋ฐฐ๊ฒฝ์„ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ๋ฐฐ๊ฒฝ์œผ๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ์„ธ์š”. ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ์ฃผ์š” ํ”ผ์‚ฌ์ฒด๋Š” ์œ ์ง€ํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์˜ ๋ฐฐ๊ฒฝ๊ณผ ์กฐํ™”๋กญ๊ฒŒ ํ•ฉ์„ฑํ•ด์ฃผ์„ธ์š”."
278
+
279
+ elif "6. ์ด๋ฏธ์ง€ ํ•ฉ์„ฑ(์ƒํ’ˆํฌํ•จ)" in prompt:
280
+ prompt = "์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€์™€ ๋‘ ๋ฒˆ์งธ ์ด๋ฏธ์ง€(๋˜๋Š” ์„ธ ๋ฒˆ์งธ ์ด๋ฏธ์ง€)๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ•ฉ์„ฑํ•ด์ฃผ์„ธ์š”. ๋ชจ๋“  ์ด๋ฏธ์ง€์˜ ์ฃผ์š” ์š”์†Œ๋ฅผ ํฌํ•จํ•˜๊ณ , ํŠนํžˆ ์ƒํ’ˆ์ด ๋‹๋ณด์ด๋„๋ก ์กฐํ™”๋กญ๊ฒŒ ํ†ตํ•ฉํ•ด์ฃผ์„ธ์š”."
281
+
282
+ prompt += " ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”. ์ด๋ฏธ์ง€์— ํ…์ŠคํŠธ๋‚˜ ๊ธ€์ž๋ฅผ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”."
283
+ return prompt
284
+
285
+ def generate_with_images(prompt, images, variation_index=0):
286
+ try:
287
+ if not GEMINI_API_KEY:
288
+ return None, "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
289
+
290
+ model = genai.GenerativeModel('gemini-2.0-flash-exp-image-generation')
291
+ logger.info(f"Gemini API ์š”์ฒญ ์‹œ์ž‘ - ํ”„๋กฌํ”„ํŠธ: {prompt}, ๋ณ€ํ˜• ์ธ๋ฑ์Šค: {variation_index}")
292
+
293
+ variation_suffixes = [
294
+ " Create this as the first variation. Do not add any text, watermarks, or labels to the image.",
295
+ " Create this as the second variation with more vivid colors. Do not add any text, watermarks, or labels to the image.",
296
+ " Create this as the third variation with a more creative style. Do not add any text, watermarks, or labels to the image.",
297
+ " Create this as the fourth variation with enhanced details. Do not add any text, watermarks, or labels to the image."
298
+ ]
299
 
300
+ if variation_index < len(variation_suffixes):
301
+ prompt = prompt + variation_suffixes[variation_index]
302
+ else:
303
+ prompt = prompt + " Do not add any text, watermarks, or labels to the image."
304
+
305
+ contents = [prompt]
306
+ for idx, img in enumerate(images, 1):
307
+ if img is not None:
308
+ contents.append(img)
309
+ logger.info(f"์ด๋ฏธ์ง€ #{idx} ์ถ”๊ฐ€๋จ")
310
+
311
+ response = model.generate_content(
312
+ contents=contents,
313
+ generation_config=genai.GenerationConfig(
314
+ temperature=1,
315
+ top_p=0.95,
316
+ top_k=40,
317
+ max_output_tokens=8192
318
+ )
319
  )
320
+
321
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
322
+ temp_path = tmp.name
323
+ result_text = ""
324
+ image_found = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
+ if hasattr(response, 'candidates') and response.candidates:
327
+ candidate = response.candidates[0]
328
+ if hasattr(candidate, 'content') and candidate.content:
329
+ for part in candidate.content.parts:
330
+ if hasattr(part, 'text') and part.text:
331
+ result_text += part.text
332
+ logger.info(f"์‘๋‹ต ํ…์ŠคํŠธ: {part.text}")
333
+ elif hasattr(part, 'inline_data') and part.inline_data:
334
+ save_binary_file(temp_path, part.inline_data.data)
335
+ image_found = True
336
+ logger.info("์‘๋‹ต์—์„œ ์ด๋ฏธ์ง€ ์ถ”์ถœ ์„ฑ๊ณต")
337
 
338
+ if not image_found:
339
+ return None, f"API์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์‘๋‹ต ํ…์ŠคํŠธ: {result_text}"
340
 
341
+ result_img = Image.open(temp_path)
342
+ if result_img.mode == "RGBA":
343
+ result_img = result_img.convert("RGB")
344
+
345
+ return result_img, f"์ด๋ฏธ์ง€๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. {result_text}"
346
+ except Exception as e:
347
+ logger.exception("์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:")
348
+ return None, f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
349
+
350
+ def process_images_with_prompt(image1, image2, image3, prompt, variation_index=0, max_retries=3):
351
+ retry_count = 0
352
+ last_error = None
353
+
354
+ while retry_count < max_retries:
355
+ try:
356
+ images = [image1, image2, image3]
357
+ valid_images = [img for img in images if img is not None]
358
+ if not valid_images:
359
+ return None, "์ ์–ด๋„ ํ•˜๋‚˜์˜ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.", ""
360
+
361
+ if prompt and prompt.strip():
362
+ processed_prompt = preprocess_prompt(prompt, image1, image2, image3)
363
+ if re.search("[๊ฐ€-ํžฃ]", processed_prompt):
364
+ final_prompt = translate_prompt_to_english(processed_prompt)
365
+ else:
366
+ final_prompt = processed_prompt
367
+ else:
368
+ if len(valid_images) == 1:
369
+ final_prompt = "Please creatively transform this image into a more vivid and artistic version. Do not include any text or watermarks in the generated image."
370
+ logger.info("Default prompt generated for single image")
371
+ elif len(valid_images) == 2:
372
+ final_prompt = "Please seamlessly composite these two images, integrating their key elements harmoniously into a single image. Do not include any text or watermarks in the generated image."
373
+ logger.info("Default prompt generated for two images")
374
+ else:
375
+ final_prompt = "Please creatively composite these three images, combining their main elements into a cohesive and natural scene. Do not include any text or watermarks in the generated image."
376
+ logger.info("Default prompt generated for three images")
377
+
378
+ result_img, status = generate_with_images(final_prompt, valid_images, variation_index)
379
+ if result_img is not None:
380
+ return result_img, status, final_prompt
381
+ else:
382
+ last_error = status
383
+ retry_count += 1
384
+ logger.warning(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ, ์žฌ์‹œ๋„ {retry_count}/{max_retries}: {status}")
385
+ time.sleep(1)
386
+ except Exception as e:
387
+ last_error = str(e)
388
+ retry_count += 1
389
+ logger.exception(f"์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ, ์žฌ์‹œ๋„ {retry_count}/{max_retries}:")
390
+ time.sleep(1)
391
+
392
+ return None, f"์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜({max_retries}ํšŒ) ์ดˆ๊ณผ ํ›„ ์‹คํŒจ: {last_error}", prompt
393
+
394
+ def generate_multiple_images(image1, image2, image3, prompt, progress=gr.Progress()):
395
+ results = []
396
+ statuses = []
397
+ prompts = []
398
+
399
+ num_images = 4
400
+ max_retries = 3
401
+
402
+ progress(0, desc="์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค€๋น„ ์ค‘...")
403
+
404
+ for i in range(num_images):
405
+ progress((i / num_images), desc=f"{i+1}/{num_images} ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...")
406
+ result_img, status, final_prompt = process_images_with_prompt(image1, image2, image3, prompt, i, max_retries)
407
 
408
+ if result_img is not None:
409
+ results.append(result_img)
410
+ statuses.append(f"์ด๋ฏธ์ง€ #{i+1}: {status}")
411
+ prompts.append(f"์ด๋ฏธ์ง€ #{i+1}: {final_prompt}")
412
+ else:
413
+ results.append(None)
414
+ statuses.append(f"์ด๋ฏธ์ง€ #{i+1} ์ƒ์„ฑ ์‹คํŒจ: {status}")
415
+ prompts.append(f"์ด๋ฏธ์ง€ #{i+1}: {final_prompt}")
416
+
417
+ time.sleep(1)
418
+
419
+ progress(1.0, desc="์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ!")
420
+
421
+ while len(results) < 4:
422
+ results.append(None)
 
 
 
 
 
423
 
424
+ combined_status = "\n".join(statuses)
425
+ combined_prompts = "\n".join(prompts)
426
+
427
+ return results[0], results[1], results[2], results[3], combined_status, combined_prompts
428
+
429
+ # ์ปค์Šคํ…€ CSS ์Šคํƒ€์ผ
430
+ custom_css = """
431
+ :root {
432
+ --primary-color: #5561e9;
433
+ --secondary-color: #6c8aff;
434
+ --accent-color: #ff6b6b;
435
+ --background-color: #f0f5ff;
436
+ --card-bg: #ffffff;
437
+ --text-color: #334155;
438
+ --border-radius: 18px;
439
+ --shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
440
+ }
441
+
442
+ body {
443
+ font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
444
+ background-color: var(--background-color);
445
+ color: var(--text-color);
446
+ line-height: 1.6;
447
+ }
448
+
449
+ /* Gradio ์ปจํ…Œ์ด๋„ˆ ์˜ค๋ฒ„๋ผ์ด๋“œ */
450
+ .gradio-container {
451
+ max-width: 100% !important;
452
+ margin: 0 auto !important;
453
+ padding: 0 !important;
454
+ background-color: var(--background-color) !important;
455
+ }
456
+
457
+ /* ์ƒ๋‹จ ํ—ค๋” */
458
+ .app-header {
459
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
460
+ color: white;
461
+ padding: 2rem;
462
+ border-radius: var(--border-radius);
463
+ margin-bottom: 1.5rem;
464
+ box-shadow: var(--shadow);
465
+ text-align: center;
466
+ }
467
+
468
+ .app-header h1 {
469
+ margin: 0;
470
+ font-size: 2.5rem;
471
+ font-weight: 700;
472
+ letter-spacing: -0.5px;
473
+ }
474
+
475
+ .app-header p {
476
+ margin: 0.75rem 0 0;
477
+ font-size: 1.1rem;
478
+ opacity: 0.9;
479
+ }
480
+
481
+ /* ํŒจ๋„ ์Šคํƒ€์ผ๋ง */
482
+ .panel {
483
+ background-color: var(--card-bg);
484
+ border-radius: var(--border-radius);
485
+ box-shadow: var(--shadow);
486
+ padding: 1.5rem;
487
+ margin-bottom: 1.5rem;
488
+ border: 1px solid rgba(0, 0, 0, 0.04);
489
+ transition: transform 0.3s ease;
490
+ }
491
+
492
+ .panel:hover {
493
+ transform: translateY(-5px);
494
+ }
495
+
496
+ /* ์„น์…˜ ์ œ๋ชฉ */
497
+ .section-title {
498
+ font-size: 1.5rem;
499
+ font-weight: 700;
500
+ color: var(--primary-color);
501
+ margin-bottom: 1rem;
502
+ padding-bottom: 0.5rem;
503
+ border-bottom: 2px solid var(--secondary-color);
504
+ display: flex;
505
+ align-items: center;
506
+ }
507
+
508
+ .section-title i {
509
+ margin-right: 0.5rem;
510
+ font-size: 1.4rem;
511
+ }
512
+
513
+ /* ๋ฒ„ํŠผ ์Šคํƒ€์ผ๋ง */
514
+ .custom-button {
515
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
516
+ color: white !important;
517
+ border: none !important;
518
+ border-radius: calc(var(--border-radius) - 5px) !important;
519
+ padding: 0.8rem 1.2rem !important;
520
+ font-weight: 600 !important;
521
+ cursor: pointer !important;
522
+ transition: all 0.3s ease !important;
523
+ box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
524
+ text-transform: none !important;
525
+ display: flex !important;
526
+ align-items: center !important;
527
+ justify-content: center !important;
528
+ }
529
+
530
+ .custom-button:hover {
531
+ transform: translateY(-2px) !important;
532
+ box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08) !important;
533
+ }
534
+
535
+ .custom-button.primary {
536
+ background: linear-gradient(135deg, var(--accent-color), #ff9a8b) !important;
537
+ }
538
+
539
+ .custom-button i {
540
+ margin-right: 0.5rem;
541
+ }
542
+
543
+ /* ์ด๋ฏธ์ง€ ์ปจํ…Œ์ด๋„ˆ */
544
+ .image-container {
545
+ border-radius: var(--border-radius);
546
+ overflow: hidden;
547
+ border: 1px solid rgba(0, 0, 0, 0.08);
548
+ transition: all 0.3s ease;
549
+ background-color: white;
550
+ }
551
+
552
+ .image-container:hover {
553
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
554
+ }
555
+
556
+ /* ํƒญ ์ปจํ…Œ์ด๋„ˆ */
557
+ .custom-tabs {
558
+ background-color: transparent !important;
559
+ border: none !important;
560
+ margin-bottom: 1rem;
561
+ }
562
+
563
+ .custom-tabs button {
564
+ background-color: rgba(255, 255, 255, 0.7) !important;
565
+ border: none !important;
566
+ border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
567
+ padding: 0.8rem 1.5rem !important;
568
+ font-weight: 600 !important;
569
+ color: var(--text-color) !important;
570
+ transition: all 0.3s ease !important;
571
+ }
572
+
573
+ .custom-tabs button[aria-selected="true"] {
574
+ background-color: var(--primary-color) !important;
575
+ color: white !important;
576
+ }
577
+
578
+ /* ์ž…๋ ฅ ํ•„๋“œ */
579
+ .custom-input {
580
+ border-radius: calc(var(--border-radius) - 5px) !important;
581
+ border: 1px solid rgba(0, 0, 0, 0.1) !important;
582
+ padding: 0.8rem 1rem !important;
583
+ transition: all 0.3s ease !important;
584
+ }
585
+
586
+ .custom-input:focus {
587
+ border-color: var(--primary-color) !important;
588
+ box-shadow: 0 0 0 2px rgba(85, 97, 233, 0.2) !important;
589
+ }
590
+
591
+ /* ์‚ฌ์šฉ์ž ๋งค๋‰ด์–ผ */
592
+ .user-manual {
593
+ background-color: white;
594
+ padding: 2rem;
595
+ border-radius: var(--border-radius);
596
+ box-shadow: var(--shadow);
597
+ margin-top: 2rem;
598
+ }
599
+
600
+ .manual-title {
601
+ font-size: 1.8rem;
602
+ font-weight: 700;
603
+ color: var(--primary-color);
604
+ margin-bottom: 1.5rem;
605
+ text-align: center;
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: center;
609
+ }
610
+
611
+ .manual-title i {
612
+ margin-right: 0.5rem;
613
+ font-size: 1.8rem;
614
+ }
615
+
616
+ .manual-section {
617
+ margin-bottom: 1.5rem;
618
+ padding: 1.2rem;
619
+ background-color: #f8faff;
620
+ border-radius: calc(var(--border-radius) - 5px);
621
+ }
622
+
623
+ .manual-section-title {
624
+ font-size: 1.3rem;
625
+ font-weight: 700;
626
+ margin-bottom: 1rem;
627
+ color: var(--primary-color);
628
+ display: flex;
629
+ align-items: center;
630
+ }
631
+
632
+ .manual-section-title i {
633
+ margin-right: 0.5rem;
634
+ font-size: 1.2rem;
635
+ }
636
+
637
+ .manual-text {
638
+ font-size: 1rem;
639
+ line-height: 1.7;
640
+ }
641
+
642
+ .manual-text strong {
643
+ color: var(--accent-color);
644
+ }
645
+
646
+ .tip-box {
647
+ background-color: rgba(255, 107, 107, 0.1);
648
+ border-left: 3px solid var(--accent-color);
649
+ padding: 1rem 1.2rem;
650
+ margin: 1rem 0;
651
+ border-radius: 8px;
652
+ }
653
+
654
+ /* ๋ฒ„ํŠผ ๊ทธ๋ฃน */
655
+ .button-grid {
656
+ display: grid;
657
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
658
+ gap: 0.8rem;
659
+ margin-bottom: 1.2rem;
660
+ }
661
+
662
+ /* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
663
+ .progress-container {
664
+ background-color: rgba(255, 255, 255, 0.9);
665
+ border-radius: var(--border-radius);
666
+ padding: 2rem;
667
+ box-shadow: var(--shadow);
668
+ text-align: center;
669
+ }
670
+
671
+ .progress-bar {
672
+ height: 8px;
673
+ border-radius: 4px;
674
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
675
+ margin: 1rem 0;
676
+ }
677
+
678
+ /* ์˜ˆ์‹œ ๊ทธ๋ฆฌ๋“œ */
679
+ .examples-grid {
680
+ display: grid;
681
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
682
+ gap: 1rem;
683
+ margin: 1.5rem 0;
684
+ }
685
+
686
+ .example-card {
687
+ background: white;
688
+ border-radius: var(--border-radius);
689
+ overflow: hidden;
690
+ box-shadow: var(--shadow);
691
+ transition: transform 0.3s ease;
692
+ }
693
+
694
+ .example-card:hover {
695
+ transform: translateY(-5px);
696
+ }
697
+
698
+ /* ๋ฐ˜์‘ํ˜• */
699
+ @media (max-width: 768px) {
700
+ .button-grid {
701
+ grid-template-columns: repeat(2, 1fr);
702
+ }
703
+ }
704
+
705
+ /* ์ „์ฒด ์ธํ„ฐํŽ˜์ด์Šค ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ */
706
+ .block, .prose, .gr-prose, .gr-form, .gr-panel {
707
+ border-radius: var(--border-radius) !important;
708
+ }
709
+
710
+ /* ๋ฉ”์ธ ์ปจํ…์ธ  ์Šคํฌ๋กค๋ฐ” */
711
+ ::-webkit-scrollbar {
712
+ width: 8px;
713
+ height: 8px;
714
+ }
715
+
716
+ ::-webkit-scrollbar-track {
717
+ background: rgba(0, 0, 0, 0.05);
718
+ border-radius: 10px;
719
+ }
720
 
721
+ ::-webkit-scrollbar-thumb {
722
+ background: var(--secondary-color);
723
+ border-radius: 10px;
724
+ }