오늘은 새로운 이슈를 파고 새로운 브랜치에서 작업햇당
프로젝트 구경하기
해당 프로젝트는 청소년들을 위한 프로젝트이기 때문에,
전체 학교, 본인 학교 내의 투표 서비스를 둘다 이용한다.
따라서 학교 정보를 입력받는다. (따로 인증을 받도록 기획하진 않았다.)
커리어넷의 학교 정보 openAPI와 연결하여 textField로 입력받은 학교 이름을 검색하여 보여준다.
final class SchoolSearchViewModel: ObservableObject {
@Published var schools = [SchoolInfoModel]()
@Published var isFetching = false
private let baseURL = "http://www.career.go.kr/cnet/openapi/getOpenApi"
var apiKey: String {
guard let key = Bundle.main.object(forInfoDictionaryKey: "SCHOOL_API_KEY") as? String else {
fatalError("SCHOOL_API_KEY error")
}
return key
}
@MainActor
func setSchoolData(searchWord: String) async throws {
schools.removeAll()
isFetching = true
let highSchoolValues: HighSchoolResponse = try await fetchSchoolData(schoolType: .highSchool, searchWord: searchWord)
let middleSchoolValues: MiddleSchoolResponse = try await fetchSchoolData(schoolType: .middleSchool, searchWord: searchWord)
let highSchoolSchools = highSchoolValues.dataSearch.content.map { $0.convertToSchoolInfoModel() }
let middleSchoolSchools = middleSchoolValues.dataSearch.content.map { $0.convertToSchoolInfoModel() }
schools.append(contentsOf: highSchoolSchools + middleSchoolSchools)
isFetching = false
}
private func fetchSchoolData<T: Decodable>(schoolType: SchoolDataType, searchWord: String) async throws -> T {
guard let encodedSearchWord = searchWord.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) else {
throw URLError(.badURL)
}
let url = baseURL +
"""
?apiKey=\(apiKey)&svcType=api&svcCode=SCHOOL&contentType=json&gubun=\(schoolType.schoolParam)&searchSchulNm=\(encodedSearchWord)
"""
return try await AF.request(url, method: .get)
.validate()
.serializingDecodable(T.self)
.value
}
}
그동안 리팩한 부분에서 내 파트는 별로 없었는데... 이건 예전에 내가 구현했었다. 이때 aync await 공부를 했어서 바로 적용했던 기억이... 그리고 viewModel에서 모조리 호출해 버리기....
final class SchoolSearchViewModel: ObservableObject {
enum Action {
case submit(_ searchSchoolText: String)
}
@Published var schools: [SchoolInfoModel] = [SchoolInfoModel]()
@Published var isLoading: Bool = false
@Published var searchSchoolText: String = ""
@Published var textFieldState: SearchTextFieldState = .inactive
private let userUseCase: UserUseCaseType
private var cancellables = Set<AnyCancellable>()
init(userUseCase: UserUseCaseType) {
self.userUseCase = userUseCase
}
func send(action: Action) {
switch action {
case let .submit(searchSchoolText):
isLoading = true
userUseCase.searchSchool(searchSchoolText)
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] schools in
self?.textFieldState = .submitted
self?.isLoading = false
self?.schools = schools
}
.store(in: &cancellables)
}
}
}
useCase에 접근하여, 학교 정보를 받으면 로딩을 중지하고 @Publihsed
로 선언된 schools에 할당하여 뷰에 보여준다.
protocol UserUseCaseType {
// ...
func setProfile(_ profile: ProfileSettingModel) -> AnyPublisher<Void, WoteError>
}
final class UserUseCase: UserUseCaseType {
private let userRepository: UserRepositoryType
init(userRepository: UserRepositoryType) {
self.userRepository = userRepository
}
// ...
func setProfile(_ profile: ProfileSettingModel) -> AnyPublisher<Void, WoteError> {
userRepository.setProfile(profile)
}
}
usecase에서는 repository에 접근한다.
func getSchoolsData(_ query: String) -> AnyPublisher<[SchoolInfoModel], WoteError> {
Publishers.Zip(userDataSource.getHighSchoolData(query), userDataSource.getMiddleSchoolData(query))
.map { highSchoolObject, middleSchoolObject in
var schoolResult: [SchoolInfoModel] = .init()
schoolResult.append(contentsOf: highSchoolObject.dataSearch.content.map { $0.convertToSchoolInfoModel() })
schoolResult.append(contentsOf: middleSchoolObject.dataSearch.content.map { $0.convertToSchoolInfoModel() })
return schoolResult
}
.mapError { WoteError.error($0) }
.eraseToAnyPublisher()
}
func setProfile(_ profile: ProfileSettingModel) -> AnyPublisher<Void, WoteError> {
userDataSource.setProfile(profile.toObject())
.mapError { WoteError.error($0) }
.eraseToAnyPublisher()
}
func getHighSchoolData(_ searchText: String) -> AnyPublisher<HighSchoolResponseObject, APIError> {
provider.requestPublisher(.getSchoolData(searchText, .highSchool))
.tryMap { try JSONDecoder().decode(HighSchoolResponseObject.self, from: $0.data) }
.mapError { APIError.error($0) }
.eraseToAnyPublisher()
}
func getMiddleSchoolData(_ searchText: String) -> AnyPublisher<MiddleSchoolResponseObject, APIError> {
provider.requestPublisher(.getSchoolData(searchText, .middleSchool))
.tryMap { try JSONDecoder().decode(MiddleSchoolResponseObject.self, from: $0.data) }
.mapError { APIError.error($0) }
.eraseToAnyPublisher()
}
고등학교, 중학교 정보가 함께 응답으로 오지 않기 때문에,
요청을 보내고, 두 정보를 zip하여 하나의 공통된 모델로 매핑했다.
profileSetting도 비슷하게 진행했지만, 다른 점은 사진을 넣어야 해서 multipart form data로 보내야 한다는 점
struct MultipartFormDataHelper {
static func createMultipartFormData(from profileRequest: ProfileRequestObject) -> [MultipartFormData] {
var formData = [MultipartFormData]()
if let imageFile = profileRequest.imageFile {
let imageMultipart = MultipartFormData(
provider: .data(imageFile),
name: "imageFile",
fileName: "\(profileRequest.nickname).jpg",
mimeType: "image/jpeg"
)
formData.append(imageMultipart)
}
var profileData: [String: Any] = [
"nickname": profileRequest.nickname
]
if profileRequest.school != nil {
profileData["school"] = [
"schoolName": profileRequest.school?.schoolName,
"schoolRegion": profileRequest.school?.schoolRegion
]
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: profileData)
let jsonString = String(data: jsonData, encoding: .utf8)!
let stringData = MultipartFormData(provider: .data(jsonString.data(using: String.Encoding.utf8)!),
name: "profileRequest",
mimeType: "application/json")
formData.append(stringData)
} catch {
print("error")
}
return formData
}
}
우선 multipart form data로 변환하는 과정을 따로 빼서 진행해 줬다.
이전에는
var task: Moya.Task {
switch self {
case .postProfileSetting(let profile):
var formData: [MultipartFormData] = []
if let data = UIImage(data: profile.imageFile ?? Data())?
.jpegData(compressionQuality: 0.3) {
let imageData = MultipartFormData(provider: .data(data),
name: "imageFile",
fileName: "\(profile.nickname).jpg",
mimeType: "image/jpeg")
formData.append(imageData)
}
var profileData: [String: Any] = [
"nickname": profile.nickname
]
if profile.school != nil {
profileData["school"] = [
"schoolName": profile.school?.schoolName,
"schoolRegion": profile.school?.schoolRegion
]
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: profileData)
let jsonString = String(data: jsonData, encoding: .utf8)!
let stringData = MultipartFormData(provider: .data(jsonString.data(using: String.Encoding.utf8)!),
name: "profileRequest",
mimeType: "application/json")
formData.append(stringData)
} catch {
print("error")
}
return .uploadMultipart(formData)
// ...
위와 같이 moya task 내에서 진행했다.
// Moya target type
case let .postProfile(requestObject):
let formData = MultipartFormDataHelper.createMultipartFormData(from: requestObject)
return .uploadMultipart(formData)
그리고 이렇게 변경할 수 있었당.
프로필 세팅이 완료됐을 때는 자동 로그인 여부를 설정해 줬다.
자동 로그인 로직은 끗!
다른 건 저번 포스팅 내용들과 비슷해서 생략하겠다.
오늘의 일지 끗