이번에 실내정원용 식물 검색 기능을 구현하면서 농사로 Open API를 이용해 식물 목록 조회, 상세 조회, 필터 목록 조회, 그리고 필터 조건 기반 검색까지 구현이 목표였다.
그리고 농사로 API 가이드는 그렇게 친절하지 않았다...🫠🫠
응답은 JSON이 아니라 XML로 받아서 디코딩해야 한다.
이미 디코딩은 구현은 끝내놓아서 일단 어떤 목록이 있는지 확인 해보았다.
lightList: 광도요구 목록
grwhstleList: 생육형태 목록
lefcolrList: 잎색 목록
lefmrkList: 잎무늬 목록
flclrList: 꽃색 목록
fmldecolrList: 열매색 목록
ignSeasonList: 꽃피는 계절 목록
winterLwetList: 겨울 최저온도 목록
waterCycleList: 물주기 목록
gardenList: 실내정원용 식물 목록
gardenDtl: 실내정원용 식물 상세
이거만 가져오면 되나? 했는데 이건 그냥 필터 옵션 목록을 가져오는 API였다. 옵션을 서버에서 받아와서 버튼에서 선택지를 보여주기 위해서 필요한 항목들이었다. 여기서 부터 정신이 혼미해졌다.
저기서 가져온 옵션들에서 검색어, 검색 타입, 필터 코드들을 파라미터로 넘기면 조건에 맞는 식물 목록을 반환한다. 실제 필터링 검색은 gardenList을 다시 호출해야 완료 됐다.
1. 각 필터 종류별 옵션 목록 API를 호출
lightList, waterCycleList 등등 호출
2. 응답으로 받은 옵션들의 code, name 데이터를 저장
광도요구: 054001 / 낮은 광도, 054002 / 중간 광도, 054003 / 높은 광도
3. 사용자는 이 목록 중 하나를 선택
화면에는 code는 보이지 않게 하고 name으로 보여짐
1. 원래는 검색어와 검색 기준(식물명,학명,영명) 필수적으로 선택해야 검색이 이루어지지만 식물명으로만 검색 가능하도록 식물명 검색 강제함!
검색어: 몬스테라
검색 기준: 식물명
광도요구: 중간 광도
물주기: 주 1회
2. 선택된 필터 상태를 저장
query = "몬스테라"
searchType = sCntntsSj
light = 054002
waterCycle = 053001
3. 필터 상태 기반으로 gardenList API를 호출
{baseURL}/gardenList
?apiKey=인증키
&sType=sCntntsSj
&sText=몬스테라
&lightChkVal=054002
&waterCycleSel=053001
4. 응답으로 받은 식물 목록을 화면에 보여줌
cntntsNo: 12974
cntntsSj: 몬스테라 델리시오사
...(생략)
1. 목록에서 받은 cntntsNo를 사용해서 gardenDtl API를 호출
{baseURL}/gardenDtl?apiKey=인증키&cntntsNo=12974
2. 상세 데이터를 화면에 보여준다!!
func fetchPlantList(
keyword: String,
searchType: PlantSearchType = .plantName,
filterState: PlantFilterState = .init(),
pageNo: Int = 1,
numOfRows: Int = 10
) async throws -> [PlantSummary]
검색어, 검색 타입, 필터 상태, 페이지 정보를 받아서 gardenList를 호출
func fetchPlantDetail(contentNumber: String) async throws -> PlantDetail
목록에서 받은 cntntsNo를 이용해 상세 조회
func fetchFilterOptions(kind: PlantFilterKind) async throws -> [PlantFilterOption]
필터 종류 하나에 대한 옵션 목록을 요청
enum PlantFilterKind: CaseIterable, Hashable {
case searchType
case light
case growthStyle
case leafColor
case leafPattern
case flowerColor
case fruitColor
case bloomingSeason
case winterMinTemperature
case waterCycle
}
func fetchAllFilterOptions() async throws -> [PlantFilterKind: [PlantFilterOption]]
필터 종류가 많기때문에 하나씩 순차 호출하면 느리기 때문에 병렬로 호출했다.(나중에 이거 트러블 슈팅 써야함)
사용자가 검색하기 전에 필터를 선택하려면 먼저 선택 가능한 옵션들이 필요함
예를 들어 광도요구는 API 가이드 문서를 보면 다음 코드가 있다
055001: 낮은 광도
055002: 중간 광도
055003: 높은 광도
사실 이걸 앱에서 하드코딩하고 싶었다. 하지만 꾹 참고 서버에서 받아오는 구조로 맞췄다.
(바뀔 일은 없어보이지만 혹시라도)서버가 바뀌면 바로 오류가 날 가능성이 있고 어차피 서버상에서 코드와 표시명을 함께 보내주기 때문에 그냥 타입을 만들어서 받았다
struct PlantFilterOption: Decodable, Equatable {
let code: String
let name: String
}
HTTP 성공과 API 성공이 달랐다.
그래서 HTTP 레벨 성공만 보고 화면에 데이터를 그리려고 했지만 아무것도 뜨지 않는 문제가 발생했다. 오류는 안뜨는데 결과값도 안떠서 아직 빌드가 안된건가 하고 맥북만 원망했다.
농사로 API는 HTTP 200이어도 API 내부적으로는 실패일 수 있었다 그래서 응답의 header.resultCode를 추가로 확인해야 했다.
00: 정상 처리
11: 인증키 문제
12: 인증키 일시 중지
13: 잘못된 서비스/오퍼레이션
91: 시스템 오류
private func validate(header: PlantResponseHeader) throws {
guard header.resultCode == "00" else {
throw NetworkError.apiError(code: header.resultCode, message: header.resultMsg)
}
}
API 가이드 문서에 결과코드를 참고해서 이걸 기준으로 판단하는 메서드를 만들었다. 그리고 fetch해오는 함수 안에서 검사를 해주었다.
+) 추후 추가 예정