Skip to content

Commit 9c652cc

Browse files
committed
chore: 부가세 계산 기능 수정
1 parent f918c55 commit 9c652cc

5 files changed

Lines changed: 50 additions & 99 deletions

File tree

core/shared/constants/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
# 배송 추적 관련 키워드
2929
CUSTOMS_TRACKING_KEYWORDS = [
30-
'운송장', '배송', '통관', '조회', '추적', '배송상태', '위치', '도착', '출고', '통관번호', '운송장번호'
30+
'운송장', '통관', '조회', '추적', '배송상태', '위치', '도착', '출고', '통관번호', '운송장번호'
3131
]
3232

3333
# 의도 분류 프롬프트

core/shared/router/intent_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def intent_router(state: CustomsAgentState) -> CustomsAgentState:
7676
print(state)
7777
return state
7878

79-
# 운송장/배송 관련 키워드가 있으면 customs_tracking으로 분류
79+
# 운송장/배송 관련 키워드가 있으면 customs_tracking으로 분류 (배송은 제외)
8080
if any(keyword in current_query_lower for keyword in CUSTOMS_TRACKING_KEYWORDS):
8181
state["intent"] = "customs_tracking" # type: ignore
8282
state["messages"].append(AIMessage(content=f"의도 분류 완료: {state['intent']} (배송 추적 키워드 감지)"))

core/tariff_prediction/agent/tariff_prediction_agent.py

Lines changed: 25 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def __init__(self):
5151
'hs10_code': None,
5252
'current_step': 'scenario_selection',
5353
'session_active': False,
54-
'responses': []
54+
'responses': [],
55+
'predicted_scenario': None, # 예측된 시나리오 저장
56+
'last_user_input': None # 마지막 사용자 입력 저장
5557
}
5658

5759
# 환율 지원 국가 목록
@@ -72,7 +74,9 @@ def reset_session(self):
7274
'hs10_code': None,
7375
'current_step': 'scenario_selection',
7476
'session_active': False,
75-
'responses': []
77+
'responses': [],
78+
'predicted_scenario': None,
79+
'last_user_input': None
7680
}
7781

7882
def is_supported_country(self, country: str) -> bool:
@@ -213,33 +217,35 @@ def handle_scenario_selection(self, user_input: str) -> str:
213217
self.state['responses'].append(response)
214218
return response
215219

