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
10 changes: 9 additions & 1 deletion FoodDiary/App/Sources/AppFlowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,18 @@ extension AppFlowController {
return vc
}

typealias InsightVM = InsightViewModel<
InsightRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>
>

guard let insightVM = try? container.resolve(InsightVM.self) else {
fatalError("InsightViewModel not registered")
}
Comment on lines +197 to +199
Copy link
Member

Choose a reason for hiding this comment

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

이번에 고도화하면서 VM을 직접 DI에 등록하는거에 대해서 고민을 좀 해봐야 할 듯??

Copy link
Member Author

Choose a reason for hiding this comment

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

이거 맥락이 기억이 안나는데 VM을 머 따로 관리하기로 했었나..?

Copy link
Member

Choose a reason for hiding this comment

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

ViewModel도 .transient 로 해서 괜찮을 거 같긴 하네 큰 문제 생기진 않을 거 같으니까 직접 주입하는 방법으로 일단 가죠!


let tabBarVC = RootTabBarController(
weeklyVC: weeklyCalendarVC,
monthlyVC: monthlyCalendarVC,
insightVC: InsightViewController(),
insightVC: InsightViewController(viewModel: insightVM),
myPageViewControllerFactory: myPageVCFactory
)

Expand Down
37 changes: 37 additions & 0 deletions FoodDiary/App/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ extension SceneDelegate {
return UserRepositoryImpl(httpClient: client, tokenStorage: storage)
}

container.register(InsightRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>.self) { resolver in
guard let client = resolver.resolve(HTTPClient.self),
let storage = resolver.resolve(AuthTokenStorage<KeychainService>.self) else {
fatalError("InsightRepositoryImpl dependencies not registered")
}
return InsightRepositoryImpl(httpClient: client, tokenStorage: storage)
}

container.register(NicknameStoring.self) { _ in
NicknameStorage()
}
Expand Down Expand Up @@ -378,6 +386,19 @@ extension SceneDelegate {
return WithdrawUserUseCase(authRepository: authRepository)
}

container.register(
FetchInsightUseCase<
InsightRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>
>.self
) { resolver in
guard let repository = resolver.resolve(
InsightRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>.self
) else {
fatalError("FetchInsightUseCase dependencies not registered")
}
return FetchInsightUseCase(repository: repository)
}

