From 2b67d06cd13ca7ed158d69a6e4a1d1e01d40638c Mon Sep 17 00:00:00 2001 From: kangddong Date: Wed, 14 Jan 2026 23:38:26 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20API=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Data/DTO/BoardResponse.swift | 6 ++-- .../Sources/Data/DTO/CommentResponse.swift | 2 +- Community/Sources/Data/DTO/Querys.swift | 9 +----- Community/Sources/Domain/Entities/Board.swift | 4 +-- .../Community/CommunityView.swift | 30 +++---------------- .../Request/CursorPagingQuery.swift | 8 ++--- 6 files changed, 15 insertions(+), 44 deletions(-) diff --git a/Community/Sources/Data/DTO/BoardResponse.swift b/Community/Sources/Data/DTO/BoardResponse.swift index a18c290..b8577d6 100644 --- a/Community/Sources/Data/DTO/BoardResponse.swift +++ b/Community/Sources/Data/DTO/BoardResponse.swift @@ -16,8 +16,8 @@ public struct BoardResponseDTO: Decodable, Sendable { public let content: String public let category: String public let imageUrls: [String] - public let authorNickname: String? - public let authorId: Int? + public let authorNickname: String + public let authorId: Int public let createdAt: String public let updatedAt: String public let viewCount: Int @@ -47,7 +47,7 @@ public struct BoardListDataDTO: Decodable, Sendable { private enum CodingKeys: String, CodingKey { case content - case nextCursorId = "netCursorId" + case nextCursorId case nextPage } } diff --git a/Community/Sources/Data/DTO/CommentResponse.swift b/Community/Sources/Data/DTO/CommentResponse.swift index 73f2421..920ea45 100644 --- a/Community/Sources/Data/DTO/CommentResponse.swift +++ b/Community/Sources/Data/DTO/CommentResponse.swift @@ -38,7 +38,7 @@ public struct CommentListDataDTO: Decodable, Sendable { private enum CodingKeys: String, CodingKey { case content - case nextCursorId = "netCursorId" + case nextCursorId case nextPage } } diff --git a/Community/Sources/Data/DTO/Querys.swift b/Community/Sources/Data/DTO/Querys.swift index 5863f29..84d4de2 100644 --- a/Community/Sources/Data/DTO/Querys.swift +++ b/Community/Sources/Data/DTO/Querys.swift @@ -6,14 +6,7 @@ // import Foundation - - -/// 재사용 되는 페이지네이션 쿼리 -public struct CursorPagingQuery: Encodable, Sendable { - let lastId: Int? - let limit: Int - let order: String -} +import NetworkInterface /// CategoryPagingQuery public struct CategoryPagingQuery: Encodable, Sendable { diff --git a/Community/Sources/Domain/Entities/Board.swift b/Community/Sources/Domain/Entities/Board.swift index e53f3bb..9f7e17c 100644 --- a/Community/Sources/Domain/Entities/Board.swift +++ b/Community/Sources/Domain/Entities/Board.swift @@ -38,7 +38,7 @@ public struct Board: Identifiable, Equatable, Sendable { public let content: String public let category: BoardCategory public let imageUrls: [String] - public let authorNickname: String? + public let authorNickname: String public let authorProfileImageUrl: String? public let authorId: Int? public let createdAt: String @@ -54,7 +54,7 @@ public struct Board: Identifiable, Equatable, Sendable { content: String, category: BoardCategory, imageUrls: [String], - authorNickname: String?, + authorNickname: String, authorProfileImageUrl: String? = nil, authorId: Int?, createdAt: Date, diff --git a/Community/Sources/Presentation/Community/CommunityView.swift b/Community/Sources/Presentation/Community/CommunityView.swift index 900d888..e38f542 100644 --- a/Community/Sources/Presentation/Community/CommunityView.swift +++ b/Community/Sources/Presentation/Community/CommunityView.swift @@ -50,9 +50,9 @@ public struct CommunityView: View { VStack(spacing: 0) { // 헤더 - CommunityHeader() - .padding(.horizontal, 16) - .padding(.bottom, 20) + HeaderBar(type: .community) + .safeAreaPadding(.vertical, 18) + .safeAreaPadding(.horizontal, 15) VStack(spacing: 0) { // 카테고리 필터와 뷰 토글 @@ -122,33 +122,11 @@ public struct CommunityView: View { .refreshable { viewModel.refreshBoards() } - .navigationBarHidden(true) + .toolbar(.hidden, for: .navigationBar) .tabBarHidden(false) } } -// MARK: - Category Header -struct CommunityHeader: View { - var body: some View { - HStack { - Text("커뮤니티") - .pretendard(.title(.t2)) - .foregroundColor(.white) - - Spacer() - - Button(action: { - print("Notification tapped") - }) { - Image(.naviBell) - .resizable() - .foregroundColor(.white) - .frame(width: 20, height: 20) - } - } - } -} - // MARK: - Category Filter fileprivate struct CategoryFilterListView: View { let categories: [CommunityViewModel.Category] diff --git a/Infrastructure/Sources/NetworkInterface/Request/CursorPagingQuery.swift b/Infrastructure/Sources/NetworkInterface/Request/CursorPagingQuery.swift index 9ef1e58..ad57e9b 100644 --- a/Infrastructure/Sources/NetworkInterface/Request/CursorPagingQuery.swift +++ b/Infrastructure/Sources/NetworkInterface/Request/CursorPagingQuery.swift @@ -10,12 +10,12 @@ public struct CursorPagingQuery: Encodable, Sendable { public let lastId: Int? public let limit: Int - public let order: String + public let order: String? public init( - lastId: Int?, - limit: Int, - order: String + lastId: Int? = nil, + limit: Int = 20, + order: String? = nil ) { self.lastId = lastId self.limit = limit From 4ed82da1601c2c7de35059d492d6695124d78a4d Mon Sep 17 00:00:00 2001 From: kangddong Date: Sat, 17 Jan 2026 21:47:13 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(refactor):=20HeaderBar=EC=97=90=20?= =?UTF-8?q?=EC=A0=9C=EB=84=88=EB=A6=AD=20=ED=83=80=EC=9E=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Common/Sources/DesignSystem/HeaderBar.swift | 38 +++++++++++++++------ 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Common/Sources/DesignSystem/HeaderBar.swift b/Common/Sources/DesignSystem/HeaderBar.swift index 29643ec..7bb1be0 100644 --- a/Common/Sources/DesignSystem/HeaderBar.swift +++ b/Common/Sources/DesignSystem/HeaderBar.swift @@ -22,15 +22,33 @@ public enum HeaderType { return "마이페이지" } } + + var hasNotification: Bool { + switch self { + case .home, .community: return true + case .myPage: return false + } + } } // 상단 헤더 - 로고, 알림 -public struct HeaderBar: View { +public struct HeaderBar: View { private let fontStyle: FontStyle private let type: HeaderType + private let destination: Destination? - public init(type: HeaderType) { + public init( + type: HeaderType, + @ViewBuilder destination: () -> Destination + ) where Destination: View { fontStyle = .init(.custom("GeekbleMalang2"), size: 24.0) self.type = type + self.destination = destination() + } + + public init(type: HeaderType) where Destination == EmptyView { + fontStyle = .init(.custom("GeekbleMalang2"), size: 24.0) + self.type = type + self.destination = nil } @ViewBuilder @@ -50,7 +68,6 @@ public struct HeaderBar: View { case .community, .myPage: Text(type.displayText) -// .padding(.bottom, 15) .pretendard(.title(.t2)) .foregroundColor(type == .community ? .white : Color.textG900) } @@ -58,11 +75,9 @@ public struct HeaderBar: View { @ViewBuilder var notificationSection: some View { - switch type { - case .home, .community: + if let destination { NavigationLink { - AlarmListView() - Text("asd") + destination } label: { Image("notification") .resizable() @@ -70,10 +85,9 @@ public struct HeaderBar: View { .frame(width: 30, height: 30) .foregroundStyle(type == .home ? .black : .white) } - case .myPage: - EmptyView() } } + public var body: some View { HStack { titleSection @@ -84,5 +98,9 @@ public struct HeaderBar: View { } #Preview { - HeaderBar(type: .home) + HeaderBar(type: .home) { + AnyView(Text("Destination")) + } + + HeaderBar(type: .myPage) } From e2506f1ea8e4801fa1e7b5a6a5581ef1ff3b516a Mon Sep 17 00:00:00 2001 From: kangddong Date: Sat, 17 Jan 2026 21:48:12 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Alarm/.gitignore | 8 ++ Alarm/Package.swift | 117 ++++++++++++++++++ Alarm/Sources/DI/AlarmDIContainer.swift | 61 +++++++++ Alarm/Sources/Data/AlarmAPI.swift | 56 +++++++++ Alarm/Sources/Data/AlarmRepositoryImpl.swift | 28 +++++ Alarm/Sources/Data/NotificationDTO.swift | 81 ++++++++++++ Alarm/Sources/Domain/AlarmEntities.swift | 56 +++++++++ Alarm/Sources/Domain/AlarmRepository.swift | 12 ++ .../Sources/Domain/GetAlarmListUseCase.swift | 24 ++++ .../Sources/Presentation/AlarmListView.swift | 110 ++++++++++++++++ .../Presentation/AlarmListViewModel.swift | 28 +++++ Community/Package.swift | 2 + Hambug.xcodeproj/project.pbxproj | 2 + Hambug/Info.plist | 2 + Home/Package.swift | 2 + 15 files changed, 589 insertions(+) create mode 100644 Alarm/.gitignore create mode 100644 Alarm/Package.swift create mode 100644 Alarm/Sources/DI/AlarmDIContainer.swift create mode 100644 Alarm/Sources/Data/AlarmAPI.swift create mode 100644 Alarm/Sources/Data/AlarmRepositoryImpl.swift create mode 100644 Alarm/Sources/Data/NotificationDTO.swift create mode 100644 Alarm/Sources/Domain/AlarmEntities.swift create mode 100644 Alarm/Sources/Domain/AlarmRepository.swift create mode 100644 Alarm/Sources/Domain/GetAlarmListUseCase.swift create mode 100644 Alarm/Sources/Presentation/AlarmListView.swift create mode 100644 Alarm/Sources/Presentation/AlarmListViewModel.swift diff --git a/Alarm/.gitignore b/Alarm/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Alarm/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Alarm/Package.swift b/Alarm/Package.swift new file mode 100644 index 0000000..53eea3b --- /dev/null +++ b/Alarm/Package.swift @@ -0,0 +1,117 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +enum Config: String, CaseIterable { + static let name: String = "Alarm" + + case di = "DI" + case data = "Data" + case domain = "Domain" + case presentation = "Presentation" + + var name: String { + Config.name + rawValue + } + + var path: String { + "Sources/\(rawValue)" + } +} + +let package = Package( + name: Config.name, + platforms: [.iOS(.v17)], + products: [ + .library( + name: Config.name, + targets: Config.allCases.map(\.name) + ), + .library( + name: Config.di.name, + targets: [ + Config.di.name, + Config.presentation.name + ] + ) + ], + dependencies: [ + .package(name: "DI", path: "../DI"), + .package(name: "Common", path: "../Common"), + .package(name: "Infrastructure", path: "../Infrastructure"), + ], + targets: [ + .target( + config: .di, + dependencies: [ + .target(config: .domain), + .target(config: .data), + .target(config: .presentation), + .product(name: "DI", package: "DI"), + .product(name: "AppDI", package: "DI"), + .product(name: "NetworkInterface", package: "Infrastructure"), + ], + ), + // Domain: 독립적 (외부 의존성 없음) + .target(config: .domain), + + // Data + .target( + config: .data, + dependencies: [ + .target(config: .domain), + .product(name: "NetworkInterface", package: "Infrastructure"), + .product(name: "Util", package: "Common"), + ] + ), + + // Presentation: Domain, DesignSystem, Layout에 의존 + .target( + config: .presentation, + dependencies: [ + .target(config: .domain), + .product(name: "DesignSystem", package: "Common"), + ], + ) + + ] +) + +extension Target { + static func target( + config: Config, + dependencies: [Dependency] = [], + exclude: [String] = [], + sources: [String]? = nil, + resources: [Resource]? = nil, + publicHeadersPath: String? = nil, + packageAccess: Bool = false, + cSettings: [CSetting]? = nil, + cxxSettings: [CXXSetting]? = nil, + swiftSettings: [SwiftSetting]? = nil, + linkerSettings: [LinkerSetting]? = nil, + plugins: [PluginUsage]? = nil, + ) -> Target { + return .target( + name: config.name, + dependencies: dependencies, + path: config.path, + exclude: exclude, + sources: sources, + resources: resources, + publicHeadersPath: publicHeadersPath, + packageAccess: packageAccess, + cSettings: cSettings, + cxxSettings: cxxSettings, + swiftSettings: swiftSettings, + linkerSettings: linkerSettings, + plugins: plugins) + } +} + +extension Target.Dependency { + static func target(config: Config) -> Self { + return .target(name: config.name) + } +} diff --git a/Alarm/Sources/DI/AlarmDIContainer.swift b/Alarm/Sources/DI/AlarmDIContainer.swift new file mode 100644 index 0000000..3c08f17 --- /dev/null +++ b/Alarm/Sources/DI/AlarmDIContainer.swift @@ -0,0 +1,61 @@ +// +// AlarmDIContainer.swift +// Alarm +// +// Created by 강동영 on 10/17/25. +// + +import Foundation +import DIKit +import AppDI +import NetworkInterface +import AlarmDomain +import AlarmData +import AlarmPresentation + +struct AlarmAssembly: Assembly { + + func assemble(container: GenericDIContainer) { + // Repository registration + container.register(AlarmRepository.self) { resolver in + AlarmRepositoryImpl( + networkService: resolver.resolve(NetworkServiceInterface.self) + ) + } + + container.register(GetAlarmListUseCase.self) { resolver in + GetAlarmListUseCaseImpl( + repository: resolver.resolve(AlarmRepository.self) + ) + } + + container.register(AlarmListViewModel.self) { resolver in + AlarmListViewModel( + usecase: resolver.resolve(GetAlarmListUseCase.self) + ) + } + } +} + +// MARK: - Alarm DI Container +public final class AlarmDIContainer { + + // MARK: - Properties + private let container: GenericDIContainer + + // MARK: - Initialization + public init(appContainer: GenericDIContainer) { + self.container = appContainer + AlarmAssembly().assemble(container: container) + } +} + +extension AlarmDIContainer: AlarmListDependecy { + public func makeAlarmListViewModel() -> AlarmListViewModel { + return container.resolve(AlarmListViewModel.self) + } +} +//#Preview { +// let diContainer = CommunityDIContainer() +// CommunityView(viewModel: diContainer.makeCommunityViewModel(), diContainer: diContainer) +//} diff --git a/Alarm/Sources/Data/AlarmAPI.swift b/Alarm/Sources/Data/AlarmAPI.swift new file mode 100644 index 0000000..484df66 --- /dev/null +++ b/Alarm/Sources/Data/AlarmAPI.swift @@ -0,0 +1,56 @@ +// +// AlarmEndpoint.swift +// Hambug +// +// Created by 강동영 on 10/17/25. +// + +import Foundation +import NetworkInterface + +struct AlarmEndpoint: Endpoint { + var baseURL: String = NetworkConfig.baseURL + "/api/v1" + var path: String = "/notifications" + var method: NetworkInterface.HTTPMethod = .GET + var headers: [String : String] = [:] + var queryParameters: [String : Any] = [:] + var body: Data? = nil +} +public enum BoardEndpoint: Endpoint { + case fetchNotificaitons(CursorPagingQuery) + + public var baseURL: String { + return NetworkConfig.baseURL + "/api/v1" + } + + public var path: String { + switch self { + case .fetchNotificaitons: + return "/notifications" + } + } + + public var method: HTTPMethod { + switch self { + case .fetchNotificaitons: + return .GET + } + } + + public var headers: [String: String] { + return [:] + } + + public var queryParameters: [String: Any] { + switch self { + case let .fetchNotificaitons(dto): + return queryEncoder.encode(dto) + default: + return [:] + } + } + + public var body: Data? { + return nil + } +} diff --git a/Alarm/Sources/Data/AlarmRepositoryImpl.swift b/Alarm/Sources/Data/AlarmRepositoryImpl.swift new file mode 100644 index 0000000..18e9cfb --- /dev/null +++ b/Alarm/Sources/Data/AlarmRepositoryImpl.swift @@ -0,0 +1,28 @@ +// +// AlarmRepositoryImpl.swift +// Alarm +// +// Created by 강동영 on 1/17/26. +// + +import AlarmDomain +import NetworkInterface +import Util + +public final class AlarmRepositoryImpl: AlarmRepository { + private let networkService: NetworkServiceInterface + + public init(networkService: NetworkServiceInterface) { + self.networkService = networkService + } + + public func fetchAlarms() async throws -> NotificationListData { + return try await networkService.request( + AlarmEndpoint(), + responseType: SuccessResponse.self + ) + .map(\.data) + .map { $0.toDomain() } + .async() + } +} diff --git a/Alarm/Sources/Data/NotificationDTO.swift b/Alarm/Sources/Data/NotificationDTO.swift new file mode 100644 index 0000000..811d57e --- /dev/null +++ b/Alarm/Sources/Data/NotificationDTO.swift @@ -0,0 +1,81 @@ +// +// NotificationDTO.swift +// Alarm +// +// Created by 강동영 on 1/16/26. +// + +import Foundation +import AlarmDomain + +public struct NotificationListDataDTO: Decodable, Sendable { + public let content: [NotificationResponseDTO] + public let lastId: Int? + public let hasNext: Bool + + private enum CodingKeys: String, CodingKey { + case content + case lastId + case hasNext + } +} + +extension NotificationListDataDTO { + func toDomain() -> NotificationListData { + return NotificationListData( + content: content.map { $0.toDomain()}, + lastId: lastId, + hasNext: hasNext + ) + } +} + +public struct NotificationResponseDTO: Decodable, Sendable { + let id: Int64 + let title: String + let content: String + let type: NotificationType + let targetId: Int64 + let thumbnailUrl: String? + let isRead: Bool + let createdAt: String + + func toDomain() -> AlarmPayload { + let dateFormatter = DateFormatter.iso8601WithMicroseconds + + return .init( + id: id, + date: Self.timeAgoDisplay(dateFormatter.date(from: createdAt) ?? Date()), + content: "\(title)이 \(content)", + imageUrl: thumbnailUrl + ) + } + + private static func timeAgoDisplay(_ date: Date) -> String { + let now = Date() + let timeInterval = now.timeIntervalSince(date) + + if timeInterval < 60 { + return "방금 전" + } else if timeInterval < 3600 { + let minutes = Int(timeInterval / 60) + return "\(minutes)분 전" + } else if timeInterval < 86400 { + let hours = Int(timeInterval / 3600) + return "\(hours)시간 전" + } else if timeInterval < 604800 { + let days = Int(timeInterval / 86400) + return "\(days)일 전" + } else { + let formatter = DateFormatter() + formatter.dateFormat = "MM.dd" + return formatter.string(from: date) + } + } +} + +enum NotificationType: String, Decodable { + case login = "LOGIN_COMPLETE" + case comment = "COMMENT_NOTIFICATION" + case like = "LIKE_NOTIFICATION" +} diff --git a/Alarm/Sources/Domain/AlarmEntities.swift b/Alarm/Sources/Domain/AlarmEntities.swift new file mode 100644 index 0000000..cc36942 --- /dev/null +++ b/Alarm/Sources/Domain/AlarmEntities.swift @@ -0,0 +1,56 @@ +// +// AlarmEntities.swift +// Alarm +// +// Created by 강동영 on 1/16/26. +// + +import Foundation + +public struct AlarmPayload: Identifiable, Sendable { + public let id: Int64 + public let date: String + public let content: String + public let imageUrl: String? + + public init( + id: Int64, + date: String, + content: String, + imageUrl: String? + ) { + self.id = id + self.date = date + self.content = content + self.imageUrl = imageUrl + } +} + +extension [AlarmPayload] { + static let dummy: [AlarmPayload] = (1...50).map { + AlarmPayload( + id: $0, + date: "2026.01.1\($0)", + content: "안녕하세요\($0)", + imageUrl: nil + ) + } +} + +public struct NotificationListData: Sendable { + public let content: [AlarmPayload] + public let lastId: Int? + public let hasNext: Bool + + private enum CodingKeys: String, CodingKey { + case content + case lastId + case hasNext + } + + public init(content: [AlarmPayload], lastId: Int?, hasNext: Bool) { + self.content = content + self.lastId = lastId + self.hasNext = hasNext + } +} diff --git a/Alarm/Sources/Domain/AlarmRepository.swift b/Alarm/Sources/Domain/AlarmRepository.swift new file mode 100644 index 0000000..d07bfe3 --- /dev/null +++ b/Alarm/Sources/Domain/AlarmRepository.swift @@ -0,0 +1,12 @@ +// +// AlarmRepository.swift +// Alarm +// +// Created by 강동영 on 10/17/25. +// + +import Foundation + +public protocol AlarmRepository: Sendable { + func fetchAlarms() async throws -> NotificationListData +} diff --git a/Alarm/Sources/Domain/GetAlarmListUseCase.swift b/Alarm/Sources/Domain/GetAlarmListUseCase.swift new file mode 100644 index 0000000..8faec7c --- /dev/null +++ b/Alarm/Sources/Domain/GetAlarmListUseCase.swift @@ -0,0 +1,24 @@ +// +// GetAlarmListUseCase.swift +// Alarm +// +// Created by 강동영 on 1/16/26. +// + + +public protocol GetAlarmListUseCase: Sendable { + func execute() async throws -> NotificationListData +} + +public final class GetAlarmListUseCaseImpl: GetAlarmListUseCase { + + private let repository: AlarmRepository + + public init(repository: AlarmRepository) { + self.repository = repository + } + + public func execute() async throws -> NotificationListData { + return try await repository.fetchAlarms() + } +} diff --git a/Alarm/Sources/Presentation/AlarmListView.swift b/Alarm/Sources/Presentation/AlarmListView.swift new file mode 100644 index 0000000..5f83dcb --- /dev/null +++ b/Alarm/Sources/Presentation/AlarmListView.swift @@ -0,0 +1,110 @@ +// +// AlarmListView.swift +// Home +// +// Created by 강동영 on 1/14/26. +// + +import SwiftUI +import AlarmDomain +import DesignSystem + +public protocol AlarmListDependecy: AlarmListFactory {} + + +public protocol AlarmListFactory { + func makeAlarmListViewModel() -> AlarmListViewModel +} + +public struct AlarmListView: View { + @Environment(\.dismiss) private var dismiss + @State private var viewModel: AlarmListViewModel + private let dependency: AlarmListDependecy + + public init(dependency: AlarmListDependecy) { + self.dependency = dependency + self._viewModel = State(initialValue: dependency.makeAlarmListViewModel()) + } + + public var body: some View { + VStack { + navigationBar + + ScrollView { + ForEach(viewModel.palyload) { prop in + LazyVStack { + AlarmView(prop) + } + } + } + .task { + await viewModel.fetchAlarmList() + } + } + .toolbar(.hidden, for: .navigationBar) + } + + private var navigationBar: some View { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .medium)) + .foregroundColor(.iconG800) + } + + Spacer() + + Text("알림") + .pretendard(.title(.t2)) + .foregroundColor(.textG900) + + Spacer() + + Color.clear + .frame(width: 18, height: 18) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color.bgWhite) + } +} + +struct AlarmView: View { + private let payload: AlarmPayload + + init(_ payload: AlarmPayload) { + self.payload = payload + } + var body: some View { + HStack(spacing: 10) { + VStack(alignment: .leading, spacing: 4) { + Text(payload.date) + .pretendard(.caption(.base)) + .foregroundColor(Color.textG600) + Text(payload.content) + .pretendard(.body(.sEmphasis)) + .foregroundColor(Color.textG800) + } + + if let imageURL = payload.imageUrl { + Spacer() + + AsyncThumbnailImage( + imageURL: imageURL, + width: 40, + height: 40, + cornerRadius: 0 + ) + } + + } + .padding(16) + } +} + +//#Preview { +// AlarmListView() +//} + diff --git a/Alarm/Sources/Presentation/AlarmListViewModel.swift b/Alarm/Sources/Presentation/AlarmListViewModel.swift new file mode 100644 index 0000000..300200c --- /dev/null +++ b/Alarm/Sources/Presentation/AlarmListViewModel.swift @@ -0,0 +1,28 @@ +// +// AlarmListViewModel.swift +// Alarm +// +// Created by 강동영 on 1/18/26. +// + +import Observation +import AlarmDomain + +@Observable +public class AlarmListViewModel { + private let usecase: GetAlarmListUseCase + var palyload: [AlarmPayload] = [] + + public init(usecase: GetAlarmListUseCase) { + self.usecase = usecase + } + + func fetchAlarmList() async { + do { + palyload = try await usecase.execute().content + + } catch { + print(error) + } + } +} diff --git a/Community/Package.swift b/Community/Package.swift index 9b1c01c..e6cb936 100644 --- a/Community/Package.swift +++ b/Community/Package.swift @@ -37,6 +37,7 @@ let package = Package( .package(name: "DI", path: "../DI"), .package(name: "Common", path: "../Common"), .package(name: "Infrastructure", path: "../Infrastructure"), + .package(name: "Alarm", path: "../Alarm"), ], targets: [ .target( @@ -77,6 +78,7 @@ let package = Package( .target(config: .domain), .product(name: "DesignSystem", package: "Common"), .product(name: "SharedUI", package: "Common"), + .product(name: "AlarmDI", package: "Alarm"), ], ), ] diff --git a/Hambug.xcodeproj/project.pbxproj b/Hambug.xcodeproj/project.pbxproj index dd3858d..639b147 100644 --- a/Hambug.xcodeproj/project.pbxproj +++ b/Hambug.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ B54134FA2EF57F4100CC4938 /* Home */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Home; sourceTree = ""; }; B54134FB2EF57F6400CC4938 /* Community */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Community; sourceTree = ""; }; B54135972EF5B58100CC4938 /* 3rdParty */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = 3rdParty; sourceTree = ""; }; + B5C7D1B32F1B7B6600C20E14 /* Alarm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Alarm; sourceTree = ""; }; B5E822EB2EA2791900F3E10E /* Intro */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intro; sourceTree = ""; }; /* End PBXFileReference section */ @@ -135,6 +136,7 @@ 915BC5C92E3CB9B50062B78E = { isa = PBXGroup; children = ( + B5C7D1B32F1B7B6600C20E14 /* Alarm */, B54135972EF5B58100CC4938 /* 3rdParty */, B54134FB2EF57F6400CC4938 /* Community */, B54134FA2EF57F4100CC4938 /* Home */, diff --git a/Hambug/Info.plist b/Hambug/Info.plist index 58fca4c..57b5819 100644 --- a/Hambug/Info.plist +++ b/Hambug/Info.plist @@ -21,6 +21,8 @@ ${KAKAO_NATIVE_APP_KEY} BASE_URL $(BASE_URL) + UIUserInterfaceStyle + Light LSApplicationQueriesSchemes kakaokompassauth diff --git a/Home/Package.swift b/Home/Package.swift index 98d24b2..bd4e63a 100644 --- a/Home/Package.swift +++ b/Home/Package.swift @@ -34,6 +34,7 @@ let package = Package( .package(name: "DI", path: "../DI"), .package(name: "Infrastructure", path: "../Infrastructure"), .package(name: "Community", path: "../Community"), + .package(name: "Alarm", path: "../Alarm"), ], targets: [ .target( @@ -76,6 +77,7 @@ let package = Package( .product(name: "DesignSystem", package: "Common"), .product(name: "SharedUI", package: "Common"), .product(name: "Community", package: "Community"), + .product(name: "AlarmDI", package: "Alarm"), ], ), ] From 7e689634b2c3d17a6e69bd919dd95446c39c9b0c Mon Sep 17 00:00:00 2001 From: kangddong Date: Sun, 18 Jan 2026 03:30:00 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(delete):=20DI=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DI/.gitignore | 8 -- DI/Package.swift | 81 -------------- DI/Sources/AppDI/AppDIContainer.swift | 112 -------------------- DI/Sources/DIKit/DIContainer+Assembly.swift | 76 ------------- DI/Sources/IntroDI/IntroDIContainer.swift | 53 --------- DI/Sources/LoginDI/LoginDIContainer.swift | 75 ------------- 6 files changed, 405 deletions(-) delete mode 100644 DI/.gitignore delete mode 100644 DI/Package.swift delete mode 100644 DI/Sources/AppDI/AppDIContainer.swift delete mode 100644 DI/Sources/DIKit/DIContainer+Assembly.swift delete mode 100644 DI/Sources/IntroDI/IntroDIContainer.swift delete mode 100644 DI/Sources/LoginDI/LoginDIContainer.swift diff --git a/DI/.gitignore b/DI/.gitignore deleted file mode 100644 index 0023a53..0000000 --- a/DI/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/DI/Package.swift b/DI/Package.swift deleted file mode 100644 index 36fb438..0000000 --- a/DI/Package.swift +++ /dev/null @@ -1,81 +0,0 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -enum Config: String, CaseIterable { - static let name: String = "DI" - - case interface = "DI" - case app = "App" - case intro = "Intro" - case login = "Login" - - var name: String { - switch self { - case .interface: "\(rawValue)Kit" - default: "\(rawValue)DI" - } - - } -} -let package = Package( - name: Config.name, - platforms: [.iOS(.v17)], - products: [ - .library( - name: Config.interface.name, - targets: Config.allCases.map(\.name) - ), - .library( - name: Config.interface.rawValue, - targets: [Config.interface.name] - ), - .library( - name: Config.app.name, - targets: [Config.app.name] - ) - ], - dependencies: [ - .package(name: "3rdParty", path: "../3rdParty"), - .package(name: "Common", path: "../Common"), - .package(name: "Infrastructure", path: "../Infrastructure"), - .package(name: "Intro", path: "../Intro"), - .package(name: "Login", path: "../Login"), - ], - targets: [ - .target(name: Config.interface.name), - .target( - name: Config.app.name, - dependencies: [ - .target(config: .interface), - .product(name: "DataSources", package: "Common"), - .product(name: "Managers", package: "Common"), - .product(name: "NetworkInterface", package: "Infrastructure"), - .product(name: "NetworkImpl", package: "Infrastructure"), - .product(name: "FCMService", package: "3rdParty"), - ] - ), - .target( - name: Config.intro.name, - dependencies: [ - .target(config: .app), - .product(name: "Onboarding", package: "Intro"), - .product(name: "Splash", package: "Intro"), - ] - ), - .target( - name: Config.login.name, - dependencies: [ - .target(config: .app), - .product(name: "Login", package: "Login"), - ] - ) - ] -) - -extension Target.Dependency { - static func target(config: Config) -> Self { - .target(name: config.name) - } -} diff --git a/DI/Sources/AppDI/AppDIContainer.swift b/DI/Sources/AppDI/AppDIContainer.swift deleted file mode 100644 index 3bc00b0..0000000 --- a/DI/Sources/AppDI/AppDIContainer.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// AppDIContainer.swift -// Hambug -// -// Created by 강동영 on 12/18/25. -// - -import Foundation -import SwiftUI -import Observation -import DIKit -import DataSources -import Managers -import NetworkInterface -import NetworkImpl -import FCMService - -// MARK: - App Assembly -struct AppAssembly: Assembly { - func assemble(container: GenericDIContainer) { - // Register TokenStorage as singleton - container.register(TokenStorage.self, scope: .singleton) { _ in - KeychainTokenStorage() - } - - container.register(JWTTokenStorageable.self, scope: .singleton) { _ in - JWTokenStorage() - } - - container.register(FCMTokenStorageable.self, scope: .singleton) { _ in - FCMTokenStorage() - } - - // Register UserDefaultsManager as singleton - container.register(UserDefaultsManager.self, scope: .singleton) { _ in - UserDefaultsManager.shared - } - - // Register NetworkServiceInterface as singleton - container.register(NetworkServiceInterface.self, scope: .singleton) { resolver in - let tokenStorage = resolver.resolve(JWTTokenStorageable.self) - -#if DEBUG - let logger = NetworkLogger() - return NetworkServiceImpl( - interceptor: AuthInterceptor(tokenManager: tokenStorage), - logger: logger - ) -#else - return NetworkServiceImpl( - interceptor: AuthInterceptor(tokenManager: tokenStorage) - ) -#endif - } - - container.register(FCMManager.self, scope: .transient) { resolver in - FCMManager( - service: resolver.resolve( - NetworkServiceInterface.self - ), - storage: resolver.resolve( - FCMTokenStorageable.self - ) - ) - } - - // Register AppStateManager as singleton - container.register(AppStateManager.self, scope: .singleton) { resolver in - AppStateManager( - tokenStorage: resolver.resolve(TokenStorage.self), - udManager: resolver.resolve(UserDefaultsManager.self) - ) - } - } -} - -// MARK: - App DI Container -@Observable -public final class AppDIContainer: @unchecked Sendable { - - // MARK: - Singleton - public static let shared = AppDIContainer() - - // MARK: - Properties - private let container = GenericDIContainer() - - // MARK: - Initialization - private init() { - AppAssembly().assemble(container: container) - } - - // MARK: - Access Methods - - /// Get the underlying container for child containers - public var baseContainer: GenericDIContainer { - return container - } - - /// Direct access to AppStateManager for app initialization - public func makeAppStateManager() -> AppStateManager { - return container.resolve(AppStateManager.self) - } - - public func makeFCMManager() -> FCMManager { - return container.resolve(FCMManager.self) - } - - /// Generic resolve for any registered type - public func resolve(_ type: T.Type) -> T { - return container.resolve(type) - } -} diff --git a/DI/Sources/DIKit/DIContainer+Assembly.swift b/DI/Sources/DIKit/DIContainer+Assembly.swift deleted file mode 100644 index 2ea1fa1..0000000 --- a/DI/Sources/DIKit/DIContainer+Assembly.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// DIContainer+Assembly.swift -// Hambug -// -// Created by 강동영 on 12/17/25. -// - -// MARK: - Scope -public enum Scope { - case singleton // 한 번 생성, 캐싱 - case transient // 매번 새로 생성 -} - -// MARK: - Assembly Pattern -public protocol Assembly { - func assemble(container: GenericDIContainer) -} - -// MARK: - Generic DIContainer Protocol -public protocol DIContainer { - func resolve(_ type: T.Type) -> T -} - -// MARK: - Generic DIContainer Implementation -public class GenericDIContainer: DIContainer { - private struct FactoryEntry { - let scope: Scope - let factory: Any - } - - private var factories: [String: FactoryEntry] = [:] - private var singletons: [String: Any] = [:] - private weak var parent: GenericDIContainer? - - public init(parent: GenericDIContainer? = nil) { - self.parent = parent - } - - public func register( - _ type: T.Type, - scope: Scope = .transient, - _ factory: @escaping (GenericDIContainer) -> T - ) { - let key = String(describing: type) - factories[key] = FactoryEntry(scope: scope, factory: factory) - } - - public func resolve(_ type: T.Type) -> T { - let key = String(describing: type) - - // Check for singleton cache - if let singleton = singletons[key] as? T { - return singleton - } - - // Try local factory - if let entry = factories[key], - let factory = entry.factory as? (GenericDIContainer) -> T { - let instance = factory(self) - - // Cache if singleton - if entry.scope == .singleton { - singletons[key] = instance - } - - return instance - } - - // Fallback to parent if available - if let parent = parent { - return parent.resolve(type) - } - - fatalError("❌ \(key) is not registered") - } -} diff --git a/DI/Sources/IntroDI/IntroDIContainer.swift b/DI/Sources/IntroDI/IntroDIContainer.swift deleted file mode 100644 index 65e31ab..0000000 --- a/DI/Sources/IntroDI/IntroDIContainer.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// IntroDIContainer.swift -// Hambug -// -// Created by 강동영 on 12/18/25. -// - -import DIKit -import AppDI -import Splash -import Onboarding -import Managers - -// MARK: - Login Assembly -struct IntroAssembly: Assembly { - private let appStateManager: AppStateManager - - init(appStateManager: AppStateManager) { - self.appStateManager = appStateManager - } - - func assemble(container: GenericDIContainer) { - container.register(OnboardingViewModel.self) { resolver in - OnboardingViewModel( - appStateManager: self.appStateManager - ) - } - - container.register(SplashViewModel.self) { resolver in - SplashViewModel( - appStateManager: self.appStateManager - ) - } - } -} - -// MARK: - Login DI Container -public final class IntroDIContainer { - - // MARK: - Properties - private let container: GenericDIContainer - - // MARK: - Initialization - public init(appContainer: AppDIContainer, appStateManager: AppStateManager) { - self.container = GenericDIContainer(parent: appContainer.baseContainer) - IntroAssembly(appStateManager: appStateManager).assemble(container: container) - } - - // MARK: - Factory Methods - public func resolve(_ type: T.Type) -> T { - return container.resolve(type) - } -} diff --git a/DI/Sources/LoginDI/LoginDIContainer.swift b/DI/Sources/LoginDI/LoginDIContainer.swift deleted file mode 100644 index 2c3e809..0000000 --- a/DI/Sources/LoginDI/LoginDIContainer.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// LoginDIContainer.swift -// Hambug -// -// Created by 강동영 on 12/18/25. -// - -import Foundation -import DIKit -import AppDI -import LoginDomain -import LoginData -import LoginPresentation -import DataSources -import Managers -import NetworkInterface - -// MARK: - Login Assembly -struct LoginAssembly: Assembly { - private let appStateManager: AppStateManager - - init(appStateManager: AppStateManager) { - self.appStateManager = appStateManager - } - - func assemble(container: GenericDIContainer) { - // Note: NetworkService and TokenStorage come from parent container - - // Repository registration - container.register(LoginRepository.self) { resolver in - LoginRepositoryImpl( - networkService: resolver.resolve(NetworkServiceInterface.self), - tokenStorage: resolver.resolve(JWTTokenStorageable.self), - userDefaultsManager: resolver.resolve(UserDefaultsManager.self) - ) - } - - // UseCase registration - container.register(LoginUseCase.self) { resolver in - LoginUseCaseImpl( - repository: resolver.resolve(LoginRepository.self) - ) - } - - // ViewModel registration - container.register(LoginViewModel.self) { resolver in - LoginViewModel( - useCase: resolver.resolve(LoginUseCase.self), - appStateManager: self.appStateManager - ) - } - } -} - -// MARK: - Login DI Container -public final class LoginDIContainer { - - // MARK: - Properties - private let container: GenericDIContainer - - // MARK: - Initialization - public init(appContainer: AppDIContainer, appStateManager: AppStateManager) { - self.container = GenericDIContainer(parent: appContainer.baseContainer) - LoginAssembly(appStateManager: appStateManager).assemble(container: container) - } - - // MARK: - Factory Methods - public func makeLoginViewModel() -> LoginViewModel { - return container.resolve(LoginViewModel.self) - } - - public func resolve(_ type: T.Type) -> T { - return container.resolve(type) - } -} From d3ec550479f79cfb1f8fd48fe5e0d8a53f2265f8 Mon Sep 17 00:00:00 2001 From: kangddong Date: Sun, 18 Jan 2026 03:32:12 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat(chore):=20DI=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20->=20Common,=20Intro,=20Login=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20DI=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Alarm/Package.swift | 4 +- Alarm/Sources/DI/AlarmDIContainer.swift | 1 - AppCore/.gitignore | 8 + AppCore/Package.swift | 98 ++++++++++ AppCore/Sources/DI/AppDIContainer.swift | 170 ++++++++++++++++++ Common/Package.swift | 5 + .../Sources/DIKit/DIContainer+Assembly.swift | 76 ++++++++ Community/Package.swift | 16 +- .../Sources/DI/CommunityDIContainer.swift | 6 +- Hambug.xcodeproj/project.pbxproj | 56 +++--- Hambug/AppDelegate.swift | 2 +- Hambug/ContentView.swift | 34 +--- Hambug/HambugApp.swift | 2 +- Hambug/RootView.swift | 21 +-- Home/Package.swift | 12 +- Home/Sources/DI/HomeDIContainer.swift | 6 +- Intro/Package.swift | 138 ++++++++++---- Intro/Sources/DI/IntroDIContainer.swift | 50 ++++++ Login/Package.swift | 23 ++- Login/Sources/DI/LoginDIContainer.swift | 64 +++++++ MyPage/Package.swift | 13 +- MyPage/Sources/DI/MyPageDIContainer.swift | 6 +- 22 files changed, 677 insertions(+), 134 deletions(-) create mode 100644 AppCore/.gitignore create mode 100644 AppCore/Package.swift create mode 100644 AppCore/Sources/DI/AppDIContainer.swift create mode 100644 Common/Sources/DIKit/DIContainer+Assembly.swift create mode 100644 Intro/Sources/DI/IntroDIContainer.swift create mode 100644 Login/Sources/DI/LoginDIContainer.swift diff --git a/Alarm/Package.swift b/Alarm/Package.swift index 53eea3b..049bae6 100644 --- a/Alarm/Package.swift +++ b/Alarm/Package.swift @@ -37,7 +37,6 @@ let package = Package( ) ], dependencies: [ - .package(name: "DI", path: "../DI"), .package(name: "Common", path: "../Common"), .package(name: "Infrastructure", path: "../Infrastructure"), ], @@ -48,8 +47,7 @@ let package = Package( .target(config: .domain), .target(config: .data), .target(config: .presentation), - .product(name: "DI", package: "DI"), - .product(name: "AppDI", package: "DI"), + .product(name: "DIKit", package: "Common"), .product(name: "NetworkInterface", package: "Infrastructure"), ], ), diff --git a/Alarm/Sources/DI/AlarmDIContainer.swift b/Alarm/Sources/DI/AlarmDIContainer.swift index 3c08f17..ffbce80 100644 --- a/Alarm/Sources/DI/AlarmDIContainer.swift +++ b/Alarm/Sources/DI/AlarmDIContainer.swift @@ -7,7 +7,6 @@ import Foundation import DIKit -import AppDI import NetworkInterface import AlarmDomain import AlarmData diff --git a/AppCore/.gitignore b/AppCore/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/AppCore/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/AppCore/Package.swift b/AppCore/Package.swift new file mode 100644 index 0000000..a453a68 --- /dev/null +++ b/AppCore/Package.swift @@ -0,0 +1,98 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +enum Config: String, CaseIterable { + static let name: String = "AppCore" + + case di = "DI" + + var name: String { + return "\(Config.name)\(rawValue)" + } + + var path: String { + "Sources/\(rawValue)" + } +} + +let package = Package( + name: Config.name, + platforms: [.iOS(.v17)], + products: [ + .library( + name: "AppCore", + targets: Config.allCases.map(\.name) + ), + ], + dependencies: [ + .package(name: "Intro", path: "../Intro"), + .package(name: "Login", path: "../Login"), + .package(name: "Home", path: "../Home"), + .package(name: "Community", path: "../Community"), + .package(name: "MyPage", path: "../MyPage"), + .package(name: "Alarm", path: "../Alarm"), + + .package(name: "3rdParty", path: "../3rdParty"), + .package(name: "Common", path: "../Common"), + .package(name: "Infrastructure", path: "../Infrastructure"), + ], + targets: [ + .target( + config: .di, + dependencies: [ + .product(name: "DataSources", package: "Common"), + .product(name: "Managers", package: "Common"), + .product(name: "NetworkInterface", package: "Infrastructure"), + .product(name: "NetworkImpl", package: "Infrastructure"), + .product(name: "FCMService", package: "3rdParty"), + + .product(name: "IntroDI", package: "Intro"), + .product(name: "LoginDI", package: "Login"), + .product(name: "HomeDI", package: "Home"), + .product(name: "CommunityDI", package: "Community"), + .product(name: "MyPageDI", package: "MyPage"), + .product(name: "AlarmDI", package: "Alarm"), + ] + ) + ] +) + +extension Target { + static func target( + config: Config, + dependencies: [Dependency] = [], + exclude: [String] = [], + sources: [String]? = nil, + resources: [Resource]? = nil, + publicHeadersPath: String? = nil, + packageAccess: Bool = false, + cSettings: [CSetting]? = nil, + cxxSettings: [CXXSetting]? = nil, + swiftSettings: [SwiftSetting]? = nil, + linkerSettings: [LinkerSetting]? = nil, + plugins: [PluginUsage]? = nil, + ) -> Target { + return .target( + name: config.name, + dependencies: dependencies, + path: config.path, + exclude: exclude, + sources: sources, + resources: resources, + publicHeadersPath: publicHeadersPath, + packageAccess: packageAccess, + cSettings: cSettings, + cxxSettings: cxxSettings, + swiftSettings: swiftSettings, + linkerSettings: linkerSettings, + plugins: plugins) + } +} + +extension Target.Dependency { + static func target(config: Config) -> Self { + return .target(name: config.name) + } +} diff --git a/AppCore/Sources/DI/AppDIContainer.swift b/AppCore/Sources/DI/AppDIContainer.swift new file mode 100644 index 0000000..841d76e --- /dev/null +++ b/AppCore/Sources/DI/AppDIContainer.swift @@ -0,0 +1,170 @@ +// +// AppDIContainer.swift +// Hambug +// +// Created by 강동영 on 12/18/25. +// + +import Foundation +import SwiftUI +import Observation +import DIKit +import DataSources +import Managers +import NetworkInterface +import NetworkImpl +import FCMService + +import IntroDI +import LoginDI +import HomeDI +import CommunityDI +import MyPageDI +import AlarmDI + +// MARK: - App Assembly +struct AppAssembly: Assembly { + func assemble(container: GenericDIContainer) { + // Register TokenStorage as singleton + container.register(TokenStorage.self, scope: .singleton) { _ in + KeychainTokenStorage() + } + + container.register(JWTTokenStorageable.self, scope: .singleton) { _ in + JWTokenStorage() + } + + container.register(FCMTokenStorageable.self, scope: .singleton) { _ in + FCMTokenStorage() + } + + // Register UserDefaultsManager as singleton + container.register(UserDefaultsManager.self, scope: .singleton) { _ in + UserDefaultsManager.shared + } + + // Register NetworkServiceInterface as singleton + container.register(NetworkServiceInterface.self, scope: .singleton) { resolver in + let tokenStorage = resolver.resolve(JWTTokenStorageable.self) + +#if DEBUG + let logger = NetworkLogger() + return NetworkServiceImpl( + interceptor: AuthInterceptor(tokenManager: tokenStorage), + logger: logger + ) +#else + return NetworkServiceImpl( + interceptor: AuthInterceptor(tokenManager: tokenStorage) + ) +#endif + } + + container.register(FCMManager.self, scope: .transient) { resolver in + FCMManager( + service: resolver.resolve( + NetworkServiceInterface.self + ), + storage: resolver.resolve( + FCMTokenStorageable.self + ) + ) + } + + // Register AppStateManager as singleton + container.register(AppStateManager.self, scope: .singleton) { resolver in + AppStateManager( + tokenStorage: resolver.resolve(TokenStorage.self), + udManager: resolver.resolve(UserDefaultsManager.self) + ) + } + + // MARK: - IntroDIContainer + container.register(IntroDIContainer.self, scope: .singleton) { r in + IntroDIContainer(appContainer: container) + } + + // MARK: - LoginDIContainer + container.register(LoginDIContainer.self, scope: .singleton) { r in + LoginDIContainer(appContainer: container) + } + + // MARK: - HomeDIContainer + container.register(HomeDIContainer.self, scope: .singleton) { r in + HomeDIContainer(appContainer: container) + } + + // MARK: - CommunityDIContainer + container.register(CommunityDIContainer.self, scope: .singleton) { r in + CommunityDIContainer(appContainer: container) + } + + // MARK: - MyPageDIContainer + container.register(MyPageDIContainer.self, scope: .singleton) { r in + MyPageDIContainer(appContainer: container) + } + + // MARK: - AlarmDIContainer + container.register(AlarmDIContainer.self, scope: .singleton) { r in + AlarmDIContainer(appContainer: container) + } + + } +} + +// MARK: - App DI Container +@Observable +public final class AppDIContainer: @unchecked Sendable { + + // MARK: - Singleton + public static let shared = AppDIContainer() + + // MARK: - Properties + private let container = GenericDIContainer() + + // MARK: - Initialization + private init() { + AppAssembly().assemble(container: container) + } + + // MARK: - Access Methods + + /// Get the underlying container for child containers + public var baseContainer: GenericDIContainer { + return container + } + + public var introDIContainer: IntroDIContainer { + return container.resolve(IntroDIContainer.self) + } + + public var loginDIContainer: LoginDIContainer { + return container.resolve(LoginDIContainer.self) + } + + public var homeDIContainer: HomeDIContainer { + return container.resolve(HomeDIContainer.self) + } + + public var communityDIContainer: CommunityDIContainer { + return container.resolve(CommunityDIContainer.self) + } + + public var mypageDIContainer: MyPageDIContainer { + return container.resolve(MyPageDIContainer.self) + } + + /// Direct access to AppStateManager for app initialization + public func makeAppStateManager() -> AppStateManager { + return container.resolve(AppStateManager.self) + } + + public func makeFCMManager() -> FCMManager { + return container.resolve(FCMManager.self) + } + + /// Generic resolve for any registered type + public func resolve(_ type: T.Type) -> T { + return container.resolve(type) + } +} diff --git a/Common/Package.swift b/Common/Package.swift index 2cc80e0..1a11159 100644 --- a/Common/Package.swift +++ b/Common/Package.swift @@ -19,6 +19,10 @@ let package = Package( name: "DataSources", targets: ["DataSources"] ), + .library( + name: "DIKit", + targets: ["DIKit"] + ), .library( name: "LocalizedString", targets: ["LocalizedString"] @@ -48,6 +52,7 @@ let package = Package( ] ), .target(name: "DataSources"), + .target(name: "DIKit"), .target(name: "LocalizedString"), .target(name: "Util"), .target( diff --git a/Common/Sources/DIKit/DIContainer+Assembly.swift b/Common/Sources/DIKit/DIContainer+Assembly.swift new file mode 100644 index 0000000..2ea1fa1 --- /dev/null +++ b/Common/Sources/DIKit/DIContainer+Assembly.swift @@ -0,0 +1,76 @@ +// +// DIContainer+Assembly.swift +// Hambug +// +// Created by 강동영 on 12/17/25. +// + +// MARK: - Scope +public enum Scope { + case singleton // 한 번 생성, 캐싱 + case transient // 매번 새로 생성 +} + +// MARK: - Assembly Pattern +public protocol Assembly { + func assemble(container: GenericDIContainer) +} + +// MARK: - Generic DIContainer Protocol +public protocol DIContainer { + func resolve(_ type: T.Type) -> T +} + +// MARK: - Generic DIContainer Implementation +public class GenericDIContainer: DIContainer { + private struct FactoryEntry { + let scope: Scope + let factory: Any + } + + private var factories: [String: FactoryEntry] = [:] + private var singletons: [String: Any] = [:] + private weak var parent: GenericDIContainer? + + public init(parent: GenericDIContainer? = nil) { + self.parent = parent + } + + public func register( + _ type: T.Type, + scope: Scope = .transient, + _ factory: @escaping (GenericDIContainer) -> T + ) { + let key = String(describing: type) + factories[key] = FactoryEntry(scope: scope, factory: factory) + } + + public func resolve(_ type: T.Type) -> T { + let key = String(describing: type) + + // Check for singleton cache + if let singleton = singletons[key] as? T { + return singleton + } + + // Try local factory + if let entry = factories[key], + let factory = entry.factory as? (GenericDIContainer) -> T { + let instance = factory(self) + + // Cache if singleton + if entry.scope == .singleton { + singletons[key] = instance + } + + return instance + } + + // Fallback to parent if available + if let parent = parent { + return parent.resolve(type) + } + + fatalError("❌ \(key) is not registered") + } +} diff --git a/Community/Package.swift b/Community/Package.swift index e6cb936..69f86ea 100644 --- a/Community/Package.swift +++ b/Community/Package.swift @@ -29,12 +29,19 @@ let package = Package( targets: Config.allCases.map(\.name) ), .library( - name: "CommunityDomain", - targets: ["CommunityDomain"] + name: Config.di.name, + targets: [Config.di.name] + ), + .library( + name: Config.domain.name, + targets: [Config.domain.name] + ), + .library( + name: Config.presentation.name, + targets: [Config.presentation.name] ), ], dependencies: [ - .package(name: "DI", path: "../DI"), .package(name: "Common", path: "../Common"), .package(name: "Infrastructure", path: "../Infrastructure"), .package(name: "Alarm", path: "../Alarm"), @@ -46,8 +53,7 @@ let package = Package( .target(config: .domain), .target(config: .data), .target(config: .presentation), - .product(name: "DI", package: "DI"), - .product(name: "AppDI", package: "DI"), + .product(name: "DIKit", package: "Common"), .product(name: "NetworkInterface", package: "Infrastructure"), .product(name: "NetworkImpl", package: "Infrastructure"), ], diff --git a/Community/Sources/DI/CommunityDIContainer.swift b/Community/Sources/DI/CommunityDIContainer.swift index 1d5a7f1..10c5a2f 100644 --- a/Community/Sources/DI/CommunityDIContainer.swift +++ b/Community/Sources/DI/CommunityDIContainer.swift @@ -7,7 +7,6 @@ import Foundation import DIKit -import AppDI import NetworkInterface import NetworkImpl import CommunityDomain @@ -100,11 +99,12 @@ public final class CommunityDIContainer { private let container: GenericDIContainer // MARK: - Initialization - public init(appContainer: AppDIContainer = .shared) { - self.container = GenericDIContainer(parent: appContainer.baseContainer) + public init(appContainer: GenericDIContainer) { + self.container = appContainer CommunityAssembly().assemble(container: container) CommunityWriteAssembly().assemble(container: container) } +} // MARK: - Factory Methods @MainActor diff --git a/Hambug.xcodeproj/project.pbxproj b/Hambug.xcodeproj/project.pbxproj index 639b147..4c83f9a 100644 --- a/Hambug.xcodeproj/project.pbxproj +++ b/Hambug.xcodeproj/project.pbxproj @@ -15,13 +15,13 @@ B54128772EF1CD3100CC4938 /* Login in Frameworks */ = {isa = PBXBuildFile; productRef = B54128762EF1CD3100CC4938 /* Login */; }; B5412A882EF277A200CC4938 /* NetworkImpl in Frameworks */ = {isa = PBXBuildFile; productRef = B5412A872EF277A200CC4938 /* NetworkImpl */; }; B5412A8A2EF277A200CC4938 /* NetworkInterface in Frameworks */ = {isa = PBXBuildFile; productRef = B5412A892EF277A200CC4938 /* NetworkInterface */; }; - B54134082EF535FC00CC4938 /* MyPage in Frameworks */ = {isa = PBXBuildFile; productRef = B54134072EF535FC00CC4938 /* MyPage */; }; - B54134FD2EF5B09D00CC4938 /* Community in Frameworks */ = {isa = PBXBuildFile; productRef = B54134FC2EF5B09D00CC4938 /* Community */; }; - B54134FF2EF5B0A400CC4938 /* Home in Frameworks */ = {isa = PBXBuildFile; productRef = B54134FE2EF5B0A400CC4938 /* Home */; }; B54135802EF5B4CC00CC4938 /* KakaoLogin in Frameworks */ = {isa = PBXBuildFile; productRef = B541357F2EF5B4CC00CC4938 /* KakaoLogin */; }; B57DE3412E92CB6100575CDA /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = B57DE3402E92CB6100575CDA /* Onboarding */; }; B57DE3432E92CB6100575CDA /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = B57DE3422E92CB6100575CDA /* Splash */; }; - B5EDA8F82F0B9C0C002F72B9 /* DIKit in Frameworks */ = {isa = PBXBuildFile; productRef = B5EDA8F72F0B9C0C002F72B9 /* DIKit */; }; + B5C7D2112F1C09C100C20E14 /* AppCore in Frameworks */ = {isa = PBXBuildFile; productRef = B5C7D2102F1C09C100C20E14 /* AppCore */; }; + B5C7D2132F1C0AD300C20E14 /* CommunityPresentation in Frameworks */ = {isa = PBXBuildFile; productRef = B5C7D2122F1C0AD300C20E14 /* CommunityPresentation */; }; + B5C7D2152F1C0AD300C20E14 /* HomePresentation in Frameworks */ = {isa = PBXBuildFile; productRef = B5C7D2142F1C0AD300C20E14 /* HomePresentation */; }; + B5C7D2172F1C0AD900C20E14 /* MyPagePresentation in Frameworks */ = {isa = PBXBuildFile; productRef = B5C7D2162F1C0AD900C20E14 /* MyPagePresentation */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,12 +49,12 @@ B52285C62E8844BD00678ECC /* Common */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Common; sourceTree = ""; }; B54125132EF161BF00CC4938 /* Login */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Login; sourceTree = ""; }; B54126322EF1A99B00CC4938 /* Infrastructure */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Infrastructure; sourceTree = ""; }; - B5412CD62EF3C9EA00CC4938 /* DI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DI; sourceTree = ""; }; B54134062EF5330A00CC4938 /* MyPage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MyPage; sourceTree = ""; }; B54134FA2EF57F4100CC4938 /* Home */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Home; sourceTree = ""; }; B54134FB2EF57F6400CC4938 /* Community */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Community; sourceTree = ""; }; B54135972EF5B58100CC4938 /* 3rdParty */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = 3rdParty; sourceTree = ""; }; B5C7D1B32F1B7B6600C20E14 /* Alarm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Alarm; sourceTree = ""; }; + B5C7D20B2F1BEFAB00C20E14 /* AppCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AppCore; sourceTree = ""; }; B5E822EB2EA2791900F3E10E /* Intro */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intro; sourceTree = ""; }; /* End PBXFileReference section */ @@ -101,16 +101,16 @@ files = ( B57DE3432E92CB6100575CDA /* Splash in Frameworks */, B52286032E884E5B00678ECC /* DesignSystem in Frameworks */, - B54134082EF535FC00CC4938 /* MyPage in Frameworks */, - B5EDA8F82F0B9C0C002F72B9 /* DIKit in Frameworks */, - B54134FF2EF5B0A400CC4938 /* Home in Frameworks */, + B5C7D2132F1C0AD300C20E14 /* CommunityPresentation in Frameworks */, + B5C7D2172F1C0AD900C20E14 /* MyPagePresentation in Frameworks */, + B5C7D2152F1C0AD300C20E14 /* HomePresentation in Frameworks */, B57DE3412E92CB6100575CDA /* Onboarding in Frameworks */, B52285C92E88469C00678ECC /* Managers in Frameworks */, B54128772EF1CD3100CC4938 /* Login in Frameworks */, + B5C7D2112F1C09C100C20E14 /* AppCore in Frameworks */, B54135802EF5B4CC00CC4938 /* KakaoLogin in Frameworks */, B53088172F15712500586850 /* FCMService in Frameworks */, B5412A882EF277A200CC4938 /* NetworkImpl in Frameworks */, - B54134FD2EF5B09D00CC4938 /* Community in Frameworks */, B5412A8A2EF277A200CC4938 /* NetworkInterface in Frameworks */, B53086D12F115A3800586850 /* SharedUI in Frameworks */, ); @@ -136,12 +136,12 @@ 915BC5C92E3CB9B50062B78E = { isa = PBXGroup; children = ( + B5C7D20B2F1BEFAB00C20E14 /* AppCore */, B5C7D1B32F1B7B6600C20E14 /* Alarm */, B54135972EF5B58100CC4938 /* 3rdParty */, B54134FB2EF57F6400CC4938 /* Community */, B54134FA2EF57F4100CC4938 /* Home */, B54134062EF5330A00CC4938 /* MyPage */, - B5412CD62EF3C9EA00CC4938 /* DI */, B54126322EF1A99B00CC4938 /* Infrastructure */, B54125132EF161BF00CC4938 /* Login */, B513799A2EE2ED8F00DAF2F7 /* Common.xcconfig */, @@ -209,13 +209,13 @@ B54128762EF1CD3100CC4938 /* Login */, B5412A872EF277A200CC4938 /* NetworkImpl */, B5412A892EF277A200CC4938 /* NetworkInterface */, - B54134072EF535FC00CC4938 /* MyPage */, - B54134FC2EF5B09D00CC4938 /* Community */, - B54134FE2EF5B0A400CC4938 /* Home */, B541357F2EF5B4CC00CC4938 /* KakaoLogin */, - B5EDA8F72F0B9C0C002F72B9 /* DIKit */, B53086D02F115A3800586850 /* SharedUI */, B53088162F15712500586850 /* FCMService */, + B5C7D2102F1C09C100C20E14 /* AppCore */, + B5C7D2122F1C0AD300C20E14 /* CommunityPresentation */, + B5C7D2142F1C0AD300C20E14 /* HomePresentation */, + B5C7D2162F1C0AD900C20E14 /* MyPagePresentation */, ); productName = Hambug; productReference = 915BC5D22E3CB9B50062B78E /* Hambug.app */; @@ -741,18 +741,6 @@ isa = XCSwiftPackageProductDependency; productName = NetworkInterface; }; - B54134072EF535FC00CC4938 /* MyPage */ = { - isa = XCSwiftPackageProductDependency; - productName = MyPage; - }; - B54134FC2EF5B09D00CC4938 /* Community */ = { - isa = XCSwiftPackageProductDependency; - productName = Community; - }; - B54134FE2EF5B0A400CC4938 /* Home */ = { - isa = XCSwiftPackageProductDependency; - productName = Home; - }; B541357F2EF5B4CC00CC4938 /* KakaoLogin */ = { isa = XCSwiftPackageProductDependency; productName = KakaoLogin; @@ -765,9 +753,21 @@ isa = XCSwiftPackageProductDependency; productName = Splash; }; - B5EDA8F72F0B9C0C002F72B9 /* DIKit */ = { + B5C7D2102F1C09C100C20E14 /* AppCore */ = { + isa = XCSwiftPackageProductDependency; + productName = AppCore; + }; + B5C7D2122F1C0AD300C20E14 /* CommunityPresentation */ = { + isa = XCSwiftPackageProductDependency; + productName = CommunityPresentation; + }; + B5C7D2142F1C0AD300C20E14 /* HomePresentation */ = { + isa = XCSwiftPackageProductDependency; + productName = HomePresentation; + }; + B5C7D2162F1C0AD900C20E14 /* MyPagePresentation */ = { isa = XCSwiftPackageProductDependency; - productName = DIKit; + productName = MyPagePresentation; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Hambug/AppDelegate.swift b/Hambug/AppDelegate.swift index 504156d..53bb3bb 100644 --- a/Hambug/AppDelegate.swift +++ b/Hambug/AppDelegate.swift @@ -6,8 +6,8 @@ // import UIKit +import AppCoreDI import FCMService -import AppDI class AppDelegate: NSObject, UIApplicationDelegate { private let appDIContainer: AppDIContainer = .shared diff --git a/Hambug/ContentView.swift b/Hambug/ContentView.swift index b67f396..0181736 100644 --- a/Hambug/ContentView.swift +++ b/Hambug/ContentView.swift @@ -6,56 +6,36 @@ // import SwiftUI -import AppDI +import AppCoreDI +import SharedUI import HomePresentation -import HomeDI import CommunityPresentation -import CommunityDI -import SharedUI import MyPagePresentation -import MyPageDI struct ContentView: View { @Environment(AppDIContainer.self) var appContainer - private var homeDIContainer: HomeDIContainer { - HomeDIContainer(appContainer: appContainer) - } - - private var communityDIContainer: CommunityDIContainer { - CommunityDIContainer(appContainer: appContainer) - } - - private var mypageDIContainer: MyPageDIContainer { - MyPageDIContainer(appContainer: appContainer) - } - @State private var selectedTab: Int = 0 var body: some View { CustomTabView(selectedTab: $selectedTab) { Group { NavigationStack { - HomeView(viewModel: homeDIContainer.homeViewModel) + HomeView(dependency: appContainer.homeDIContainer) } .tag(0) NavigationStack { - CommunityView( - viewModel: communityDIContainer.makeCommunityViewModel(), - writeFactory: communityDIContainer, - detailFactory: communityDIContainer, - updateFactory: communityDIContainer, - reportFactory: communityDIContainer - ) + CommunityView(dependency: appContainer.communityDIContainer) } .tag(1) NavigationStack { MyPageView( - viewModel: mypageDIContainer.makeMyPageViewModel(), - activitesFactory: mypageDIContainer + viewModel: appContainer.mypageDIContainer.makeMyPageViewModel(), + activitesFactory: appContainer.mypageDIContainer, + dependency: appContainer.communityDIContainer ) } .tag(2) diff --git a/Hambug/HambugApp.swift b/Hambug/HambugApp.swift index 8917daf..a7ca78c 100644 --- a/Hambug/HambugApp.swift +++ b/Hambug/HambugApp.swift @@ -9,7 +9,7 @@ import SwiftUI import Managers import DesignSystem import KakaoLogin -import AppDI +import AppCoreDI @main struct HambugApp: App { diff --git a/Hambug/RootView.swift b/Hambug/RootView.swift index 6e081f7..40edd93 100644 --- a/Hambug/RootView.swift +++ b/Hambug/RootView.swift @@ -11,10 +11,7 @@ import Managers import Splash import Onboarding import LoginPresentation -import LoginDI -import AppDI -import IntroDI -import NetworkImpl +import AppCoreDI import Util import FCMService @@ -32,27 +29,19 @@ struct RootView: View { Group { switch appStateManager.currentState { case .splash: + SplashView( - viewModel: IntroDIContainer( - appContainer: appContainer, - appStateManager: appStateManager - ).resolve(SplashViewModel.self) + viewModel: appContainer.introDIContainer.splashViewModel ) case .onboarding: OnboardingView( - viewModel: IntroDIContainer( - appContainer: appContainer, - appStateManager: appStateManager - ).resolve(OnboardingViewModel.self) + viewModel: appContainer.introDIContainer.onboardingViewModel ) case .login: LoginView( - viewModel: LoginDIContainer( - appContainer: appContainer, - appStateManager: appStateManager - ).makeLoginViewModel() + viewModel: appContainer.loginDIContainer.makeLoginViewModel() ) case .main: diff --git a/Home/Package.swift b/Home/Package.swift index bd4e63a..70d958e 100644 --- a/Home/Package.swift +++ b/Home/Package.swift @@ -28,10 +28,17 @@ let package = Package( name: Config.name, targets: Config.allCases.map(\.name) ), + .library( + name: Config.di.name, + targets: [Config.di.name] + ), + .library( + name: Config.presentation.name, + targets: [Config.presentation.name] + ), ], dependencies: [ .package(name: "Common", path: "../Common"), - .package(name: "DI", path: "../DI"), .package(name: "Infrastructure", path: "../Infrastructure"), .package(name: "Community", path: "../Community"), .package(name: "Alarm", path: "../Alarm"), @@ -43,8 +50,7 @@ let package = Package( .target(config: .domain), .target(config: .data), .target(config: .presentation), - .product(name: "DI", package: "DI"), - .product(name: "AppDI", package: "DI"), + .product(name: "DIKit", package: "Common"), .product(name: "Managers", package: "Common"), .product(name: "DataSources", package: "Common"), ], diff --git a/Home/Sources/DI/HomeDIContainer.swift b/Home/Sources/DI/HomeDIContainer.swift index c3eab32..83a73e5 100644 --- a/Home/Sources/DI/HomeDIContainer.swift +++ b/Home/Sources/DI/HomeDIContainer.swift @@ -7,7 +7,6 @@ import Foundation import DIKit -import AppDI import Managers import DataSources import NetworkInterface @@ -48,10 +47,11 @@ public final class HomeDIContainer { private let container: GenericDIContainer // MARK: - Initialization - public init(appContainer: AppDIContainer = .shared) { - self.container = GenericDIContainer(parent: appContainer.baseContainer) + public init(appContainer: GenericDIContainer) { + self.container = appContainer HomeAssembly().assemble(container: container) } +} // MARK: - Factory Methods public var homeViewModel: HomeViewModel { diff --git a/Intro/Package.swift b/Intro/Package.swift index 19ffe3b..4db0864 100644 --- a/Intro/Package.swift +++ b/Intro/Package.swift @@ -1,40 +1,110 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +enum Config: String, CaseIterable { + static let name: String = "Intro" + + case di = "DI" + case onboarding = "Onboarding" + case splash = "Splash" + + var name: String { + switch self { + case .di: + return Config.name + rawValue + case .onboarding, .splash: + return rawValue + } + } + + var path: String { + "Sources/\(rawValue)" + } +} + let package = Package( - name: "Intro", - platforms: [ - .iOS(.v17) - ], - products: [ - .library( - name: "Onboarding", - targets: ["Onboarding"] - ), - .library( - name: "Splash", - targets: ["Splash"] - ), - ], - dependencies: [ - .package(name: "Common", path: "../Common") - ], - targets: [ - .target( - name: "Onboarding", - dependencies: [ - .product(name: "Managers", package: "Common"), - .product(name: "DesignSystem", package: "Common") - ] - ), - .target( - name: "Splash", - dependencies: [ - .product(name: "Managers", package: "Common"), - .product(name: "DesignSystem", package: "Common") - ], - ), - ] + name: Config.name, + platforms: [ + .iOS(.v17) + ], + products: [ + .library( + name: Config.di.name, + targets: [Config.di.name] + ), + .library( + name: Config.onboarding.name, + targets: [Config.onboarding.name] + ), + .library( + name: Config.splash.name, + targets: [Config.splash.name] + ), + ], + dependencies: [ + .package(name: "Common", path: "../Common") + ], + targets: [ + .target( + config: .di, + dependencies: [ + .target(config: .onboarding), + .target(config: .splash), + ] + ), + .target( + config: .onboarding, + dependencies: [ + .product(name: "Managers", package: "Common"), + .product(name: "DesignSystem", package: "Common") + ] + ), + .target( + config: .splash, + dependencies: [ + .product(name: "Managers", package: "Common"), + .product(name: "DesignSystem", package: "Common") + ], + ), + ] ) + +extension Target { + static func target( + config: Config, + dependencies: [Dependency] = [], + exclude: [String] = [], + sources: [String]? = nil, + resources: [Resource]? = nil, + publicHeadersPath: String? = nil, + packageAccess: Bool = false, + cSettings: [CSetting]? = nil, + cxxSettings: [CXXSetting]? = nil, + swiftSettings: [SwiftSetting]? = nil, + linkerSettings: [LinkerSetting]? = nil, + plugins: [PluginUsage]? = nil, + ) -> Target { + return .target( + name: config.name, + dependencies: dependencies, + path: config.path, + exclude: exclude, + sources: sources, + resources: resources, + publicHeadersPath: publicHeadersPath, + packageAccess: packageAccess, + cSettings: cSettings, + cxxSettings: cxxSettings, + swiftSettings: swiftSettings, + linkerSettings: linkerSettings, + plugins: plugins) + } +} + +extension Target.Dependency { + static func target(config: Config) -> Self { + return .target(name: config.name) + } +} diff --git a/Intro/Sources/DI/IntroDIContainer.swift b/Intro/Sources/DI/IntroDIContainer.swift new file mode 100644 index 0000000..66a91ca --- /dev/null +++ b/Intro/Sources/DI/IntroDIContainer.swift @@ -0,0 +1,50 @@ +// +// IntroDIContainer.swift +// Hambug +// +// Created by 강동영 on 12/18/25. +// + +import DIKit +import Splash +import Onboarding +import Managers + +// MARK: - Login Assembly +struct IntroAssembly: Assembly { + func assemble(container: GenericDIContainer) { + container.register(OnboardingViewModel.self) { resolver in + OnboardingViewModel( + appStateManager: resolver.resolve(AppStateManager.self) + ) + } + + container.register(SplashViewModel.self) { resolver in + SplashViewModel( + appStateManager: resolver.resolve(AppStateManager.self) + ) + } + } +} + +// MARK: - Login DI Container +public final class IntroDIContainer { + + // MARK: - Properties + private let container: GenericDIContainer + + // MARK: - Initialization + public init(appContainer: GenericDIContainer) { + self.container = appContainer + IntroAssembly().assemble(container: container) + } + + // MARK: - Factory + public var onboardingViewModel: OnboardingViewModel { + return container.resolve(OnboardingViewModel.self) + } + + public var splashViewModel: SplashViewModel { + return container.resolve(SplashViewModel.self) + } +} diff --git a/Login/Package.swift b/Login/Package.swift index 4f36f4b..c1bcd66 100644 --- a/Login/Package.swift +++ b/Login/Package.swift @@ -6,6 +6,7 @@ import PackageDescription enum Config: String, CaseIterable { static let name: String = "Login" + case di = "DI" case data = "Data" case domain = "Domain" case presentation = "Presentation" @@ -25,10 +26,17 @@ let package = Package( .iOS(.v17) ], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: Config.di.name, + targets: [Config.di.name] + ), .library( name: Config.name, - targets: Config.allCases.map(\.name) + targets: [ + Config.data.name, + Config.domain.name, + Config.presentation.name + ] ), ], dependencies: [ @@ -37,6 +45,17 @@ let package = Package( .package(name: "Infrastructure", path: "../Infrastructure") ], targets: [ + // Domain: 독립적 (외부 SDK만 의존) + .target( + name: Config.di.name, + dependencies: [ + .target(config: Config.domain), + .target(config: Config.data), + .target(config: Config.presentation), + ], + path: Config.di.path + ), + // Domain: 독립적 (외부 SDK만 의존) .target( name: Config.domain.name, diff --git a/Login/Sources/DI/LoginDIContainer.swift b/Login/Sources/DI/LoginDIContainer.swift new file mode 100644 index 0000000..d11719e --- /dev/null +++ b/Login/Sources/DI/LoginDIContainer.swift @@ -0,0 +1,64 @@ +// +// LoginDIContainer.swift +// Hambug +// +// Created by 강동영 on 12/18/25. +// + +import Foundation +import DIKit +import LoginDomain +import LoginData +import LoginPresentation +import DataSources +import Managers +import NetworkInterface + +// MARK: - Login Assembly +struct LoginAssembly: Assembly { + func assemble(container: GenericDIContainer) { + // Note: NetworkService and TokenStorage come from parent container + + // Repository registration + container.register(LoginRepository.self) { resolver in + LoginRepositoryImpl( + networkService: resolver.resolve(NetworkServiceInterface.self), + tokenStorage: resolver.resolve(JWTTokenStorageable.self), + userDefaultsManager: resolver.resolve(UserDefaultsManager.self) + ) + } + + // UseCase registration + container.register(LoginUseCase.self) { resolver in + LoginUseCaseImpl( + repository: resolver.resolve(LoginRepository.self) + ) + } + + // ViewModel registration + container.register(LoginViewModel.self) { resolver in + LoginViewModel( + useCase: resolver.resolve(LoginUseCase.self), + appStateManager: resolver.resolve(AppStateManager.self) + ) + } + } +} + +// MARK: - Login DI Container +public final class LoginDIContainer { + + // MARK: - Properties + private let container: GenericDIContainer + + // MARK: - Initialization + public init(appContainer: GenericDIContainer) { + self.container = appContainer + LoginAssembly().assemble(container: container) + } + + // MARK: - Factory Methods + public func makeLoginViewModel() -> LoginViewModel { + return container.resolve(LoginViewModel.self) + } +} diff --git a/MyPage/Package.swift b/MyPage/Package.swift index 5fc7542..198ac22 100644 --- a/MyPage/Package.swift +++ b/MyPage/Package.swift @@ -28,12 +28,19 @@ let package = Package( name: Config.name, targets: Config.allCases.map(\.name) ), + .library( + name: Config.di.name, + targets: [Config.di.name] + ), + .library( + name: Config.presentation.name, + targets: [Config.presentation.name] + ), ], dependencies: [ .package(name: "Common", path: "../Common"), .package(name: "Infrastructure", path: "../Infrastructure"), .package(name: "Community", path: "../Community"), - .package(name: "DI", path: "../DI"), ], targets: [ .target( @@ -44,8 +51,7 @@ let package = Package( .target(config: .presentation), .product(name: "NetworkInterface", package: "Infrastructure"), .product(name: "NetworkImpl", package: "Infrastructure"), - .product(name: "DIKit", package: "DI"), - .product(name: "AppDI", package: "DI"), + .product(name: "DIKit", package: "Common"), ], path: Config.di.path ), @@ -77,6 +83,7 @@ let package = Package( .product(name: "DesignSystem", package: "Common"), .product(name: "Community", package: "Community"), .product(name: "CommunityDomain", package: "Community"), + .product(name: "CommunityDI", package: "Community"), ], path: Config.presentation.path ), diff --git a/MyPage/Sources/DI/MyPageDIContainer.swift b/MyPage/Sources/DI/MyPageDIContainer.swift index ffc069b..e8393a0 100644 --- a/MyPage/Sources/DI/MyPageDIContainer.swift +++ b/MyPage/Sources/DI/MyPageDIContainer.swift @@ -6,7 +6,6 @@ // import DIKit -import AppDI import NetworkInterface import NetworkImpl import MyPageDomain @@ -68,9 +67,8 @@ public final class MyPageDIContainer { private let container: GenericDIContainer // MARK: - Initialization - public init(appContainer: AppDIContainer? = nil) { - let parent = appContainer ?? AppDIContainer.shared - self.container = AppDIContainer.shared.baseContainer + public init(appContainer: GenericDIContainer) { + self.container = appContainer MyPageAssembly().assemble(container: container) } From ed7986ecc20b5a07212861f4e6b06b4ea04103eb Mon Sep 17 00:00:00 2001 From: kangddong Date: Sun, 18 Jan 2026 03:35:49 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(refactor):=20=EB=8A=98=EC=96=B4?= =?UTF-8?q?=EB=82=98=EB=8A=94=20=EC=9D=98=EC=A1=B4=EC=84=B1=20dependency?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이후에 라우팅 프로토콜도 분리하여 의존성 분리 예정 --- Common/Sources/Managers/AppStateManager.swift | 8 +- .../Sources/DI/CommunityDIContainer.swift | 39 +++--- .../Community/CommunityView.swift | 113 ++++++++---------- .../Presentation/Detail/CommunityDetail.swift | 21 ++-- Home/Sources/DI/HomeDIContainer.swift | 27 +++-- Home/Sources/Presentation/HomeView.swift | 50 +++++--- .../Presentation/MyActivitiesView.swift | 43 +++---- .../Presentation/MyPage/MyPageView.swift | 12 +- 8 files changed, 164 insertions(+), 149 deletions(-) diff --git a/Common/Sources/Managers/AppStateManager.swift b/Common/Sources/Managers/AppStateManager.swift index badb8cf..0c01cc9 100644 --- a/Common/Sources/Managers/AppStateManager.swift +++ b/Common/Sources/Managers/AppStateManager.swift @@ -49,8 +49,10 @@ public final class AppStateManager { } public func completeSplash() { - if isOnboardingCompleted { - state = isLoginCompleted ? .main : .login + if isLoginCompleted { + state = .main + } else if isOnboardingCompleted { + state = .main } else { state = .onboarding } @@ -59,7 +61,7 @@ public final class AppStateManager { // 온보딩 완료 public func completeOnboarding() { udManager.isOnboardingCompleted = true - state = isLoginCompleted ? .main : .login + state = .login } // 로그인 완료 diff --git a/Community/Sources/DI/CommunityDIContainer.swift b/Community/Sources/DI/CommunityDIContainer.swift index 10c5a2f..8a05eac 100644 --- a/Community/Sources/DI/CommunityDIContainer.swift +++ b/Community/Sources/DI/CommunityDIContainer.swift @@ -14,6 +14,9 @@ import CommunityData import CommunityPresentation import Managers +import AlarmDI +import AlarmPresentation + struct CommunityWriteAssembly: Assembly { func assemble(container: GenericDIContainer) { container.register(CreateBoardUseCase.self) { resolver in @@ -106,36 +109,38 @@ public final class CommunityDIContainer { } } + +extension CommunityDIContainer: CommunityDependency { + public var alarmListComponent: any AlarmPresentation.AlarmListDependecy { + container.resolve(AlarmDIContainer.self) + } + + public var component: any CommunityPresentation.CommunityDetailDependency { + self + } + // MARK: - Factory Methods - @MainActor public func makeCommunityViewModel() -> CommunityViewModel { return container.resolve(CommunityViewModel.self) } -} - -extension CommunityDIContainer: CommunityWriteFactory { - public func makeWriteViewModel() -> any CommunityWriteViewModelProtocol { + + public func makeWriteViewModel() -> any CommunityPresentation.CommunityWriteViewModelProtocol { container.resolve(CommunityWriteViewModel.self) } } -extension CommunityDIContainer: CommunityDetailFactory { - @MainActor - public func makeDetailViewModel() -> CommunityDetailViewModel { - return container.resolve(CommunityDetailViewModel.self) +extension CommunityDIContainer: CommunityDetailDependency { + public func makeDetailViewModel() -> CommunityPresentation.CommunityDetailViewModel { + container.resolve(CommunityDetailViewModel.self) } -} - -extension CommunityDIContainer: UpdateBoardFactory { - public func makeViewModel(boardId: Int) -> CommunityWriteViewModelProtocol { - return UpdateBoardViewModel( + + public func makeViewModel(boardId: Int) -> any CommunityPresentation.CommunityWriteViewModelProtocol { + UpdateBoardViewModel( boardId: boardId, updateBoardUseCase: container.resolve(UpdateBoardUseCase.self) ) } -} - -extension CommunityDIContainer: ReportBoardFactory { + public func makeViewModel(req: ReportRequest) -> CommunityReportViewModel { return CommunityReportViewModel( usecase: container.resolve(ReportContentUseCase.self), diff --git a/Community/Sources/Presentation/Community/CommunityView.swift b/Community/Sources/Presentation/Community/CommunityView.swift index e38f542..7721632 100644 --- a/Community/Sources/Presentation/Community/CommunityView.swift +++ b/Community/Sources/Presentation/Community/CommunityView.swift @@ -9,6 +9,20 @@ import SwiftUI import DesignSystem import CommunityDomain import SharedUI +import AlarmPresentation + +public protocol CommunityDependency: CommunityFactory,CommunityWriteFactory { + var component: CommunityDetailDependency { get } + var alarmListComponent: AlarmListDependecy { get } +} + +public protocol CommunityDetailDependency: CommunityDetailFactory, UpdateBoardFactory, ReportBoardFactory { + +} + +public protocol CommunityFactory { + func makeCommunityViewModel() -> CommunityViewModel +} public protocol CommunityWriteFactory { func makeWriteViewModel() -> CommunityWriteViewModelProtocol @@ -20,23 +34,13 @@ public protocol CommunityDetailFactory { public struct CommunityView: View { @State private var viewModel: CommunityViewModel - private let writeFactory: CommunityWriteFactory - private let detailFactory: CommunityDetailFactory - private let updateFactory: UpdateBoardFactory - private let reportFactory: ReportBoardFactory + private let dependency: CommunityDependency public init( - viewModel: CommunityViewModel, - writeFactory: CommunityWriteFactory, - detailFactory: CommunityDetailFactory, - updateFactory: UpdateBoardFactory, - reportFactory: ReportBoardFactory + dependency: CommunityDependency, ) { - self._viewModel = State(initialValue: viewModel) - self.writeFactory = writeFactory - self.detailFactory = detailFactory - self.updateFactory = updateFactory - self.reportFactory = reportFactory + self._viewModel = State(initialValue: dependency.makeCommunityViewModel()) + self.dependency = dependency } public var body: some View { @@ -50,9 +54,13 @@ public struct CommunityView: View { VStack(spacing: 0) { // 헤더 - HeaderBar(type: .community) - .safeAreaPadding(.vertical, 18) - .safeAreaPadding(.horizontal, 15) + HeaderBar(type: .community) { + AlarmListView( + dependency: dependency.alarmListComponent + ) + } + .safeAreaPadding(.vertical, 18) + .safeAreaPadding(.horizontal, 15) VStack(spacing: 0) { // 카테고리 필터와 뷰 토글 @@ -73,19 +81,15 @@ public struct CommunityView: View { ZStack { if viewModel.isListView { CommunityListView( - boards: viewModel.filteredBoards, - detailFactory: detailFactory, - updateFactory: updateFactory, - reportFactory: reportFactory, - viewModel: viewModel + viewModel: viewModel, + dependency: dependency, + boards: viewModel.filteredBoards ) } else { CommunityFeedView( - boards: viewModel.filteredBoards, - detailFactory: detailFactory, - updateFactory: updateFactory, - reportFactory: reportFactory, - viewModel: viewModel + viewModel: viewModel, + dependency: dependency, + boards: viewModel.filteredBoards ) } } @@ -100,7 +104,7 @@ public struct CommunityView: View { Spacer() NavigationLink( destination: CommunityWriteView( - viewModel: writeFactory.makeWriteViewModel() + viewModel: dependency.makeWriteViewModel() ) ) { Color.bgPencil @@ -169,25 +173,18 @@ fileprivate struct CommunityFilterChip: View { // MARK: - List View public struct CommunityListView: View { + @State private var viewModel: CommunityViewModel + private let dependency: CommunityDependency let boards: [Board] - let detailFactory: CommunityDetailFactory - let updateFactory: UpdateBoardFactory - let reportFactory: ReportBoardFactory - @State private var viewModel: CommunityViewModel - public init( + viewModel: CommunityViewModel, + dependency: CommunityDependency, boards: [Board], - detailFactory: CommunityDetailFactory, - updateFactory: UpdateBoardFactory, - reportFactory: ReportBoardFactory, - viewModel: CommunityViewModel ) { - self.boards = boards - self.detailFactory = detailFactory - self.updateFactory = updateFactory - self.reportFactory = reportFactory self._viewModel = State(initialValue: viewModel) + self.dependency = dependency + self.boards = boards } public var body: some View { @@ -196,11 +193,10 @@ public struct CommunityListView: View { ForEach(Array(boards.enumerated()), id: \.element.id) { index, board in NavigationLink( destination: CommunityDetailView( - viewModel: detailFactory.makeDetailViewModel(), - boardId: board.id, - updateFactory: updateFactory, - reportFactory: reportFactory - )) { + dependency: dependency.component, + boardId: board.id + ) + ) { CommunityPostListCard(board: board) } .buttonStyle(PlainButtonStyle()) @@ -234,25 +230,18 @@ public struct CommunityListView: View { // MARK: - Feed View fileprivate struct CommunityFeedView: View { - let boards: [Board] - let detailFactory: CommunityDetailFactory - let updateFactory: UpdateBoardFactory - let reportFactory: ReportBoardFactory - @State private var viewModel: CommunityViewModel + private let dependency: CommunityDependency + let boards: [Board] init( + viewModel: CommunityViewModel, + dependency: CommunityDependency, boards: [Board], - detailFactory: CommunityDetailFactory, - updateFactory: UpdateBoardFactory, - reportFactory: ReportBoardFactory, - viewModel: CommunityViewModel ) { - self.boards = boards - self.detailFactory = detailFactory - self.updateFactory = updateFactory - self.reportFactory = reportFactory self._viewModel = State(initialValue: viewModel) + self.dependency = dependency + self.boards = boards } var body: some View { @@ -261,10 +250,8 @@ fileprivate struct CommunityFeedView: View { ForEach(Array(boards.enumerated()), id: \.element.id) { index, board in NavigationLink( destination: CommunityDetailView( - viewModel: detailFactory.makeDetailViewModel(), - boardId: board.id, - updateFactory: updateFactory, - reportFactory: reportFactory + dependency: dependency.component, + boardId: board.id ) ) { CommunityPostFeedCard(board: board) diff --git a/Community/Sources/Presentation/Detail/CommunityDetail.swift b/Community/Sources/Presentation/Detail/CommunityDetail.swift index 5adb4a9..d6ce55c 100644 --- a/Community/Sources/Presentation/Detail/CommunityDetail.swift +++ b/Community/Sources/Presentation/Detail/CommunityDetail.swift @@ -39,19 +39,14 @@ public struct CommunityDetailView: View { @State private var reportReason: String = "" private let boardId: Int - private let updateFactory: UpdateBoardFactory - private let reportFactory: ReportBoardFactory - + private let dependency: CommunityDetailDependency public init( - viewModel: CommunityDetailViewModel, - boardId: Int, - updateFactory: UpdateBoardFactory, - reportFactory: ReportBoardFactory, + dependency: CommunityDetailDependency, + boardId: Int ) { - _viewModel = State(initialValue: viewModel) + _viewModel = State(initialValue: dependency.makeDetailViewModel()) self.boardId = boardId - self.updateFactory = updateFactory - self.reportFactory = reportFactory + self.dependency = dependency } public var body: some View { @@ -132,7 +127,7 @@ public struct CommunityDetailView: View { Int64(comment.authorId) != currentUserId { NavigationLink( destination: CommunityReportView( - viewModel: reportFactory.makeViewModel( + viewModel: dependency.makeViewModel( req: CommunityDomain.ReportRequest.init( targetId: comment.id, targetType: .comment, @@ -154,7 +149,7 @@ public struct CommunityDetailView: View { Int64(authorId) == currentUserId { NavigationLink( destination: CommunityWriteView( - viewModel: updateFactory.makeViewModel(boardId: boardId), + viewModel: dependency.makeViewModel(boardId: boardId), title: viewModel.board?.title ?? "", content: viewModel.board?.content ?? "" ) @@ -174,7 +169,7 @@ public struct CommunityDetailView: View { Int64(authorId) != currentUserId { NavigationLink( destination: CommunityReportView( - viewModel: reportFactory.makeViewModel( + viewModel: dependency.makeViewModel( req: CommunityDomain.ReportRequest.init( targetId: boardId, targetType: .board, diff --git a/Home/Sources/DI/HomeDIContainer.swift b/Home/Sources/DI/HomeDIContainer.swift index 83a73e5..ab392e1 100644 --- a/Home/Sources/DI/HomeDIContainer.swift +++ b/Home/Sources/DI/HomeDIContainer.swift @@ -14,7 +14,12 @@ import HomeDomain import HomeData import HomePresentation +import CommunityPresentation +import AlarmPresentation + import SwiftUI +import CommunityDI +import AlarmDI // MARK: - Home Assembly struct HomeAssembly: Assembly { @@ -53,16 +58,20 @@ public final class HomeDIContainer { } } - // MARK: - Factory Methods - public var homeViewModel: HomeViewModel { - return container.resolve(HomeViewModel.self) +extension HomeDIContainer: Homedependency { + public var component: any CommunityPresentation.CommunityDetailDependency { + container.resolve(CommunityDIContainer.self) } - - public func resolve(_ type: T.Type) -> T { - return container.resolve(type) + + public var alarmListComponent: any AlarmPresentation.AlarmListDependecy { + container.resolve(AlarmDIContainer.self) + } + + public func makeHomeViewModel() -> HomePresentation.HomeViewModel { + container.resolve(HomeViewModel.self) } } -#Preview { - HomeView(viewModel: HomeDIContainer.init().homeViewModel) -} +//#Preview { +// HomeView(viewModel: HomeDIContainer.init().homeViewModel) +//} diff --git a/Home/Sources/Presentation/HomeView.swift b/Home/Sources/Presentation/HomeView.swift index d2a1207..3b59a29 100644 --- a/Home/Sources/Presentation/HomeView.swift +++ b/Home/Sources/Presentation/HomeView.swift @@ -9,25 +9,41 @@ import SwiftUI import HomeDomain import DesignSystem import SharedUI -import CommunityDI import CommunityPresentation +import AlarmPresentation + +public protocol Homedependency: HomeFactory { + var component: CommunityDetailDependency { get } + var alarmListComponent: AlarmListDependecy { get } +} +public protocol HomeFactory { + func makeHomeViewModel() -> HomeViewModel +} public struct HomeView: View { @State private var viewModel: HomeViewModel - - public init(viewModel: HomeViewModel) { - self._viewModel = State(initialValue: viewModel) + private let dependency: Homedependency + + public init( + dependency: Homedependency, + ) { + self.dependency = dependency + self._viewModel = State(initialValue: dependency.makeHomeViewModel()) } - + public var body: some View { ZStack { Color.bgG100 .ignoresSafeArea(.container, edges: .top) VStack { - HeaderBar(type: .home) - .safeAreaPadding(18) + HeaderBar(type: .home) { + AlarmListView( + dependency: dependency.alarmListComponent + ) + } + .safeAreaPadding(18) ScrollView { SuggestView(burgers: viewModel.recommendedBurgers) @@ -37,7 +53,10 @@ public struct HomeView: View { Spacer() .frame(height: 30) - PopularPostsView(postItems: viewModel.trendingPosts) + PopularPostsView( + postItems: viewModel.trendingPosts, + dependency: dependency + ) .padding(.horizontal, 18) } @@ -53,11 +72,11 @@ public struct HomeView: View { struct PopularPostsView: View { private let postItems: [TrendingPost] - private let communityDIContainer: CommunityDIContainer + private let dependency: Homedependency - init(postItems: [TrendingPost]) { + init(postItems: [TrendingPost], dependency: Homedependency) { self.postItems = postItems - self.communityDIContainer = .init(appContainer: .shared) + self.dependency = dependency } var body: some View { @@ -68,11 +87,10 @@ struct PopularPostsView: View { ForEach(postItems) { post in NavigationLink( destination: CommunityDetailView( - viewModel: communityDIContainer.makeDetailViewModel(), - boardId: post.id, - updateFactory: communityDIContainer, - reportFactory: communityDIContainer - )) { + dependency: dependency.component, + boardId: post.id + ) + ) { PostView(post: post) } } diff --git a/MyPage/Sources/Presentation/MyActivitiesView.swift b/MyPage/Sources/Presentation/MyActivitiesView.swift index 185eacb..9bb9d8a 100644 --- a/MyPage/Sources/Presentation/MyActivitiesView.swift +++ b/MyPage/Sources/Presentation/MyActivitiesView.swift @@ -16,11 +16,14 @@ import CommunityPresentation public struct MyActivitiesView: View { @State private var viewModel: MyActivitiesViewModel - private let communityDIContainer: CommunityDIContainer + private let dependency: CommunityDependency - public init(viewModel: MyActivitiesViewModel) { + public init( + viewModel: MyActivitiesViewModel, + dependency: CommunityDependency + ) { self._viewModel = State(initialValue: viewModel) - self.communityDIContainer = .init(appContainer: .shared) + self.dependency = dependency } public var body: some View { @@ -68,17 +71,15 @@ public struct MyActivitiesView: View { private var contentView: some View { if viewModel.selectedTab == .posts { CommunityListView( - boards: viewModel.myBoards, - detailFactory: communityDIContainer, - updateFactory: communityDIContainer, - reportFactory: communityDIContainer, - viewModel: communityDIContainer.makeCommunityViewModel() + viewModel: dependency.makeCommunityViewModel(), + dependency: dependency, + boards: viewModel.myBoards ) .padding(.horizontal, 20) .padding(.vertical, 12) } else { MyCommentsListView( - communityDIContainer: communityDIContainer, + dependency: dependency.component, comments: viewModel.myComments, isLoadingMore: viewModel.isLoadingMoreComments, onLoadMore: { index in @@ -188,7 +189,7 @@ fileprivate struct MyBoardListCard: View { // MARK: - 댓글 리스트 뷰 struct MyCommentsListView: View { - let communityDIContainer: CommunityDIContainer + let dependency: CommunityDetailDependency let comments: [MyCommentActivity] let isLoadingMore: Bool let onLoadMore: (Int) -> Void @@ -198,12 +199,12 @@ struct MyCommentsListView: View { LazyVStack(spacing: 0) { ForEach(Array(comments.enumerated()), id: \.element.id) { index, comment in NavigationLink( - destination: CommunityDetailView( - viewModel: communityDIContainer.makeDetailViewModel(), - boardId: Int(comment.boardId), - updateFactory: communityDIContainer, - reportFactory: communityDIContainer - )) { + destination: + CommunityDetailView( + dependency: dependency, + boardId: Int(comment.boardId) + ) + ) { MyCommentActivityCard(comment: comment) } .buttonStyle(PlainButtonStyle()) @@ -290,16 +291,6 @@ struct HambugNavigationView: View { } } -#Preview { - // Preview requires mocked dependencies - MyActivitiesView( - viewModel: MyActivitiesViewModel( - getMyBoardsUseCase: MockGetMyBoardsUseCase(), - getMyCommentsUseCase: MockGetMyCommentsUseCase() - ) - ) -} - // MARK: - Mock UseCases for Preview private final class MockGetMyBoardsUseCase: GetMyBoardsUseCase { func execute(lastId: Int?, limit: Int, order: String) async throws -> BoardListData { diff --git a/MyPage/Sources/Presentation/MyPage/MyPageView.swift b/MyPage/Sources/Presentation/MyPage/MyPageView.swift index af63fb1..cb3a23b 100644 --- a/MyPage/Sources/Presentation/MyPage/MyPageView.swift +++ b/MyPage/Sources/Presentation/MyPage/MyPageView.swift @@ -9,6 +9,8 @@ import SwiftUI import PhotosUI import DesignSystem import Util +import CommunityDI +import CommunityPresentation public protocol ActivitesFactory { func makeMyActivitiesViewModel() -> MyActivitiesViewModel @@ -28,13 +30,16 @@ public struct MyPageView: View { @State private var showPhotoPicker: Bool = false private let activitesFactory: ActivitesFactory + private let dependency: CommunityDependency public init( viewModel: MyPageViewModel, - activitesFactory: ActivitesFactory + activitesFactory: ActivitesFactory, + dependency: CommunityDependency ) { self._viewModel = Bindable(viewModel) self.activitesFactory = activitesFactory + self.dependency = dependency } public var body: some View { @@ -52,7 +57,10 @@ public struct MyPageView: View { Spacer() } .navigationDestination(isPresented: $showMyActivitiesView, destination: { - MyActivitiesView(viewModel: activitesFactory.makeMyActivitiesViewModel()) + MyActivitiesView( + viewModel: activitesFactory.makeMyActivitiesViewModel(), + dependency: dependency + ) }) } .confirmationDialog("프로필 편집", isPresented: $showInfoActionSheet, actions: { From f231d3fbdfc5392f69c115c315fc715814f932f7 Mon Sep 17 00:00:00 2001 From: KANG DONGYEONG Date: Sun, 18 Jan 2026 03:50:38 +0900 Subject: [PATCH 7/7] Update iOS Simulator destination OS version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1230b26..e2cb683 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,5 +40,5 @@ jobs: -scheme "$SCHEME" \ -configuration Debug \ -skipMacroValidation \ - -destination "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.4" \ + -destination "platform=iOS Simulator,name=iPhone 16 Pro,OS=26.2" \ clean build