Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol {
DIContainer.shared.register(type: RecommendedRoutineRepositoryProtocol.self) { _ in
return RecommendedRoutineRepository()
}

DIContainer.shared.register(type: EmotionRepositoryProtocol.self) { _ in
return EmotionRepository()
}
}
}
30 changes: 30 additions & 0 deletions Projects/DataSource/Sources/DTO/EmotionResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// EmotionResponseDTO.swift
// DataSource
//
// Created by 최정인 on 7/29/25.
//

import Domain
import Foundation

struct EmotionResponseDTO: Decodable {
let type: String
let name: String
let imageUrl: String

enum CodingKeys: String, CodingKey {
case type = "emotionMarbleType"
case name = "emotionMarbleName"
case imageUrl
}
}

extension EmotionResponseDTO {
func toEmotionEntity() -> EmotionEntity {
return EmotionEntity(
emotionType: type,
emotionName: name,
emotionImageUrl: URL(string: imageUrl))
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

URL 생성 시 안전성 확보 필요

URL(string:)은 유효하지 않은 URL 문자열의 경우 nil을 반환할 수 있습니다. API 응답의 imageUrl이 항상 유효한 URL 형식임을 보장할 수 없다면, 안전한 처리가 필요합니다.

EmotionEntityemotionImageUrl이 옵셔널 타입이라면 현재 코드가 적절합니다. 그렇지 않다면 기본값 처리나 에러 처리를 고려해주세요.

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/DTO/EmotionResponseDTO.swift at line 28, the
URL(string: imageUrl) initializer can return nil if imageUrl is invalid. Check
if emotionImageUrl in EmotionEntity is optional; if not, modify the code to
safely unwrap the URL or provide a default URL or error handling to avoid
runtime crashes due to nil values.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞는 말씀이지만, 서버님들이 유효한 URL을 내려주실거라 믿어요~
어차피 유효한 URL이 아니면 기본 이미지를 표시하겟죠???????

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마주아요 !!! 유효한 URL을 내려주실거지만 ~ 없다면 default 구슬 (empty 구슬을 보여드리겠슴니다 !!!)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@taipaise 네, 말씀하신 대로 EmotionEntityemotionImageUrl이 옵셔널 타입(URL?)이라서 현재 구현이 안전합니다!

그리고 Kingfisher를 사용하고 계시니 nil이나 유효하지 않은 URL의 경우 자동으로 placeholder나 기본 이미지를 표시해줄 거예요. 서버 팀을 믿고 가시는 것도 합리적인 접근 방식이네요 👍

   /|   /|  
  ( :v:  ) 
   |(_)|
  /     \
 /       \
/_________\

✏️ Learnings added
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#29
File: Projects/DataSource/Sources/DTO/EmotionResponseDTO.swift:28-28
Timestamp: 2025-07-30T13:04:33.867Z
Learning: 사용자 taipaise는 서버에서 제공하는 데이터의 품질을 신뢰하는 편이며, URL 검증보다는 서버 팀의 데이터 검증을 믿는 접근 방식을 선호한다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}
}
53 changes: 53 additions & 0 deletions Projects/DataSource/Sources/Endpoint/EmotionEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// EmotionEndpoint.swift
// DataSource
//
// Created by 최정인 on 7/28/25.
//

enum EmotionEndpoint {
case fetchEmotions
case registerEmotion(emotion: String)
}

extension EmotionEndpoint: Endpoint {
var baseURL: String {
return AppProperties.baseURL + "/api/v1/emotion-marbles"
}

var path: String {
return baseURL
}

var method: HTTPMethod {
switch self {
case .fetchEmotions: .get
case .registerEmotion: .post
}
}

var headers: [String : String] {
let headers: [String: String] = [
"Content-Type": "application/json",
"accept": "*/*"
]
return headers
}

var queryParameters: [String : String] {
return [:]
}

var bodyParameters: [String : Any] {
switch self {
case .fetchEmotions:
return [:]
case .registerEmotion(let emotion):
return ["emotionMarbleType": emotion]
}
}

var isAuthorized: Bool {
return true
}
}
30 changes: 30 additions & 0 deletions Projects/DataSource/Sources/Repository/EmotionRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// EmotionRepository.swift
// DataSource
//
// Created by 최정인 on 7/28/25.
//

