Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

팝업 모듈 구현 #329

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
--funcattributes same-line
--typeattributes prev-line
--disable redundantLet
--extensionacl on-declarations
--maxwidth 119
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// APIClientError.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

import OpenAPIRuntime

public typealias APIClientError = ClientError
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ public enum APIClientKey: TestDependencyKey {
)
}

public extension DependencyValues {
var apiClient: any APIProtocol {
extension DependencyValues {
public var apiClient: any APIProtocol {
get { self[APIClientKey.self] }
set { self[APIClientKey.self] = newValue }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

enum LocalizedErrorCode: Int, LocalizedError {
public enum LocalizedErrorCode: Int, LocalizedError {
case serverFault = 0x0000
case noNetwork = 0x0001
case unknownError = 0x0002
Expand Down Expand Up @@ -86,7 +86,7 @@ enum LocalizedErrorCode: Int, LocalizedError {

case excessiveEmailVerificationRequest = 0xA000

var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .serverFault:
APIClientInterfaceStrings.errorDescriptionServerFault
Expand Down Expand Up @@ -146,7 +146,9 @@ enum LocalizedErrorCode: Int, LocalizedError {
APIClientInterfaceStrings.errorDescriptionCustomLecture
case .userHasNoFCMKey:
APIClientInterfaceStrings.errorDescriptionNoFCMKey
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound, .colorlistNotFound, .emailNotFound:
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound,
.colorlistNotFound,
.emailNotFound:
APIClientInterfaceStrings.errorDescriptionNotFound
case .cantChangeOthersTheme:
APIClientInterfaceStrings.errorDescriptionCantChangeTheme
Expand All @@ -171,7 +173,7 @@ enum LocalizedErrorCode: Int, LocalizedError {
}
}

var failureReason: String? {
public var failureReason: String? {
switch self {
case .serverFault:
APIClientInterfaceStrings.errorFailureReasonServerFault
Expand Down Expand Up @@ -231,7 +233,9 @@ enum LocalizedErrorCode: Int, LocalizedError {
APIClientInterfaceStrings.errorFailureReasonCustomLecture
case .userHasNoFCMKey:
APIClientInterfaceStrings.errorFailureReasonNoFCMKey
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound, .colorlistNotFound, .emailNotFound:
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound,
.colorlistNotFound,
.emailNotFound:
APIClientInterfaceStrings.errorFailureReasonNotFound
case .cantChangeOthersTheme:
APIClientInterfaceStrings.errorFailureReasonCantChangeTheme
Expand All @@ -256,7 +260,7 @@ enum LocalizedErrorCode: Int, LocalizedError {
}
}

var recoverySuggestion: String? {
public var recoverySuggestion: String? {
switch self {
case .serverFault:
APIClientInterfaceStrings.errorRecoverySuggestionServerFault
Expand Down Expand Up @@ -316,7 +320,9 @@ enum LocalizedErrorCode: Int, LocalizedError {
APIClientInterfaceStrings.errorRecoverySuggestionCustomLecture
case .userHasNoFCMKey:
APIClientInterfaceStrings.errorRecoverySuggestionNoFCMKey
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound, .colorlistNotFound, .emailNotFound:
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound,
.colorlistNotFound,
.emailNotFound:
APIClientInterfaceStrings.errorRecoverySuggestionNotFound
case .cantChangeOthersTheme:
APIClientInterfaceStrings.errorRecoverySuggestionCantChangeTheme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public enum AuthRepositoryKey: TestDependencyKey {
}()
}

public extension DependencyValues {
var authRepository: any AuthRepository {
extension DependencyValues {
public var authRepository: any AuthRepository {
get { self[AuthRepositoryKey.self] }
set { self[AuthRepositoryKey.self] = newValue }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public enum AuthSecureRepositoryKey: TestDependencyKey {
public static let testValue: any AuthSecureRepository = AuthSecureRepositorySpy()
}

public extension DependencyValues {
var authSecureRepository: any AuthSecureRepository {
extension DependencyValues {
public var authSecureRepository: any AuthSecureRepository {
get { self[AuthSecureRepositoryKey.self] }
set { self[AuthSecureRepositoryKey.self] = newValue }
}
Expand Down
4 changes: 2 additions & 2 deletions SNUTT/Modules/Feature/AuthInterface/Sources/AuthState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public enum AuthStateKey: TestDependencyKey {
}()
}

public extension DependencyValues {
var authState: any AuthState {
extension DependencyValues {
public var authState: any AuthState {
get { self[AuthStateKey.self] }
set { self[AuthStateKey.self] = newValue }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Localizable.strings
This file is used to manage localizable strings for the feature module in English.

How to use:
- Add your localized strings here in the format "key" = "value";
- The key is what you will use in your code, and the value is the translated string.

Examples:
"welcome_message" = "Welcome to the app!";
"error_message" = "An error has occurred. Please try again.";

Usage in code with Tuist synthesized strings:
let welcomeMessage = PopupStrings.welcomeMessage
let errorMessage = PopupStrings.errorMessage

Note:
- Ensure that each key is unique within this file.
- Add comments to provide context for translators.
*/

/* Add your translations below */

"popup.dontShowForWhile" = "당분간 보지 않기";
"popup.dismiss" = "닫기";
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Localizable.strings
This file is used to manage localizable strings for the feature module in English.

How to use:
- Add your localized strings here in the format "key" = "value";
- The key is what you will use in your code, and the value is the translated string.

Examples:
"welcome_message" = "Welcome to the app!";
"error_message" = "An error has occurred. Please try again.";

Usage in code with Tuist synthesized strings:
let welcomeMessage = PopupStrings.welcomeMessage
let errorMessage = PopupStrings.errorMessage

Note:
- Ensure that each key is unique within this file.
- Add comments to provide context for translators.
*/

/* Add your translations below */

"popup.dontShowForWhile" = "Hide for a while";
"popup.dismiss" = "Dismiss";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// PopupLocalRepository.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

protocol PopupLocalRepository: Sendable {
func fetchPopups() -> [LocalPopup]
func storePopups(_ popups: [LocalPopup])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// PopupServerRepository.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

import Spyable

@Spyable
protocol PopupServerRepository: Sendable {
func fetchPopups() async throws -> [ServerPopup]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// PopupUseCase.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

import Dependencies

struct PopupUseCase {
@Dependency(\.popupLocalRepository) private var localRepository
@Dependency(\.popupServerRepository) private var serverRepository

func fetchRecentPopupList() async throws -> [PopupModel] {
let serverPopups = try await serverRepository.fetchPopups()
let localPopups = localRepository.fetchPopups()
return merge(localPopups: localPopups, into: serverPopups)
}

func storePopupList(_ popups: [PopupModel]) {
let localPopups = popups.compactMap { $0.localPopup }
localRepository.storePopups(localPopups)
}

private func merge(localPopups: [LocalPopup], into serverPopups: [ServerPopup]) -> [PopupModel] {
let localPopupByKey = Dictionary(grouping: localPopups, by: { $0.key })
return serverPopups.map { serverPopup in
guard let localPopup = localPopupByKey[serverPopup.key]?.first else {
return .init(serverPopup: serverPopup)
}
if serverPopup.hiddenDays != localPopup.dismissInfo.hiddenDays {
return .init(serverPopup: serverPopup)
}
return .init(serverPopup: serverPopup, localPopup: localPopup)
}
}
}

struct PopupUseCaseKey: DependencyKey {
static let liveValue: PopupUseCase = .init()
}

extension DependencyValues {
var popupUseCase: PopupUseCase {
get { self[PopupUseCaseKey.self] }
set { self[PopupUseCaseKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// LocalPopup.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

import Foundation

struct LocalPopup: Codable, Identifiable, Sendable {
var id: String { key }
let key: String
let dismissInfo: DismissInfo

struct DismissInfo: Codable, Sendable {
let hiddenDays: Int?
let dismissedAt: Date
let dontShowForWhile: Bool
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// PopupModel.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

import Foundation
import FoundationUtility

struct PopupModel: Sendable, Identifiable {
var id: String { key }
let key: String

/// Indicates whether the popup is dismissed by the user in the current session.
var isDismissed = false

init(serverPopup: ServerPopup, localPopup: LocalPopup? = nil) {
key = serverPopup.key
self.serverPopup = serverPopup
self.localPopup = localPopup
}

var imageURL: URL? {
URL(string: serverPopup.imageUri)
}

private let serverPopup: ServerPopup
var localPopup: LocalPopup?

var shouldShow: Bool {
if isDismissed {
// The user dismissed the popup in the current session
return false
}
guard let dismissInfo = localPopup?.dismissInfo else {
// The user has never dismissed the popup yet
return true
}
if !dismissInfo.dontShowForWhile {
// The user dismissed the popup, but didn't ask to hide it for a while
return true
}
guard let hiddenDays = dismissInfo.hiddenDays else {
// If `hiddenDays` is nil, never show the popup again
return false
}
return Date().daysFrom(dismissInfo.dismissedAt) >= hiddenDays
}

mutating func markAsDismissed(dontShowForWhile: Bool) {
isDismissed = true
if serverPopup.hiddenDays == 0 {
// If `hiddenDays` is 0, always show the popup next time
localPopup = .init(
key: key,
dismissInfo: .init(hiddenDays: serverPopup.hiddenDays, dismissedAt: .now, dontShowForWhile: false)
)
return
}
localPopup = .init(
key: key,
dismissInfo: .init(
hiddenDays: serverPopup.hiddenDays,
dismissedAt: .now,
dontShowForWhile: dontShowForWhile
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ServerPopup.swift
// SNUTT
//
// Copyright © 2025 wafflestudio.com. All rights reserved.
//

import Foundation

struct ServerPopup: Sendable, Identifiable {
var id: String { key }
let key: String
let imageUri: String
let hiddenDays: Int?
}
Loading