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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Alarm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
117 changes: 117 additions & 0 deletions Alarm/Package.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
61 changes: 61 additions & 0 deletions Alarm/Sources/DI/AlarmDIContainer.swift
Original file line number Diff line number Diff line change
@@ -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)
//}
56 changes: 56 additions & 0 deletions Alarm/Sources/Data/AlarmAPI.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
28 changes: 28 additions & 0 deletions Alarm/Sources/Data/AlarmRepositoryImpl.swift
Original file line number Diff line number Diff line change
@@ -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<NotificationListDataDTO>.self
)
.map(\.data)
.map { $0.toDomain() }
.async()
}
}
81 changes: 81 additions & 0 deletions Alarm/Sources/Data/NotificationDTO.swift
Original file line number Diff line number Diff line change
@@ -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"
}
Loading
Loading