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
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import Foundation
import ProjectDescription

extension String {
public static func appVersion(version: String = "1.0.2") -> String {
public static func appVersion(version: String = "1.0.4") -> String {
return version
}

public static func mainBundleID() -> String {
return Project.Environment.bundlePrefix
}

public static func appBuildVersion(buildVersion: String = "72") -> String {
public static func appBuildVersion(buildVersion: String = "74") -> String {
return buildVersion
}

Expand Down
1 change: 1 addition & 0 deletions Projects/App/Sources/Di/DiRegister.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class AppDIManager: @unchecked Sendable {
return KeychainTokenProvider(keychainManager: keychainManager) as TokenProviding
}
.register(ProfileInterface.self) { ProfileRepositoryImpl() }
.register(AppUpdateInterface.self) { AppUpdateRepositoryImpl() as AppUpdateInterface }
// MARK: - 둜그인
.register { AuthRepositoryImpl() as AuthInterface }
.register { GoogleOAuthRepositoryImpl() as GoogleOAuthInterface }
Expand Down
40 changes: 40 additions & 0 deletions Projects/Data/Model/Sources/AppUpdate/AppUpdateDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// AppUpdateDTO.swift
// Model
//
// Created by Wonji Suh on 3/9/26.
//

import Foundation

public struct AppUpdateResponseDTO: Codable, Sendable {
public let resultCount: Int
public let results: [AppStoreInfoDTO]

public init(resultCount: Int, results: [AppStoreInfoDTO]) {
self.resultCount = resultCount
self.results = results
}
}

public struct AppStoreInfoDTO: Codable, Sendable {
public let version: String
public let releaseNotes: String?
public let trackViewUrl: String
public let bundleId: String
public let trackName: String

public init(
version: String,
releaseNotes: String?,
trackViewUrl: String,
bundleId: String,
trackName: String
) {
self.version = version
self.releaseNotes = releaseNotes
self.trackViewUrl = trackViewUrl
self.bundleId = bundleId
self.trackName = trackName
}
}
30 changes: 30 additions & 0 deletions Projects/Data/Model/Sources/AppUpdate/Mapper/AppUpdateDTO+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// AppUpdateDTO+.swift
// Model
//
// Created by Wonji Suh on 3/9/26.
//

import Foundation
import Entity

public extension AppStoreInfoDTO {
func toEntity(currentVersion: String) -> AppUpdateInfo {
let isUpdateAvailable = isNewerVersion(
storeVersion: version,
currentVersion: currentVersion
)

return AppUpdateInfo(
currentVersion: currentVersion,
latestVersion: version,
releaseNotes: releaseNotes,
appStoreUrl: trackViewUrl,
isUpdateAvailable: isUpdateAvailable
)
}

private func isNewerVersion(storeVersion: String, currentVersion: String) -> Bool {
return storeVersion.compare(currentVersion, options: .numeric) == .orderedDescending
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public extension AttendanceDTOResponse {
userID: "\(self.userID)",
userName: self.userName,
userInfo: self.userInfo,
status: AttendanceStatus.from(apiKey: self.attendanceStatus) ?? .absent
status: AttendanceStatus.from(apiKey: self.attendanceStatus) ?? .defaults
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// AppUpdateRepositoryImpl.swift
// Repository
//
// Created by Wonji Suh on 3/9/26.
//

import Foundation
import DomainInterface
import Entity
import Model
import LogMacro

public final class AppUpdateRepositoryImpl: AppUpdateInterface {
private let urlSession: URLSession
private let bundleId: String

public init(
urlSession: URLSession = .shared,
bundleId: String? = nil
) {
self.urlSession = urlSession
self.bundleId = bundleId ?? Bundle.main.bundleIdentifier ?? ""
}

public func checkForUpdate() async throws -> AppUpdateInfo {
guard !bundleId.isEmpty else {
throw AppUpdateError.invalidBundleId
}

let currentVersion = getCurrentAppVersion()
let appStoreInfo = try await fetchAppStoreInfo()

return appStoreInfo.toEntity(currentVersion: currentVersion)
}

// MARK: - Private Methods

private func getCurrentAppVersion() -> String {
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
}

private func getCurrentAppLanguage() -> String {
// 1. ν˜„μž¬ Locale의 μ–Έμ–΄ μ½”λ“œ 확인
if let languageCode = Locale.current.language.languageCode?.identifier {
return languageCode
}

// 2. μ‹œμŠ€ν…œ μ„ ν˜Έ μ–Έμ–΄ 확인
if let preferredLanguage = NSLocale.preferredLanguages.first {
let language = String(preferredLanguage.prefix(2))
return language
}

// 3. Bundle의 κΈ°λ³Έ μ–Έμ–΄ 확인
if let bundleLanguage = Bundle.main.preferredLocalizations.first {
let language = String(bundleLanguage.prefix(2))
return language
}

// 4. μ΅œμ’… fallback - μ˜μ–΄
return "en"
}

private func fetchAppStoreInfo() async throws -> AppStoreInfoDTO {
// ν•œκ΅­κ³Ό λ―Έκ΅­ μ•±μŠ€ν† μ–΄ 정보λ₯Ό λ™μ‹œμ— κ°€μ Έμ˜€κΈ°
async let koTask = fetchAppStoreInfo(country: "kr")
async let usTask = fetchAppStoreInfo(country: "us")

do {
let koResult = try await koTask
#logDebug("[AppUpdate] Korean store result", koResult)

do {
let usResult = try await usTask
#logDebug("[AppUpdate] US store result", usResult)

// μ•± μ–Έμ–΄ 섀정에 따라 μ μ ˆν•œ 버전 선택
let currentLanguage = getCurrentAppLanguage()
#logDebug("[AppUpdate] Current app language", currentLanguage)

if currentLanguage == "ko" {
#logDebug("[AppUpdate] Using Korean version (app language: ko)")
return koResult
} else {
#logDebug("[AppUpdate] Using US version (app language: \(currentLanguage))")
return usResult
}
} catch {
#logDebug("[AppUpdate] US store failed, using Korean result")
return koResult
}
} catch {
#logDebug("[AppUpdate] Korean store failed, trying US only")

do {
let usResult = try await usTask
#logDebug("[AppUpdate] Using US store as fallback")
return usResult
} catch {
#logError("[AppUpdate] Both stores failed", error.localizedDescription)
throw error
}
}
}

private func fetchAppStoreInfo(country: String) async throws -> AppStoreInfoDTO {
let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
guard let url = URL(string: urlString) else {
throw AppUpdateError.invalidBundleId
}

do {
let (data, _) = try await urlSession.data(from: url)
let response = try JSONDecoder().decode(AppUpdateResponseDTO.self, from: data)

guard let appInfo = response.results.first else {
throw AppUpdateError.appNotFound
}

return appInfo
} catch let decodingError as DecodingError {
#logError("[AppUpdate] Decoding error for \(country)", decodingError.localizedDescription)
throw AppUpdateError.decodingError
} catch {
#logError("[AppUpdate] Network error for \(country)", error.localizedDescription)
throw AppUpdateError.from(error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum OnBoardingService {
}


extension OnBoardingService: BaseTargetType {
extension OnBoardingService: BaseTargetType {
public typealias Domain = AttendanceDomain

public var domain: AttendanceDomain {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// AppUpdateInterface.swift
// DomainInterface
//
// Created by Wonji Suh on 3/9/26.
//

import Foundation
import WeaveDI
import Entity

/// App Update κ΄€λ ¨ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μœ„ν•œ Interface ν”„λ‘œν† μ½œ
public protocol AppUpdateInterface: Sendable {
func checkForUpdate() async throws -> AppUpdateInfo
}

/// AppUpdate Repository의 DependencyKey ꡬ쑰체
public struct AppUpdateRepositoryDependency: DependencyKey {
public static var liveValue: AppUpdateInterface {
UnifiedDI.resolve(AppUpdateInterface.self) ?? DefaultAppUpdateRepositoryImpl()
}

public static var testValue: AppUpdateInterface {
UnifiedDI.resolve(AppUpdateInterface.self) ?? DefaultAppUpdateRepositoryImpl()
}

public static var previewValue: AppUpdateInterface = liveValue
}

/// DependencyValues extension으둜 κ°„νŽΈν•œ μ ‘κ·Ό 제곡
public extension DependencyValues {
var appUpdateRepository: AppUpdateInterface {
get { self[AppUpdateRepositoryDependency.self] }
set { self[AppUpdateRepositoryDependency.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// DefaultAppUpdateRepositoryImpl.swift
// DomainInterface
//
// Created by Wonji Suh on 3/9/26.
//

import Foundation
import Entity

public final class DefaultAppUpdateRepositoryImpl: AppUpdateInterface {
public init() {}

public func checkForUpdate() async throws -> AppUpdateInfo {
// Mock implementation - μ‹€μ œ κ΅¬ν˜„μ€ Repositoryμ—μ„œ
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"

return AppUpdateInfo(
currentVersion: currentVersion,
latestVersion: currentVersion,
releaseNotes: nil,
appStoreUrl: "https://apps.apple.com",
isUpdateAvailable: false
)
}
}
54 changes: 54 additions & 0 deletions Projects/Domain/Entity/Sources/AppUpdate/AppUpdateInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// AppUpdateInfo.swift
// Entity
//
// Created by Wonji Suh on 3/9/26.
//

import Foundation

public struct AppUpdateInfo: Codable, Equatable, Sendable {
public let currentVersion: String
public let latestVersion: String
public let releaseNotes: String?
public let appStoreUrl: String
public let isUpdateAvailable: Bool

public init(
currentVersion: String,
latestVersion: String,
releaseNotes: String?,
appStoreUrl: String,
isUpdateAvailable: Bool
) {
self.currentVersion = currentVersion
self.latestVersion = latestVersion
self.releaseNotes = releaseNotes
self.appStoreUrl = appStoreUrl
self.isUpdateAvailable = isUpdateAvailable
}
}

public struct iTunesLookupResponse: Codable, Sendable {
public let results: [iTunesAppInfo]

public init(results: [iTunesAppInfo]) {
self.results = results
}
}

public struct iTunesAppInfo: Codable, Sendable {
public let version: String
public let releaseNotes: String?
public let trackViewUrl: String

public init(
version: String,
releaseNotes: String?,
trackViewUrl: String
) {
self.version = version
self.releaseNotes = releaseNotes
self.trackViewUrl = trackViewUrl
}
}
4 changes: 2 additions & 2 deletions Projects/Domain/Entity/Sources/Attendance/Attendance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public extension Attendance {
case "WEB2νŒ€": return .web2
case "IOS1νŒ€": return .ios1
case "IOS2νŒ€": return .ios2
case "ANDROID1νŒ€": return .and1
case "ANDROID2νŒ€": return .and2
case "ANDROID1νŒ€", "AND1νŒ€": return .and1
case "ANDROID2νŒ€", "AND2νŒ€": return .and2
default: return nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum AttendanceStatus: String, CaseIterable, Equatable, Identifiable {
case attended = "ATTENDED"
case late = "LATE"
case absent = "ABSENT"
case defaults = "DEFAULT"

public var id: String {
rawValue
Expand All @@ -24,6 +25,8 @@ public enum AttendanceStatus: String, CaseIterable, Equatable, Identifiable {
return "지각"
case .absent:
return "결석"
case .defaults:
return "λŒ€κΈ°"
}
}

Expand Down
Loading
Loading