1. 프로젝트 개요
고기 가격 정보 대시보드에서 KAMIS API 연동을 개선하고, 카테고리별 필터링 로직을 최적화했습니다.
2. 주요 작업 내용
2-1 KAMIS API 액션 구분 및 파라미터 처리
[문제점]
- 수입 소고기 데이터가 제대로 조회되지 않음
- p_productrankcode=00 전송 시 API 에러 발생
- 국내 소고기 등급 선택 시 모든 등급이 동일한 가격으로 표시됨
[해결 방법]
- 수입 소고기는 periodRetailProductList, 국내 소고기/돼지는 periodProductList를 사용하도록 구분했습니다.
1) apis.py - _fetch_kamis_price_single 함수
# 수입 소고기는 periodRetailProductList 액션 사용
if is_import_beef:
params = {
"action": "periodRetailProductList",
"p_startday": target_day,
"p_endday": target_day,
"p_itemcategorycode": codes.get("category", "500"),
"p_itemcode": codes.get("itemcode", ""),
"p_kindcode": codes.get("kindcode", ""),
"p_productrankcode": product_rank_code, # 원산지 코드 (81=미국산, 82=호주산)
"p_countrycode": county_code,
"p_convert_kg_yn": "N",
"p_cert_key": key,
"p_cert_id": cert_id,
"p_returntype": "xml",
}
else:
# 국내 소고기/돼지: periodProductList 액션 사용
params = {
"action": "periodProductList",
"p_productclscode": "01", # 필수 파라미터
"p_startday": target_day,
"p_endday": target_day,
# ... 나머지 파라미터
}
2) apis.py - fetch_kamis_price 함수 (p_productrankcode 처리)
p_productrankcode=00은 에러를 발생시키므로, "전체" 선택 시 빈 문자열("")을 전송하거나 각 등급을 개별 조회하도록 변경했습니다.
# 전체 등급(00) 선택 시: 국내 소고기는 각 등급을 개별 조회
if grade_code == "00" and is_domestic_beef:
grade_prices: list[dict[str, Any]] = []
grade_codes_to_fetch = ["01", "02", "03"]
for gc in grade_codes_to_fetch:
try:
price_data = await _fetch_kamis_price_single(
part_name=part_name,
region=region,
grade_code=gc, # 개별 등급 조회
target_day=target_day,
# ... 기타 파라미터
)
if price_data:
grade_code_map = codes.get("grade_codes", {})
grade_name = grade_code_map.get(gc, f"{gc}등급")
grade_prices.append({
"grade": grade_name,
"price": price_data["price"],
"unit": "100g",
"priceDate": price_data["date"],
"trend": "flat",
})
except Exception as e:
logger.warning(f"등급 {gc} 조회 실패: {e}")
continue
# 전체 평균 계산
avg_price = sum(gp["price"] for gp in grade_prices) / len(grade_prices)
return {
"currentPrice": int(avg_price),
"unit": "100g",
"trend": "flat",
"price_date": grade_prices[0]["priceDate"],
"source": "api",
"gradePrices": grade_prices,
"selectedGrade": "전체",
}
else:
# 돼지나 수입 소고기는 등급이 없으므로 빈 문자열 사용
if is_pork:
product_rank_code = ""
elif is_import_beef:
if "_US" in part_name:
product_rank_code = "81" # 미국산
elif "_AU" in part_name:
product_rank_code = "82" # 호주산
else:
product_rank_code = "" # 전체 선택 시
3) apis.py - 날짜 파싱 로직 개선
API는 어제 날짜까지만 데이터가 제공되므로, 연도 누락을 방지하며 파싱하도록 개선했습니다.
# API는 어제 날짜까지만 데이터가 있으므로 어제 날짜를 사용
today = date.today()
yesterday = today - timedelta(days=1)
target_day = yesterday.strftime("%Y-%m-%d")
# 날짜 파싱 시 target_day의 연도를 기본값으로 사용
target_year = target_day[:4] if target_day and len(target_day) >= 4 else None
if "/" in regday_str:
parts = regday_str.split("/")
if len(parts) == 2:
# MM/DD 형식: target_day의 연도 사용
if target_year:
regday = f"{target_year}-{parts[0].zfill(2)}-{parts[1].zfill(2)}"
2-2 수입 소고기/돼지고기 데이터 정리
[변경 사항]
- 수입 소고기: 척아이롤, 양지 삭제 → 갈비, 갈비살만 유지 (미국산/호주산)
- 수입 돼지고기: 삼겹살만 유지
1) apis.py - PART_TO_CODES 딕셔너리
# 수입 소고기 - itemcode 4401 (갈비, 갈비살만 유지)
"Import_Beef_Rib_US": {
"itemcode": "4401", "kindcode": "31", "category": "500",
"food_nm": "수입 소고기/갈비", "grades": ["미국산"], "grade_codes": {"81": "미국산"},
},
"Import_Beef_Rib_AU": {
"itemcode": "4401", "kindcode": "31", "category": "500",
"food_nm": "수입 소고기/갈비", "grades": ["호주산"], "grade_codes": {"82": "호주산"},
},
"Import_Beef_Ribeye_US": {
"itemcode": "4401", "kindcode": "37", "category": "500",
"food_nm": "수입 소고기/갈비살", "grades": ["미국산"], "grade_codes": {"81": "미국산"},
},
"Import_Beef_Ribeye_AU": {
"itemcode": "4401", "kindcode": "37", "category": "500",
"food_nm": "수입 소고기/갈비살", "grades": ["호주산"], "grade_codes": {"82": "호주산"},
},
# 수입 돼지고기 - itemcode 4402
"Import_Pork_Belly": {
"itemcode": "4402", "kindcode": "27", "category": "500",
"food_nm": "수입 돼지고기/삼겹살", "grades": ["전체"], "grade_codes": {"00": "전체"},
},
2-3 프론트엔드 카테고리별 필터링 로직 개선
[문제점]
- "전체" 선택 시 모든 카테고리 데이터가 섞여서 표시됨
- 수입육 선택 시에도 국내산 데이터가 표시되는 현상
[해결 방법] 카테고리별 기본 부위 목록을 정의하고, "전체" 선택 시 해당 부위만 개별 조회하도록 변경했습니다.
1) dashboard-view.tsx - getDefaultPartsForCategory
const getDefaultPartsForCategory = () => {
if (selectedCategory === "소") {
return { beef: ["Beef_Ribeye", "Beef_Rib"], pork: [] };
} else if (selectedCategory === "돼지") {
return { beef: [], pork: ["Pork_Shoulder", "Pork_Belly", "Pork_Rib", "Pork_Loin"] };
} else if (selectedCategory === "수입 소고기") {
return { beef: ["Import_Beef_Rib_US", "Import_Beef_Rib_AU", "Import_Beef_Ribeye_US", "Import_Beef_Ribeye_AU"], pork: [] };
} else if (selectedCategory === "수입 돼지고기") {
return { beef: [], pork: ["Import_Pork_Belly"] };
}
return { beef: [], pork: [] };
};
2) dashboard-view.tsx - UI 개선 (원산지 카테고리 제어)
const shouldHideGradeCategory = () => {
return selectedCategory === "수입 소고기" || selectedCategory === "수입 돼지고기";
};
// UI 렌더링 부분 (원산지가 품종에 포함된 경우 등급 선택 UI 숨김)
{!shouldHideGradeCategory() && (
<Select value={selectedGrade} onValueChange={setSelectedGrade}>
<SelectTrigger className="w-full">
<SelectValue placeholder="등급/원산지 선택" />
</SelectTrigger>
<SelectContent>
{gradeOptions[selectedCategory]?.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
3. 핵심 학습 포인트
- API 파라미터 처리의 정밀함: p_productrankcode=00 에러 대응을 위해 빈 문자열 처리나 개별 루프 조회가 필요하다는 점을 배웠습니다.
- 프론트엔드 필터링 전략: 카테고리별 기본 부위(Default List)를 명확히 정의함으로써 데이터 혼선을 방지하고 로직을 단순화할 수 있었습니다.
- 날짜 데이터 가공: 공공 API의 데이터 갱신 시점(어제 날짜)을 고려한 타겟팅과 연도 보정 로직의 중요성을 확인했습니다.
4. 결과
- ✅ 수입 소고기/돼지고기 데이터 정리 완료
- ✅ 카테고리별 필터링 로직 개선으로 데이터 혼선 해결
- ✅ 대시보드 로딩 속도 최적화
- ✅ UI 개선 (원산지 카테고리 스마트 숨김, 텍스트 잘림 해결)
마치며: 이번 작업을 통해 KAMIS API의 특성을 깊이 이해하고, 사용자에게 정확한 데이터를 전달하기 위한 백엔드와 프론트엔드의 협업 로직 설계 과정을 경험할 수 있었습니다.
'4. [팀] 프로젝트 및 공모전 > 4-2 Meat-A-Eye' 카테고리의 다른 글
| [프로젝트 회고] Meat-A-Eye: AI 기반 축산물 부위 인식 및 관리 플랫폼 개발기 (0) | 2026.02.24 |
|---|---|
| [Meat-A-Eye] 성능 개선 프로세스 상세 메모 블로그 (0) | 2026.02.06 |
| [Meat_A_Eye] 소고기 부위 분류 모델 성능 개량 (0) | 2026.02.03 |
| [개발 기록] 대시보드 가격 API 응답 시간 줄이기 - 병렬 호출과 캐시 사용 (0) | 2026.02.02 |
| [Meat-A-Eye] 데이터 수집 과정 정리 (0) | 2026.01.24 |
