Skip to content

Commit ef20e88

Browse files
committed
feat: 알림화면 개발
1 parent 1ed5292 commit ef20e88

15 files changed

Lines changed: 589 additions & 0 deletions

Alarm/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Alarm/Package.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
enum Config: String, CaseIterable {
7+
static let name: String = "Alarm"
8+
9+
case di = "DI"
10+
case data = "Data"
11+
case domain = "Domain"
12+
case presentation = "Presentation"
13+
14+
var name: String {
15+
Config.name + rawValue
16+
}
17+
18+
var path: String {
19+
"Sources/\(rawValue)"
20+
}
21+
}
22+
23+
let package = Package(
24+
name: Config.name,
25+
platforms: [.iOS(.v17)],
26+
products: [
27+
.library(
28+
name: Config.name,
29+
targets: Config.allCases.map(\.name)
30+
),
31+
.library(
32+
name: Config.di.name,
33+
targets: [
34+
Config.di.name,
35+
Config.presentation.name
36+
]
37+
)
38+
],
39+
dependencies: [
40+
.package(name: "DI", path: "../DI"),
41+
.package(name: "Common", path: "../Common"),
42+
.package(name: "Infrastructure", path: "../Infrastructure"),
43+
],
44+
targets: [
45+
.target(
46+
config: .di,
47+
dependencies: [
48+
.target(config: .domain),
49+
.target(config: .data),
50+
.target(config: .presentation),
51+
.product(name: "DI", package: "DI"),
52+
.product(name: "AppDI", package: "DI"),
53+
.product(name: "NetworkInterface", package: "Infrastructure"),
54+
],
55+
),
56+
// Domain: 독립적 (외부 의존성 없음)
57+
.target(config: .domain),
58+
59+
// Data
60+
.target(
61+
config: .data,
62+
dependencies: [
63+
.target(config: .domain),
64+
.product(name: "NetworkInterface", package: "Infrastructure"),
65+
.product(name: "Util", package: "Common"),
66+
]
67+
),
68+
69+
// Presentation: Domain, DesignSystem, Layout에 의존
70+
.target(
71+
config: .presentation,
72+
dependencies: [
73+
.target(config: .domain),
74+
.product(name: "DesignSystem", package: "Common"),
75+
],
76+
)
77+
78+
]
79+
)
80+
81+
extension Target {
82+
static func target(
83+
config: Config,
84+
dependencies: [Dependency] = [],
85+
exclude: [String] = [],
86+
sources: [String]? = nil,
87+
resources: [Resource]? = nil,
88+
publicHeadersPath: String? = nil,
89+
packageAccess: Bool = false,
90+
cSettings: [CSetting]? = nil,
91+
cxxSettings: [CXXSetting]? = nil,
92+
swiftSettings: [SwiftSetting]? = nil,
93+
linkerSettings: [LinkerSetting]? = nil,
94+
plugins: [PluginUsage]? = nil,
95+
) -> Target {
96+
return .target(
97+
name: config.name,
98+
dependencies: dependencies,
99+
path: config.path,
100+
exclude: exclude,
101+
sources: sources,
102+
resources: resources,
103+
publicHeadersPath: publicHeadersPath,
104+
packageAccess: packageAccess,
105+
cSettings: cSettings,
106+
cxxSettings: cxxSettings,
107+
swiftSettings: swiftSettings,
108+
linkerSettings: linkerSettings,
109+
plugins: plugins)
110+
}
111+
}
112+
113+
extension Target.Dependency {
114+
static func target(config: Config) -> Self {
115+
return .target(name: config.name)
116+
}
117+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// AlarmDIContainer.swift
3+
// Alarm
4+
//
5+
// Created by 강동영 on 10/17/25.
6+
//
7+
8+
import Foundation
9+
import DIKit
10+
import AppDI
11+
import NetworkInterface
12+
import AlarmDomain
13+
import AlarmData
14+
import AlarmPresentation
15+
16+
struct AlarmAssembly: Assembly {
17+
18+
func assemble(container: GenericDIContainer) {
19+
// Repository registration
20+
container.register(AlarmRepository.self) { resolver in
21+
AlarmRepositoryImpl(
22+
networkService: resolver.resolve(NetworkServiceInterface.self)
23+
)
24+
}
25+
26+
container.register(GetAlarmListUseCase.self) { resolver in
27+
GetAlarmListUseCaseImpl(
28+
repository: resolver.resolve(AlarmRepository.self)
29+
)
30+
}
31+
32+
container.register(AlarmListViewModel.self) { resolver in
33+
AlarmListViewModel(
34+
usecase: resolver.resolve(GetAlarmListUseCase.self)
35+
)
36+
}
37+
}
38+
}
39+
40+
// MARK: - Alarm DI Container
41+
public final class AlarmDIContainer {
42+
43+
// MARK: - Properties
44+
private let container: GenericDIContainer
45+
46+
// MARK: - Initialization
47+
public init(appContainer: GenericDIContainer) {
48+
self.container = appContainer
49+
AlarmAssembly().assemble(container: container)
50+
}
51+
}
52+
53+
extension AlarmDIContainer: AlarmListDependecy {
54+
public func makeAlarmListViewModel() -> AlarmListViewModel {
55+
return container.resolve(AlarmListViewModel.self)
56+
}
57+
}
58+
//#Preview {
59+
// let diContainer = CommunityDIContainer()
60+
// CommunityView(viewModel: diContainer.makeCommunityViewModel(), diContainer: diContainer)
61+
//}

Alarm/Sources/Data/AlarmAPI.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// AlarmEndpoint.swift
3+
// Hambug
4+
//
5+
// Created by 강동영 on 10/17/25.
6+
//
7+
8+
import Foundation
9+
import NetworkInterface
10+
11+
struct AlarmEndpoint: Endpoint {
12+
var baseURL: String = NetworkConfig.baseURL + "/api/v1"
13+
var path: String = "/notifications"
14+
var method: NetworkInterface.HTTPMethod = .GET
15+
var headers: [String : String] = [:]
16+
var queryParameters: [String : Any] = [:]
17+
var body: Data? = nil
18+
}
19+
public enum BoardEndpoint: Endpoint {
20+
case fetchNotificaitons(CursorPagingQuery)
21+
22+
public var baseURL: String {
23+
return NetworkConfig.baseURL + "/api/v1"
24+
}
25+
26+
public var path: String {
27+
switch self {
28+
case .fetchNotificaitons:
29+
return "/notifications"
30+
}
31+
}
32+
33+
public var method: HTTPMethod {
34+
switch self {
35+
case .fetchNotificaitons:
36+
return .GET
37+
}
38+
}
39+
40+
public var headers: [String: String] {
41+
return [:]
42+
}
43+
44+
public var queryParameters: [String: Any] {
45+
switch self {
46+
case let .fetchNotificaitons(dto):
47+
return queryEncoder.encode(dto)
48+
default:
49+
return [:]
50+
}
51+
}
52+
53+
public var body: Data? {
54+
return nil
55+
}
56+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// AlarmRepositoryImpl.swift
3+
// Alarm
4+
//
5+
// Created by 강동영 on 1/17/26.
6+
//
7+
8+
import AlarmDomain
9+
import NetworkInterface
10+
import Util
11+
12+
public final class AlarmRepositoryImpl: AlarmRepository {
13+
private let networkService: NetworkServiceInterface
14+
15+
public init(networkService: NetworkServiceInterface) {
16+
self.networkService = networkService
17+
}
18+
19+
public func fetchAlarms() async throws -> NotificationListData {
20+
return try await networkService.request(
21+
AlarmEndpoint(),
22+
responseType: SuccessResponse<NotificationListDataDTO>.self
23+
)
24+
.map(\.data)
25+
.map { $0.toDomain() }
26+
.async()
27+
}
28+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// NotificationDTO.swift
3+
// Alarm
4+
//
5+
// Created by 강동영 on 1/16/26.
6+
//
7+
8+
import Foundation
9+
import AlarmDomain
10+
11+
public struct NotificationListDataDTO: Decodable, Sendable {
12+
public let content: [NotificationResponseDTO]
13+
public let lastId: Int?
14+
public let hasNext: Bool
15+
16+
private enum CodingKeys: String, CodingKey {
17+
case content
18+
case lastId
19+
case hasNext
20+
}
21+
}
22+
23+
extension NotificationListDataDTO {
24+
func toDomain() -> NotificationListData {
25+
return NotificationListData(
26+
content: content.map { $0.toDomain()},
27+
lastId: lastId,
28+
hasNext: hasNext
29+
)
30+
}
31+
}
32+
33+
public struct NotificationResponseDTO: Decodable, Sendable {
34+
let id: Int64
35+
let title: String
36+
let content: String
37+
let type: NotificationType
38+
let targetId: Int64
39+
let thumbnailUrl: String?
40+
let isRead: Bool
41+
let createdAt: String
42+
43+
func toDomain() -> AlarmPayload {
44+
let dateFormatter = DateFormatter.iso8601WithMicroseconds
45+
46+
return .init(
47+
id: id,
48+
date: Self.timeAgoDisplay(dateFormatter.date(from: createdAt) ?? Date()),
49+
content: "\(title)\(content)",
50+
imageUrl: thumbnailUrl
51+
)
52+
}
53+
54+
private static func timeAgoDisplay(_ date: Date) -> String {
55+
let now = Date()
56+
let timeInterval = now.timeIntervalSince(date)
57+
58+
if timeInterval < 60 {
59+
return "방금 전"
60+
} else if timeInterval < 3600 {
61+
let minutes = Int(timeInterval / 60)
62+
return "\(minutes)분 전"
63+
} else if timeInterval < 86400 {
64+
let hours = Int(timeInterval / 3600)
65+
return "\(hours)시간 전"
66+
} else if timeInterval < 604800 {
67+
let days = Int(timeInterval / 86400)
68+
return "\(days)일 전"
69+
} else {
70+
let formatter = DateFormatter()
71+
formatter.dateFormat = "MM.dd"
72+
return formatter.string(from: date)
73+
}
74+
}
75+
}
76+
77+
enum NotificationType: String, Decodable {
78+
case login = "LOGIN_COMPLETE"
79+
case comment = "COMMENT_NOTIFICATION"
80+
case like = "LIKE_NOTIFICATION"
81+
}

0 commit comments

Comments
 (0)