Skip to content
Closed
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
## [Unreleased]
### 신규 도구 추가 (New Tools)

- 툴 레지스트리에 날짜 계산기 (`date_calculator`) 및 환율 변환기 (`currency_converter`) 도구를 추가했습니다. 사용자의 요구에 따라 기능을 직접 기획하고 구현하여 생산성을 향상시켰습니다.
### 보안 패치 (CodeQL extended current-head)

- CodeQL `extended` 기본 설정이 current `develop`에서 확인한 Critical 8건·High 21건·Medium 1건을 코드 경계에서 제거합니다. 서버 요청은 검증된 loopback/HTTPS origin, 동일 OIDC issuer origin, 허용 API 경로·쿼리만 재구성하고 redirect를 자동 추종하지 않으며, 공개 IPv6 authority를 보존합니다. UI smoke는 고정 Node/Next 실행 파일과 인자, localhost:3001 allowlist, private `mkdtemp` artifact 디렉터리 및 containment 검사만 사용합니다.
Expand Down
62 changes: 62 additions & 0 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from collections import Counter
from collections.abc import Callable
from typing import Any, Dict, List, Optional
from datetime import datetime, timedelta

import httpx
from core.url_validation import (
Expand Down Expand Up @@ -855,6 +856,67 @@ def create_tool(tool_data: ToolCreate) -> ToolInfo:
return tool_info


async def date_calculator_handler(params: Dict[str, Any]) -> Any:
"""Calculate a new date by adding or subtracting days from a base date."""
base_date_str = params.get("base_date", "")
try:
base_date = datetime.strptime(base_date_str, "%Y-%m-%d")
except ValueError:
base_date = datetime.now()

days = int(params.get("days", 0))
result_date = base_date + timedelta(days=days)
return {"result_date": result_date.strftime("%Y-%m-%d")}

registry.register(
ToolInfo(
code="date_calculator",
name="날짜 계산기 (Date Calculator)",
description="기준 날짜에 특정 일수를 더하거나 빼서 새로운 날짜를 계산합니다.",
category="유틸리티",
parameters={"base_date": "string", "days": "integer"},
),
date_calculator_handler,
)

async def currency_converter_handler(params: Dict[str, Any]) -> Any:
"""Convert an amount from one currency to another using mock rates."""
amount = float(params.get("amount", 0.0))
from_currency = params.get("from_currency", "USD").upper()
to_currency = params.get("to_currency", "KRW").upper()

# Mock exchange rates relative to USD
rates = {
"USD": 1.0,
"KRW": 1350.0,
"EUR": 0.92,
"JPY": 150.0,
}

if from_currency not in rates or to_currency not in rates:
return {"error": "Unsupported currency. Supported: USD, KRW, EUR, JPY"}

amount_in_usd = amount / rates[from_currency]
converted_amount = amount_in_usd * rates[to_currency]

return {
"amount": amount,
"from_currency": from_currency,
"to_currency": to_currency,
"converted_amount": round(converted_amount, 2)
}

registry.register(
ToolInfo(
code="currency_converter",
name="환율 변환기 (Currency Converter)",
description="주요 통화 간의 금액을 변환합니다 (USD, KRW, EUR, JPY 지원).",
category="유틸리티",
parameters={"amount": "number", "from_currency": "string", "to_currency": "string"},
),
currency_converter_handler,
)

@router.get("/tools/{code}", response_model=ToolInfo)
def get_tool(code: str) -> ToolInfo:
"""
Expand Down
53 changes: 53 additions & 0 deletions backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1252,3 +1252,56 @@ def test_execute_analysis_tool_rejects_oversized_text():
f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters"
),
}


@pytest.mark.asyncio
async def test_date_calculator_tool_success():
with TestClient(app) as client:
response = client.post(
"/api/tools/date_calculator/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"base_date": "2023-10-25", "days": 5}},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert data["result"]["result_date"] == "2023-10-30"

@pytest.mark.asyncio
async def test_date_calculator_tool_invalid_date():
with TestClient(app) as client:
response = client.post(
"/api/tools/date_calculator/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"base_date": "invalid-date", "days": 1}},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
# Should use current date

@pytest.mark.asyncio
async def test_currency_converter_tool_success():
with TestClient(app) as client:
response = client.post(
"/api/tools/currency_converter/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"amount": 100, "from_currency": "USD", "to_currency": "KRW"}},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert data["result"]["converted_amount"] == 135000.0

@pytest.mark.asyncio
async def test_currency_converter_tool_unsupported():
with TestClient(app) as client:
response = client.post(
"/api/tools/currency_converter/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"amount": 100, "from_currency": "XYZ", "to_currency": "KRW"}},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert "error" in data["result"]
Loading