container.register(
FetchUserProfileUseCase<
UserRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>,
Expand Down Expand Up @@ -435,6 +456,22 @@ extension SceneDelegate {
return LoginViewModel(finalizeAppleLoginUseCase: useCase)
}

typealias InsightVM = InsightViewModel<
InsightRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>
>

container.register(InsightVM.self, scope: .transient) { resolver in
guard let fetchInsightUseCase = resolver.resolve(
FetchInsightUseCase<
InsightRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>
>.self
) else {
fatalError("FetchInsightUseCase not registered")
}

return InsightViewModel(fetchInsightUseCase: fetchInsightUseCase)
}

// WeeklyCalendarViewModel 타입 별칭
typealias WeeklyVM = WeeklyCalendarViewModel<
FoodRecordRepositoryImpl<HTTPClient, AuthTokenStorage<KeychainService>>,
Expand Down
122 changes: 122 additions & 0 deletions FoodDiary/Data/Sources/DTO/InsightResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// InsightResponseDTO.swift
// Data
//

import Domain
import Foundation

struct InsightResponseDTO: Decodable {
let month: String
let photoStats: PhotoStatsDTO
let categoryStats: CategoryStatsDTO
let topMenu: TopMenuDTO
let diaryTimeStats: DiaryTimeStatsDTO
let keywords: [String]

enum CodingKeys: String, CodingKey {
case month
case photoStats = "photo_stats"
case categoryStats = "category_stats"
case topMenu = "top_menu"
case diaryTimeStats = "diary_time_stats"
case keywords
}

func toInsight() -> Insight {
Insight(
month: month,
photoStats: photoStats.toEntity(),
categoryStats: categoryStats.toEntity(),
topMenu: topMenu.toEntity(),
diaryTimeStats: diaryTimeStats.toEntity(),
keywords: keywords
)
}
}

struct PhotoStatsDTO: Decodable {
let currentMonthCount: Int
let previousMonthCount: Int
let changeRate: Double

enum CodingKeys: String, CodingKey {
case currentMonthCount = "current_month_count"
case previousMonthCount = "previous_month_count"
case changeRate = "change_rate"
}

func toEntity() -> PhotoStats {
PhotoStats(
currentMonthCount: currentMonthCount,
previousMonthCount: previousMonthCount,
changeRate: changeRate
)
}
}

struct CategoryStatsDTO: Decodable {
let currentMonth: CategoryStatDTO
let previousMonth: CategoryStatDTO

enum CodingKeys: String, CodingKey {
case currentMonth = "current_month"
case previousMonth = "previous_month"
}

func toEntity() -> CategoryStats {
CategoryStats(
currentMonth: currentMonth.toEntity(),
previousMonth: previousMonth.toEntity()
)
}
}

struct CategoryStatDTO: Decodable {
let topCategory: String
let count: Int

enum CodingKeys: String, CodingKey {
case topCategory = "top_category"
case count
}

func toEntity() -> CategoryStat {
CategoryStat(topCategory: topCategory, count: count)
}
}

struct TopMenuDTO: Decodable {
let name: String
let count: Int

func toEntity() -> TopMenu {
TopMenu(name: name, count: count)
}
}

struct DiaryTimeStatsDTO: Decodable {
let mostActiveHour: Int
let distribution: [HourCountDTO]

enum CodingKeys: String, CodingKey {
case mostActiveHour = "most_active_hour"
case distribution
}

func toEntity() -> DiaryTimeStats {
DiaryTimeStats(
mostActiveHour: mostActiveHour,
distribution: distribution.map { $0.toEntity() }
)
}
}

struct HourCountDTO: Decodable {
let hour: Int
let count: Int

func toEntity() -> HourCount {
HourCount(hour: hour, count: count)
}
}
55 changes: 55 additions & 0 deletions FoodDiary/Data/Sources/Network/Endpoints/InsightEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// InsightEndpoint.swift
// Data
//

import Foundation

public enum InsightEndpoint {
case fetch
}

extension InsightEndpoint: Requestable {
public var baseURL: String {
guard let url = Bundle.main.infoDictionary?["BASE_URL"] as? String else {
fatalError("BASE_URL이 Info.plist에 설정되지 않았습니다.")
}

return url
}

public var path: String {
switch self {
case .fetch:
"/me/insights"
}
}

public var httpMethod: HTTPMethod {
switch self {
case .fetch:
.get
}
}

public var queryParameters: Encodable? {
switch self {
case .fetch:
nil
}
}

public var bodyParameters: HTTPBody {
switch self {
case .fetch:
.none
}
}

public var headers: [String: String] {
switch self {
case .fetch:
[:]
}
}
}
29 changes: 29 additions & 0 deletions FoodDiary/Data/Sources/Repository/InsightRepositoryImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// InsightRepositoryImpl.swift
// Data
//

import Domain
import Foundation

public struct InsightRepositoryImpl<Client: HTTPClienting, Storage: AuthTokenStoring>: InsightRepository {
private let httpClient: Client
private let tokenStorage: Storage

public init(httpClient: Client, tokenStorage: Storage) {
self.httpClient = httpClient
self.tokenStorage = tokenStorage
}

public func fetchInsight() async throws -> Insight {
do {
let response: InsightResponseDTO = try await httpClient.request(
InsightEndpoint.fetch,
accessToken: tokenStorage.get()
)
return response.toInsight()
} catch NetworkError.httpError(statusCode: 400, _) {
throw InsightError.insufficientData
}
}
}
10 changes: 10 additions & 0 deletions FoodDiary/Domain/Sources/Core/Error/InsightError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// InsightError.swift
// Domain
//

import Foundation

public enum InsightError: Error {
case insufficientData
}
93 changes: 93 additions & 0 deletions FoodDiary/Domain/Sources/Entity/Insight.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// Insight.swift
// Domain
//

import Foundation

public struct Insight: Equatable, Sendable {
public let month: String
public let photoStats: PhotoStats
public let categoryStats: CategoryStats
public let topMenu: TopMenu
public let diaryTimeStats: DiaryTimeStats
public let keywords: [String]

public init(
month: String,
photoStats: PhotoStats,
categoryStats: CategoryStats,
topMenu: TopMenu,
diaryTimeStats: DiaryTimeStats,
keywords: [String]
) {
self.month = month
self.photoStats = photoStats
self.categoryStats = categoryStats
self.topMenu = topMenu
self.diaryTimeStats = diaryTimeStats
self.keywords = keywords
}
}

public struct PhotoStats: Equatable, Sendable {
public let currentMonthCount: Int
public let previousMonthCount: Int
public let changeRate: Double

public init(currentMonthCount: Int, previousMonthCount: Int, changeRate: Double) {
self.currentMonthCount = currentMonthCount
self.previousMonthCount = previousMonthCount
self.changeRate = changeRate
}
}

public struct CategoryStats: Equatable, Sendable {
public let currentMonth: CategoryStat
public let previousMonth: CategoryStat

public init(currentMonth: CategoryStat, previousMonth: CategoryStat) {
self.currentMonth = currentMonth
self.previousMonth = previousMonth
}
}

public struct CategoryStat: Equatable, Sendable {
public let topCategory: String
public let count: Int

public init(topCategory: String, count: Int) {
self.topCategory = topCategory
self.count = count
}
}

public struct TopMenu: Equatable, Sendable {
public let name: String
public let count: Int

public init(name: String, count: Int) {
self.name = name
self.count = count
}
}

public struct DiaryTimeStats: Equatable, Sendable {
public let mostActiveHour: Int
public let distribution: [HourCount]

public init(mostActiveHour: Int, distribution: [HourCount]) {
self.mostActiveHour = mostActiveHour
self.distribution = distribution
}
}

public struct HourCount: Equatable, Sendable {
public let hour: Int
public let count: Int

public init(hour: Int, count: Int) {
self.hour = hour
self.count = count
}
}
Loading
Loading