import Domain

final class EmotionRepository: EmotionRepositoryProtocol {
private let networkService = NetworkService.shared

func fetchEmotions() async throws -> [EmotionEntity] {
let endpoint = EmotionEndpoint.fetchEmotions
guard let response = try await networkService.request(endpoint: endpoint, type: [EmotionResponseDTO].self)
else { return [] }

let emotionEntities = response.compactMap({ $0.toEmotionEntity() })
return emotionEntities
}
Comment on lines +14 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

에러 처리 개선 필요

두 메서드 모두 네트워크 요청 실패 시 빈 배열을 반환하고 있습니다. 이는 실제로 빈 결과와 에러를 구분하기 어렵게 만듭니다.

호출하는 측에서 에러를 적절히 처리할 수 있도록 에러를 throw하는 것이 좋습니다:

-guard let response = try await networkService.request(endpoint: endpoint, type: [EmotionResponseDTO].self)
-else { return [] }
+let response = try await networkService.request(endpoint: endpoint, type: [EmotionResponseDTO].self)

Also applies to: 22-29

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/EmotionRepository.swift around lines
14 to 20 and 22 to 29, the current implementation returns an empty array on
network request failure, which makes it impossible to distinguish between an
actual empty result and an error. Modify both methods to throw errors instead of
returning empty arrays when the network request fails. Update the function
signatures to include throws and propagate the caught errors so that callers can
handle them appropriately.


func registerEmotion(emotion: String) async throws -> [RecommendedRoutineEntity] {
let endpoint = EmotionEndpoint.registerEmotion(emotion: emotion)
guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self)
else { return [] }

let recommendedRoutineEntity = response.recommendedRoutines.compactMap({ $0.toRecommendedRoutineEntity() })
return recommendedRoutineEntity
}
}
21 changes: 14 additions & 7 deletions Projects/Domain/Sources/DomainDependencyAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,25 @@ public struct DomainDependencyAssembler: DependencyAssemblerProtocol {
return WithdrawUseCase(authRepository: authRepository)
}

DIContainer.shared.register(type: OnboardingUseCaseProtocol.self) { container in
guard let onboardingRepository = container.resolve(type: OnboardingRepositoryProtocol.self)
else { fatalError("onboardingRepository 의존성이 등록되지 않았습니다.") }

return OnboardingUseCase(onboardingRepository: onboardingRepository)
}

DIContainer.shared.register(type: RecommendedRoutineUseCaseProtocol.self) { container in
guard let recommendedRoutineRepository = container.resolve(type: RecommendedRoutineRepositoryProtocol.self)
else { fatalError("recommendedRoutineRepository 의존성이 등록되지 않았습니다.") }

return RecommendedRoutineUseCase(recommendedRoutineRepository: recommendedRoutineRepository)
}

guard let emotionRepository = DIContainer.shared.resolve(type: EmotionRepositoryProtocol.self)
else { fatalError("emotionRepository 의존성이 등록되지 않았습니다.") }

DIContainer.shared.register(type: EmotionUseCaseProtocol.self) { _ in
return EmotionUseCase(emotionRepository: emotionRepository)
}

DIContainer.shared.register(type: ResultRecommendedRoutineUseCaseProtocol.self) { container in
guard let onboardingRepository = container.resolve(type: OnboardingRepositoryProtocol.self)
else { fatalError("onboardingRepository 의존성이 등록되지 않았습니다.") }

return ResultRecommendedRoutineUseCase(onboardingRepository: onboardingRepository, emotionRepository: emotionRepository)
}
}
}
24 changes: 24 additions & 0 deletions Projects/Domain/Sources/Entity/EmotionEntity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// EmotionEntity.swift
// Domain
//
// Created by 최정인 on 7/29/25.
//

import Foundation