220+
def josa_으로(self, word: str) -> str:
221+
if not word:
222+
return ""
223+
last_char = word[-1]
224+
if (ord(last_char) - 44032) % 28 == 0:
225+
return word + "로"
226+
else:
227+
return word + "으로"
228+
216229
def handle_input_collection(self, user_input: str) -> str:
217-
# 컨텍스트에서 추가 정보 추출 시도
230+
# 최초 진입: 시나리오 예측
231+
if not self.state.get('scenario'):
232+
predicted = self.detect_scenario_from_input(user_input)
233+
if predicted:
234+
self.state['scenario'] = predicted
218235
enhanced_input = user_input
219-
220-
# 이전 대화에서 상품 정보가 누락된 경우 컨텍스트에서 찾기
221236
if "이전 대화:" in user_input:
222237
context_part = user_input.split("현재 질문:")[0].replace("이전 대화:", "").strip()
223238
current_part = user_input.split("현재 질문:")[1].strip() if "현재 질문:" in user_input else user_input
224-
225-
# 컨텍스트에서 상품 정보 추출 시도
226239
context_info = extract_info_from_context(context_part)
227240
if context_info:
228-
# 현재 입력에 누락된 정보를 컨텍스트에서 보완
229241
enhanced_input = merge_context_with_current(context_info, current_part)
230-
231242
parsed = self.parse_user_input(enhanced_input)
232-
233-
# 상품명이 없으면 입력 전체를 상품명으로 사용
234243
if 'product_name' not in parsed or not parsed['product_name']:
235-
# 입력에서 불필요한 키워드 제거 후 상품명으로 사용
236244
cleaned_input = user_input.strip()
237245
for keyword in ['관세', '예측', '계산', '해줘', '알려줘', '어떻게', '해주세요']:
238246
cleaned_input = cleaned_input.replace(keyword, '').strip()
239247
if cleaned_input:
240248
parsed['product_name'] = cleaned_input
241-
242-
# 필수 정보 확인
243249
missing_info = []
244250
if 'product_name' not in parsed or not parsed['product_name']:
245251
missing_info.append("상품명")
@@ -248,7 +254,6 @@ def handle_input_collection(self, user_input: str) -> str:
248254
if 'price' not in parsed or not parsed['price']:
249255
missing_info.append("상품 가격")
250256
if missing_info:
251-
# 이미 입력된 정보는 보여주고, 누락된 정보만 안내
252257
info_lines = []
253258
if 'product_name' in parsed and parsed['product_name']:
254259
info_lines.append(f"상품명: {parsed['product_name']}")
@@ -272,11 +277,8 @@ def handle_input_collection(self, user_input: str) -> str:
272277
)
273278
self.state['responses'].append(response)
274279
return response
275-
# 환율 변환 처리
276280
price = parsed['price']
277281
price_unit = parsed.get('price_unit', '원')
278-
279-
# 원화가 아닌 경우 환율 변환
280282
if price_unit != '원':
281283
try:
282284
from core.tariff_prediction.tools.get_exchange_rate_info import get_exchange_rate_api
@@ -285,23 +287,16 @@ def handle_input_collection(self, user_input: str) -> str:
285287
price = price * exchange_rate
286288
price_unit = '원'
287289
else:
288-
# 환율 조회 실패 시 기본 환율 사용
289290
if price_unit in DEFAULT_EXCHANGE_RATES:
290291
price = price * DEFAULT_EXCHANGE_RATES[price_unit]
291292
price_unit = '원'
292-
except Exception as e:
293-
print(f"[DEBUG] 환율 변환 오류: {e}")
294-
# 오류 시 기본 환율 사용
293+
except Exception:
295294
if price_unit in DEFAULT_EXCHANGE_RATES:
296295
price = price * DEFAULT_EXCHANGE_RATES[price_unit]
297296
price_unit = '원'
298-
299-
# 상태 업데이트
300297
self.state.update(parsed)
301298
self.state['price'] = price
302299
self.state['price_unit'] = price_unit
303-
304-
# step_api.py 활용
305300
req = TariffPredictionRequest(
306301
step="input",
307302
product_description=parsed['product_name'],
@@ -318,15 +313,12 @@ def handle_input_collection(self, user_input: str) -> str:
318313
self.state['hs6_candidates'] = resp.hs6_candidates
319314
self.state['current_step'] = 'hs6_selection'
320315
scenario_str = self.state.get('scenario', '')
321-
scenario_guide = f"{scenario_str}로 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
322-
323-
# 가격 표시 (원화 변환된 경우)
316+
scenario_guide = f"{self.josa_으로(scenario_str)} 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
324317
price_display = f"{price:,.0f}원"
325318
if price_unit != '원' and parsed.get('price_unit') != '원':
326319
original_price = parsed.get('price', price)
327320
original_unit = parsed.get('price_unit', price_unit)
328321
price_display = f"{original_price} {original_unit} (약 {price:,.0f}원)"
329-
330322
response = scenario_guide + f"상품묘사: {parsed['product_name']}\n국가: {parsed['country']}\n가격: {price_display}\n수량: {parsed.get('quantity', 1)}\n\nHS 코드 예측 모델로부터 HS6 코드 후보를 찾았습니다. 번호를 선택해 주세요:\n" + '\n'.join([
331323
f"{i+1}. {c['description']} (신뢰도: {c['confidence']:.1%})" for i, c in enumerate(resp.hs6_candidates or [])
332324
]) + f"\n\n💡 **위 후보 중 하나를 선택해 주세요.**\n예시: \"1번\", \"2번\", \"3번\" 등"
@@ -415,8 +407,7 @@ def handle_hs6_selection(self, user_input: str) -> str:
415407
self.state['responses'].append(response)
416408
return response
417409

418-
except Exception as e:
419-
print(f"[DEBUG] handle_hs6_selection intent detection error: {e}")
410+
except Exception:
420411
# 예외 발생 시 안내 메시지로 graceful 처리
421412
response = f"입력 처리 중 오류가 발생했습니다. 숫자를 입력하거나, 재예측을 원하시면 '다시', '재예측' 등으로 입력해 주세요."
422413
self.state['responses'].append(response)
@@ -487,68 +478,41 @@ def handle_hs10_selection(self, user_input: str) -> str:
487478

488479

489480
def _perform_hs6_reprediction(self, user_input: str) -> str:
490-
"""HS6 코드 재예측을 수행합니다."""
491481
from core.tariff_prediction.tools.parse_hs_results import parse_hs6_result
492482
from core.shared.utils.llm import get_llm
493-
494483
product_name = self.state.get('product_name')
495484
if not product_name or not isinstance(product_name, str) or not product_name.strip():
496485
response = "상품명을 알 수 없어 HS 코드 예측을 다시 시도할 수 없습니다. 처음부터 다시 입력해 주세요."
497486
self.state['responses'].append(response)
498487
return response
499-
500488
try:
501-
# 재예측을 위한 명확한 프롬프트
502-
reprediction_prompt = f"""아래 상품명과 사용자의 추가 의견을 참고하여 HS 코드 후보를 예측해주세요.
503-
504-
상품명: {product_name}
505-
사용자 추가 의견: {user_input}
506-
507-
다음 형식으로 HS 코드 후보 3개 이내를 반환하세요:
508-
1. [6자리 HS코드] (확률: [확률]%)
509-
2. [6자리 HS코드] (확률: [확률]%)
510-
3. [6자리 HS코드] (확률: [확률]%)
511-
512-
예시:
513-
1. 851770 (확률: 85.5%)
514-
2. 851712 (확률: 12.3%)
515-
3. 851713 (확률: 2.2%)"""
516-
489+
reprediction_prompt = f"""아래 상품명과 사용자의 추가 의견을 참고하여 HS 코드 후보를 예측해주세요.\n\n상품명: {product_name}\n사용자 추가 의견: {user_input}\n\n다음 형식으로 HS 코드 후보 3개 이내를 반환하세요:\n1. [6자리 HS코드] (확률: [확률]%)\n2. [6자리 HS코드] (확률: [확률]%)\n3. [6자리 HS코드] (확률: [확률]%)\n\n예시:\n1. 851770 (확률: 85.5%)\n2. 851712 (확률: 12.3%)\n3. 851713 (확률: 2.2%)"""
517490
llm = get_llm()
518491
hs6_response = llm.invoke([{"role": "user", "content": reprediction_prompt}])
519492
hs6_result = extract_llm_response(hs6_response)
520-
521-
# LLM 응답이 비어있거나 잘못된 경우 처리
522493
if not hs6_result or len(hs6_result.strip()) < 10:
523494
response = "HS 코드 예측에 실패했습니다. 상품명을 더 구체적으로 입력해 주세요."
524495
self.state['responses'].append(response)
525496
return response
526-
527-
# parse_hs6_result 함수 호출 시 예외 처리
528497
try:
529498
hs6_candidates = parse_hs6_result(hs6_result)
530499
except Exception as parse_error:
531500
print(f"[DEBUG] parse_hs6_result error: {parse_error}")
532501
response = "HS 코드 예측 결과를 처리하는 중 오류가 발생했습니다. 다시 시도해 주세요."
533502
self.state['responses'].append(response)
534503
return response
535-
536504
if not hs6_candidates:
537505
response = "HS 코드 예측에 다시 실패했습니다. 상품명을 더 구체적으로 입력해 주세요."
538506
self.state['responses'].append(response)
539507
return response
540-
541508
self.state['hs6_candidates'] = hs6_candidates
542509
scenario_str = self.state.get('scenario', '')
543-
scenario_guide = f"{scenario_str}로 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
544-
510+
scenario_guide = f"{self.josa_으로(scenario_str)} 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
545511
response = scenario_guide + f"상품묘사: {product_name}\n국가: {self.state.get('country','')}\n가격: {self.state.get('price',0):,}\n수량: {self.state.get('quantity',1)}\n\nHS 코드 재예측 결과입니다. 번호를 선택해 주세요:\n" + '\n'.join([
546512
f"{i+1}. {c['description']} (신뢰도: {c['confidence']:.1%})" for i, c in enumerate(hs6_candidates)
547513
]) + f"\n\n💡 **위 후보 중 하나를 선택해 주세요.**\n예시: \"1번\", \"2번\", \"3번\" 등"
548-
549514
self.state['responses'].append(response)
550515
return response
551-
552516
except Exception as e:
553517
print(f"[DEBUG] _perform_hs6_reprediction error: {e}")
554518
response = "HS 코드 재예측 중 오류가 발생했습니다. 다시 시도해 주세요."

core/tariff_prediction/tools/calculate_tariff_amount.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,16 @@ def calculate_tariff_amount(product_code: str, value: float, origin_country: str
166166

167167
# 관세 계산
168168
tax_info = calculate_tax_amount(value, item_count, shipping_cost, float(tariff_info['관세율']), krw_rate, situation)
169-
170-
# 부가가치세 계산
169+
170+
# 부가가치세 계산 (미국 $200, 기타 $150 초과 시 무조건 부과)
171171
VAT = 0
172-
if situation == '해외직구' and tax_info['tax_amount'] != 0:
172+
# USD 환율 계산
173+
total_price_krw = tax_info['total_price']
174+
total_price_usd = total_price_krw / krw_rate if krw_rate else 0
175+
vat_threshold = 200 if origin_country == '미국' else 150
176+
if total_price_usd > vat_threshold:
173177
VAT = (tax_info['total_price'] + tax_info['tax_amount']) * 0.1
174-
178+
175179
# 최종 결과
176180
result = {
177181
'HS코드': product_code,

core/tariff_prediction/tools/detect_scenario.py

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,28 @@
55

66
@tool
77
def detect_scenario_from_input(user_input: str) -> str | None:
8-
"""사용자 입력에서 시나리오를 자동 감지합니다."""
8+
"""
9+
사용자 입력에서 관세 예측 시나리오(해외직구, 해외체류 중 구매, 해외배송)를 자동으로 감지합니다.
10+
키워드 기반 우선 매칭 후, 실패 시 LLM을 사용해 감지합니다.
11+
"""
912
try:
13+
lowered = user_input.lower()
14+
# 키워드 기반 우선 매칭
15+
if any(word in lowered for word in ["여행", "직접", "휴대", "체류"]):
16+
return "해외체류 중 구매"
17+
if any(word in lowered for word in ["온라인", "쇼핑", "직구"]):
18+
return "해외직구"
19+
if any(word in lowered for word in ["배송", "택배", "운송"]):
20+
return "해외배송"
21+
# 키워드 매칭 실패 시 LLM fallback
1022
llm = get_llm()
11-
prompt = f"""다음은 관세 계산을 위한 사용자 입력입니다. 이 입력이 어떤 시나리오에 해당하는지 판단해주세요.
12-
13-
시나리오 종류:
14-
1. 해외직구: 온라인 쇼핑몰에서 해외 상품을 구매하는 경우
15-
2. 해외체류 중 구매: 해외 여행 중에 직접 구매하여 휴대품으로 가져오는 경우
16-
3. 해외배송: 해외에서 한국으로 택배나 운송을 통해 배송받는 경우
17-
18-
사용자 입력: "{user_input}"
19-
20-
위 입력을 분석하여 다음 중 하나로 답변해주세요:
21-
- "해외직구"
22-
- "해외체류 중 구매"
23-
- "해외배송"
24-
25-
답변:"""
23+
prompt = f"""다음은 관세 계산을 위한 사용자 입력입니다. 이 입력이 어떤 시나리오에 해당하는지 판단해주세요.\n\n시나리오 종류:\n1. 해외직구: 온라인 쇼핑몰에서 해외 상품을 구매하는 경우\n2. 해외체류 중 구매: 해외 여행 중에 직접 구매하여 휴대품으로 가져오는 경우 \n3. 해외배송: 해외에서 한국으로 택배나 운송을 통해 배송받는 경우\n\n사용자 입력: \"{user_input}\"\n\n위 입력을 분석하여 다음 중 하나로 답변해주세요:\n- \"해외직구\"\n- \"해외체류 중 구매\" \n- \"해외배송\"\n\n답변:"""
2624
response = llm.invoke([HumanMessage(content=prompt)])
2725
result = str(response.content) if hasattr(response, 'content') else str(response)
28-
result = result.strip()
29-
30-
# 응답에서 시나리오 추출 (따옴표나 "답변:" 등의 접두사 제거)
31-
32-
# 직접 매칭 시도
33-
if result in VALID_SCENARIOS:
34-
return result
35-
36-
# 응답에서 시나리오 추출 시도
26+
result = result.strip().replace("답변:", "").replace("입니다", "").replace("에 해당합니다", "").replace(".", "").replace("\"", "").replace("'", "").strip()
3727
for scenario in VALID_SCENARIOS:
3828
if scenario in result:
3929
return scenario
40-
41-
# "답변:" 접두사 제거 후 시도
42-
if result.startswith('답변:'):
43-
clean_result = result.replace('답변:', '').strip().strip('"').strip("'")
44-
if clean_result in VALID_SCENARIOS:
45-
return clean_result
46-
4730
return None
4831
except Exception:
4932
return None

0 commit comments

Comments
 (0)