Skip to content

Commit

Permalink
Merge pull request #18 from f-lab-edu/feature/UploadDomainData
Browse files Browse the repository at this point in the history
레시피 업로드 Domain, Data 영역을 정의해 보았습니다.
  • Loading branch information
GeonH0 authored Jul 24, 2024
2 parents 6632383 + f527923 commit 890f83d
Show file tree
Hide file tree
Showing 10 changed files with 1,505 additions and 1,056 deletions.
2,113 changes: 1,057 additions & 1,056 deletions HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// RecipeUploadDTO.swift
// HomeCafeRecipes
//
// Created by 김건호 on 7/1/24.
//

import Foundation

struct RecipeUploadDTO {
let userID: Int
let recipeType: String
let recipeName: String
let recipeDescription: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// RecipeUploadResponseDTO.swift
// HomeCafeRecipes
//
// Created by 김건호 on 7/2/24.
//

import Foundation

struct RecipeUploadResponseDTO: Decodable {

let ID: Int
let type: String
let name: String
let description: String
let likesCount: Int
let createdAt: String
let writer: UserDTO
let imageUrls: [RecipeImageDTO]
let isLikedByCurrentUser: Bool

enum CodingKeys: String, CodingKey {
case ID = "recipeId"
case type = "recipeType"
case name = "recipeName"
case description = "recipeDescription"
case likesCount = "recipeLikesCnt"
case createdAt = "createdAt"
case writer = "writer"
case imageUrls = "recipeImgUrls"
case isLikedByCurrentUser = "isLiked"
}

}

extension RecipeUploadResponseDTO {
func toDomain() -> Recipe {
return Recipe(
id: ID,
type: RecipeType(rawValue: type) ?? .coffee,
name: name,
description: description,
writer: writer.toDomain(),
imageUrls: imageUrls.map { $0.recipeImageUrl },
isLikedByCurrentUser: isLikedByCurrentUser,
likeCount: likesCount,
createdAt: DateFormatter.iso8601.date(from: createdAt) ?? Date()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// MultipartFormDataRequest.swift
// HomeCafeRecipes
//
// Created by 김건호 on 7/16/24.
//

import Foundation

struct MultipartFormDataRequest {
private let boundary: String = UUID().uuidString
private var httpBody = NSMutableData()
let url: URL

init(url: URL) {
self.url = url
}

mutating func addTextField(named name: String, value: String) {
httpBody.append(textFormField(named: name, value: value).data(using: .utf8)!)
}

private func textFormField(named name: String, value: String) -> String {
var fieldString = "--\(boundary)\r\n"
fieldString += "Content-Disposition: form-data; name=\"\(name)\"\r\n"
fieldString += "\r\n"
fieldString += "\(value)\r\n"

return fieldString
}

mutating func addDataField(named name: String, data: Data, filename: String, mimeType: String) {
httpBody.append(dataFormField(named: name, data: data, filename: filename, mimeType: mimeType))
}

private func dataFormField(named name: String, data: Data, filename: String, mimeType: String) -> Data {
let fieldData = NSMutableData()

fieldData.append("--\(boundary)\r\n".data(using: .utf8)!)
fieldData.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
fieldData.append("Content-Type: \(mimeType)\r\n".data(using: .utf8)!)
fieldData.append("\r\n".data(using: .utf8)!)
fieldData.append(data)
fieldData.append("\r\n".data(using: .utf8)!)

return fieldData as Data
}

mutating func finalize() {
httpBody.append("--\(boundary)--\r\n".data(using: .utf8)!)
}

func asURLRequest() -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = httpBody as Data
return request
}
}
59 changes: 59 additions & 0 deletions HomeCafeRecipes/HomeCafeRecipes/Data/Network/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
//

import Foundation
import UIKit

import RxSwift

protocol NetworkService {
func getRequest<T: Decodable>(url: URL, responseType: T.Type) -> Single<T>
func postRequest<T: Decodable>(url: URL, parameters: [String: Any], imageDatas: [Data], responseType: T.Type) -> Single<T>
}

class BaseNetworkService: NetworkService {
Expand Down Expand Up @@ -39,4 +42,60 @@ class BaseNetworkService: NetworkService {
}
}
}

func postRequest<T: Decodable>(
url: URL, parameters: [String: Any],
imageDatas: [Data],
responseType: T.Type
) -> Single<T> {
return Single.create { single in
var formDataRequest = MultipartFormDataRequest(url: url)

for (key, value) in parameters {
formDataRequest.addTextField(named: key, value: String(describing: value))
}

for (index, imageData) in imageDatas.enumerated() {
let filename = "image\(index).jpg"
formDataRequest.addDataField(
named: "recipeImgUrls",
data: imageData,
filename: filename,
mimeType: "image/jpeg"
)
}

formDataRequest.finalize()
let request = formDataRequest.asURLRequest()

let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
single(.failure(error))
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
let statusCode = httpResponse.statusCode
let responseString = data.flatMap { String(data: $0, encoding: .utf8) } ?? "No response data"
let error = NSError(
domain: "",
code: statusCode,
userInfo: [NSLocalizedDescriptionKey: "HTTP \(statusCode): \(responseString)"]
)
single(.failure(error))
} else if let data = data {
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let responseObject = try decoder.decode(T.self, from: data)
single(.success(responseObject))
} catch let decodingError {
single(.failure(decodingError))
}
}
}
task.resume()

return Disposables.create {
task.cancel()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// RecipePostService.swift
// HomeCafeRecipes
//
// Created by 김건호 on 7/1/24.
//

import Foundation
import UIKit

import RxSwift

protocol RecipePostService {
func postRecipe(recipe: RecipeUploadDTO, images: [UIImage]) -> Single<Recipe>
}

class RecipePostServiceImpl: RecipePostService {
private let networkService: NetworkService

init(networkService: NetworkService) {
self.networkService = networkService
}

private func makeURL(endpoint: String) -> URL {
return APIConfig().baseURL.appendingPathComponent(endpoint)
}

func postRecipe(recipe: RecipeUploadDTO, images: [UIImage]) -> Single<Recipe> {
let url = makeURL(endpoint: "recipes")
let parameters: [String: Any] = [
"userId": recipe.userID,
"recipeType": recipe.recipeType,
"recipeName": recipe.recipeName,
"recipeDescription": recipe.recipeDescription
]

let imageDatas = images.compactMap { $0.jpegData(compressionQuality: 0.5) }

return networkService.postRequest(
url: url,
parameters: parameters,
imageDatas: imageDatas,
responseType: NetworkResponseDTO<RecipeUploadResponseDTO>.self
)
.map { $0.data.toDomain() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// AddRecipeRepository.swift
// HomeCafeRecipes
//
// Created by 김건호 on 7/1/24.
//

import Foundation
import UIKit

import RxSwift

protocol AddRecipeRepository {
func saveRecipe(
userID: Int,
recipeType: String,
title: String,
description: String,
images: [UIImage]
) -> Single<Recipe>
}

class AddRecipeRepositoryImpl: AddRecipeRepository {
private let recipePostService: RecipePostService

init(recipePostService: RecipePostService) {
self.recipePostService = recipePostService
}

func saveRecipe(
userID: Int,
recipeType: String,
title: String,
description: String,
images: [UIImage]
) -> Single<Recipe> {
let recipeUploadDTO = RecipeUploadDTO(
userID: userID,
recipeType: recipeType,
recipeName: title,
recipeDescription: description
)
return recipePostService.postRecipe(recipe: recipeUploadDTO, images: images)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// AddRecipeError.swift
// HomeCafeRecipes
//
// Created by 김건호 on 7/17/24.
//

import Foundation

enum AddRecipeError: Error {
case noImages
case titleIsEmpty
case descriptionTooShort
case genericError(Error)
}

extension AddRecipeError: LocalizedError {

var title: String {
switch self {
case .noImages:
return "이미지 없음"
case .titleIsEmpty:
return "제목 없음"
case .descriptionTooShort:
return "설명 부족"
case .genericError:
return "업로드 실패"
}
}

var errorDescription: String? {
switch self {
case .noImages:
return "최소 한 장의 이미지를 추가해 주세요."
case .titleIsEmpty:
return "제목을 입력해 주세요."
case .descriptionTooShort:
return "설명을 10자 이상 입력해 주세요."
case .genericError(let error):
return error.localizedDescription
}
}
}
Loading

0 comments on commit 890f83d

Please sign in to comment.