Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
{
"metadata": {
"lastUpdated": "2026-01-27T07:12:32Z",
"currentVersion": "1.0.7",
"lastUpdated": "2026-01-27T07:51:45Z",
"currentVersion": "1.0.8",
"projectType": "python",
"totalReleases": 9
"totalReleases": 10
},
"releases": [
{
"version": "1.0.8",
"project_type": "python",
"date": "2026-01-27",
"pr_number": 21,
"raw_summary": "## Summary by CodeRabbit\n\n## ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ\n\n* **์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ**\n * SNS ์ฝ˜ํ…์ธ ์—์„œ ํ†ตํ•ฉ ์žฅ์†Œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€\n * Instagram ๋งํฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์žฅ์†Œ๋ช… ์ž๋™ ์ถ”์ถœ ๋ฐ ์ง€๋„ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰\n\n* **๊ฐœ์„  ์‚ฌํ•ญ**\n * ์žฅ์†Œ๋ช… ์ถ”์ถœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •ํ™•๋„ ํ–ฅ์ƒ\n\n* **๋ฌธ์„œ**\n * ๋ฒ„์ „ 1.0.8๋กœ ์—…๋ฐ์ดํŠธ",
"parsed_changes": {
"์ƒˆ๋กœ์šด_๊ธฐ๋Šฅ": {
"title": "์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ",
"items": [
"SNS ์ฝ˜ํ…์ธ ์—์„œ ํ†ตํ•ฉ ์žฅ์†Œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€",
"Instagram ๋งํฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์žฅ์†Œ๋ช… ์ž๋™ ์ถ”์ถœ ๋ฐ ์ง€๋„ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰"
]
},
"๊ฐœ์„ _์‚ฌํ•ญ": {
"title": "๊ฐœ์„  ์‚ฌํ•ญ",
"items": [
"์žฅ์†Œ๋ช… ์ถ”์ถœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •ํ™•๋„ ํ–ฅ์ƒ"
]
},
"๋ฌธ์„œ": {
"title": "๋ฌธ์„œ",
"items": [
"๋ฒ„์ „ 1.0.8๋กœ ์—…๋ฐ์ดํŠธ"
]
}
},
"parse_method": "markdown"
},
{
"version": "1.0.7",
"project_type": "python",
Expand Down
20 changes: 18 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
# Changelog

**ํ˜„์žฌ ๋ฒ„์ „:** 1.0.7
**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:** 2026-01-27T07:12:32Z
**ํ˜„์žฌ ๋ฒ„์ „:** 1.0.8
**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:** 2026-01-27T07:51:45Z

---

## [1.0.8] - 2026-01-27

**PR:** #21

**์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ**
- SNS ์ฝ˜ํ…์ธ ์—์„œ ํ†ตํ•ฉ ์žฅ์†Œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
- Instagram ๋งํฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์žฅ์†Œ๋ช… ์ž๋™ ์ถ”์ถœ ๋ฐ ์ง€๋„ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰

**๊ฐœ์„  ์‚ฌํ•ญ**
- ์žฅ์†Œ๋ช… ์ถ”์ถœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •ํ™•๋„ ํ–ฅ์ƒ

**๋ฌธ์„œ**
- ๋ฒ„์ „ 1.0.8๋กœ ์—…๋ฐ์ดํŠธ

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MapSy-AI

<!-- ์ˆ˜์ •ํ•˜์ง€๋งˆ์„ธ์š” ์ž๋™์œผ๋กœ ๋™๊ธฐํ™” ๋ฉ๋‹ˆ๋‹ค -->
## ์ตœ์‹  ๋ฒ„์ „ : v1.0.4 (2026-01-18)
## ์ตœ์‹  ๋ฒ„์ „ : v1.0.7 (2026-01-27)

[์ „์ฒด ๋ฒ„์ „ ๊ธฐ๋ก ๋ณด๊ธฐ](CHANGELOG.md)

Expand Down
110 changes: 109 additions & 1 deletion src/apis/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
geocode_with_nominatim
)
from src.services.modules.ollama_llm import extract_place_names_with_ollama, OllamaPlaceResult
from src.models.integrated_search import SnsInfo, IntegratedPlaceSearchResponse
from src.core.exceptions import CustomError

logger = logging.getLogger(__name__)
Expand All @@ -43,6 +44,11 @@ class LlmPlaceExtractRequest(BaseModel):
caption: str = Field(..., description="์ธ์Šคํƒ€๊ทธ๋žจ ๊ฒŒ์‹œ๋ฌผ ๋ณธ๋ฌธ ํ…์ŠคํŠธ", min_length=1)


class IntegratedSearchRequest(BaseModel):
"""ํ†ตํ•ฉ ์žฅ์†Œ ๊ฒ€์ƒ‰ ์š”์ฒญ"""
url: str = Field(..., description="Instagram URL")


@router.post("/scrape", status_code=200)
async def scrape_url(request: ScrapeRequest):
"""
Expand Down Expand Up @@ -173,4 +179,106 @@ async def extract_place_names(request: LlmPlaceExtractRequest):
result = await extract_place_names_with_ollama(request.caption)

logger.info(f"์žฅ์†Œ๋ช… ์ถ”์ถœ ์™„๋ฃŒ: {result.place_names}")
return result
return result


@router.post("/integrated-place-search", response_model=IntegratedPlaceSearchResponse, status_code=200)
async def integrated_place_search(request: IntegratedSearchRequest):
"""
Instagram URL์—์„œ ์žฅ์†Œ ์ •๋ณด๋ฅผ ํ†ตํ•ฉ ์ถ”์ถœ

์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ:
1. Instagram ํŒŒ์‹ฑ โ†’ caption ์ถ”์ถœ
2. Ollama LLM โ†’ ์žฅ์†Œ๋ช… ์ถ”์ถœ
3. ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ โ†’ ์ƒ์„ธ ์ •๋ณด

- POST /api/test/integrated-place-search
- Body: {"url": "https://www.instagram.com/p/..."}
- ์„ฑ๊ณต: 200 + IntegratedPlaceSearchResponse
- ์‹คํŒจ: 400 (Instagram ํŒŒ์‹ฑ ์‹คํŒจ)

๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰์€ ๊ฐœ๋ณ„ ์‹คํŒจ ํ—ˆ์šฉ (failed_searches์— ๊ธฐ๋ก)
"""
# Step 1: ์š”์ฒญ ์ˆ˜์‹ 
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 1/5: ์š”์ฒญ ์ˆ˜์‹  - url={request.url}")

# Step 2: Instagram ํŒŒ์‹ฑ
logger.info("[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 2/5: Instagram ํŒŒ์‹ฑ ์‹œ์ž‘")
try:
sns_data = await route_and_scrape(request.url)
except HTTPException:
raise
except Exception as error:
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 2/5: Instagram ํŒŒ์‹ฑ ์‹คํŒจ - {error}")
raise HTTPException(status_code=400, detail=f"Instagram ํŒŒ์‹ฑ ์‹คํŒจ: {error}")

# SNS ์ •๋ณด ๊ตฌ์„ฑ
sns_info = SnsInfo(
platform=sns_data.get("platform", "unknown"),
content_type=sns_data.get("content_type", "unknown"),
url=sns_data.get("url", request.url),
author=sns_data.get("author"),
caption=sns_data.get("caption"),
likes_count=sns_data.get("likes_count"),
comments_count=sns_data.get("comments_count"),
posted_at=sns_data.get("posted_at"),
hashtags=sns_data.get("hashtags", []),
og_image=sns_data.get("og_image"),
image_urls=sns_data.get("image_urls", []),
author_profile_image_url=sns_data.get("author_profile_image_url")
)

caption = sns_data.get("caption") or ""
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 2/5: Instagram ํŒŒ์‹ฑ ์™„๋ฃŒ - author={sns_info.author}, caption ๊ธธ์ด={len(caption)}")

# Step 3: LLM ์žฅ์†Œ ์ถ”์ถœ
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 3/5: LLM ์žฅ์†Œ ์ถ”์ถœ ์‹œ์ž‘ - caption ๊ธธ์ด={len(caption)}")

if not caption.strip():
logger.info("[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 3/5: caption์ด ๋น„์–ด์žˆ์–ด ์žฅ์†Œ ์ถ”์ถœ ์Šคํ‚ต")
extracted_place_names = []
has_places = False
else:
llm_result = await extract_place_names_with_ollama(caption)
extracted_place_names = llm_result.place_names
has_places = llm_result.has_places

logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 3/5: LLM ์žฅ์†Œ ์ถ”์ถœ ์™„๋ฃŒ - ์ถ”์ถœ๋œ ์žฅ์†Œ ์ˆ˜={len(extracted_place_names)}")

# Step 4: ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰
place_details = []
failed_searches = []

if extracted_place_names:
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 4/5: ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ ์‹œ์ž‘ - ๊ฒ€์ƒ‰ํ•  ์žฅ์†Œ ์ˆ˜={len(extracted_place_names)}")
scraper = NaverMapScraper()

for index, place_name in enumerate(extracted_place_names, 1):
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 4/5: ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ ({index}/{len(extracted_place_names)}) - query={place_name}")
try:
place_info = await scraper.search_and_scrape(place_name)
place_details.append(place_info)
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 4/5: ๊ฒ€์ƒ‰ ์„ฑ๊ณต - {place_info.name} ({place_info.place_id})")
except Exception as error:
logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 4/5: ๊ฒ€์ƒ‰ ์‹คํŒจ - query={place_name}, error={error}")
failed_searches.append(place_name)

logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 4/5: ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ ์™„๋ฃŒ - ์„ฑ๊ณต={len(place_details)}, ์‹คํŒจ={len(failed_searches)}")
else:
logger.info("[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 4/5: ์ถ”์ถœ๋œ ์žฅ์†Œ๊ฐ€ ์—†์–ด ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ ์Šคํ‚ต")

# Step 5: ๊ฒฐ๊ณผ ํ†ตํ•ฉ
total_extracted = len(extracted_place_names)
total_found = len(place_details)

logger.info(f"[ํ†ตํ•ฉ ๊ฒ€์ƒ‰] Step 5/5: ๊ฒฐ๊ณผ ํ†ตํ•ฉ ์™„๋ฃŒ - ์ด ์ถ”์ถœ={total_extracted}, ์ด ๋ฐœ๊ฒฌ={total_found}")

return IntegratedPlaceSearchResponse(
sns_info=sns_info,
extracted_place_names=extracted_place_names,
has_places=has_places,
place_details=place_details,
total_extracted=total_extracted,
total_found=total_found,
failed_searches=failed_searches
)
42 changes: 42 additions & 0 deletions src/models/integrated_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""src.models.integrated_search
ํ†ตํ•ฉ ์žฅ์†Œ ๊ฒ€์ƒ‰ ์‘๋‹ต ๋ชจ๋ธ
Instagram โ†’ LLM โ†’ ๋„ค์ด๋ฒ„ ์ง€๋„ ํŒŒ์ดํ”„๋ผ์ธ ๊ฒฐ๊ณผ
"""
from pydantic import BaseModel, Field

from src.models.naver_place_info import NaverPlaceInfo


class SnsInfo(BaseModel):
"""SNS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ"""
platform: str = Field(..., description="ํ”Œ๋žซํผ (instagram, youtube ๋“ฑ)")
content_type: str = Field(..., description="์ฝ˜ํ…์ธ  ํƒ€์ž… (post, reel, igtv ๋“ฑ)")
url: str = Field(..., description="์›๋ณธ URL")
author: str | None = Field(default=None, description="์ž‘์„ฑ์ž")
caption: str | None = Field(default=None, description="๊ฒŒ์‹œ๋ฌผ ๋ณธ๋ฌธ")
likes_count: int | None = Field(default=None, description="์ข‹์•„์š” ์ˆ˜")
comments_count: int | None = Field(default=None, description="๋Œ“๊ธ€ ์ˆ˜")
posted_at: str | None = Field(default=None, description="๊ฒŒ์‹œ ๋‚ ์งœ")
hashtags: list[str] = Field(default_factory=list, description="ํ•ด์‹œํƒœ๊ทธ ๋ฆฌ์ŠคํŠธ")
og_image: str | None = Field(default=None, description="๋Œ€ํ‘œ ์ด๋ฏธ์ง€ URL")
image_urls: list[str] = Field(default_factory=list, description="์ด๋ฏธ์ง€ URL ๋ฆฌ์ŠคํŠธ")
author_profile_image_url: str | None = Field(default=None, description="์ž‘์„ฑ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€")


class IntegratedPlaceSearchResponse(BaseModel):
"""ํ†ตํ•ฉ ์žฅ์†Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ"""

# SNS ์ •๋ณด
sns_info: SnsInfo = Field(..., description="SNS ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ")

# LLM ์ถ”์ถœ ๊ฒฐ๊ณผ
extracted_place_names: list[str] = Field(default_factory=list, description="LLM์ด ์ถ”์ถœํ•œ ์žฅ์†Œ๋ช… ๋ฆฌ์ŠคํŠธ")
has_places: bool = Field(default=False, description="์žฅ์†Œ ์กด์žฌ ์—ฌ๋ถ€")

# ๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
place_details: list[NaverPlaceInfo] = Field(default_factory=list, description="๋„ค์ด๋ฒ„ ์ง€๋„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ")

# ์ฒ˜๋ฆฌ ํ†ต๊ณ„
total_extracted: int = Field(default=0, description="์ถ”์ถœ๋œ ์žฅ์†Œ ์ˆ˜")
total_found: int = Field(default=0, description="๋„ค์ด๋ฒ„์—์„œ ์ฐพ์€ ์žฅ์†Œ ์ˆ˜")
failed_searches: list[str] = Field(default_factory=list, description="๊ฒ€์ƒ‰ ์‹คํŒจํ•œ ์žฅ์†Œ๋ช…")
59 changes: 47 additions & 12 deletions src/services/modules/ollama_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,48 @@ class OllamaPlaceResult(BaseModel):
# =============================================
# ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ
# =============================================
PLACE_EXTRACTION_PROMPT = """๋‹ค์Œ ํ…์ŠคํŠธ์—์„œ ์žฅ์†Œ๋ช…(๊ฐ€๊ฒŒ๋ช…, ์ƒํ˜ธ๋ช…, ์‹๋‹น๋ช…, ์นดํŽ˜๋ช…, ๊ด€๊ด‘์ง€๋ช… ๋“ฑ)์„ ์ถ”์ถœํ•˜์„ธ์š”.
PLACE_EXTRACTION_PROMPT = """๋‹น์‹ ์€ ์ธ์Šคํƒ€๊ทธ๋žจ ๊ฒŒ์‹œ๋ฌผ์—์„œ **์‹ค์ œ ๋ฐฉ๋ฌธํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์†Œ๋ช…**์„ ์ถ”์ถœํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.

์žฅ์†Œ๋ช… ์˜ˆ์‹œ:
- ์Šคํƒ€๋ฒ…์Šค ์ข…ํ•ฉ์šด๋™์žฅ์‚ฌ๊ฑฐ๋ฆฌ์ 
- ๋ธ”๋ฃจ๋ณดํ‹€ ์„ฑ์ˆ˜
- ์Šค์‹œํ˜ธ
- ์‚ฌ์‚ฌ๋…ธํ•˜
## ๋‹น์‹ ์˜ ์ž„๋ฌด
ํ…์ŠคํŠธ๋ฅผ ์ฝ๊ณ , ์‚ฌ๋žŒ๋“ค์ด **๋„ค์ด๋ฒ„ ์ง€๋„์—์„œ ๊ฒ€์ƒ‰ํ•ด์„œ ์ฐพ์•„๊ฐˆ ์ˆ˜ ์žˆ๋Š” ์žฅ์†Œ๋ช…**์„ ์ถ”์ถœํ•˜์„ธ์š”.

๊ทœ์น™:
1. ํ…์ŠคํŠธ์— ์–ธ๊ธ‰๋œ ์‹ค์ œ ์žฅ์†Œ๋ช…๋งŒ ์ถ”์ถœํ•˜์„ธ์š”.
2. ํ•ด์‹œํƒœ๊ทธ(#)๊ฐ€ ๋ถ™์–ด์žˆ์–ด๋„ ์žฅ์†Œ๋ช…์ด๋ฉด ์ถ”์ถœํ•˜์„ธ์š”. (#์Šค์‹œํ˜ธ โ†’ ์Šค์‹œํ˜ธ)
3. ์ผ๋ฐ˜ ๋ช…์‚ฌ(๋ง›์ง‘, ์ดˆ๋ฐฅ, ์นดํŽ˜ ๋“ฑ)๋Š” ์žฅ์†Œ๋ช…์ด ์•„๋‹™๋‹ˆ๋‹ค.
4. ์žฅ์†Œ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด []์„ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”.
## ์ค‘์š”: ์ •ํ™•ํ•œ ์žฅ์†Œ๋ช… ์ถ”์ถœ
- ์ง€์ ๋ช…์ด ์žˆ์œผ๋ฉด **์ง€์ ๋ช…๊นŒ์ง€ ํฌํ•จ**ํ•ด์„œ ์ถ”์ถœํ•˜์„ธ์š”
- "์Šคํƒ€๋ฒ…์Šค" (X) โ†’ "์Šคํƒ€๋ฒ…์Šค ์ข…ํ•ฉ์šด๋™์žฅ์—ญ์ " (O)
- "๋ธ”๋ฃจ๋ณดํ‹€" (X) โ†’ "๋ธ”๋ฃจ๋ณดํ‹€ ์„ฑ์ˆ˜" (O)

## ์˜ˆ์‹œ

### ์˜ˆ์‹œ 1
์ž…๋ ฅ: "๊ฐ•๋‚จ์—ญ ๊ทผ์ฒ˜ ํŒŒ์Šคํƒ€ ๋ง›์ง‘ #๋ผ๋ผ๋ธŒ๋ ˆ๋“œ ๋‹ค๋…€์™”์–ด์š”! ๋ถ„์œ„๊ธฐ ์ข‹๊ณ  ๋ง›์žˆ์Œ"
์ถœ๋ ฅ: ["๋ผ๋ผ๋ธŒ๋ ˆ๋“œ"]
์ด์œ : "๋ผ๋ผ๋ธŒ๋ ˆ๋“œ"๊ฐ€ ๊ฐ€๊ฒŒ๋ช…. "๊ฐ•๋‚จ์—ญ", "ํŒŒ์Šคํƒ€", "๋ง›์ง‘"์€ ์žฅ์†Œ๋ช… ์•„๋‹˜

### ์˜ˆ์‹œ 2
์ž…๋ ฅ: "1. #์Šค์‹œํ˜ธ -์œ„์น˜_์„œ์šธ ์†กํŒŒ๊ตฌ 2. #๋ฉ˜์•ผํ•˜๋‚˜๋น„ ๊ฐ•๋‚จ์ "
์ถœ๋ ฅ: ["์Šค์‹œํ˜ธ", "๋ฉ˜์•ผํ•˜๋‚˜๋น„ ๊ฐ•๋‚จ์ "]
์ด์œ : ์ง€์ ๋ช…์ด ์žˆ์œผ๋ฉด ํฌํ•จ. "์†กํŒŒ๊ตฌ"๋Š” ์ฃผ์†Œ์ผ ๋ฟ

### ์˜ˆ์‹œ 3
์ž…๋ ฅ: "์Šคํƒ€๋ฒ…์Šค ์ข…ํ•ฉ์šด๋™์žฅ์—ญ์ ์—์„œ ์ปคํ”ผ ๋งˆ์‹œ๊ณ  ๋ธ”๋ฃจ๋ณดํ‹€ ์„ฑ์ˆ˜ ๊ฐ”๋‹ค์˜ด"
์ถœ๋ ฅ: ["์Šคํƒ€๋ฒ…์Šค ์ข…ํ•ฉ์šด๋™์žฅ์—ญ์ ", "๋ธ”๋ฃจ๋ณดํ‹€ ์„ฑ์ˆ˜"]
์ด์œ : ์ง€์ ๋ช…๊นŒ์ง€ ํฌํ•จ๋œ ์ „์ฒด ์ด๋ฆ„์ด ์žฅ์†Œ๋ช…

### ์˜ˆ์‹œ 4
์ž…๋ ฅ: "์„œ์šธ ์นดํŽ˜ ์ถ”์ฒœ! ์š”์ฆ˜ ํ•ซํ•œ ๊ณณ๋“ค #์„ฑ์ˆ˜๋™์นดํŽ˜ํˆฌ์–ด"
์ถœ๋ ฅ: []
์ด์œ : ๊ตฌ์ฒด์ ์ธ ๊ฐ€๊ฒŒ๋ช… ์—†์Œ. "์„ฑ์ˆ˜๋™์นดํŽ˜ํˆฌ์–ด"๋Š” ํ•ด์‹œํƒœ๊ทธ ํ‚ค์›Œ๋“œ

### ์˜ˆ์‹œ 5
์ž…๋ ฅ: "ํ™๋Œ€ ๋ง›์ง‘ ๋ฆฌ์ŠคํŠธ ์ •๋ฆฌ ์ค‘... ๋ง›์ง‘, ์นดํŽ˜, ์ˆ ์ง‘ ๋‹ค ๋ชจ์•„๋ด„"
์ถœ๋ ฅ: []
์ด์œ : "๋ง›์ง‘", "์นดํŽ˜", "์ˆ ์ง‘"์€ ์นดํ…Œ๊ณ ๋ฆฌ, ๊ฐ€๊ฒŒ๋ช… ์•„๋‹˜

## ์ฃผ์˜์‚ฌํ•ญ
- ํ•ด์‹œํƒœ๊ทธ(#)๋Š” ์ œ๊ฑฐํ•˜๊ณ  ๋ฐ˜ํ™˜
- ์ธ์Šคํƒ€ ๊ณ„์ •(@username)์€ ๊ฐ€๊ฒŒ๋ช…์ด ์•„๋‹˜
- ๋‹จ๋… ์ง€์—ญ๋ช…(์†กํŒŒ๊ตฌ, ๊ฐ•๋‚จ์—ญ)์€ ์žฅ์†Œ๋ช… ์•„๋‹˜
- ํ•˜์ง€๋งŒ "์Šคํƒ€๋ฒ…์Šค ๊ฐ•๋‚จ์—ญ์ "์ฒ˜๋Ÿผ ์ง€์ ๋ช…์˜ ์ผ๋ถ€๋ฉด ํฌํ•จ

<Context>
{caption}
Expand Down Expand Up @@ -131,7 +160,13 @@ async def extract_place_names_with_ollama(
# JSON ํŒŒ์‹ฑ
try:
parsed = json.loads(content)
result = OllamaPlaceResult.model_validate(parsed)
parsed_place_names = parsed.get("place_names", [])

# has_places๋Š” place_names ๊ธธ์ด๋กœ ์ž๋™ ๊ณ„์‚ฐ (LLM ์‘๋‹ต ๋ฌด์‹œ)
result = OllamaPlaceResult(
place_names=parsed_place_names,
has_places=len(parsed_place_names) > 0
)

logger.info(f"์žฅ์†Œ๋ช… ์ถ”์ถœ ์„ฑ๊ณต: {result.place_names}")
return result
Expand Down
6 changes: 3 additions & 3 deletions version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
# - ๋ฒ„์ „์€ ํ•ญ์ƒ ๋†’์€ ๋ฒ„์ „์œผ๋กœ ์ž๋™ ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค
# ===================================================================

version: "1.0.7"
version_code: 23 # app build number
version: "1.0.8"
version_code: 24 # app build number
project_type: "python" # spring, flutter, react, react-native, react-native-expo, node, python, basic
metadata:
last_updated: "2026-01-27 07:10:52"
last_updated: "2026-01-27 07:45:31"
last_updated_by: "Cassiiopeia"
default_branch: "main"
integrated_from: "SUH-DEVOPS-TEMPLATE"
Expand Down