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
152 changes: 144 additions & 8 deletions Wable-iOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Wable-iOS/Data/Mapper/NotificationMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
import Foundation

enum NotificationMapper {
static func toDomain(_ dtos: [DTO.Response.FetchInfoNotifications]) -> [InfoNotification] {
static func toDomain(_ dtos: [DTO.Response.FetchInfoNotifications]) -> [InformationNotification] {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(abbreviation: "KST")

return dtos.compactMap { dto in
InfoNotification(
InformationNotification(
id: dto.infoNotificationID,
type: InfoNotificationType(rawValue: dto.infoNotificationType),
type: InformationNotificationType(rawValue: dto.infoNotificationType),
time: dateFormatter.date(from: dto.time),
imageURL: URL(string: dto.imageURL)
)
Expand All @@ -34,7 +34,7 @@ enum NotificationMapper {
triggerID: dto.notificationTriggerID,
type: TriggerType.ActivityNotification(rawValue: dto.notificationTriggerType),
time: dateFormatter.date(from: dto.time),
text: dto.notificationText,
targetContentText: dto.notificationText,
userID: dto.memberID,
userNickname: dto.memberNickname,
triggerUserID: dto.triggerMemberID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class NotificationRepositoryImpl: NotificationRepository {
self.provider = provider
}

func fetchInfoNotifications(cursor: Int) -> AnyPublisher<[InfoNotification], WableError> {
func fetchInfoNotifications(cursor: Int) -> AnyPublisher<[InformationNotification], WableError> {
return provider.request(
.fetchInfoNotifications(cursor: cursor),
for: [DTO.Response.FetchInfoNotifications].self
Expand Down
6 changes: 3 additions & 3 deletions Wable-iOS/Domain/Entity/Notification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct ActivityNotification: Identifiable, Hashable {
let triggerID: Int
let type: TriggerType.ActivityNotification?
let time: Date?
let text: String
let targetContentText: String
let userID: Int
let userNickname: String
let triggerUserID: Int
Expand All @@ -26,9 +26,9 @@ struct ActivityNotification: Identifiable, Hashable {

// MARK: - 정보 푸쉬 알림

struct InfoNotification: Identifiable, Hashable {
struct InformationNotification: Identifiable, Hashable {
let id: Int
let type: InfoNotificationType?
let type: InformationNotificationType?
let time: Date?
let imageURL: URL?
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// InfoNotificationType.swift
// InformationNotificationType.swift
// Wable-iOS
//
// Created by 김진웅 on 3/1/25.
Expand All @@ -9,7 +9,7 @@ import Foundation

// MARK: - 정보 알림에 대한 종류

enum InfoNotificationType: String {
enum InformationNotificationType: String {
case gameDone = "GAMEDONE"
case gameStart = "GAMESTART"
case weekDone = "WEEKDONE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Combine
import Foundation

protocol NotificationRepository {
func fetchInfoNotifications(cursor: Int) -> AnyPublisher<[InfoNotification], WableError>
func fetchInfoNotifications(cursor: Int) -> AnyPublisher<[InformationNotification], WableError>
func checkNotification() -> AnyPublisher<Void, WableError>
func fetchUserNotifications(cursor: Int) -> AnyPublisher<[ActivityNotification], WableError>
func fetchUncheckedNotificationNumber() -> AnyPublisher<Int, WableError>
Expand Down
122 changes: 122 additions & 0 deletions Wable-iOS/Domain/UseCase/Notification/NotificationUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,125 @@
// Created by YOUJIM on 3/6/25.
//

import Combine
import Foundation

// MARK: - NotificationUseCase

protocol NotificationUseCase {
func fetchActivityNotifications(for lastItemID: Int) -> AnyPublisher<[ActivityNotification], WableError>
func fetchInformationNotifications(for lastItemID: Int) -> AnyPublisher<[InformationNotification], WableError>
}

// MARK: - NotificationUseCaseImpl

final class NotificationUseCaseImpl: NotificationUseCase {
private let notificationRepository: NotificationRepository

init(notificationRepository: NotificationRepository) {
self.notificationRepository = notificationRepository
}

func fetchActivityNotifications(for lastItemID: Int) -> AnyPublisher<[ActivityNotification], WableError> {
_ = notificationRepository.checkNotification()

return notificationRepository.fetchUserNotifications(cursor: lastItemID)
.eraseToAnyPublisher()
}

func fetchInformationNotifications(for lastItemID: Int) -> AnyPublisher<[InformationNotification], WableError> {
return notificationRepository.fetchInfoNotifications(cursor: lastItemID)
.eraseToAnyPublisher()
}
}

// MARK: - MockNotificationUseCaseImpl

struct MockNotificationUseCaseImpl: NotificationUseCase {
private var randomDelaySecond: Double { .random(in: 0.3...1.0) }

func fetchActivityNotifications(for lastItemID: Int) -> AnyPublisher<[ActivityNotification], WableError> {
let range = getPaginationRange(for: lastItemID)

if range == nil {
return emptyPublisher()
}

let types: [TriggerType.ActivityNotification] = [
.contentLike, .commentLike, .comment, .contentGhost,
.commentGhost, .beGhost, .actingContinue, .userBan,
.popularWriter, .popularContent, .childComment, .childCommentLike
]

return .just(range!.map { id in
ActivityNotification(
id: id,
triggerID: Int.random(in: 100...1000),
type: types.randomElement(),
time: getRelativeDate(for: id),
targetContentText: "샘플 콘텐츠 \(id)",
userID: Int.random(in: 1...100),
userNickname: "사용자\(id)",
triggerUserID: Int.random(in: 1...100),
triggerUserNickname: "트리거사용자\(id)",
triggerUserProfileURL: getSampleImageURL(),
isChecked: Bool.random(),
isDeletedUser: Bool.random()
)
})
.delay(for: .seconds(randomDelaySecond), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}

func fetchInformationNotifications(for lastItemID: Int) -> AnyPublisher<[InformationNotification], WableError> {
let range = getPaginationRange(for: lastItemID, maxPage: 33)

if range == nil {
return emptyPublisher()
}

let types: [InformationNotificationType] = [.gameDone, .gameStart, .weekDone]

return .just(range!.map { id in
InformationNotification(
id: id,
type: types.randomElement(),
time: getRelativeDate(for: id),
imageURL: getSampleImageURL()
)
})
.delay(for: .seconds(randomDelaySecond), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}

private func getPaginationRange(for lastItemID: Int, maxPage: Int = 33) -> ClosedRange<Int>? {
switch lastItemID {
case -1:
return 1...15
case 15:
return 16...30
case 30:
return 31...maxPage
default:
return nil
}
}

private func getRelativeDate(for id: Int) -> Date? {
return Calendar.current.date(byAdding: .day, value: -id, to: Date())
}

private func getSampleImageURL() -> URL? {
return URL(string: Constant.imageURLText)
}

private func emptyPublisher<T>() -> AnyPublisher<[T], WableError> {
return .just([])
.delay(for: .seconds(randomDelaySecond), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}

private enum Constant {
static let imageURLText: String = "https://private-user-images.githubusercontent.com/80394340/349682631-566a0a8c-c673-4650-b9f4-3b74d7443aa9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDMzODUwNjMsIm5iZiI6MTc0MzM4NDc2MywicGF0aCI6Ii84MDM5NDM0MC8zNDk2ODI2MzEtNTY2YTBhOGMtYzY3My00NjUwLWI5ZjQtM2I3NGQ3NDQzYWE5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMzElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzMxVDAxMzI0M1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTRiMjJlZDIyOGU2M2E3NTBiMGQyMjUyNWI0MGQxYTk0ZGVkZmIyNWY2ZjY0YjVmZTQxNzdiMzQ0NzkxNTMzNmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.fslk0G5432-vBjha8bXJ6OAcCOusEowIPST_de3arwU"
}
Comment on lines +126 to +128
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace long GitHub URL with a more reliable placeholder.

The hardcoded GitHub URL is lengthy and might expire. Consider using a more permanent placeholder service URL.

private enum Constant {
-    static let imageURLText: String = "https://private-user-images.githubusercontent.com/80394340/349682631-566a0a8c-c673-4650-b9f4-3b74d7443aa9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDMzODUwNjMsIm5iZiI6MTc0MzM4NDc2MywicGF0aCI6Ii84MDM5NDM0MC8zNDk2ODI2MzEtNTY2YTBhOGMtYzY3My00NjUwLWI5ZjQtM2I3NGQ3NDQzYWE5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMzElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzMxVDAxMzI0M1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTRiMjJlZDIyOGU2M2E3NTBiMGQyMjUyNWI0MGQxYTk0ZGVkZmIyNWY2ZjY0YjVmZTQxNzdiMzQ0NzkxNTMzNmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.fslk0G5432-vBjha8bXJ6OAcCOusEowIPST_de3arwU"
+    static let imageURLText: String = "https://picsum.photos/200"
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private enum Constant {
static let imageURLText: String = "https://private-user-images.githubusercontent.com/80394340/349682631-566a0a8c-c673-4650-b9f4-3b74d7443aa9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDMzODUwNjMsIm5iZiI6MTc0MzM4NDc2MywicGF0aCI6Ii84MDM5NDM0MC8zNDk2ODI2MzEtNTY2YTBhOGMtYzY3My00NjUwLWI5ZjQtM2I3NGQ3NDQzYWE5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMzElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzMxVDAxMzI0M1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTRiMjJlZDIyOGU2M2E3NTBiMGQyMjUyNWI0MGQxYTk0ZGVkZmIyNWY2ZjY0YjVmZTQxNzdiMzQ0NzkxNTMzNmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.fslk0G5432-vBjha8bXJ6OAcCOusEowIPST_de3arwU"
}
private enum Constant {
static let imageURLText: String = "https://picsum.photos/200"
}

}
18 changes: 18 additions & 0 deletions Wable-iOS/Presentation/Helper/Extension/String+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,22 @@ extension String {
let nsAttributedString: NSAttributedString = self.pretendardString(with: style)
return AttributedString(nsAttributedString)
}

// MARK: - truncated

/// 문자열을 지정된 길이로 제한하고 필요한 경우 생략 부호를 추가합니다.
/// - Parameters:
/// - maxLength: 최대 문자 수
/// - appendEllipsis: 생략 부호 추가 여부
/// - Returns: 제한된 문자열
func truncated(toLength maxLength: Int, appendingEllipsis: Bool = true) -> String {
if count <= maxLength {
return self
}

let index = self.index(startIndex, offsetBy: maxLength)
let truncated = self[..<index]

return appendingEllipsis ? "\(truncated)..." : "\(truncated)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// ActivityNotification+.swift
// Wable-iOS
//
// Created by 김진웅 on 3/30/25.
//

import Foundation

extension ActivityNotification {
var message: String {
guard let type else {
return ""
}

switch type {
case .contentLike:
return "\(triggerUserNickname)님이 \(userNickname)님의 게시물을 좋아합니다."
case .commentLike:
return "\(triggerUserNickname)님이 \(userNickname)님의 댓글을 좋아합니다."
case .comment:
return "\(triggerUserNickname)님이 댓글을 작성했습니다."
case .contentGhost:
return "\(userNickname)님, 작성하신 게시글로 인해 점점 투명해지고 있어요."
case .commentGhost:
return "\(userNickname)님, 작성하신 댓글로 인해 점점 투명해지고 있어요."
case .beGhost:
return "\(userNickname)님, 투명해져서 당분간 글을 작성할 수 없어요."
case .actingContinue:
return "\(userNickname)님, 이제 글을 다시 작성할 수 있어요! 오랜만에 와블에 인사를 남겨주세요!"
case .userBan:
return "\(userNickname)님, 신고가 누적되어 작성하신 글이 블라인드 처리되었습니다. 자세한 내용은 문의사항으로 남겨주세요."
case .popularWriter:
return "\(userNickname)님이 작성하신 글이 인기글로 선정 되었어요!🥳🥳"
case .popularContent:
return "어제 가장 인기있던 글이에요."
case .childComment:
return "\(triggerUserNickname)님이 \(userNickname)님에게 대댓글을 작성했습니다."
case .childCommentLike:
return "\(triggerUserNickname)님이 \(userNickname)님의 대댓글을 좋아합니다."
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// ActivityNotificationTriggerType+.swift
// Wable-iOS
//
// Created by 김진웅 on 3/30/25.
//

import Foundation

extension TriggerType.ActivityNotification {
/// 알림 셀에서 프로필 이미지 뷰를 눌렀을 때, 상호작용이 필요한 경우를 정의합니다.
///
/// 이 Set에 포함된 알림 유형들은 사용자가 프로필 이미지를 탭했을 때
/// 해당 사용자의 프로필로 이동하거나 추가 정보를 표시하는 등의
/// 인터랙션이 필요한 알림 유형들입니다.
///
/// - 포함된 알림 유형:
/// - `.commentLike`: 댓글에 좋아요를 받은 경우
/// - `.contentLike`: 게시물에 좋아요를 받은 경우
/// - `.comment`: 게시물에 댓글을 받은 경우
/// - `.childComment`: 댓글에 대댓글을 받은 경우
/// - `.childCommentLike`: 대댓글에 좋아요를 받은 경우
///
/// - Note: 이 Set에 포함되지 않은 알림 유형(예: 시스템 알림)은
/// 프로필 이미지 탭 시 아무런 동작을 수행하지 않습니다.
static let profileInteractionTypes: Set<TriggerType.ActivityNotification> = [
.commentLike,
.contentLike,
.comment,
.childComment,
.childCommentLike
]

/// 알림 셀을 눌렀을 때, 게시물 상세 페이지로 이동해야 하는 경우를 정의합니다.
///
/// 이 Set에 포함된 알림 유형들은 셀을 탭했을 때 관련된 게시물로 이동해야 하는
/// 인터랙션이 필요한 알림 유형들입니다.
///
/// - 포함된 알림 유형:
/// - `.contentLike`: 게시물에 좋아요를 받은 경우
/// - `.comment`: 게시물에 댓글을 받은 경우
/// - `.commentLike`: 댓글에 좋아요를 받은 경우
/// - `.popularContent`: 어제 가장 인기 있었던 글의 경우
/// - `.popularWriter`: 인기글로 선정된 경우
/// - `.childComment`: 댓글에 대댓글을 받은 경우
/// - `.childCommentLike`: 대댓글에 좋아요를 받은 경우
/// - `.contentGhost`: 작성한 게시물로 인해 투명도가 낮아진 경우
/// - `.commentGhost`: 작성한 댓글로 인해 투명도가 낮아진 경우
/// - `.beGhost`: 작성 제한이 되버린 경우
static let contentTypes: Set<TriggerType.ActivityNotification> = [
.contentLike,
.comment,
.commentLike,
.popularContent,
.popularWriter,
.childComment,
.childCommentLike,
.contentGhost,
.commentGhost,
.beGhost
]

/// 알림 셀을 눌렀을 때, 글쓰기 페이지로 이동해야 하는 경우를 정의합니다.
///
/// 이 Set에 포함된 알림 유형들은 셀을 탭했을 때 글쓰기 게시물로 이동해야 하는
/// 인터랙션이 필요한 알림 유형들입니다.
///
/// - 포함된 알림 유형:
/// - `.actingContinue`: 작성 제한이 풀린 경우
static let writeContentTypes: Set<TriggerType.ActivityNotification> = [
.actingContinue
]

/// 알림 셀을 눌렀을 때, 구글폼으로 이동해야 하는 경우를 정의합니다.
///
/// 이 Set에 포함된 알림 유형들은 셀을 탭했을 때 구글폼으로 이동해야 하는
/// 인터랙션이 필요한 알림 유형들입니다.
///
/// - 포함된 알림 유형:
/// - `.userBan`: 유저가 밴이 된 경우
static let googleFormTypes: Set<TriggerType.ActivityNotification> = [
.userBan
]
}
Loading