| """ |
| |
| logger.info("Gemini API νΈμΆ μμ...") |
| # Gemini API νΈμΆ - μ¨λλ₯Ό λμ¬μ λ λ€μν κ²°κ³Ό μμ± |
| response = client.models.generate_content( |
| model="gemini-2.0-flash", |
| contents=prompt, |
| config=GenerateContentConfig( |
| tools=config_tools, # κ²μ μμ§μ λ°λΌ λꡬ μ€μ |
| response_modalities=["TEXT"], |
| temperature=0.95, # μ¨λλ₯Ό λμ¬μ λ λ€μν κ²°κ³Ό |
| max_output_tokens=2000, |
| top_p=0.9, # λ λ€μν ν ν° μ ν |
| top_k=40 # ν보 ν ν° μ μ¦κ° |
| ) |
| ) |
| |
| logger.info("Gemini API μλ΅ μμ μλ£") |
| |
| # μλ΅μμ ν
μ€νΈ μΆμΆ |
| result_text = "" |
| for part in response.candidates[0].content.parts: |
| if hasattr(part, 'text'): |
| result_text += part.text |
| |
| # κ²°κ³Ό μ μ - λ²νΈ, μ€λͺ
, κΈ°νΈ μ κ±° λ° μ€λ³΅ μ κ±° κ°ν |
| lines = result_text.strip().split('\n') |
| clean_keywords = [] |
| seen_keywords = set() # μ€λ³΅ λ°©μ§λ₯Ό μν μ§ν© |
| |
| for line in lines: |
| # λΉ μ€ κ±΄λλ°κΈ° |
| if not line.strip(): |
| continue |
| |
| # μ μ μμ
|
| clean_line = line.strip() |
| |
| # λ²νΈ μ κ±° (1., 2., 3. λ±) |
| import re |
| clean_line = re.sub(r'^\d+\.?\s*', '', clean_line) |
| |
| # λΆλ¦Ώ ν¬μΈνΈ μ κ±° (-, *, β’, β
, β λ±) |
| clean_line = re.sub(r'^[-*β’β
β]\s*', '', clean_line) |
| |
| # κ΄νΈ μ μ€λͺ
μ κ±° |
| clean_line = re.sub(r'\([^)]*\)', '', clean_line) |
| |
| # μΆκ° μ€λͺ
μ κ±° (: μ΄ν λ΄μ©) |
| if ':' in clean_line: |
| clean_line = clean_line.split(':')[0] |
| |
| # 곡백 μ 리 |
| clean_line = clean_line.strip() |
| |
| # μ ν¨ν ν€μλλ§ μΆκ° (2κΈμ μ΄μ, 50κΈμ μ΄ν) |
| if clean_line and 2 <= len(clean_line) <= 50: |
| # λΈλλλͺ
μ΄λ μ μ‘°μ
체λͺ
νν°λ§ |
| brand_keywords = ['μΌμ±', 'μμ§', 'LG', 'μ ν', 'μμ΄ν°', 'κ°€λμ', 'λμ΄ν€', 'μλλ€μ€', 'μ€νλ²
μ€'] |
| if not any(brand in clean_line for brand in brand_keywords): |
| # μ€λ³΅ κ²μ¬ - λμλ¬Έμ κ΅¬λΆ μμ΄ μ²΄ν¬ |
| clean_lower = clean_line.lower() |
| if clean_lower not in seen_keywords: |
| seen_keywords.add(clean_lower) |
| clean_keywords.append(clean_line) |
| |
| # 50κ°λ‘ μ ννλ, λΆμ‘±νλ©΄ μΆκ° μμ± μμ² |
| if len(clean_keywords) < 50: |
| logger.info(f"ν€μλ λΆμ‘± ({len(clean_keywords)}κ°), μΆκ° μμ± νμ") |
| |
| # λΆμ‘±ν λ§νΌ μΆκ° μμ±μ μν 보쑰 ν둬ννΈ |
| additional_prompt = f""" |
| κΈ°μ‘΄μ μμ±λ ν€μλ: {', '.join(clean_keywords)} |
|
|
| μ ν€μλλ€κ³Ό μ λ μ€λ³΅λμ§ μλ μμ ν μλ‘μ΄ {category} κ΄λ ¨ μΌνν€μλλ₯Ό {50 - len(clean_keywords)}κ° λ μμ±νμΈμ. |
|
|
| λ°λμ μ§μΌμΌ ν κ·μΉ: |
| - κΈ°μ‘΄ ν€μλμ μ λ μ€λ³΅λμ§ μμ |
| - μμ¬, νν, κΈ°λ₯μ λ€μνκ² μ‘°ν© |
| - λΈλλλͺ
μ λ κΈμ§ |
| - μμ ν€μλλ§ μΆλ ₯ (λ²νΈ, μ€λͺ
, κΈ°νΈ κΈμ§) |
|
|
| μμ μΆλ ₯: |
| μ€ν
μΈλ¦¬μ€ μλ° |
| κ³ λ¬΄ λ§€νΈ |
| μ 리 μ©κΈ° |
| """ |
| |
| # μΆκ° μμ± μμ² |
| additional_response = client.models.generate_content( |
| model="gemini-2.0-flash", |
| contents=additional_prompt, |
| config=GenerateContentConfig( |
| response_modalities=["TEXT"], |
| temperature=0.98, # λ λμ μ¨λλ‘ λ€μμ± λ³΄μ₯ |
| max_output_tokens=1000, |
| top_p=0.95, |
| top_k=50 |
| ) |
| ) |
| |
| # μΆκ° ν€μλ μ²λ¦¬ |
| additional_text = "" |
| for part in additional_response.candidates[0].content.parts: |
| if hasattr(part, 'text'): |
| additional_text += part.text |
| |
| additional_lines = additional_text.strip().split('\n') |
| for line in additional_lines: |
| if not line.strip(): |
| continue |
| |
| clean_line = line.strip() |
| clean_line = re.sub(r'^\d+\.?\s*', '', clean_line) |
| clean_line = re.sub(r'^[-*β’β
β]\s*', '', clean_line) |
| clean_line = re.sub(r'\([^)]*\)', '', clean_line) |
| |
| if ':' in clean_line: |
| clean_line = clean_line.split(':')[0] |
| |
| clean_line = clean_line.strip() |
| |
| if clean_line and 2 <= len(clean_line) <= 50: |
| brand_keywords = ['μΌμ±', 'μμ§', 'LG', 'μ ν', 'μμ΄ν°', 'κ°€λμ', 'λμ΄ν€', 'μλλ€μ€', 'μ€νλ²
μ€'] |
| if not any(brand in clean_line for brand in brand_keywords): |
| clean_lower = clean_line.lower() |
| if clean_lower not in seen_keywords and len(clean_keywords) < 50: |
| seen_keywords.add(clean_lower) |
| clean_keywords.append(clean_line) |
| |
| # 50κ°λ‘ μ ν |
| clean_keywords = clean_keywords[:50] |
| |
| # μ΅μ’
μ
νλ‘ μμλ 무μμν |
| random.shuffle(clean_keywords) |
| |
| # μ΅μ’
κ²°κ³Ό λ¬Έμμ΄ μμ± |
| final_result = '\n'.join(clean_keywords) |
| |
| logger.info(f"μ΅μ’
μ μ λ ν€μλ κ°μ: {len(clean_keywords)}κ°") |
| logger.info(f"μ€λ³΅ μ κ±°λ ν€μλ μ: {len(seen_keywords)}κ°") |
| logger.info("=== λ€μμ± κ°ν μΌνν€μλ μμ± μλ£ ===") |
| |
| # κ·ΈλΌμ΄λ© λ©νλ°μ΄ν° λ‘κ·Έ |
| if hasattr(response.candidates[0], 'grounding_metadata'): |
| logger.info("Google κ²μ κ·ΈλΌμ΄λ© λ©νλ°μ΄ν° νμΈλ¨") |
| if hasattr(response.candidates[0].grounding_metadata, 'web_search_queries'): |
| queries = response.candidates[0].grounding_metadata.web_search_queries |
| logger.info(f"μ€νλ κ²μ 쿼리: {queries}") |
| |
| return final_result |
| |
| except Exception as e: |
| error_msg = f"μ€λ₯ λ°μ: {str(e)}" |
| logger.error(error_msg) |
| logger.error("GEMINI_API_KEY νκ²½λ³μκ° μ¬λ°λ₯΄κ² μ€μ λμλμ§ νμΈν΄μ£ΌμΈμ.") |
| return f"{error_msg}\n\nGEMINI_API_KEY νκ²½λ³μκ° μ¬λ°λ₯΄κ² μ€μ λμλμ§ νμΈν΄μ£ΌμΈμ." |
| def generate_with_logs(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine): |
| """ν€μλ μμ±κ³Ό λ‘κ·Έλ₯Ό ν¨κ» λ°ννλ ν¨μ""" |
| logger.info("=== λ€μμ± κ°ν μΌνν€μλ μμ± μμ ===") |
| |
| # ν€μλ μμ± |
| result = generate_sourcing_keywords(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine) |
| |
| # μ΅κ·Ό λ‘κ·Έ κ°μ Έμ€κΈ° |
| logs = get_recent_logs() |
| |
| return result, logs |
| # Gradio μΈν°νμ΄μ€ κ΅¬μ± |
| def create_interface(): |
| with gr.Blocks( |
| title="π― λ€μμ± κ°ν μΌνν€μλ μμ€ν
", |
| theme=gr.themes.Soft(), |
| css=""" |
| .gradio-container { |
| max-width: 1200px !important; |
| } |
| .title-header { |
| text-align: center; |
| background: linear-gradient(45deg, |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| font-size: 2.5em; |
| font-weight: bold; |
| margin-bottom: 20px; |
| } |
| .subtitle { |
| text-align: center; |
| color: |
| font-size: 1.2em; |
| margin-bottom: 30px; |
| } |
| """ |
| ) as demo: |
| |
| # ν€λ |
| gr.HTML(""" |
| <div class="title-header">π― λ€μμ± κ°ν μΌνν€μλ μμ€ν
</div> |
| <div class="subtitle">π λ§€λ² μμ ν λ€λ₯Έ κ²°κ³Ό! μ€λ³΅ μλ μΌνν€μλ μ λ¬Έ λ°κ΅΄ νλ‘κ·Έλ¨</div> |
| """) |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("### π λ€μμ± κ°ν μ€μ ") |
| |
| # κ²μ μμ§ μ ν μΆκ° |
| search_engine = gr.Dropdown( |
| choices=[ |
| "λͺ¨λ κ²μ μμ§ ν΅ν© λΆμ (μΆμ²)", |
| "Google κ²μ κ·ΈλΌμ΄λ©λ§", |
| "λ€μ΄λ² κ²μ APIλ§", |
| "DuckDuckGo κ²μλ§", |
| "κ²μ μμ΄ AIλ§ μ¬μ©" |
| ], |
| label="π κ²μ μμ§ μ ν", |
| value="λͺ¨λ κ²μ μμ§ ν΅ν© λΆμ (μΆμ²)" |
| ) |
| |
| # 1. μΌν μΉ΄ν
κ³ λ¦¬ μ ν |
| category = gr.Dropdown( |
| choices=["λλ€μ μ©", "ν¨μ
μ‘ν", "μν/건κ°", "μΆμ°/μ‘μ", "μ€ν¬μΈ /λ μ ", "λμ§νΈ/κ°μ ", "κ°κ΅¬/μΈν
리μ΄", "ν¨μ
μλ₯", "νμ₯ν/λ―Έμ©"], |
| label="ποΈ μΌν μΉ΄ν
κ³ λ¦¬", |
| value="μν/건κ°" |
| ) |
| |
| # 2. μΆκ° μμ²μ¬ν |
| additional_request = gr.Textbox( |
| label="π μΆκ° μμ²μ¬ν (λ€μμ± μ€μ¬)", |
| placeholder="μ: λ€μν μμ¬, μλ‘μ΄ νν, λ
νΉν κΈ°λ₯ λ±", |
| lines=2 |
| ) |
| |
| # 3. μΆμ νμ΄λ° |
| launch_timing = gr.Radio( |
| choices=["λλ€μ μ©", "μ¦μμμ±", "κΈ°νν"], |
| label="β° μΆμ νμ΄λ°", |
| value="μ¦μμμ±" |
| ) |
| |
| # 4. κ³μ μ± |
| seasonality = gr.Radio( |
| choices=["λλ€μ μ©", "λ΄", "μ¬λ¦", "κ°μ", "겨μΈ", "λΉκ³μ "], |
| label="π± κ³μ μ±", |
| value="λΉκ³μ " |
| ) |
| |
| with gr.Column(scale=1): |
| gr.Markdown("### π° λͺ©ν μ€μ ") |
| |
| # 5. λ§€μΆ λͺ©ν |
| sales_target = gr.Radio( |
| choices=["λλ€μ μ©", "100λ§μ μ΄ν", "100-500λ§μ", "500-1μ²λ§μ", "1μ²-5μ²λ§μ", "5μ²λ§μ μ΄μ"], |
| label="π΅ λ§€μΆ λͺ©ν", |
| value="100-500λ§μ" |
| ) |
| |
| # 6. νλ§€ μ±λ |
| sales_channel = gr.Radio( |
| choices=["λλ€μ μ©", "μ€νλ§μΌ", "SNSλ§μΌν
", "κ΄κ³ μ§ν", "μ€νλΌμΈ"], |
| label="π± νλ§€ μ±λ", |
| value="μ€νλ§μΌ" |
| ) |
| |
| # 7. κ²½μ κ°λ |
| competition_level = gr.Radio( |
| choices=[ |
| "λλ€μ μ©", |
| "μ΄λ³΄", |
| "μ€μ", |
| "κ³ μ" |
| ], |
| label="βοΈ κ²½μ κ°λ", |
| value="μ΄λ³΄" |
| ) |
| |
| # μ€ν λ²νΌ |
| generate_btn = gr.Button( |
| "π λ€μμ± κ°ν μΌνν€μλ λ°κ΅΄ μμ (λ§€λ² λ€λ₯Έ κ²°κ³Ό)", |
| variant="primary", |
| size="lg" |
| ) |
| |
| # κ²°κ³Ό μΆλ ₯ |
| with gr.Row(): |
| with gr.Column(scale=2): |
| gr.Markdown("### π λ€μμ± κ°ν μΌνν€μλ (50κ°)") |
| output = gr.Textbox( |
| label="μ€λ³΅ μλ μΌνν€μλ κ²°κ³Ό (λ§€λ² μμ ν λ€λ¦)", |
| lines=30, |
| max_lines=50, |
| placeholder="μ¬κΈ°μ λ§€λ² λ€λ₯Έ 50κ°μ μΌνν€μλκ° μΆλ ₯λ©λλ€...", |
| show_copy_button=True |
| ) |
| |
| with gr.Column(scale=1): |
| gr.Markdown("### π μ€ν λ‘κ·Έ") |
| log_output = gr.Textbox( |
| label="μμ€ν
λ‘κ·Έ", |
| lines=30, |
| max_lines=50, |
| placeholder="μμ€ν
μ€ν λ‘κ·Έκ° μ¬κΈ°μ νμλ©λλ€...", |
| show_copy_button=True |
| ) |
| |
| # μ΄λ²€νΈ μ°κ²° |
| generate_btn.click( |
| fn=generate_with_logs, |
| inputs=[ |
| category, |
| additional_request, |
| launch_timing, |
| seasonality, |
| sales_target, |
| sales_channel, |
| competition_level, |
| search_engine |
| ], |
| outputs=[output, log_output], |
| show_progress=True |
| ) |
| |
| # μ¬μ©λ² μλ΄ |
| with gr.Accordion("π λ€μμ± κ°ν μ¬μ©λ² μλ΄", open=False): |
| gr.Markdown(""" |
| |
| |
| |
| - **μμ ν μ€λ³΅ λ°©μ§**: λ§€λ² μ€νν λλ§λ€ μμ ν λ€λ₯Έ ν€μλ μμ± |
| - **λλ€ μλ μμ€ν
**: νμ¬ μκ°μ κΈ°λ°μΌλ‘ ν λλ€ μλλ‘ μμΈ‘ λΆκ°λ₯ |
| - **λ€μν μ‘°ν© λ³΄μ₯**: μμ¬ΓννΓκΈ°λ₯μ 3μ°¨μ μ‘°ν©μΌλ‘ 무ν λ€μμ± |
| - **μ€λ³΅ κ²μ¬ κ°ν**: λμλ¬Έμ κ΅¬λΆ μλ μ격ν μ€λ³΅ μ κ±° |
| - **μ¨λ μ‘°μ **: AI μμ± νλΌλ―Έν° μ΅μ νλ‘ μ°½μμ± κ·Ήλν |
| |
| |
| 1. **μλ κΈ°λ° λλ€ν**: λ§μ΄ν¬λ‘μ΄ λ¨μ μκ° κΈ°λ° λλ€ μλ |
| 2. **3μ°¨μ μ‘°ν© μμ€ν
**: |
| - μμ¬: μ€λ¦¬μ½, μ€ν
μΈλ¦¬μ€, μΈλΌλ―Ή, λλ무 λ± 20μ’
|
| - νν: μ μ΄μ, μν, μ¬λ¦Ό, ν΄λμ© λ± 20μ’
|
| - κΈ°λ₯: λ°©μ, νκ· , λ§κ·Έλ€ν±, λ³΄μ¨ λ± 20μ’
|
| 3. **μ€λ³΅ λ°©μ§ μκ³ λ¦¬μ¦**: μμ± μ€ μ€μκ° μ€λ³΅ κ²μ¬ |
| 4. **μΆκ° μμ± μμ€ν
**: λΆμ‘±μ μλμΌλ‘ μΆκ° ν€μλ μμ± |
| |
| |
| - **ν€μλλ³ λ
립 μ μ©**: κ° ν€μλλ§λ€ λ€λ₯Έ 쑰건 μ‘°ν© |
| - **μμΈ‘ λΆκ°λ₯μ±**: κ°μ μ€μ μ΄λΌλ λ§€λ² λ€λ₯Έ κ²°κ³Ό |
| - **μ‘°ν© νλ°**: μμ² κ°μ§ κ°λ₯ν μ‘°ν©μΌλ‘ 무ν λ€μμ± |
| |
| |
| import os |
| import logging |
| import sys |
| import random |
| import requests |
| import json |
| from datetime import datetime |
| from google import genai |
| from google.genai.types import Tool, GenerateContentConfig, GoogleSearch |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(levelname)s - %(message)s', |
| handlers=[ |
| logging.StreamHandler(sys.stdout), |
| logging.FileHandler('sourcing_app.log', encoding='utf-8') |
| ] |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
| DIVERSE_SEED_POOLS = { |
| "ν¨μ
μ‘ν": [ |
| "μ‘μΈμ리", "μ₯μ ꡬ", "κ°λ°©", "μ§κ°", "λͺ¨μ", "μ€μΉ΄ν", "벨νΈ", "μ κΈλΌμ€", "ν€μ΄μ‘μΈμ리", "μκ³μ€", |
| "ν€λ§", "λΈλ‘μΉ", "λͺ©κ±Έμ΄", "νμ°", "λ°μ§", "κ·κ±Έμ΄", "νΈλν°μΌμ΄μ€", "νμ°μΉ", "ν΄λ¬μΉ", "ν νΈλ°±" |
| ], |
| "μν/건κ°": [ |
| "μ£Όλ°©μ©ν", "μμ€μ©ν", "μ²μμ©ν", "μλ©μ©ν", "건κ°μ©ν", "μλ£μ©ν", "λ§μ¬μ§μ©ν", "μ΄λμ©ν", |
| "λ€μ΄μ΄νΈμ©ν", "νμ₯μ§", "μΈμ ", "μ΄νΈ", "μΉμ½", "λΉλ", "μ건", "λ² κ°", "μ΄λΆ", "μΏ μ
", "λ§€νΈ" |
| ], |
| "μΆμ°/μ‘μ": [ |
| "μ μμ©ν", "μ‘μμ©ν", "μΆμ°μ©ν", "μμ°λΆμ©ν", "μ μμμ©ν", "μ΄μ μμ©ν", "κΈ°μ κ·", "μ λ³", |
| "μ λͺ¨μ°¨", "μΉ΄μνΈ", "μκΈ°μ·", "μ₯λκ°", "κ΅μ‘μ©ν", "μ±
", "κ·Έλ¦Όμ±
", "νΌμ¦", "λΈλ‘", "μΈν", "λμ΄λ§€νΈ" |
| ], |
| "μ€ν¬μΈ /λ μ ": [ |
| "μ΄λμ©ν", "ν¬μ€μ©ν", "μκ°μ©ν", "μμμ©ν", "λ±μ°μ©ν", "μΊ νμ©ν", "λμμ©ν", "골νμ©ν", |
| "μΆκ΅¬μ©ν", "λꡬμ©ν", "λ°°λλ―Όν΄μ©ν", "νꡬμ©ν", "ν
λμ€μ©ν", "μμ κ±°μ©ν", "μ€μΌμ΄νΈλ³΄λμ©ν" |
| ], |
| "λμ§νΈ/κ°μ ": [ |
| "μ€λ§νΈν°μ‘μΈμ리", "μ»΄ν¨ν°μ©ν", "νλΈλ¦Ώμ©ν", "μ΄μ΄ν°", "μ€νΌμ»€", "μΆ©μ κΈ°", "μΌμ΄λΈ", "λ§μ°μ€ν¨λ", |
| "ν€λ³΄λ", "λ§μ°μ€", "μΉμΊ ", "λ§μ΄ν¬", "ν€λμ
", "κ²μν¨λ", "USB", "λ©λͺ¨λ¦¬μΉ΄λ", "νμλ±
ν¬" |
| ], |
| "κ°κ΅¬/μΈν
리μ΄": [ |
| "μλ©κ°κ΅¬", "μΉ¨μ€κ°κ΅¬", "κ±°μ€κ°κ΅¬", "μ£Όλ°©κ°κ΅¬", "μμ€κ°κ΅¬", "μ¬λ¬΄μ©κ°κ΅¬", "μΈν
리μ΄μν", "μ‘°λͺ
", |
| "컀νΌ", "λΈλΌμΈλ", "μΉ΄ν«", "λ¬κ·Έ", "μ‘μ", "κ±°μΈ", "μκ³", "νλΆ", "κ½λ³", "μΊλ€", "λ°©ν₯μ " |
| ], |
| "ν¨μ
μλ₯": [ |
| "ν°μ
μΈ ", "μ
μΈ ", "λΈλΌμ°μ€", "μνΌμ€", "μ€μ»€νΈ", "λ°μ§", "μ²λ°μ§", "λ κΉ
μ€", "μμΌ", "μ½νΈ", |
| "μ νΌ", "κ°λ건", "λνΈ", "νλ", "μ‘°λΌ", "μμ·", "μ μ·", "μλ§", "μ€ννΉ", "μ΄λ볡" |
| ], |
| "νμ₯ν/λ―Έμ©": [ |
| "μ€ν¨μΌμ΄", "λ©μ΄ν¬μ
", "ν΄λ μ§", "λ§μ€ν¬ν©", "μ ν¬λ¦Ό", "λ‘μ
", "μμΌμ€", "ν¬λ¦Ό", "립밀", |
| "립μ€ν±", "μμ΄μλ", "λ§μ€μΉ΄λΌ", "νμ΄λ°μ΄μ
", "컨μ€λ¬", "λΈλ¬μ
", "νμ΄λΌμ΄ν°", "λ€μΌ", "ν₯μ" |
| ] |
| } |
|
|
| |
| MATERIAL_KEYWORDS = [ |
| "μ€λ¦¬μ½", "μ€ν
μΈλ¦¬μ€", "μΈλΌλ―Ή", "μ 리", "λ무", "λλ무", "λ©΄", "λ¦°λ¨", "ν΄λ¦¬μμ€ν°", "λμΌλ‘ ", |
| "κ³ λ¬΄", "νλΌμ€ν±", "μ’
μ΄", "κ°μ£½", "μΈμ‘°κ°μ£½", "λ©ν", "μ루미λ", "μ² ", "ꡬ리", "ν©λ" |
| ] |
|
|
| |
| SHAPE_KEYWORDS = [ |
| "μ μ΄μ", "ν΄λμ©", "λ―Έλ", "λν", "μν", "μ¬κ°", "νμ", "μ§μ¬κ°", "μΌκ°", "μ‘κ°", |
| "μ¬λ¦Ό", "λκΊΌμ΄", "μμ", "κΈ΄", "μ§§μ", "λμ", "μ’μ", "κΉμ", "μμ", "곑μ " |
| ] |
|
|
| |
| FUNCTION_KEYWORDS = [ |
| "λ°©μ", "λ―ΈλλΌλ°©μ§", "νκ· ", "λμμ κ±°", "보μ¨", "보λ", "μ건", "ν‘μ", "μ°¨λ¨", "보νΈ", |
| "λ§κ·Έλ€ν±", "μμ", "λμ ", "ν¬λͺ
", "λΆν¬λͺ
", "λ°κ΄", "λ°μ¬", "μ μΆ", "νλ ₯", "κ³ μ " |
| ] |
|
|
| |
| def initialize_gemini(): |
| logger.info("Gemini API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μμ") |
| api_key = os.getenv("GEMINI_API_KEY") |
| if not api_key: |
| logger.error("GEMINI_API_KEY νκ²½λ³μκ° μ€μ λμ§ μμμ΅λλ€.") |
| raise ValueError("GEMINI_API_KEY νκ²½λ³μκ° μ€μ λμ§ μμμ΅λλ€.") |
| |
| client = genai.Client(api_key=api_key) |
| logger.info("Gemini API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μλ£") |
| return client |
|
|
| def get_recent_logs(): |
| """μ΅κ·Ό λ‘κ·Έλ₯Ό κ°μ Έμ€λ ν¨μ""" |
| try: |
| with open('sourcing_app.log', 'r', encoding='utf-8') as f: |
| lines = f.readlines() |
| |
| return ''.join(lines[-50:]) |
| except FileNotFoundError: |
| return "λ‘κ·Έ νμΌμ μ°Ύμ μ μμ΅λλ€." |
| except Exception as e: |
| return f"λ‘κ·Έ μ½κΈ° μ€λ₯: {str(e)}" |
|
|
| def generate_diverse_keyword_combinations(category, count=60): |
| """λ€μν ν€μλ μ‘°ν©μ μμ±νλ ν¨μ""" |
| logger.info(f"λ€μν ν€μλ μ‘°ν© μμ± μμ: {category}, {count}κ°") |
| |
| combinations = [] |
| category_pool = DIVERSE_SEED_POOLS.get(category, DIVERSE_SEED_POOLS["μν/건κ°"]) |
| |
| |
| single_keywords = random.sample(category_pool, min(12, len(category_pool))) |
| combinations.extend(single_keywords) |
| |
| |
| for _ in range(18): |
| material = random.choice(MATERIAL_KEYWORDS) |
| item = random.choice(category_pool) |
| combinations.append(f"{material} {item}") |
| |
| |
| for _ in range(18): |
| shape = random.choice(SHAPE_KEYWORDS) |
| item = random.choice(category_pool) |
| combinations.append(f"{shape} {item}") |
| |
| |
| for _ in range(12): |
| function = random.choice(FUNCTION_KEYWORDS) |
| item = random.choice(category_pool) |
| combinations.append(f"{function} {item}") |
| |
| |
| combinations = list(set(combinations)) |
| random.shuffle(combinations) |
| |
| logger.info(f"μμ±λ μ‘°ν© μ: {len(combinations)}κ°") |
| return combinations[:count] |
|
|
| def search_all_engines(query): |
| """λͺ¨λ κ²μ μμ§μ μ¬μ©νμ¬ λ°μ΄ν°λ₯Ό μ·¨ν©νλ ν¨μ""" |
| logger.info(f"λͺ¨λ κ²μ μμ§μΌλ‘ κ²μ μμ: {query}") |
| |
| all_results = { |
| "google": "", |
| "naver": "", |
| "duckduckgo": "" |
| } |
| |
| |
| try: |
| naver_client_id = os.getenv("NAVER_CLIENT_ID") |
| naver_client_secret = os.getenv("NAVER_CLIENT_SECRET") |
| |
| if naver_client_id and naver_client_secret: |
| url = "https://openapi.naver.com/v1/search/shop.json" |
| headers = { |
| "X-Naver-Client-Id": naver_client_id, |
| "X-Naver-Client-Secret": naver_client_secret |
| } |
| params = {"query": query, "display": 10} |
| |
| response = requests.get(url, headers=headers, params=params, timeout=10) |
| if response.status_code == 200: |
| data = response.json() |
| naver_data = [] |
| for item in data.get('items', [])[:5]: |
| naver_data.append(f"μν: {item.get('title', '').replace('<b>', '').replace('</b>', '')}") |
| naver_data.append(f"κ°κ²©: {item.get('lprice', '')}μ") |
| naver_data.append(f"μΉ΄ν
κ³ λ¦¬: {item.get('category1', '')}") |
| all_results["naver"] = "\n".join(naver_data) |
| logger.info("λ€μ΄λ² κ²μ μλ£") |
| else: |
| all_results["naver"] = "λ€μ΄λ² API κ²μ μ€ν¨" |
| else: |
| all_results["naver"] = "λ€μ΄λ² API ν€κ° μ€μ λμ§ μμ" |
| except Exception as e: |
| all_results["naver"] = f"λ€μ΄λ² κ²μ μ€λ₯: {str(e)}" |
| |
| |
| try: |
| url = "https://api.duckduckgo.com/" |
| params = { |
| "q": query, |
| "format": "json", |
| "no_html": "1", |
| "skip_disambig": "1" |
| } |
| |
| response = requests.get(url, params=params, timeout=10) |
| if response.status_code == 200: |
| data = response.json() |
| |
| ddg_data = [] |
| |
| if data.get('Abstract'): |
| ddg_data.append(f"μμ½: {data['Abstract']}") |
| |
| |
| for topic in data.get('RelatedTopics', [])[:3]: |
| if isinstance(topic, dict) and topic.get('Text'): |
| ddg_data.append(f"κ΄λ ¨μ 보: {topic['Text']}") |
| |
| all_results["duckduckgo"] = "\n".join(ddg_data) if ddg_data else "DuckDuckGoμμ κ΄λ ¨ μ 보 μμ" |
| logger.info("DuckDuckGo κ²μ μλ£") |
| else: |
| all_results["duckduckgo"] = "DuckDuckGo κ²μ μ€ν¨" |
| except Exception as e: |
| all_results["duckduckgo"] = f"DuckDuckGo κ²μ μ€λ₯: {str(e)}" |
| |
| |
| all_results["google"] = "Google κ²μ κ·ΈλΌμ΄λ© μλ μ€ν" |
| |
| logger.info("λͺ¨λ κ²μ μμ§ λ°μ΄ν° μμ§ μλ£") |
| return all_results |
|
|
| def comprehensive_market_analysis(category, seasonality, sales_target): |
| """λ€μμ± κ°νλ μμ₯ λΆμ""" |
| logger.info("λ€μμ± κ°ν μμ₯ λΆμ μμ") |
| |
| |
| random.seed(datetime.now().microsecond) |
| |
| |
| search_angles = [ |
| "νμμν", "μ μν", "μΈκΈ°μν", "μ κ°μν", "κ³ κΈμν", "ν μΈμν", |
| "κ°νΈμν", "μ€μ©μν", "νΈλ λμν", "μ¨μμν", "λ² μ€νΈμν", "μΆμ²μν" |
| ] |
| |
| search_queries = [] |
| |
| |
| category_pool = DIVERSE_SEED_POOLS.get(category, DIVERSE_SEED_POOLS["μν/건κ°"]) |
| |
| for angle in search_angles: |
| for item in random.sample(category_pool, 3): |
| search_queries.append(f"{item} {angle}") |
| |
| |
| for material in random.sample(MATERIAL_KEYWORDS, 5): |
| search_queries.append(f"{material} {category} μν") |
| |
| |
| for shape in random.sample(SHAPE_KEYWORDS, 5): |
| search_queries.append(f"{shape} {category} μμ΄ν
") |
| |
| |
| random.shuffle(search_queries) |
| search_queries = search_queries[:15] |
| |
| comprehensive_data = {} |
| |
| for i, query in enumerate(search_queries): |
| logger.info(f"λ€μμ± κ²μ {i+1}/15: {query}") |
| comprehensive_data[f"query_{i+1}"] = search_all_engines(query) |
| |
| |
| import time |
| time.sleep(0.5) |
| |
| |
| summary = "=== λ€μμ± κ°ν μμ₯ λΆμ κ²°κ³Ό ===\n\n" |
| |
| |
| result_keys = list(comprehensive_data.keys()) |
| random.shuffle(result_keys) |
| |
| summary += "π λ€μν μμ₯ κ²μ κ²°κ³Ό:\n" |
| for key in result_keys[:10]: |
| results = comprehensive_data.get(key, {}) |
| if results.get("naver"): |
| summary += f"β’ {results['naver'][:60]}...\n" |
| summary += "\n" |
| |
| logger.info("λ€μμ± κ°ν λΆμ μλ£") |
| return summary |
|
|
| def search_with_api(query, search_engine="Google κ²μ κ·ΈλΌμ΄λ©λ§"): |
| """κ°λ³ κ²μ μμ§μΌλ‘ κ²μνλ ν¨μ (λ¨μΌ μμ§ μ νμ μ¬μ©)""" |
| logger.info(f"κ²μ μμ§: {search_engine}, 쿼리: {query}") |
| |
| search_results = "" |
| |
| try: |
| if search_engine == "λ€μ΄λ² κ²μ APIλ§": |
| |
| naver_client_id = os.getenv("NAVER_CLIENT_ID") |
| naver_client_secret = os.getenv("NAVER_CLIENT_SECRET") |
| |
| if naver_client_id and naver_client_secret: |
| url = "https://openapi.naver.com/v1/search/shop.json" |
| headers = { |
| "X-Naver-Client-Id": naver_client_id, |
| "X-Naver-Client-Secret": naver_client_secret |
| } |
| params = {"query": query, "display": 10} |
| |
| response = requests.get(url, headers=headers, params=params) |
| if response.status_code == 200: |
| data = response.json() |
| for item in data.get('items', [])[:5]: |
| search_results += f"μνλͺ
: {item.get('title', '')}\n" |
| search_results += f"κ°κ²©: {item.get('lprice', '')}μ\n" |
| search_results += f"μΉ΄ν
κ³ λ¦¬: {item.get('category1', '')}\n\n" |
| else: |
| search_results = "λ€μ΄λ² API κ²μ μ€ν¨" |
| else: |
| search_results = "λ€μ΄λ² API ν€κ° μ€μ λμ§ μμ" |
| |
| elif search_engine == "DuckDuckGo κ²μλ§": |
| |
| try: |
| url = "https://api.duckduckgo.com/" |
| params = { |
| "q": query, |
| "format": "json", |
| "no_html": "1", |
| "skip_disambig": "1" |
| } |
| |
| response = requests.get(url, params=params, timeout=10) |
| if response.status_code == 200: |
| data = response.json() |
| |
| |
| if data.get('Abstract'): |
| search_results += f"μμ½: {data['Abstract']}\n\n" |
| |
| |
| for topic in data.get('RelatedTopics', [])[:5]: |
| if isinstance(topic, dict) and topic.get('Text'): |
| search_results += f"κ΄λ ¨ μ 보: {topic['Text']}\n" |
| |
| if not search_results: |
| search_results = "DuckDuckGoμμ κ΄λ ¨ μ 보λ₯Ό μ°Ύμ§ λͺ»ν¨" |
| else: |
| search_results = "DuckDuckGo κ²μ μ€ν¨" |
| except Exception as e: |
| search_results = f"DuckDuckGo κ²μ μ€λ₯: {str(e)}" |
| |
| elif search_engine == "κ²μ μμ΄ AIλ§ μ¬μ©": |
| search_results = "κ²μ μμ΄ AI μ§μλ§ μ¬μ©νμ¬ ν€μλ μμ±" |
| |
| else: |
| |
| search_results = "Google κ²μ κ·ΈλΌμ΄λ© μ¬μ©" |
| |
| except Exception as e: |
| logger.error(f"κ²μ μ€λ₯: {str(e)}") |
| search_results = f"κ²μ μ€λ₯: {str(e)}" |
| |
| logger.info(f"κ²μ κ²°κ³Ό κΈΈμ΄: {len(search_results)} λ¬Έμ") |
| return search_results |
|
|
| def apply_random_selection_for_keywords(category, launch_timing, seasonality, sales_target, sales_channel, competition_level): |
| """κ° ν€μλλ§λ€ λλ€νκ² μ‘°κ±΄μ μ μ©νκΈ° μν μ€μ λ¬Έμμ΄ μμ±""" |
| |
| |
| categories = ["ν¨μ
μ‘ν", "μν/건κ°", "μΆμ°/μ‘μ", "μ€ν¬μΈ /λ μ ", "λμ§νΈ/κ°μ ", "κ°κ΅¬/μΈν
리μ΄", "ν¨μ
μλ₯", "νμ₯ν/λ―Έμ©"] |
| launch_timings = ["μ¦μμμ±", "κΈ°νν"] |
| seasonalities = ["λ΄", "μ¬λ¦", "κ°μ", "겨μΈ", "λΉκ³μ "] |
| sales_targets = ["100λ§μ μ΄ν", "100-500λ§μ", "500-1μ²λ§μ", "1μ²-5μ²λ§μ", "5μ²λ§μ μ΄μ"] |
| sales_channels = ["μ€νλ§μΌ", "SNSλ§μΌν
", "κ΄κ³ μ§ν", "μ€νλΌμΈ"] |
| competition_levels = ["μ΄λ³΄", "μ€μ", "κ³ μ"] |
| |
| |
| random_settings = { |
| 'category_random': category == "λλ€μ μ©", |
| 'launch_timing_random': launch_timing == "λλ€μ μ©", |
| 'seasonality_random': seasonality == "λλ€μ μ©", |
| 'sales_target_random': sales_target == "λλ€μ μ©", |
| 'sales_channel_random': sales_channel == "λλ€μ μ©", |
| 'competition_level_random': competition_level == "λλ€μ μ©", |
| 'categories': categories, |
| 'launch_timings': launch_timings, |
| 'seasonalities': seasonalities, |
| 'sales_targets': sales_targets, |
| 'sales_channels': sales_channels, |
| 'competition_levels': competition_levels |
| } |
| |
| |
| fixed_values = { |
| 'category': category if category != "λλ€μ μ©" else None, |
| 'launch_timing': launch_timing if launch_timing != "λλ€μ μ©" else None, |
| 'seasonality': seasonality if seasonality != "λλ€μ μ©" else None, |
| 'sales_target': sales_target if sales_target != "λλ€μ μ©" else None, |
| 'sales_channel': sales_channel if sales_channel != "λλ€μ μ©" else None, |
| 'competition_level': competition_level if competition_level != "λλ€μ μ©" else None |
| } |
| |
| logger.info("=== ν€μλλ³ λλ€ μ€μ ===") |
| logger.info(f"μΉ΄ν
κ³ λ¦¬ λλ€: {random_settings['category_random']}") |
| logger.info(f"μΆμνμ΄λ° λλ€: {random_settings['launch_timing_random']}") |
| logger.info(f"κ³μ μ± λλ€: {random_settings['seasonality_random']}") |
| logger.info(f"λ§€μΆλͺ©ν λλ€: {random_settings['sales_target_random']}") |
| logger.info(f"νλ§€μ±λ λλ€: {random_settings['sales_channel_random']}") |
| logger.info(f"κ²½μκ°λ λλ€: {random_settings['competition_level_random']}") |
| |
| return random_settings, fixed_values |
|
|
| def generate_sourcing_keywords(category, additional_request, launch_timing, seasonality, sales_target, sales_channel, competition_level, search_engine="Google κ²μ κ·ΈλΌμ΄λ©λ§"): |
| """λ€μμ± κ°νλ μΌν ν€μλ 50κ°λ₯Ό μμ±νλ ν¨μ""" |
| logger.info("=== λ€μμ± κ°ν μΌνν€μλ μμ± μμ ===") |
| logger.info(f"μ
λ ₯ 쑰건 - κ²μμμ§: {search_engine}") |
| logger.info(f"μ
λ ₯ 쑰건 - μΉ΄ν
κ³ λ¦¬: {category}") |
| logger.info(f"μ
λ ₯ 쑰건 - μΆκ°μμ²: {additional_request}") |
| logger.info(f"μ
λ ₯ 쑰건 - μΆμνμ΄λ°: {launch_timing}") |
| logger.info(f"μ
λ ₯ 쑰건 - κ³μ μ±: {seasonality}") |
| logger.info(f"μ
λ ₯ 쑰건 - λ§€μΆλͺ©ν: {sales_target}") |
| logger.info(f"μ
λ ₯ 쑰건 - νλ§€μ±λ: {sales_channel}") |
| logger.info(f"μ
λ ₯ 쑰건 - κ²½μκ°λ: {competition_level}") |
| |
| try: |
| logger.info("Gemini ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μ€...") |
| client = initialize_gemini() |
| |
| |
| current_time = datetime.now() |
| random_seed = current_time.microsecond + current_time.second * 1000 |
| random.seed(random_seed) |
| logger.info(f"λλ€ μλ μ€μ : {random_seed}") |
| |
| |
| logger.info("λ€μμ± κ°ν ν둬ννΈ κ΅¬μ± μ€...") |
| |
| |
| random_settings, fixed_values = apply_random_selection_for_keywords( |
| category, launch_timing, seasonality, sales_target, sales_channel, competition_level |
| ) |
| |
| |
| diverse_combinations = generate_diverse_keyword_combinations(category, 60) |
| logger.info(f"λ€μν μ‘°ν© μμ± μλ£: {len(diverse_combinations)}κ°") |
| |
| |
| search_info = "" |
| config_tools = [] |
| |
| if search_engine == "λͺ¨λ κ²μ μμ§ ν΅ν© λΆμ (μΆμ²)": |
| logger.info("π λ€μμ± κ°ν ν΅ν© λΆμ μμ...") |
| |
| |
| google_search_tool = Tool(google_search=GoogleSearch()) |
| config_tools = [google_search_tool] |
| |
| |
| comprehensive_analysis = comprehensive_market_analysis(category, seasonality, sales_target) |
| |
| search_info = f""" |
| π === λ€μμ± κ°ν ν΅ν© λΆμ κ²°κ³Ό === |
| π Google κ²μ κ·ΈλΌμ΄λ©: μ€μκ° λ€μν μΌνν€μλ νΈλ λ λΆμ (μλ μ€ν) |
| π λ€μ΄λ² μΌν API: νκ΅ μΌνλͺ° λ€μν ν€μλ λ°μ΄ν° λΆμ |
| π DuckDuckGo κ²μ: κΈλ‘λ² λ€μν μΌνν€μλ μ 보 λΆμ |
| {comprehensive_analysis} |
| π‘ μ λͺ¨λ λ°μ΄ν°λ₯Ό μ’
ν©νμ¬ λ§€λ² λ€λ₯Έ μ‘°ν©μ μΌνν€μλλ₯Ό μμ±ν©λλ€. |
| π² λλ€ μλ: {random_seed} (λ§€λ² λ€λ₯Έ κ²°κ³Ό 보μ₯) |
| """ |
| |
| elif search_engine == "Google κ²μ κ·ΈλΌμ΄λ©λ§": |
| logger.info("Google κ²μ λꡬ μ€μ μ€...") |
| google_search_tool = Tool(google_search=GoogleSearch()) |
| config_tools = [google_search_tool] |
| search_info = f"Google κ²μ κ·ΈλΌμ΄λ©μ ν΅ν λ€μν μ€μκ° μΌνν€μλ λΆμ (μλ: {random_seed})" |
| |
| elif search_engine in ["λ€μ΄λ² κ²μ APIλ§", "DuckDuckGo κ²μλ§"]: |
| logger.info(f"{search_engine} μ¬μ©νμ¬ λ€μν μΌνν€μλ μ‘°μ¬ μ€...") |
| |
| search_queries = [] |
| |
| |
| base_items = random.sample(diverse_combinations, 8) |
| for item in base_items: |
| search_queries.append(f"{item} μΌνν€μλ") |
| |
| search_results = "" |
| for query in search_queries: |
| result = search_with_api(query, search_engine) |
| search_results += f"[κ²μμ΄: {query}]\n{result}\n\n" |
| |
| search_info = f"{search_engine} λ€μν μΌνν€μλ κ²μ κ²°κ³Ό (μλ: {random_seed}):\n{search_results}" |
| |
| else: |
| logger.info("κ²μ μμ΄ AI μ§μλ§ μ¬μ©") |
| search_info = f"AI λ΄μ₯ μ§μμ κΈ°λ°μΌλ‘ λ€μν μΌνν€μλ μμ± (μλ: {random_seed})" |
| |
| |
| diverse_sample = random.sample(diverse_combinations, 20) |
| |
| prompt = f""" |
| π― λ€μμ± κ°ν μΌνν€μλ λ°κ΅΄ μμ€ν
v5.0 |
| β‘ μ€μ: μ λ μ€λ³΅λμ§ μλ λ€μν ν€μλλ§ μμ±νμΈμ! |
| π¬ μν μ μ |
| λΉμ μ λ§€λ² μμ ν λ€λ₯Έ μ‘°ν©μ μΌνν€μλλ₯Ό μμ±νλ μ λ¬Έκ°μ
λλ€. |
| π― λͺ©ν |
| μ£Όμ΄μ§ 쑰건μ λ§λ μ€μ μΌνν€μλ 50κ°λ₯Ό λ°κ΅΄νλ, μ λ μ€λ³΅λμ§ μκ³ λ§€λ² λ€λ₯Έ μ‘°ν©μΌλ‘ ꡬμ±νμμμ€. |
| π μ
λ ₯λ 쑰건: |
| μΉ΄ν
κ³ λ¦¬: {category} |
| μΆκ° μμ²μ¬ν: {additional_request} |
| μΆμνμ΄λ°: {launch_timing} |
| κ³μ μ±: {seasonality} |
| λ§€μΆλͺ©ν: {sales_target} |
| νλ§€μ±λ: {sales_channel} |
| κ²½μκ°λ: {competition_level} |
| κ²μμμ§: {search_engine} |
| π μΌνν€μλ λΆμ μ 보: |
| {search_info} |
| π² λ€μμ± λ³΄μ₯ μ°Έκ³ μ‘°ν© μμ (μ΄κ²κ³Ό λ€λ₯΄κ² μμ±νμΈμ): |
| {', '.join(diverse_sample[:10])} |
| β οΈ ν€μλλ³ λλ€ μ μ© κ·μΉ: |
| κ° ν€μλλ§λ€ λ€μκ³Ό κ°μ΄ μ μ©νμΈμ: |
| {"- μΉ΄ν
κ³ λ¦¬: λ§€ ν€μλλ§λ€ " + str(random_settings['categories']) + " μ€μμ λλ€ μ ν" if random_settings['category_random'] else f"- μΉ΄ν
κ³ λ¦¬: {fixed_values['category']} κ³ μ "} |
| {"- μΆμνμ΄λ°: λ§€ ν€μλλ§λ€ " + str(random_settings['launch_timings']) + " μ€μμ λλ€ μ ν" if random_settings['launch_timing_random'] else f"- μΆμνμ΄λ°: {fixed_values['launch_timing']} κ³ μ "} |
| {"- κ³μ μ±: λ§€ ν€μλλ§λ€ " + str(random_settings['seasonalities']) + " μ€μμ λλ€ μ ν" if random_settings['seasonality_random'] else f"- κ³μ μ±: {fixed_values['seasonality']} κ³ μ "} |
| {"- λ§€μΆλͺ©ν: λ§€ ν€μλλ§λ€ " + str(random_settings['sales_targets']) + " μ€μμ λλ€ μ ν" if random_settings['sales_target_random'] else f"- λ§€μΆλͺ©ν: {fixed_values['sales_target']} κ³ μ "} |
| {"- νλ§€μ±λ: λ§€ ν€μλλ§λ€ " + str(random_settings['sales_channels']) + " μ€μμ λλ€ μ ν" if random_settings['sales_channel_random'] else f"- νλ§€μ±λ: {fixed_values['sales_channel']} κ³ μ "} |
| {"- κ²½μκ°λ: λ§€ ν€μλλ§λ€ " + str(random_settings['competition_levels']) + " μ€μμ λλ€ μ ν" if random_settings['competition_level_random'] else f"- κ²½μκ°λ: {fixed_values['competition_level']} κ³ μ "} |
| βοΈ λ€μμ± κ°ν μν¬νλ‘μ° |
| 1λ¨κ³: μμ ν μλ‘μ΄ μ‘°ν© μμ± |
| - μ΄μ κ²°κ³Όμ μ λ μ€λ³΅λμ§ μλ ν€μλ μ‘°ν© |
| - μμ¬({', '.join(MATERIAL_KEYWORDS[:5])}) + μνλͺ
μ‘°ν© |
| - νν({', '.join(SHAPE_KEYWORDS[:5])}) + μνλͺ
μ‘°ν© |
| - κΈ°λ₯({', '.join(FUNCTION_KEYWORDS[:5])}) + μνλͺ
μ‘°ν© |
| 2λ¨κ³: μ€λ³΅ λ°©μ§ νν°λ§ |
| - λμΌν ν€μλ μ‘°ν© μμ λ°°μ |
| - μ μ¬ν μλ―Έμ ν€μλ μ‘°ν© λ°°μ |
| - λ§€λ² μλ‘μ΄ κ°λλ‘ μ κ·Ό |
| 3λ¨κ³: 50κ° λ€μν ν€μλ μ λ³ |
| - λΈλλλͺ
μ λ κΈμ§ |
| - 볡μ‘ν κΈ°μ μ©μ΄ κΈμ§ |
| - μ΅λ 2κ° λ¨μ΄ μ‘°ν©λ§ νμ© |
| β οΈ λ€μμ± κ°ν ν€μλ κ΅¬μ± κ·μΉ (λ§€μ° μ€μ): |
| π« μ λ κΈμ§ μ¬ν: |
| - λμΌνκ±°λ μ μ¬ν ν€μλ λ°λ³΅ |
| - λΈλλλͺ
(μΌμ±, LG, λμ΄ν€ λ±) |
| - 볡μ‘ν κΈ°μ μ©μ΄ |
| - 3κ° μ΄μ 볡ν©μ΄ |
| β
λ°λμ λ€μνκ² ν¬ν¨ν΄μΌ ν νν: |
| 1. μμ¬λ³ ν€μλ (μ: λλ무 λλ§, ꡬ리 μ»΅) |
| 2. ννλ³ ν€μλ (μ: μν μ μ, μ¬λ¦Ό μΌμ΄μ€) |
| 3. κΈ°λ₯λ³ ν€μλ (μ: λ°©μ νμ°μΉ, νκ· μ건) |
| 4. μΉ΄ν
κ³ λ¦¬λ³ ν€μλ (μ: μλ©ν¨, 쑰리λꡬ) |
| π― λ€μμ± λ³΄μ₯ μ λ΅: |
| - μ λ κ°μ μμ¬λ₯Ό 2λ² μ΄μ μ¬μ©νμ§ λ§μΈμ |
| - μ λ κ°μ ννλ₯Ό 2λ² μ΄μ μ¬μ©νμ§ λ§μΈμ |
| - μ λ κ°μ κΈ°λ₯μ 2λ² μ΄μ μ¬μ©νμ§ λ§μΈμ |
| - λ§€ ν€μλλ§λ€ μμ ν λ€λ₯Έ μ‘°ν©μΌλ‘ μμ±νμΈμ |
| μ¬λ°λ₯Έ λ€μν ν€μλ μμ: |
| β
λλ무 λλ§ (μμ¬+μν) |
| β
μν μ μ (νν+μν) |
| β
λ°©μ νμ°μΉ (κΈ°λ₯+μν) |
| β
μΈλΌλ―Ή λ¨Έκ·Έμ»΅ (μμ¬+μν) |
| β
μ μ΄μ μ λ° (νν+μν) |
| β
νκ· μ건 (κΈ°λ₯+μν) |
| μλͺ»λ λ°λ³΅ ν€μλ μμ: |
| β λλ무 λλ§, λλ무 μ κ°λ½ (μμ¬ λ°λ³΅) |
| β μν μ μ, μν μλ° (νν λ°λ³΅) |
| β λ°©μ νμ°μΉ, λ°©μ μΌμ΄μ€ (κΈ°λ₯ λ°λ³΅) |
| π μΆλ ₯ νμ: |
| μ€μ§ μμ ν λ€λ₯Έ μΌνν€μλλ§ ν μ€μ© 50κ° μΆλ ₯ |
| - λ²νΈ κΈμ§ |
| - μ€λͺ
κΈμ§ |
| - κΈ°νΈλ νΉμλ¬Έμ κΈμ§ |
| - κ΄νΈ μ μ€λͺ
κΈμ§ |
| - μμ ν€μλλ§ μΆλ ₯ |
| - μ λ μ€λ³΅ κΈμ§ |
| μμ μΆλ ₯ νν (λ§€λ² μμ ν λ€λ₯΄κ²): |
| μ 리 νλΆ |
| μ μ΄μ μμ |
| νκ· λλ§ |
| μ루미λ ν
λΈλ¬ |
| μ¬λ¦Ό νμΌν¨ |
| β‘ μ§κΈ λ°λ‘ μ λ μ€λ³΅λμ§ μλ μμ ν μλ‘μ΄ μΌνν€μλ 50κ°λ₯Ό κ°κ° λ€λ₯Έ λλ€ μ‘°κ±΄μ μ μ©νμ¬ μΆλ ₯νμΈμ. |
| λ§€λ² μ€νν λλ§λ€ μμ ν λ€λ₯Έ κ²°κ³Όκ° λμμΌ ν©λλ€! |