[๊ธฐ์ ์ ๋ฆฌ] ๐ค Gemini API ์ฐ๋ ๋ฐ ํ๋กฌํํธ ์์ง๋์ด๋ง - DDAL-KKAK-DOT/DDALKKAK GitHub Wiki
๐ค Gemini API ์ฐ๋ ๋ฐ ํ๋กฌํํธ ์์ง๋์ด๋ง
DDALKKAK์ ์ฌ์ฉ์์ ํ๋กํ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ธํ ์ด๋ ฅ์๋ฅผ ์๋์ผ๋ก ์์ฑํ๊ธฐ ์ํด Google Gemini API๋ฅผ ์ฐ๋ํ์ฌ ํ์ฉํ์ต๋๋ค. ์ฌ๊ธฐ์๋ Gemini API ์ฌ์ฉ๋ฒ๊ณผ ํจ๊ป ํจ์จ์ ์ธ ํ๋กฌํํธ ์์ง๋์ด๋ง ์ ๋ต์ ๋ํด ์ค๋ช ํฉ๋๋ค.
1๏ธโฃ Gemini API ์ฐ๋ ๊ฐ์
DDALKKAK ํ๋ก์ ํธ์์ Gemini API๋ ์ฃผ์ด์ง ์ฌ์ฉ์ ์ ๋ ฅ ์ ๋ณด์ ์ถ๊ฐ ์น ํ์ด์ง ์ ๋ณด๋ฅผ ๊ฒฐํฉํด ๊ณ ํ์ง์ ์์ธ ์ด๋ ฅ์๋ฅผ JSON ํ์์ผ๋ก ์์ฑํฉ๋๋ค.
โ๏ธ API ์ฐ๋ ํ๋ฆ
flowchart LR
InputProfile(InputProfile) --> Prompt[ํ๋กฌํํธ ๊ตฌ์ฑ]
URL["์ธ๋ถ URL ๋งํฌ"] --> Fetch["๋ณธ๋ฌธ ์ถ์ถ(fetch_page_text)"] --> Prompt
Prompt --> GeminiAPI[Gemini API ํธ์ถ]
GeminiAPI --> JSON[์ด๋ ฅ์ JSON]
JSON --> OutputProfile(OutputProfile)
generate_profile_from_input
)
๐ Gemini ์ฐ๋ ์ฝ๋ (cfg = types.GenerateContentConfig(response_mime_type="application/json")
resp = genai_client.models.generate_content(
model="models/gemini-2.5-flash-preview-04-17",
contents=prompt,
config=cfg,
)
raw = json.loads(resp.text)
# ๋๋ฝ ํ๋ ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ
raw.setdefault("skills", [])
raw.setdefault("projects", [])
raw.setdefault("careers", [])
raw.setdefault("educations", [])
raw.setdefault("clubs", [])
return OutputProfile(**raw)
- ํ๊ฒฝ๋ณ์(
GEMINI_API_KEY
)๋ก API ํค ๋ณด์ ์ ์ง - Gemini์์ ์ ๊ณตํ ์๋ต์ JSON์ผ๋ก ๋ฐ๋ก ๋ณํํ์ฌ ๊ฒ์ฆ ๋ฐ ํ์ฉ
2๏ธโฃ ํ๋กฌํํธ ์์ง๋์ด๋ง ์ ๋ต
Gemini API๊ฐ ์ด๋ ฅ์๋ฅผ ์ ํํ๊ณ ์์ธํ๊ฒ ์์ฑํ ์ ์๋๋ก ํ๋กฌํํธ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ์ค๊ณํ์ต๋๋ค.
๐ ํ๋กฌํํธ ์ฃผ์ ๊ตฌ์ฑ
ํ๋กฌํํธ๋ ํฌ๊ฒ ๋ค์ ์ธ ๊ฐ์ง๋ก ๊ตฌ์ฑ๋ฉ๋๋ค:
- ์ฌ์ฉ์ ๊ธฐ๋ณธ ์ ๋ณด: ์ด๋ฆ, ์ด๋ฉ์ผ, ์ฐ๋ฝ์ฒ, ํ๋ ฅ ๋ฑ
- ๊ธฐ์ ์คํ: ์ฌ์ฉ์๊ฐ ๋ณด์ ํ ๊ธฐ์ ์ ๋ณด
- ํ๋ ๋งํฌ ๋ฐ ์น ์ฝํ ์ธ : ์ธ๋ถ ๋งํฌ์์ ์ถ์ถํ ์ฝํ ์ธ ๋ฅผ ํฌํจํ์ฌ ์ถ๊ฐ ๋ฌธ๋งฅ ์ ๊ณต
build_resume_prompt
)
์์ ์ฝ๋ (def build_resume_prompt(profile: dict, urls: list[str]) -> str:
sections = []
for idx, url in enumerate(urls, start=1):
content = fetch_page_text(url) # ์นํ์ด์ง ๋ณธ๋ฌธ ์ถ์ถ
sections.append(f"[{idx}] URL: {url}\nCONTENT:\n{content}\n")
links_block = "\n".join(sections)
return f"""
๋น์ ์ ๊ฒฝ๋ ฅ ๊ฐ๋ฐ์๋ฅผ ๋ฝ์ผ๋ ค๊ณ ํ๋ ๋ฉด์ ์์
๋๋ค.
์๋ ํ๋กํยทํ๋ก์ ํธยท๊ฒฝ๋ ฅยท๊ต์กยท๋์๋ฆฌ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก,
JSON ํฌ๋งท์ผ๋ก **๋งค์ฐ ์์ธํ** ์ด๋ ฅ์๋ฅผ ๋ง๋ค์ด ์ฃผ์ธ์.
ํ๋กํ:
- ์ด๋ฆ: {profile['name']}
- ์ด๋ฉ์ผ: {profile['email']}
- ์ฐ๋ฝ์ฒ: {profile['phone']}
- ํ๋ ฅ: {profile['educations']}
- ๊ธฐ์ ์คํ: {', '.join(profile['skills'])}
๋งํฌ ๋ฐ์ท:
{links_block}
### ์์ฑ ๊ท์น
- ๊ฐ ํ๋๋ ์์ธํ ์ค๋ช
ํฌํจ
- ํ๊ตญ์ด๋ก ์์ฑํ๋ฉฐ JSON ํคยท๊ตฌ์กฐ ๋ณ๊ฒฝ ๊ธ์ง
- ๋งํฌ๋ค์ด ์์ด ์์ JSON ์ถ๋ ฅ
"""
- ์นํ์ด์ง ์ ๋ณด๋ฅผ ์ถ๊ฐํด Gemini๊ฐ ์ด๋ ฅ์๋ฅผ ์์ฑํ ๋ ์ฐธ๊ณ ํ ์ ์๋ ํ๋ถํ ๋ฌธ๋งฅ ์ ๊ณต
- Gemini๊ฐ ๋ฐ๋ผ์ผ ํ ๋ช ํํ ํ์๊ณผ ๊ท์น ๋ช ์
3๏ธโฃ ์ธ๋ถ ์ฝํ ์ธ ์์ง (Crawling ์ ๋ต)
Gemini ํ๋กฌํํธ์ ํจ๊ณผ๋ฅผ ๋์ด๊ธฐ ์ํด ์น ํ์ด์ง ์ฝํ ์ธ ๋ฅผ ์๋ ์ถ์ถํ๋ ํฌ๋กค๋ง ๋ก์ง์ ๊ตฌํํ์ต๋๋ค.
ํฌ๋กค๋ง ์ ๊ทผ ๋ฐฉ์
-
์ ์ ํฌ๋กค๋ง(Requests + readability + BeautifulSoup) ๋น ๋ฅด๊ฒ ๋ณธ๋ฌธ์ ์ถ์ถํ๋, 200์ ๋ฏธ๋ง์ด๋ฉด ๋ถ์ถฉ๋ถํ๋ค๊ณ ํ๋จ
-
๋์ ํฌ๋กค๋ง(Selenium) ์ ์ ํฌ๋กค๋ง ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํ๋ฉด JS ๊ธฐ๋ฐ์ ์น ํ์ด์ง๊น์ง ์์ ํ ๋ ๋๋งํด ์ฝํ ์ธ ํ๋ณด
fetch_page_text
)
๐ ์ฃผ์ ์ฝ๋ (@functools.lru_cache(maxsize=256)
def fetch_page_text(url: str) -> str:
txt = _static_fetch(url)
if len(txt) >= STATIC_THRESHOLD:
return txt[:MAX_CHARS]
return _dynamic_fetch(url)[:MAX_CHARS]
STATIC_THRESHOLD
: ์ ์ ํฌ๋กค๋ง ๊ฒฐ๊ณผ๊ฐ ์ด ๊ธธ์ด๋ฅผ ๋์ง ์์ผ๋ฉด ๋์ ํฌ๋กค๋ง์ผ๋ก ์ ํMAX_CHARS
: Gemini API์ ํ ํฐ ์ ํ์ ๋์ง ์๋๋ก ๋ณธ๋ฌธ ๊ธธ์ด๋ฅผ ์ ํ
_static_fetch
)
๐ธ๏ธ ์ ์ ํฌ๋กค๋ง (def _static_fetch(url: str) -> str:
res = requests.get(url, timeout=8, headers=UA)
doc = Document(res.text)
cleaned_html = doc.summary()
return BeautifulSoup(cleaned_html, "html.parser").get_text(strip=True)
_dynamic_fetch
)
๐ท๏ธ ๋์ ํฌ๋กค๋ง (def _dynamic_fetch(url: str) -> str:
options = Options()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(service=_select_driver(options), options=options)
try:
driver.get(url)
time.sleep(2)
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(1)
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
text = driver.find_element("tag name", "body").text
finally:
driver.quit()
return text.strip()
โ ๊ฒฐ๋ก ๋ฐ ์ฑ๊ณผ
- Gemini API๋ฅผ ํตํด ์ด๋ ฅ์๋ฅผ ์๋์ผ๋ก ์์ธํ๊ฒ ์์ฑํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ํฅ์์ํด
- ์ฒด๊ณ์ ์ธ ํ๋กฌํํธ ์์ง๋์ด๋ง๊ณผ ์น ํฌ๋กค๋ง ์ ๋ต์ ๊ฒฐํฉํด ์ ํ์ฑ๊ณผ ํ์ง์ ๋ณด์ฅ
- Gemini API ๋ฐ ์น ์ฝํ ์ธ ํ์ฉ์ผ๋ก ํ์ฅ์ฑ๊ณผ ์ ์ง ๋ณด์์ฑ์ ํ๋ณด
๐ ํฅํ ๋ฐ์ ๋ฐฉํฅ
- ๋ ์ ๊ตํ ์บ์ฑ ์ ๋ต ๋์ ์ผ๋ก API ํธ์ถ ์ต์ ํ
- ๋ค์ํ ์ ํ์ ์น ์ฝํ ์ธ ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๋ค๋ฃจ๊ธฐ ์ํ ํฌ๋กค๋ง ๊ฐ์
- ํ๋กฌํํธ ๋ฐ ๊ฒฐ๊ณผ ํ์ง์ ๋์ด๊ธฐ ์ํ ์ถ๊ฐ ํ๋ ๋ฐ ์ฌ์ฉ์ ํผ๋๋ฐฑ ๋ฐ์