public struct EmotionEntity {
public let emotionType: String
public let emotionName: String
public let emotionImageUrl: URL?

public init(
emotionType: String,
emotionName: String,
emotionImageUrl: URL?
) {
self.emotionType = emotionType
self.emotionName = emotionName
self.emotionImageUrl = emotionImageUrl
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// EmotionRepositoryProtocol.swift
// Domain
//
// Created by 최정인 on 7/28/25.
//

/// 감정 구슬에 대한 로직을 처리하는 Repository
public protocol EmotionRepositoryProtocol {
/// 감정 구슬 목록을 불러옵니다.
/// - Returns: 조회된 감정 구슬 목록
func fetchEmotions() async throws -> [EmotionEntity]

/// 감정 구슬을 등록합니다.
/// - Parameter emotion: 감정 구슬 String 값
/// - Returns: 등록한 감정 구슬에 따른 추천 루틴 리스트
func registerEmotion(emotion: String) async throws -> [RecommendedRoutineEntity]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// EmotionUseCaseProtocol.swift
// Domain
//
// Created by 최정인 on 7/28/25.
//

public protocol EmotionUseCaseProtocol {
/// 감정 구슬 목록을 불러옵니다.
/// - Returns: 조회된 감정 구슬 목록
func fetchEmotions() async throws -> [EmotionEntity]
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ResultRecommendedRoutineUseCaseProtocol.swift
// Domain
//
// Created by 최정인 on 7/29/25.
//

import Foundation

public protocol ResultRecommendedRoutineUseCaseProtocol {
/// 선택한 온보딩 결과를 저장하고, 추천 루틴을 받습니다.
/// - Parameter onboardingChoices: 선택한 온보딩 항목 list
/// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록
func fetchResultRecommendedRoutines(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity]

/// 감정 구슬을 등록하고 그에 따른 추천 루틴 리스트를 받습니다.
/// - Parameter emotion: 감정 구슬 타입 String
/// - Returns: 등록한 감정 구슬에 따른 추천 루틴 리스트
func fetchResultRecommendedRoutines(emotion: String) async throws -> [RecommendedRoutineEntity]

/// 선택한 추천 루틴을 등록합니다.
/// - Parameter selectedRoutines: 선택한 추천 루틴 ID 목록
func registerRecommendedRoutines(selectedRoutines: [Int]) async throws
}
19 changes: 19 additions & 0 deletions Projects/Domain/Sources/UseCase/Emotion/EmotionUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// EmotionUseCase.swift
// Domain
//
// Created by 최정인 on 7/28/25.
//

public final class EmotionUseCase: EmotionUseCaseProtocol {
private let emotionRepository: EmotionRepositoryProtocol

public init(emotionRepository: EmotionRepositoryProtocol) {
self.emotionRepository = emotionRepository
}

public func fetchEmotions() async throws -> [EmotionEntity] {
let emotions = try await emotionRepository.fetchEmotions()
return emotions
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
//
// OnboardingUseCase.swift
// ResultRecommendedRoutineUseCase.swift
// Domain
//
// Created by 최정인 on 7/15/25.
// Created by 최정인 on 7/29/25.
//

public final class OnboardingUseCase: OnboardingUseCaseProtocol {

public final class ResultRecommendedRoutineUseCase: ResultRecommendedRoutineUseCaseProtocol {
private let onboardingRepository: OnboardingRepositoryProtocol
private let emotionRepository: EmotionRepositoryProtocol

public init(onboardingRepository: OnboardingRepositoryProtocol) {
public init(onboardingRepository: OnboardingRepositoryProtocol, emotionRepository: EmotionRepositoryProtocol) {
self.onboardingRepository = onboardingRepository
self.emotionRepository = emotionRepository
}

public func registerOnboarding(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity] {
public func fetchResultRecommendedRoutines(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity] {
let choices = convertToDictionary(onboardingChoices: onboardingChoices)
let recommendedRoutines = try await onboardingRepository.registerOnboarding(onboardingChoices: choices)
return recommendedRoutines
}

public func fetchResultRecommendedRoutines(emotion: String) async throws -> [RecommendedRoutineEntity] {
let recommendedRoutines = try await emotionRepository.registerEmotion(emotion: emotion)
return recommendedRoutines
}

public func registerRecommendedRoutines(selectedRoutines: [Int]) async throws {
try await onboardingRepository.registerRecommendedRoutines(selectedRoutines: selectedRoutines)
}
Expand Down
1 change: 1 addition & 0 deletions Projects/Presentation/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ let project = Project(
resources: ["Resources/**"],
dependencies: [
.external(name: "SnapKit"),
.external(name: "Kingfisher"),
.project(target: "Domain", path: "../Domain"),
.project(target: "Shared", path: "../Shared")
]
Expand Down

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading