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
31 changes: 28 additions & 3 deletions CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
{
"metadata": {
"lastUpdated": "2026-01-18T12:11:27Z",
"currentVersion": "0.1.3",
"lastUpdated": "2026-01-18T12:31:21Z",
"currentVersion": "1.0.1",
"projectType": "python",
"totalReleases": 5
"totalReleases": 6
},
"releases": [
{
"version": "1.0.1",
"project_type": "python",
"date": "2026-01-18",
"pr_number": 15,
"raw_summary": "## Summary by CodeRabbit\n\n* **์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ**\n * Geocoding API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ - ์ฃผ์†Œ๋ฅผ ์ขŒํ‘œ(์œ„๋„/๊ฒฝ๋„)๋กœ ๋ณ€ํ™˜\n * ์นด์นด์˜ค๋งต ๋ฐ Nominatim ์ œ๊ณต์ž ์ง€์›\n * ๋‹ค์ค‘ ์ œ๊ณต์ž ์ž๋™ ํด๋ฐฑ ๊ธฐ๋Šฅ\n * ์žฅ์†Œ ์ •๋ณด์˜ ๋ˆ„๋ฝ๋œ ์ขŒํ‘œ ์ž๋™ ์ฑ„์šฐ๊ธฐ\n\n* **์—…๋ฐ์ดํŠธ**\n * ๋ฒ„์ „ 1.0.1 ์ถœ์‹œ",
"parsed_changes": {
"์ƒˆ๋กœ์šด_๊ธฐ๋Šฅ": {
"title": "์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ",
"items": [
"Geocoding API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ - ์ฃผ์†Œ๋ฅผ ์ขŒํ‘œ(์œ„๋„/๊ฒฝ๋„)๋กœ ๋ณ€ํ™˜",
"์นด์นด์˜ค๋งต ๋ฐ Nominatim ์ œ๊ณต์ž ์ง€์›",
"๋‹ค์ค‘ ์ œ๊ณต์ž ์ž๋™ ํด๋ฐฑ ๊ธฐ๋Šฅ",
"์žฅ์†Œ ์ •๋ณด์˜ ๋ˆ„๋ฝ๋œ ์ขŒํ‘œ ์ž๋™ ์ฑ„์šฐ๊ธฐ"
]
},
"์—…๋ฐ์ดํŠธ": {
"title": "์—…๋ฐ์ดํŠธ",
"items": [
"๋ฒ„์ „ 1.0.1 ์ถœ์‹œ"
]
}
},
"parse_method": "markdown"
},
{
"version": "0.1.3",
"project_type": "python",
Expand Down
19 changes: 17 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
# Changelog

**ํ˜„์žฌ ๋ฒ„์ „:** 0.1.3
**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:** 2026-01-18T12:11:27Z
**ํ˜„์žฌ ๋ฒ„์ „:** 1.0.1
**๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:** 2026-01-18T12:31:21Z

---

## [1.0.1] - 2026-01-18

**PR:** #15

**์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ**
- Geocoding API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ - ์ฃผ์†Œ๋ฅผ ์ขŒํ‘œ(์œ„๋„/๊ฒฝ๋„)๋กœ ๋ณ€ํ™˜
- ์นด์นด์˜ค๋งต ๋ฐ Nominatim ์ œ๊ณต์ž ์ง€์›
- ๋‹ค์ค‘ ์ œ๊ณต์ž ์ž๋™ ํด๋ฐฑ ๊ธฐ๋Šฅ
- ์žฅ์†Œ ์ •๋ณด์˜ ๋ˆ„๋ฝ๋œ ์ขŒํ‘œ ์ž๋™ ์ฑ„์šฐ๊ธฐ

**์—…๋ฐ์ดํŠธ**
- ๋ฒ„์ „ 1.0.1 ์ถœ์‹œ

---

Expand Down
48 changes: 48 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,53 @@ URL โ†’ sns_router โ†’ get_audio โ†’ get_transcription (STT) โ†’ get_video_narra
- `ocrText`: ๋น„๋””์˜ค ํ…์ŠคํŠธ (ํ˜„์žฌ ๋น„ํ™œ์„ฑํ™”)
- `result`: ์ตœ์ข… ์ถ”์ถœ๋œ ์žฅ์†Œ๋“ค

## ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ

์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์‹œ `src/utils/common.py`์˜ ๊ณตํ†ต ํ•จ์ˆ˜๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

### HTTP ์š”์ฒญ (์™ธ๋ถ€ API ํ˜ธ์ถœ)

์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹œ ๋ฐ˜๋“œ์‹œ `http_get_json()` ์‚ฌ์šฉ:

```python
from src.utils.common import http_get_json

# โœ… ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ
data = await http_get_json(
url="https://api.example.com/data",
params={"query": "value"},
headers={"Authorization": "Bearer token"},
timeout=10.0 # ๊ธฐ๋ณธ 10์ดˆ
)

# โŒ ์ง์ ‘ httpx ์‚ฌ์šฉ ๊ธˆ์ง€
async with httpx.AsyncClient() as client:
response = await client.get(url) # ํƒ€์ž„์•„์›ƒ, ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ ์œ„ํ—˜
```

**์žฅ์ **:
- ํƒ€์ž„์•„์›ƒ ์ž๋™ ์ ์šฉ (๊ธฐ๋ณธ 10์ดˆ)
- ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ผ๊ด€์„ฑ (TimeoutException, HTTPStatusError, RequestError)
- CustomError๋กœ ๋ณ€ํ™˜๋˜์–ด API ๋ ˆ์ด์–ด์—์„œ ์ฒ˜๋ฆฌ ์šฉ์ด

### API Key ๊ฒ€์ฆ

```python
from src.utils.common import verify_api_key
from fastapi import Depends

@router.post("/endpoint")
async def endpoint(api_key: str = Depends(verify_api_key)):
# X-API-Key ํ—ค๋” ์ž๋™ ๊ฒ€์ฆ
```

### ๊ธฐํƒ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ

- `validate_url_length()`: URL ๊ธธ์ด ๊ฒ€์ฆ
- `mask_sensitive_data()`: ๋กœ๊ทธ ์ถœ๋ ฅ ์‹œ ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ ๋งˆ์Šคํ‚น
- `convert_to_bytesio()`: bytes/BytesIO ํƒ€์ž… ๋ณ€ํ™˜
- `validate_image_stream()`: ์ด๋ฏธ์ง€ ์ŠคํŠธ๋ฆผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ

## ์„ค์ •

`.env`์— ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜:
Expand All @@ -104,6 +151,7 @@ URL โ†’ sns_router โ†’ get_audio โ†’ get_transcription (STT) โ†’ get_video_narra
- `YOUTUBE_API_KEY`: YouTube Data API ํ‚ค
- `INSTAGRAM_POST_DOC_ID`, `INSTAGRAM_APP_ID`: Instagram API ์„ค์ •
- `BACKEND_CALLBACK_URL`, `BACKEND_API_KEY`: ์ฝœ๋ฐฑ ์—”๋“œํฌ์ธํŠธ ์„ค์ •
- `KAKAO_REST_API_KEY`: ์นด์นด์˜ค ๋กœ์ปฌ API ํ‚ค (Geocoding ์šฉ)
- `SMB_*`: SMB ํŒŒ์ผ ์„œ๋ฒ„ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ)

## ์ฐธ๊ณ ์‚ฌํ•ญ
Expand Down
42 changes: 42 additions & 0 deletions src/apis/geocoding_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""src.apis.geocoding_router
Geocoding API ๋ผ์šฐํ„ฐ - ์ฃผ์†Œ โ†’ ์œ„๋„/๊ฒฝ๋„ ๋ณ€ํ™˜
"""
import logging

from fastapi import APIRouter, Depends, HTTPException

from src.models.geocoding_models import GeocodingRequest, GeocodingResponse
from src.services.geocoding_service import geocode_with_kakao
from src.utils.common import verify_api_key
from src.core.exceptions import CustomError

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["Geocoding API"])


@router.post("/geocode", response_model=GeocodingResponse, status_code=200)
async def geocode(
request: GeocodingRequest,
api_key: str = Depends(verify_api_key)
):
"""
์ฃผ์†Œ๋ฅผ ์œ„๋„/๊ฒฝ๋„๋กœ ๋ณ€ํ™˜ (์นด์นด์˜ค API)

- ์ธ์ฆ: X-API-Key ํ—ค๋” ํ•„์š”
- POST /api/geocode
- Body: {"address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123"}
- ์„ฑ๊ณต: 200 + GeocodingResponse
- ์‹คํŒจ: 404 (์ฃผ์†Œ ๋ชป ์ฐพ์Œ), 401 (์ธ์ฆ ์‹คํŒจ)
"""
logger.info(f"Geocoding ์š”์ฒญ: address='{request.address}'")

try:
result = await geocode_with_kakao(request.address)
return GeocodingResponse(
address=request.address,
latitude=result.latitude,
longitude=result.longitude,
provider=result.provider
)
except CustomError as error:
raise HTTPException(status_code=404, detail=error.message)
41 changes: 40 additions & 1 deletion src/apis/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@
ํ…Œ์ŠคํŠธ API ๋ผ์šฐํ„ฐ - SNS ์Šคํฌ๋ž˜ํ•‘ ํ…Œ์ŠคํŠธ์šฉ
"""
import logging
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field

from src.services.scraper.scrape_router import route_and_scrape
from src.services.scraper.platforms.naver_map_scraper import NaverMapScraper
from src.services.scraper.platforms.google_map_scraper import GoogleMapScraper
from src.models.geocoding_models import (
GeocodingTestRequest,
GeocodingResponse,
GeocodingProvider
)
from src.services.geocoding_service import (
geocode_with_kakao,
geocode_with_nominatim
)
from src.core.exceptions import CustomError

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/test", tags=["ํ…Œ์ŠคํŠธ API"])
Expand Down Expand Up @@ -103,3 +113,32 @@ async def scrape_google_map(request: GoogleMapSearchRequest):

logger.info(f"์Šคํฌ๋ž˜ํ•‘ ์™„๋ฃŒ: {result.name} ({result.place_id})")
return result


@router.post("/geocode", response_model=GeocodingResponse, status_code=200)
async def test_geocode(request: GeocodingTestRequest):
"""
์ฃผ์†Œ๋ฅผ ์œ„๋„/๊ฒฝ๋„๋กœ ๋ณ€ํ™˜ (ํ…Œ์ŠคํŠธ์šฉ - provider ์„ ํƒ ๊ฐ€๋Šฅ)

- POST /api/test/geocode
- Body: {"address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ", "provider": "kakao"}
- provider: "kakao" (๊ธฐ๋ณธ) | "nominatim"
- ์„ฑ๊ณต: 200 + GeocodingResponse
- ์‹คํŒจ: 404 (์ฃผ์†Œ ๋ชป ์ฐพ์Œ)
"""
logger.info(f"Geocoding ํ…Œ์ŠคํŠธ ์š”์ฒญ: address='{request.address}', provider='{request.provider.value}'")

try:
if request.provider == GeocodingProvider.KAKAO:
result = await geocode_with_kakao(request.address)
else:
result = await geocode_with_nominatim(request.address)

return GeocodingResponse(
address=request.address,
latitude=result.latitude,
longitude=result.longitude,
provider=result.provider
)
except CustomError as error:
raise HTTPException(status_code=404, detail=error.message)
5 changes: 4 additions & 1 deletion src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class Settings(BaseSettings):
BACKEND_API_KEY: str
ENVIRONMENT: str = "dev" # dev: ๋กœ์ปฌ, prod: ์„œ๋ฒ„ํ™˜๊ฒฝ

# ์นด์นด์˜ค API
KAKAO_REST_API_KEY: str

# SMB ์„ค์ •
SMB_HOST: str = ""
SMB_PORT: int = 445
Expand All @@ -22,5 +25,5 @@ class Settings(BaseSettings):
SMB_REMOTE_DIR: str = "" # ์›๊ฒฉ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ (์˜ˆ: "romrom/images")
SMB_DOMAIN: str = "" # ๋„๋ฉ”์ธ (์„ ํƒ์ )

model_config = SettingsConfigDict(env_file=".env")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
settings = Settings()
2 changes: 2 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from src.core.logging import setup_logging
from src.apis.place_router import router as place_router
from src.apis.test_router import router as test_router
from src.apis.geocoding_router import router as geocoding_router

# ๋กœ๊น… ์ดˆ๊ธฐํ™”
setup_logging(log_level="INFO")
Expand Down Expand Up @@ -47,6 +48,7 @@ async def lifespan(app: FastAPI):
# ๋ผ์šฐํ„ฐ ๋“ฑ๋ก
app.include_router(place_router)
app.include_router(test_router)
app.include_router(geocoding_router)


@app.middleware("http")
Expand Down
33 changes: 33 additions & 0 deletions src/models/geocoding_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""src.models.geocoding_models
Geocoding API ์š”์ฒญ/์‘๋‹ต ์Šคํ‚ค๋งˆ
"""
from enum import Enum
from pydantic import BaseModel, Field


class GeocodingProvider(str, Enum):
"""Geocoding ์ œ๊ณต์ž"""
KAKAO = "kakao"
NOMINATIM = "nominatim"


class GeocodingRequest(BaseModel):
"""๋ฉ”์ธ API ์š”์ฒญ (์นด์นด์˜ค ์ „์šฉ)"""
address: str = Field(..., description="๋ณ€ํ™˜ํ•  ์ฃผ์†Œ", min_length=1)


class GeocodingTestRequest(BaseModel):
"""ํ…Œ์ŠคํŠธ API ์š”์ฒญ (provider ์„ ํƒ ๊ฐ€๋Šฅ)"""
address: str = Field(..., description="๋ณ€ํ™˜ํ•  ์ฃผ์†Œ", min_length=1)
provider: GeocodingProvider = Field(
default=GeocodingProvider.KAKAO,
description="Geocoding ์ œ๊ณต์ž ์„ ํƒ"
)


class GeocodingResponse(BaseModel):
"""Geocoding ์‘๋‹ต"""
address: str = Field(..., description="์ž…๋ ฅ๋œ ์ฃผ์†Œ")
latitude: float = Field(..., description="์œ„๋„")
longitude: float = Field(..., description="๊ฒฝ๋„")
provider: str = Field(..., description="์‚ฌ์šฉ๋œ ์ œ๊ณต์ž")
Loading