-
Notifications
You must be signed in to change notification settings - Fork 0
강제업데이트 로직 및 앱피처 모듈을 구성합니다. #314
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7ef1f6a
feat/#302: 강제업데이트 유즈케이스 생성 및 앱피처 모듈 생성
dongglehada 67f496d
style/#302: Apply SwiftLint autocorrect
github-actions[bot] 4c15e42
fix/#302: synchronize제거
dongglehada 9477a1e
fix/#302: nonisolated(unsafe) 제거
dongglehada 50ee87e
fix/#302: 파싱에러 전파 수정
dongglehada File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "permissions": { | ||
| "allow": [ | ||
| "Bash(swift build:*)", | ||
| "Bash(rm:*)", | ||
| "Bash(mkdir:*)", | ||
| "Bash(swift test:*)", | ||
| "Bash(find:*)" | ||
| ] | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // swift-tools-version: 6.2 | ||
| // The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
|
||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "MLSAppFeature", | ||
| platforms: [.iOS(.v15)], | ||
| products: [ | ||
| // Interface: 외부 인터페이스와 모델을 제공하는 모듈 | ||
| .library( | ||
| name: "MLSAppFeatureInterface", | ||
| targets: ["MLSAppFeatureInterface"] | ||
| ), | ||
| // Feature: 실제 기능이 구현된 모듈 | ||
| .library( | ||
| name: "MLSAppFeature", | ||
| targets: ["MLSAppFeature"] | ||
| ), | ||
| // Testing: 단위 테스트나 Example 앱에서 사용될 Mock 데이터를 제공하는 모듈 | ||
| .library( | ||
| name: "MLSAppFeatureTesting", | ||
| targets: ["MLSAppFeatureTesting"] | ||
| ) | ||
| ], | ||
| targets: [ | ||
| // Interface 모듈 (도메인 모델 및 프로토콜) | ||
| .target( | ||
| name: "MLSAppFeatureInterface", | ||
| dependencies: [] | ||
| ), | ||
| // Feature 모듈 (실제 구현) | ||
| .target( | ||
| name: "MLSAppFeature", | ||
| dependencies: ["MLSAppFeatureInterface"] | ||
| ), | ||
| // Testing 모듈 (Mock 객체) | ||
| .target( | ||
| name: "MLSAppFeatureTesting", | ||
| dependencies: ["MLSAppFeatureInterface"] | ||
| ), | ||
| // Tests 모듈 | ||
| .testTarget( | ||
| name: "MLSAppFeatureTests", | ||
| dependencies: [ | ||
| "MLSAppFeature", | ||
| "MLSAppFeatureInterface", | ||
| "MLSAppFeatureTesting" | ||
| ] | ||
| ) | ||
| ] | ||
| ) |
41 changes: 41 additions & 0 deletions
41
MLS/MLSAppFeature/Sources/MLSAppFeature/Data/DataSources/Local/UserDefaultsDataSource.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import Foundation | ||
|
|
||
| import MLSAppFeatureInterface | ||
|
|
||
| /// UserDefaults를 사용하는 Local Data Source | ||
| final class UserDefaultsDataSource { | ||
| nonisolated(unsafe) private let userDefaults: UserDefaults | ||
| private let versionKey = "com.mls.updateChecker.skippedVersion" | ||
| private let dateKey = "com.mls.updateChecker.skippedDate" | ||
|
|
||
| init(userDefaults: UserDefaults = .standard) { | ||
| self.userDefaults = userDefaults | ||
| } | ||
|
|
||
| /// 스킵 버전을 저장합니다 | ||
| func saveSkipVersion(_ version: Version, skippedAt date: Date) { | ||
| userDefaults.set(version.versionString, forKey: versionKey) | ||
| userDefaults.set(date.timeIntervalSince1970, forKey: dateKey) | ||
| } | ||
|
|
||
| /// 저장된 스킵 버전을 조회합니다 | ||
| func getSkippedVersion() -> Version? { | ||
| guard let versionString = userDefaults.string(forKey: versionKey) else { | ||
| return nil | ||
| } | ||
| return Version(versionString: versionString) | ||
| } | ||
|
|
||
| /// 스킵 날짜를 조회합니다 | ||
| func getSkippedDate() -> Date? { | ||
| let timestamp = userDefaults.double(forKey: dateKey) | ||
| guard timestamp > 0 else { return nil } | ||
| return Date(timeIntervalSince1970: timestamp) | ||
| } | ||
|
|
||
| /// 스킵 정보를 삭제합니다 | ||
| func clearSkipInfo() { | ||
| userDefaults.removeObject(forKey: versionKey) | ||
| userDefaults.removeObject(forKey: dateKey) | ||
| } | ||
dongglehada marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
50 changes: 50 additions & 0 deletions
50
MLS/MLSAppFeature/Sources/MLSAppFeature/Data/DataSources/Remote/AppStoreService.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import Foundation | ||
|
|
||
| import MLSAppFeatureInterface | ||
|
|
||
| /// iTunes Search API를 통해 앱스토어 버전 정보를 조회하는 Remote Data Source | ||
| final class AppStoreService { | ||
| private let urlSession: URLSession | ||
|
|
||
| init(urlSession: URLSession = .shared) { | ||
| self.urlSession = urlSession | ||
| } | ||
|
|
||
| /// 앱스토어에서 최신 버전을 조회합니다 | ||
| /// - Parameter appID: 앱스토어 앱 ID | ||
| /// - Returns: 최신 버전 정보 | ||
| /// - Throws: AppStoreError | ||
| func fetchLatestVersion(appID: String) async throws -> Version { | ||
| guard let url = URL(string: "https://itunes.apple.com/lookup?id=\(appID)") else { | ||
| throw AppStoreError.invalidURL | ||
| } | ||
|
|
||
| let (data, response) = try await urlSession.data(from: url) | ||
|
|
||
| guard let httpResponse = response as? HTTPURLResponse, | ||
| (200...299).contains(httpResponse.statusCode) else { | ||
| throw AppStoreError.invalidResponse | ||
| } | ||
|
|
||
| guard let lookupResponse = try? JSONDecoder().decode(AppStoreLookupResponse.self, from: data) else { | ||
| throw AppStoreError.parsingError | ||
| } | ||
|
|
||
| guard let result = lookupResponse.results.first, | ||
| let version = Version(versionString: result.version) else { | ||
| throw AppStoreError.versionNotFound | ||
| } | ||
|
|
||
| return version | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Response Models | ||
|
|
||
| private struct AppStoreLookupResponse: Decodable { | ||
| let results: [AppStoreResult] | ||
| } | ||
|
|
||
| private struct AppStoreResult: Decodable { | ||
| let version: String | ||
| } |
21 changes: 21 additions & 0 deletions
21
MLS/MLSAppFeature/Sources/MLSAppFeature/Data/Repositories/AppStoreRepository.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import Foundation | ||
|
|
||
| import MLSAppFeatureInterface | ||
|
|
||
| /// 앱스토어 정보를 조회하는 Repository 구현체 | ||
| public final class AppStoreRepository: AppStoreRepositoryProtocol, @unchecked Sendable { | ||
| private let remoteDataSource: AppStoreService | ||
|
|
||
| public init() { | ||
| self.remoteDataSource = AppStoreService() | ||
| } | ||
|
|
||
| /// 테스트용 초기화자 | ||
| init(remoteDataSource: AppStoreService) { | ||
| self.remoteDataSource = remoteDataSource | ||
| } | ||
|
|
||
| public func fetchLatestVersion(appID: String) async throws -> Version { | ||
| return try await remoteDataSource.fetchLatestVersion(appID: appID) | ||
| } | ||
| } |
45 changes: 45 additions & 0 deletions
45
MLS/MLSAppFeature/Sources/MLSAppFeature/Data/Repositories/UpdateSkipRepository.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import Foundation | ||
|
|
||
| import MLSAppFeatureInterface | ||
|
|
||
| /// 업데이트 스킵 정보를 관리하는 Repository 구현체 | ||
| public final class UpdateSkipRepository: UpdateSkipRepositoryProtocol, @unchecked Sendable { | ||
| private let localDataSource: UserDefaultsDataSource | ||
| private let skipDuration: TimeInterval | ||
|
|
||
| /// UpdateSkipRepository 초기화 | ||
| /// - Parameter skipDuration: 스킵 유효 기간 (기본값: 7일) | ||
| public init(skipDuration: TimeInterval = 7 * 24 * 60 * 60) { | ||
| self.localDataSource = UserDefaultsDataSource() | ||
| self.skipDuration = skipDuration | ||
| } | ||
|
|
||
| /// 테스트용 초기화자 | ||
| /// - Parameters: | ||
| /// - localDataSource: 로컬 데이터 소스 | ||
| /// - skipDuration: 스킵 유효 기간 | ||
| init(localDataSource: UserDefaultsDataSource, skipDuration: TimeInterval = 7 * 24 * 60 * 60) { | ||
| self.localDataSource = localDataSource | ||
| self.skipDuration = skipDuration | ||
| } | ||
|
|
||
| public func saveSkipVersion(_ version: Version, skippedAt date: Date) { | ||
| localDataSource.saveSkipVersion(version, skippedAt: date) | ||
| } | ||
|
|
||
| public func isSkipValid(for version: Version) -> Bool { | ||
| guard let skippedVersion = localDataSource.getSkippedVersion(), | ||
| let skippedDate = localDataSource.getSkippedDate(), | ||
| skippedVersion == version else { | ||
| return false | ||
| } | ||
|
|
||
| // 스킵 기간이 지났는지 확인 | ||
| let elapsed = Date().timeIntervalSince(skippedDate) | ||
| return elapsed >= 0 && elapsed < skipDuration | ||
| } | ||
|
|
||
| public func clearSkipInfo() { | ||
| localDataSource.clearSkipInfo() | ||
| } | ||
| } |
55 changes: 55 additions & 0 deletions
55
MLS/MLSAppFeature/Sources/MLSAppFeature/Domain/UseCases/UpdateCheckerUseCase.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import Foundation | ||
|
|
||
| import MLSAppFeatureInterface | ||
|
|
||
| /// 앱 업데이트 체크 Use Case 구현체 | ||
| public final class UpdateCheckerUseCase: UpdateCheckerUseCaseProtocol { | ||
| private let appStoreRepository: AppStoreRepositoryProtocol | ||
| private let skipRepository: UpdateSkipRepositoryProtocol | ||
| private let appID: String | ||
|
|
||
| /// UpdateCheckerUseCase 초기화 | ||
| /// - Parameters: | ||
| /// - appID: 앱스토어 앱 ID | ||
| /// - appStoreRepository: 앱스토어 정보 조회 Repository | ||
| /// - skipRepository: 스킵 정보 관리 Repository | ||
| public init( | ||
| appID: String, | ||
| appStoreRepository: AppStoreRepositoryProtocol, | ||
| skipRepository: UpdateSkipRepositoryProtocol | ||
| ) { | ||
| self.appID = appID | ||
| self.appStoreRepository = appStoreRepository | ||
| self.skipRepository = skipRepository | ||
| } | ||
|
|
||
| public func checkUpdate(currentVersion: Version) async throws -> UpdateStatus { | ||
| let latestVersion = try await appStoreRepository.fetchLatestVersion(appID: appID) | ||
|
|
||
| // 최신 버전이 현재 버전보다 낮거나 같으면 업데이트 불필요 | ||
| guard latestVersion > currentVersion else { | ||
| return .none | ||
| } | ||
|
|
||
| // major 버전이 다르면 강제 업데이트 | ||
| if latestVersion.major != currentVersion.major { | ||
| return .force(latestVersion: latestVersion) | ||
| } | ||
|
|
||
| // minor 또는 patch 차이면 선택 업데이트 | ||
| // 단, 사용자가 이전에 스킵했고 7일이 지나지 않았으면 none 반환 | ||
| if skipRepository.isSkipValid(for: latestVersion) { | ||
| return .none | ||
| } | ||
|
|
||
| return .optional(latestVersion: latestVersion) | ||
| } | ||
|
|
||
| public func skipUpdate(version: Version) { | ||
| skipRepository.saveSkipVersion(version, skippedAt: Date()) | ||
| } | ||
|
|
||
| public func clearSkipInfo() { | ||
| skipRepository.clearSkipInfo() | ||
| } | ||
| } |
13 changes: 13 additions & 0 deletions
13
MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Entities/UpdateStatus.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import Foundation | ||
|
|
||
| /// 앱 업데이트 상태를 나타내는 열거형 | ||
| public enum UpdateStatus: Equatable, Sendable { | ||
| /// 강제 업데이트 필요 (major 버전 차이) | ||
| case force(latestVersion: Version) | ||
|
|
||
| /// 선택적 업데이트 가능 (minor 또는 patch 차이) | ||
| case optional(latestVersion: Version) | ||
|
|
||
| /// 업데이트 불필요 | ||
| case none | ||
| } |
38 changes: 38 additions & 0 deletions
38
MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Entities/Version.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import Foundation | ||
|
|
||
| /// 앱 버전을 나타내는 도메인 엔티티 | ||
| /// major.minor.patch 형식의 시맨틱 버저닝을 지원합니다 | ||
| public struct Version: Equatable, Comparable, Sendable { | ||
| public let major: Int | ||
| public let minor: Int | ||
| public let patch: Int | ||
|
|
||
| public init(major: Int, minor: Int, patch: Int) { | ||
| self.major = major | ||
| self.minor = minor | ||
| self.patch = patch | ||
| } | ||
|
|
||
| /// 버전 문자열로부터 Version 객체를 생성합니다 | ||
| /// - Parameter versionString: "1.2.3" 형식의 버전 문자열 | ||
| /// - Returns: 파싱에 성공하면 Version 객체, 실패하면 nil | ||
| public init?(versionString: String) { | ||
| let components = versionString.split(separator: ".").compactMap { Int($0) } | ||
| guard components.count >= 2 else { return nil } | ||
|
|
||
| self.major = components[0] | ||
| self.minor = components[1] | ||
| self.patch = components.count > 2 ? components[2] : 0 | ||
| } | ||
|
|
||
| /// 버전을 문자열로 반환합니다 | ||
| public var versionString: String { | ||
| "\(major).\(minor).\(patch)" | ||
| } | ||
|
|
||
| public static func < (lhs: Version, rhs: Version) -> Bool { | ||
| if lhs.major != rhs.major { return lhs.major < rhs.major } | ||
| if lhs.minor != rhs.minor { return lhs.minor < rhs.minor } | ||
| return lhs.patch < rhs.patch | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
...ature/Sources/MLSAppFeatureInterface/Domain/Repositories/AppStoreRepositoryProtocol.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import Foundation | ||
|
|
||
| /// 앱스토어 정보를 조회하는 Repository 프로토콜 | ||
| public protocol AppStoreRepositoryProtocol: Sendable { | ||
| /// 앱스토어에서 최신 버전을 조회합니다 | ||
| /// - Parameter appID: 앱스토어 앱 ID | ||
| /// - Returns: 최신 버전 정보 | ||
| /// - Throws: 조회 실패 시 AppStoreError | ||
| func fetchLatestVersion(appID: String) async throws -> Version | ||
| } | ||
|
|
||
| /// 앱스토어 조회 시 발생할 수 있는 에러 | ||
| public enum AppStoreError: Error, Equatable, Sendable { | ||
| case invalidURL | ||
| case networkFailure | ||
| case invalidResponse | ||
| case versionNotFound | ||
| case parsingError | ||
| } |
18 changes: 18 additions & 0 deletions
18
...ure/Sources/MLSAppFeatureInterface/Domain/Repositories/UpdateSkipRepositoryProtocol.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import Foundation | ||
|
|
||
| /// 업데이트 스킵 정보를 관리하는 Repository 프로토콜 | ||
| public protocol UpdateSkipRepositoryProtocol: Sendable { | ||
| /// 특정 버전에 대한 스킵 정보를 저장합니다 | ||
| /// - Parameters: | ||
| /// - version: 스킵할 버전 | ||
| /// - date: 스킵 날짜 | ||
| func saveSkipVersion(_ version: Version, skippedAt date: Date) | ||
|
|
||
| /// 특정 버전에 대한 스킵 정보가 유효한지 확인합니다 | ||
| /// - Parameter version: 확인할 버전 | ||
| /// - Returns: 스킵 정보가 유효하면 true (7일 이내) | ||
| func isSkipValid(for version: Version) -> Bool | ||
|
|
||
| /// 저장된 스킵 정보를 삭제합니다 | ||
| func clearSkipInfo() | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.