diff --git a/TnT/Projects/Domain/Sources/Entity/AlarmItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/AlarmItemEntity.swift new file mode 100644 index 00000000..e1b5f064 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/AlarmItemEntity.swift @@ -0,0 +1,33 @@ +// +// AlarmItemEntity.swift +// Domain +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 알람 확인시 사용되는 알람 정보 구조체 +public struct AlarmItemEntity: Equatable { + /// 알람 id + public let alarmId: Int + /// 알람 타입 + public let alarmType: AlarmType + /// 알람 도착 시각 + public let alarmDate: Date + /// 알람 확인 여부 + public let alarmSeenBefore: Bool + + public init( + alarmId: Int, + alarmType: AlarmType, + alarmDate: Date, + alarmSeenBefore: Bool + ) { + self.alarmId = alarmId + self.alarmType = alarmType + self.alarmDate = alarmDate + self.alarmSeenBefore = alarmSeenBefore + } +} diff --git a/TnT/Projects/Domain/Sources/Entity/AlarmType.swift b/TnT/Projects/Domain/Sources/Entity/AlarmType.swift new file mode 100644 index 00000000..6759ddf5 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/AlarmType.swift @@ -0,0 +1,19 @@ +// +// AlarmType.swift +// Domain +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 앱에서 존재하는 알람 유형을 정의한 열거형 +public enum AlarmType: Equatable { + /// 트레이니 연결 완료 + case traineeConnected(name: String) + /// 트레이니 연결 해제 + case traineeDisconnected(name: String) + /// 트레이너 연결 해제 + case trainerDisconnected(name: String) +} diff --git a/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift b/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift index 9a9f72b6..1d32e4dd 100644 --- a/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift +++ b/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift @@ -22,6 +22,8 @@ public enum TDateFormat: String { case yyyyMMddSlash = "yyyy/MM/dd" /// "yyyy.MM.dd" case yyyyMMddDot = "yyyy.MM.dd" + /// ""M월 d일" + case M월_d일 = "M월 d일" /// "01월 10일 화요일" case MM월_dd일_EEEE = "MM월 dd일 EEEE" /// "EE" diff --git a/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckFeature.swift b/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckFeature.swift new file mode 100644 index 00000000..99e14f3c --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckFeature.swift @@ -0,0 +1,86 @@ +// +// AlarmCheckFeature.swift +// Presentation +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation +import ComposableArchitecture + +import Domain +import DesignSystem + +@Reducer +public struct AlarmCheckFeature { + + @ObservableState + public struct State: Equatable { + // MARK: Data related state + /// 유저 타입 + var userType: UserType + /// 알람 정보 목록 + var alarmList: [AlarmItemEntity] + + /// `AlarmCheckFeature.State`의 생성자 + /// - Parameters: + /// - userType: 유저 타입 + /// - alarmList: 유저에게 도착한 알람 목록 (기본값: `[]`) + public init( + userType: UserType, + alarmList: [AlarmItemEntity] = [] + ) { + self.userType = userType + self.alarmList = alarmList + } + } + + @Dependency(\.dismiss) private var dismiss + + public enum Action: Sendable, ViewAction { + /// 뷰에서 발생한 액션을 처리합니다. + case view(View) + /// 네비게이션 여부 설정 + case setNavigating + + @CasePathable + public enum View: Sendable, BindableAction { + /// 바인딩 액션 처리 + case binding(BindingAction) + /// 알람 아이템 탭 되었을 때 + case tapAlarmItem(Int) + /// 네비게이션 back 버튼 탭 되었을 때 + case tapNavBackButton + } + } + + public init() {} + + public var body: some ReducerOf { + BindingReducer(action: \.view) + + Reduce { state, action in + switch action { + case .view(let action): + switch action { + case .binding: + return .none + + case .tapAlarmItem(let id): + print("alarmId: \(id)") + return .none + + case .tapNavBackButton: + return .run { _ in + // TODO: 서버 API 명세 나오면 연결 + // 현재 모든 알람 확인 표시 + await self.dismiss() + } + } + case .setNavigating: + return .none + } + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift b/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift new file mode 100644 index 00000000..4eda7cba --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Alarm/AlarmCheckView.swift @@ -0,0 +1,94 @@ +// +// AlarmCheckView.swift +// Presentation +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import Domain +import DesignSystem + +/// 알람 목록을 입력하는 화면 +/// 유저에게 도착한 알람을 표시 - 유저 타입에 따라 분류 +@ViewAction(for: AlarmCheckFeature.self) +public struct AlarmCheckView: View { + + @Bindable public var store: StoreOf + @Environment(\.dismiss) var dismiss: DismissAction + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + VStack(spacing: 0) { + TNavigation( + type: .LButtonWithTitle(leftImage: .icnArrowLeft, centerTitle: "알림"), + leftAction: { send(.tapNavBackButton) } + ) + + ScrollView { + AlarmList() + Spacer() + } + } + .navigationBarBackButtonHidden(true) + } + + // MARK: - Sections + @ViewBuilder + private func AlarmList() -> some View { + VStack(spacing: 0) { + ForEach(store.alarmList, id: \.alarmId) { item in + AlarmListItem( + alarmTypeText: item.alarmTypeText, + alarmMainText: item.alarmMainText, + alarmTimeText: item.alarmDate.timeAgoDisplay(), + alarmSeenBefore: item.alarmSeenBefore + ) + } + } + } +} + +private extension AlarmCheckView { + struct AlarmListItem: View { + /// 연결 완료, 연결 해제 등 + let alarmTypeText: String + /// 알람 본문 + let alarmMainText: String + /// 알람 시각 + let alarmTimeText: String + /// 알람 확인 여부 + let alarmSeenBefore: Bool + + var body: some View { + HStack(spacing: 16) { + Image(.icnBombEmpty) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(alarmTypeText) + .typographyStyle(.label1Bold, with: .neutral400) + .padding(.bottom, 3) + Spacer() + Text(alarmTimeText) + .typographyStyle(.label1Medium, with: .neutral400) + } + + Text(alarmMainText) + .typographyStyle(.body2Medium, with: .neutral800) + } + } + .padding(20) + .background(alarmSeenBefore ? Color.common0 : Color.neutral100) + } + } +} diff --git a/TnT/Projects/Presentation/Sources/Extension/Date+.swift b/TnT/Projects/Presentation/Sources/Extension/Date+.swift new file mode 100644 index 00000000..deffb8b6 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Extension/Date+.swift @@ -0,0 +1,29 @@ +// +// Date+.swift +// Presentation +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +import Domain + +extension Date { + func timeAgoDisplay() -> String { + let now = Date() + let calendar = Calendar.current + let components = calendar.dateComponents([.second, .minute, .hour, .day], from: self, to: now) + + if let day = components.day, day >= 1 { + return TDateFormatUtility.formatter(for: .M월_d일).string(from: self) + } else if let hour = components.hour, hour >= 1 { + return "\(hour)시간 전" + } else if let minute = components.minute, minute >= 1 { + return "\(minute)분 전" + } else { + return "방금" + } + } +} diff --git a/TnT/Projects/Presentation/Sources/PresentationSupport/AlarmItemEntity.swift b/TnT/Projects/Presentation/Sources/PresentationSupport/AlarmItemEntity.swift new file mode 100644 index 00000000..7ff37725 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/PresentationSupport/AlarmItemEntity.swift @@ -0,0 +1,35 @@ +// +// AlarmItemEntity.swift +// Presentation +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Domain + +extension AlarmItemEntity { + /// 연결 완료, 연결 해제 등 + var alarmTypeText: String { + switch self.alarmType { + case .traineeConnected: + return "트레이니 연결 완료" + case .traineeDisconnected: + return "트레이니 연결 해제" + case .trainerDisconnected: + return "트레이너 연결 해제" + } + } + + /// 알람 본문 + var alarmMainText: String { + switch self.alarmType { + case .traineeConnected(let name): + return "\(name) 회원과 연결되었어요" + case .traineeDisconnected(let name): + return "\(name) 회원이 연결을 끊었어요" + case .trainerDisconnected(let name): + return "\(name) 트레이너가 연결을 끊었어요" + } + } +}