From 04cda6fd2396256138fe1ec63677042e046e6f5d Mon Sep 17 00:00:00 2001 From: dbqls200 Date: Tue, 15 Oct 2024 21:22:42 +0900 Subject: [PATCH 1/3] feat: #10 Add bus number search functionality using CSV data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자가 입력한 버스 번호에 대한 데이터만 필터링하는 작업 --- TMT/TMT.xcodeproj/project.pbxproj | 20 ++++++++ TMT/TMT/BusSearch/BusSearchModel.swift | 16 +++++++ TMT/TMT/BusSearch/BusSearchView.swift | 18 +++++++ TMT/TMT/BusSearch/BusSearchViewModel.swift | 56 ++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 TMT/TMT/BusSearch/BusSearchModel.swift create mode 100644 TMT/TMT/BusSearch/BusSearchView.swift create mode 100644 TMT/TMT/BusSearch/BusSearchViewModel.swift diff --git a/TMT/TMT.xcodeproj/project.pbxproj b/TMT/TMT.xcodeproj/project.pbxproj index 1cd843a..7cce122 100644 --- a/TMT/TMT.xcodeproj/project.pbxproj +++ b/TMT/TMT.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ D86739712CA933CE00FFE8ED /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D86739702CA933CE00FFE8ED /* Assets.xcassets */; }; D86739742CA933CE00FFE8ED /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D86739732CA933CE00FFE8ED /* Preview Assets.xcassets */; }; D8D377E62CBE549F0043D103 /* BusStopData.csv in Resources */ = {isa = PBXBuildFile; fileRef = D8D377E52CBE549F0043D103 /* BusStopData.csv */; }; + D8D377E92CBE95C30043D103 /* BusSearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D377E82CBE95C30043D103 /* BusSearchModel.swift */; }; + D8D377EB2CBE95CA0043D103 /* BusSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D377EA2CBE95CA0043D103 /* BusSearchViewModel.swift */; }; + D8D377ED2CBE95D10043D103 /* BusSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D377EC2CBE95D10043D103 /* BusSearchView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -21,6 +24,9 @@ D86739702CA933CE00FFE8ED /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D86739732CA933CE00FFE8ED /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; D8D377E52CBE549F0043D103 /* BusStopData.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = BusStopData.csv; sourceTree = ""; }; + D8D377E82CBE95C30043D103 /* BusSearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusSearchModel.swift; sourceTree = ""; }; + D8D377EA2CBE95CA0043D103 /* BusSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusSearchViewModel.swift; sourceTree = ""; }; + D8D377EC2CBE95D10043D103 /* BusSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusSearchView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,6 +61,7 @@ children = ( D867396C2CA933CD00FFE8ED /* TMTApp.swift */, D867396E2CA933CD00FFE8ED /* ContentView.swift */, + D8D377E72CBE95B80043D103 /* BusSearch */, D8D377E42CBE548F0043D103 /* Resource */, D86739702CA933CE00FFE8ED /* Assets.xcassets */, D86739722CA933CE00FFE8ED /* Preview Content */, @@ -78,6 +85,16 @@ path = Resource; sourceTree = ""; }; + D8D377E72CBE95B80043D103 /* BusSearch */ = { + isa = PBXGroup; + children = ( + D8D377E82CBE95C30043D103 /* BusSearchModel.swift */, + D8D377EA2CBE95CA0043D103 /* BusSearchViewModel.swift */, + D8D377EC2CBE95D10043D103 /* BusSearchView.swift */, + ); + path = BusSearch; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -149,8 +166,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D8D377E92CBE95C30043D103 /* BusSearchModel.swift in Sources */, D867396F2CA933CD00FFE8ED /* ContentView.swift in Sources */, D867396D2CA933CD00FFE8ED /* TMTApp.swift in Sources */, + D8D377EB2CBE95CA0043D103 /* BusSearchViewModel.swift in Sources */, + D8D377ED2CBE95D10043D103 /* BusSearchView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TMT/TMT/BusSearch/BusSearchModel.swift b/TMT/TMT/BusSearch/BusSearchModel.swift new file mode 100644 index 0000000..15d1eb4 --- /dev/null +++ b/TMT/TMT/BusSearch/BusSearchModel.swift @@ -0,0 +1,16 @@ +// +// BusSearchModel.swift +// TMT +// +// Created by 김유빈 on 10/15/24. +// + +struct BusStopInfo: Codable { + var busNumber: String = "" // 노선명 == 버스번호 + var busType: String = "" // 버스 타입 + var stopOrder: String = "" // 순번 + var stopName: String = "" // 정류소명 (한글) + var romanizedStopName: String = "" // 정류소명 (로마자 표기) + var xCoordinate: String = "" // x좌표 + var yCoordinate: String = "" // y좌표 +} diff --git a/TMT/TMT/BusSearch/BusSearchView.swift b/TMT/TMT/BusSearch/BusSearchView.swift new file mode 100644 index 0000000..b309f83 --- /dev/null +++ b/TMT/TMT/BusSearch/BusSearchView.swift @@ -0,0 +1,18 @@ +// +// BusSearchView.swift +// TMT +// +// Created by 김유빈 on 10/15/24. +// + +import SwiftUI + +struct BusSearchView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + BusSearchView() +} diff --git a/TMT/TMT/BusSearch/BusSearchViewModel.swift b/TMT/TMT/BusSearch/BusSearchViewModel.swift new file mode 100644 index 0000000..46beb0d --- /dev/null +++ b/TMT/TMT/BusSearch/BusSearchViewModel.swift @@ -0,0 +1,56 @@ +// +// BusSearchViewModel.swift +// TMT +// +// Created by 김유빈 on 10/15/24. +// + +import Foundation + +final class BusStopSearchViewModel: ObservableObject { + @Published var allBusStops: [BusStopInfo] = [] + @Published var busStops: [BusStopInfo] = [] + + init() { + loadCSV() + } + + private func loadCSV() { + Task { + guard let filepath = Bundle.main.path(forResource: "BusStopData", ofType: "csv") else { + print("Error \(#function) in \(#file) :: CSV file not found") + return + } + do { + let content = try String(contentsOfFile: filepath) + + let response = content.components(separatedBy: "\n") + + let searchResponse = response.map { $0.components(separatedBy: ",")} + + await self.apply(searchResponse) + + } catch { + print("Error \(#function) in \(#file)") + } + } + } + + @MainActor + private func apply(_ searchResponse: [[String]]) { + for response in searchResponse { + self.allBusStops.append(BusStopInfo(busNumber: response[0], + busType: response[1], + stopOrder: response[2], + stopName: response[3], + romanizedStopName: response[4], + xCoordinate: response[5], + yCoordinate: String(response[6].dropLast(1)))) + } + } + + func searchBus(by number: String) { + busStops = allBusStops.filter { $0.busNumber.contains(number) } + } + +} From 65632a6ac0ae3633688bb7fcbe913b5b316e3629 Mon Sep 17 00:00:00 2001 From: dbqls200 Date: Wed, 16 Oct 2024 02:47:13 +0900 Subject: [PATCH 2/3] chore: #10 Make BusStopInfo model properties optional and handle empty values during CSV parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - csv 파일 파싱하는 과정에서 빈 값을 처리하기 위해 모델 프로퍼티를 Optional Type으로 수정 - 빈 값으로 들어오는 경우도 유연하게 대처하기 위함 --- TMT/TMT/BusSearch/BusSearchModel.swift | 14 ++++---- TMT/TMT/BusSearch/BusSearchViewModel.swift | 42 ++++++++++++---------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/TMT/TMT/BusSearch/BusSearchModel.swift b/TMT/TMT/BusSearch/BusSearchModel.swift index 15d1eb4..e2b6b2d 100644 --- a/TMT/TMT/BusSearch/BusSearchModel.swift +++ b/TMT/TMT/BusSearch/BusSearchModel.swift @@ -6,11 +6,11 @@ // struct BusStopInfo: Codable { - var busNumber: String = "" // 노선명 == 버스번호 - var busType: String = "" // 버스 타입 - var stopOrder: String = "" // 순번 - var stopName: String = "" // 정류소명 (한글) - var romanizedStopName: String = "" // 정류소명 (로마자 표기) - var xCoordinate: String = "" // x좌표 - var yCoordinate: String = "" // y좌표 + var busNumber: String? // 노선명 (버스번호) + var busType: Int? // 버스 타입 + var stopOrder: Int? // 순번 + var stopName: String? // 정류소명 (한글) + var romanizedStopName: String? // 정류소명 (로마자 표기) + var xCoordinate: String? // x좌표 + var yCoordinate: String? // y좌표 } diff --git a/TMT/TMT/BusSearch/BusSearchViewModel.swift b/TMT/TMT/BusSearch/BusSearchViewModel.swift index 46beb0d..f79d634 100644 --- a/TMT/TMT/BusSearch/BusSearchViewModel.swift +++ b/TMT/TMT/BusSearch/BusSearchViewModel.swift @@ -8,13 +8,13 @@ import Foundation final class BusStopSearchViewModel: ObservableObject { - @Published var allBusStops: [BusStopInfo] = [] @Published var busStops: [BusStopInfo] = [] - + @Published var filteredBusStops: [BusStopInfo] = [] + init() { loadCSV() } - + private func loadCSV() { Task { guard let filepath = Bundle.main.path(forResource: "BusStopData", ofType: "csv") else { @@ -23,34 +23,38 @@ final class BusStopSearchViewModel: ObservableObject { } do { let content = try String(contentsOfFile: filepath) - + let response = content.components(separatedBy: "\n") - + let searchResponse = response.map { $0.components(separatedBy: ",")} - + await self.apply(searchResponse) - + } catch { print("Error \(#function) in \(#file)") } } } - + @MainActor private func apply(_ searchResponse: [[String]]) { for response in searchResponse { - self.allBusStops.append(BusStopInfo(busNumber: response[0], - busType: response[1], - stopOrder: response[2], - stopName: response[3], - romanizedStopName: response[4], - xCoordinate: response[5], - yCoordinate: String(response[6].dropLast(1)))) + self.busStops.append(BusStopInfo(busNumber: response[0].isEmpty ? nil : response[0], + busType: response[1].isEmpty ? nil : Int(response[1]), + stopOrder: response[2].isEmpty ? nil : Int(response[2]), + stopName: response[3].isEmpty ? nil : response[3], + romanizedStopName: response[4].isEmpty ? nil : response[4], + xCoordinate: response[5].isEmpty ? nil : response[5], + yCoordinate: response[6].isEmpty ? nil : String(response[6].dropLast(1)))) } } - - func searchBus(by number: String) { - busStops = allBusStops.filter { $0.busNumber.contains(number) } + + func searchBusStops(by number: String) { + filteredBusStops = busStops.filter { busStop in + if let busNumber = busStop.busNumber { + return busNumber.hasPrefix(number) + } + return false + } } - } From 981ac77606e41097cc2d2b86a3eb1b88dfcfba01 Mon Sep 17 00:00:00 2001 From: dbqls200 Date: Wed, 16 Oct 2024 02:54:26 +0900 Subject: [PATCH 3/3] feat: #10 Create function to extract and display unique bus numbers from search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자가 검색한 버스 번호에 대한 결과를 보여주기 위해 버스 번호만 뽑아내는 기능 추가 --- TMT/TMT/BusSearch/BusSearchViewModel.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/TMT/TMT/BusSearch/BusSearchViewModel.swift b/TMT/TMT/BusSearch/BusSearchViewModel.swift index f79d634..7f0246a 100644 --- a/TMT/TMT/BusSearch/BusSearchViewModel.swift +++ b/TMT/TMT/BusSearch/BusSearchViewModel.swift @@ -10,6 +10,7 @@ import Foundation final class BusStopSearchViewModel: ObservableObject { @Published var busStops: [BusStopInfo] = [] @Published var filteredBusStops: [BusStopInfo] = [] + @Published var busNumbers = [Int]() init() { loadCSV() @@ -56,5 +57,19 @@ final class BusStopSearchViewModel: ObservableObject { } return false } + + fetchBusNumbersList() + } + + /// 검색 결과 배열에는 노선 전체 데이터가 담겨 있어 동일한 버스 번호를 가진 데이터가 많습니다. + /// 사용자에게 보여줄 때는 중복된 데이터를 제외하고, 버스 번호만 추출해서 보여줘야 합니다. + /// 사용자가 검색한 버스 번호를 추출하여 결과로 보여주기 위해 버스 번호만 추출하기 위해 만든 함수입니다. + private func fetchBusNumbersList() { + busNumbers = Array(Set(filteredBusStops.compactMap { busStop in + if let busNumberString = busStop.busNumber, let busNumber = Int(busNumberString) { + return busNumber + } + return nil + })) } }