From 2bde50140ee13335657f0e4d556e44f79027f35a Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 9 Mar 2026 10:55:07 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=95=B1=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/Di/DiRegister.swift | 1 + .../Sources/AppUpdate/AppUpdateDTO.swift | 40 +++ .../AppUpdate/Mapper/AppUpdateDTO+.swift | 30 ++ .../AppUpdate/AppUpdateRepositoryImpl.swift | 130 ++++++++ .../AppUpdate/AppUpdateInterface.swift | 36 +++ .../DefaultAppUpdateRepositoryImpl.swift | 26 ++ .../Sources/AppUpdate/AppUpdateInfo.swift | 54 ++++ .../Sources/Attendance/Attendance.swift | 4 +- .../Entity/Sources/Error/AppUpdateError.swift | 38 +++ .../Sources/OnBoarding/SelectPart.swift | 8 +- .../AppUpdate/AppUpdateUseCaseImpl.swift | 44 +++ .../Examples/AttendanceModalUsage.swift | 277 ------------------ .../Reducer/AttendanceCheck.swift | 14 +- .../Splash/Sources/Reducer/Splash.swift | 173 ++++++++++- .../Splash/Sources/View/SplashView.swift | 1 + .../Alert/CustomPopup/CustomAlertState.swift | 19 ++ .../UI/Card/AttendanceCheckStatusCard.swift | 1 + 17 files changed, 594 insertions(+), 302 deletions(-) create mode 100644 Projects/Data/Model/Sources/AppUpdate/AppUpdateDTO.swift create mode 100644 Projects/Data/Model/Sources/AppUpdate/Mapper/AppUpdateDTO+.swift create mode 100644 Projects/Data/Repository/Sources/AppUpdate/AppUpdateRepositoryImpl.swift create mode 100644 Projects/Domain/DomainInterface/Sources/AppUpdate/AppUpdateInterface.swift create mode 100644 Projects/Domain/DomainInterface/Sources/AppUpdate/DefaultAppUpdateRepositoryImpl.swift create mode 100644 Projects/Domain/Entity/Sources/AppUpdate/AppUpdateInfo.swift create mode 100644 Projects/Domain/Entity/Sources/Error/AppUpdateError.swift create mode 100644 Projects/Domain/UseCase/Sources/AppUpdate/AppUpdateUseCaseImpl.swift delete mode 100644 Projects/Presentation/Management/Sources/AttendanceCheck/Examples/AttendanceModalUsage.swift diff --git a/Projects/App/Sources/Di/DiRegister.swift b/Projects/App/Sources/Di/DiRegister.swift index 89859af9..ae7a2091 100644 --- a/Projects/App/Sources/Di/DiRegister.swift +++ b/Projects/App/Sources/Di/DiRegister.swift @@ -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 } diff --git a/Projects/Data/Model/Sources/AppUpdate/AppUpdateDTO.swift b/Projects/Data/Model/Sources/AppUpdate/AppUpdateDTO.swift new file mode 100644 index 00000000..5e510782 --- /dev/null +++ b/Projects/Data/Model/Sources/AppUpdate/AppUpdateDTO.swift @@ -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 + } +} \ No newline at end of file diff --git a/Projects/Data/Model/Sources/AppUpdate/Mapper/AppUpdateDTO+.swift b/Projects/Data/Model/Sources/AppUpdate/Mapper/AppUpdateDTO+.swift new file mode 100644 index 00000000..3db912b0 --- /dev/null +++ b/Projects/Data/Model/Sources/AppUpdate/Mapper/AppUpdateDTO+.swift @@ -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 + } +} \ No newline at end of file diff --git a/Projects/Data/Repository/Sources/AppUpdate/AppUpdateRepositoryImpl.swift b/Projects/Data/Repository/Sources/AppUpdate/AppUpdateRepositoryImpl.swift new file mode 100644 index 00000000..17b9c0fd --- /dev/null +++ b/Projects/Data/Repository/Sources/AppUpdate/AppUpdateRepositoryImpl.swift @@ -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) + } + } +} diff --git a/Projects/Domain/DomainInterface/Sources/AppUpdate/AppUpdateInterface.swift b/Projects/Domain/DomainInterface/Sources/AppUpdate/AppUpdateInterface.swift new file mode 100644 index 00000000..25576707 --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/AppUpdate/AppUpdateInterface.swift @@ -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 } + } +} \ No newline at end of file diff --git a/Projects/Domain/DomainInterface/Sources/AppUpdate/DefaultAppUpdateRepositoryImpl.swift b/Projects/Domain/DomainInterface/Sources/AppUpdate/DefaultAppUpdateRepositoryImpl.swift new file mode 100644 index 00000000..53372022 --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/AppUpdate/DefaultAppUpdateRepositoryImpl.swift @@ -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 + ) + } +} \ No newline at end of file diff --git a/Projects/Domain/Entity/Sources/AppUpdate/AppUpdateInfo.swift b/Projects/Domain/Entity/Sources/AppUpdate/AppUpdateInfo.swift new file mode 100644 index 00000000..d2470e21 --- /dev/null +++ b/Projects/Domain/Entity/Sources/AppUpdate/AppUpdateInfo.swift @@ -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 + } +} \ No newline at end of file diff --git a/Projects/Domain/Entity/Sources/Attendance/Attendance.swift b/Projects/Domain/Entity/Sources/Attendance/Attendance.swift index 7dc6741d..aae7dcb4 100644 --- a/Projects/Domain/Entity/Sources/Attendance/Attendance.swift +++ b/Projects/Domain/Entity/Sources/Attendance/Attendance.swift @@ -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 } } diff --git a/Projects/Domain/Entity/Sources/Error/AppUpdateError.swift b/Projects/Domain/Entity/Sources/Error/AppUpdateError.swift new file mode 100644 index 00000000..85a46ecc --- /dev/null +++ b/Projects/Domain/Entity/Sources/Error/AppUpdateError.swift @@ -0,0 +1,38 @@ +// +// AppUpdateError.swift +// Entity +// +// Created by Wonji Suh on 3/9/26. +// + +import Foundation + +public enum AppUpdateError: Error, LocalizedError, Sendable, Equatable { + case invalidBundleId + case appNotFound + case networkError(String) + case decodingError + case unknownError + + public var errorDescription: String? { + switch self { + case .invalidBundleId: + return "Bundle ID가 유효하지 않습니다." + case .appNotFound: + return "앱스토어에서 앱을 찾을 수 없습니다." + case .networkError(let errorMessage): + return "네트워크 오류: \(errorMessage)" + case .decodingError: + return "데이터 파싱 오류가 발생했습니다." + case .unknownError: + return "알 수 없는 오류가 발생했습니다." + } + } + + public static func from(_ error: Error) -> AppUpdateError { + if let appUpdateError = error as? AppUpdateError { + return appUpdateError + } + return .networkError(error.localizedDescription) + } +} diff --git a/Projects/Domain/Entity/Sources/OnBoarding/SelectPart.swift b/Projects/Domain/Entity/Sources/OnBoarding/SelectPart.swift index 50d9433d..701f81c0 100644 --- a/Projects/Domain/Entity/Sources/OnBoarding/SelectPart.swift +++ b/Projects/Domain/Entity/Sources/OnBoarding/SelectPart.swift @@ -49,11 +49,11 @@ public enum SelectParts: String, CaseIterable, Codable, Equatable { public static func from(apiKey: String) -> SelectParts? { switch apiKey.uppercased() { - case "BACKEND": return .backend - case "FRONTEND": return .frontend - case "DESIGNER": return .designer + case "BACKEND", "BE": return .backend + case "FRONTEND", "FE": return .frontend + case "DESIGNER" ,"PD": return .designer case "PM": return .pm - case "ANDROID": return .android + case "ANDROID", "AND": return .android case "IOS": return .ios default: return nil } diff --git a/Projects/Domain/UseCase/Sources/AppUpdate/AppUpdateUseCaseImpl.swift b/Projects/Domain/UseCase/Sources/AppUpdate/AppUpdateUseCaseImpl.swift new file mode 100644 index 00000000..74af6ec9 --- /dev/null +++ b/Projects/Domain/UseCase/Sources/AppUpdate/AppUpdateUseCaseImpl.swift @@ -0,0 +1,44 @@ +// +// AppUpdateUseCaseImpl.swift +// UseCase +// +// Created by Wonji Suh on 3/9/26. +// + +import DomainInterface +import Entity +import ComposableArchitecture + +public protocol AppUpdateUseCaseInterface: Sendable { + func checkForUpdate() async throws -> AppUpdateInfo? +} + +public struct AppUpdateUseCaseImpl: AppUpdateUseCaseInterface { + @Dependency(\.appUpdateRepository) var repository + + public init() {} + + public func checkForUpdate() async throws -> AppUpdateInfo? { + let updateInfo = try await repository.checkForUpdate() + + // 업데이트가 필요한 경우만 반환 + if updateInfo.isUpdateAvailable { + return updateInfo + } + + return nil + } +} + +extension AppUpdateUseCaseImpl: DependencyKey { + public static var liveValue: AppUpdateUseCaseInterface = AppUpdateUseCaseImpl() + public static var testValue: AppUpdateUseCaseInterface = AppUpdateUseCaseImpl() + public static var previewValue: AppUpdateUseCaseInterface = liveValue +} + +public extension DependencyValues { + var appUpdateUseCase: AppUpdateUseCaseInterface { + get { self[AppUpdateUseCaseImpl.self] } + set { self[AppUpdateUseCaseImpl.self] = newValue } + } +} diff --git a/Projects/Presentation/Management/Sources/AttendanceCheck/Examples/AttendanceModalUsage.swift b/Projects/Presentation/Management/Sources/AttendanceCheck/Examples/AttendanceModalUsage.swift deleted file mode 100644 index 8f9c1ed2..00000000 --- a/Projects/Presentation/Management/Sources/AttendanceCheck/Examples/AttendanceModalUsage.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// AttendanceModalUsage.swift -// Management -// -// Created by Wonji Suh on 1/13/26. -// - -import SwiftUI -// import ComposableArchitecture // TCA 사용 시 필요 - -// MARK: - Store Scope 패턴 사용 예제 - -/* -## ProfileReducer customAlert 패턴을 따른 AttendanceModal 사용법 - -### 1. Reducer에 상태 추가 (ProfileReducer와 동일한 패턴) - -```swift -@Reducer -public struct YourFeature { - @ObservableState - public struct State: Equatable { - // 다른 상태들... - @Presents public var attendanceModal: AttendanceModalState? - } - - public enum Action: ViewAction, FeatureAction, BindableAction { - case binding(BindingAction) - case view(View) - case scope(ScopeAction) - case async(AsyncAction) - case navigation(NavigationAction) - } - - @CasePathable - public enum View { - case showAttendanceModal - case showLateTimeModal - } - - @CasePathable - public enum ScopeAction { - case attendanceModal(PresentationAction) - } - - public var body: some Reducer { - BindingReducer() - - Reduce { state, action in - switch action { - case .view(let viewAction): - return handleViewAction(state: &state, action: viewAction) - - case .scope(let scopeAction): - switch scopeAction { - case .attendanceModal(let action): - return handleAttendanceModalAction(state: &state, action: action) - } - - // 다른 액션들... - } - } - .ifLet(\.$attendanceModal, action: \.scope.attendanceModal) { - AttendanceModal() - } - } -} -``` - -### 2. ViewAction 처리 (ProfileReducer.showWithdrawAlert 패턴) - -```swift -private func handleViewAction( - state: inout State, - action: View -) -> Effect { - switch action { - case .showAttendanceModal: - state.attendanceModal = .changeAttendanceStatus( - availableStatuses: [.attended, .late, .absent] - ) - return .none - - case .showLateTimeModal: - state.attendanceModal = .lateTimeStatusChange() - return .none - } -} -``` - -### 3. AttendanceModal 액션 처리 (ProfileReducer.handleCustomAlertAction 패턴) - -```swift -private func handleAttendanceModalAction( - state: inout State, - action: PresentationAction -) -> Effect { - switch action { - case .presented(let attendanceModalAction): - switch attendanceModalAction { - case .confirmTapped(let status): - // ProfileReducer처럼 title 기반 구분도 가능하지만, status으로 직접 처리 - state.attendanceModal = nil - - // API 호출 예시 - return .run { send in - // await updateAttendanceStatus(status) - await send(.async(.updateAttendanceStatus(status))) - } - - case .cancelTapped: - state.attendanceModal = nil - return .none - } - - case .dismiss: - return .none - } -} -``` - -### 4. View에서 사용 (ProfileView 패턴) - -```swift -public struct YourView: View { - @Bindable private var store: StoreOf - - public var body: some View { - VStack { - Button("출석 상태 변경") { - store.send(.view(.showAttendanceModal)) - } - - Button("늦은 시간 모달") { - store.send(.view(.showLateTimeModal)) - } - } - // ProfileView와 동일한 패턴 - .ㄷ - } -} -``` - -### 5. 사전 정의된 팩토리 메서드들 (ProfileReducer.withdrawAccount() 패턴) - -```swift -// 기본 출석 상태 변경 -state.attendanceModal = .changeAttendanceStatus( - availableStatuses: [.attended, .late, .absent], - currentStatus: .attended -) - -// 늦은 시간 제한된 옵션 -state.attendanceModal = .lateTimeStatusChange( - availableStatuses: [.late, .absent] -) - -// 관리자용 전체 상태 변경 -state.attendanceModal = .adminStatusChange( - currentStatus: .attended -) -``` - -### 6. 핵심 차이점: ProfileReducer vs AttendanceModal - -**ProfileReducer customAlert:** -- `.confirmTapped` → title 기반으로 액션 구분 -- 단순 확인/취소 팝업 - -**AttendanceModal:** -- `.confirmTapped(String)` → status 파라미터로 직접 전달 -- 드롭다운 선택 후 상태명 반환 - -### 7. 실제 사용 시나리오 - -```swift -// 출석 체크 화면에서 -Button("출석 상태 수정") { - store.send(.view(.showAttendanceModal)) -} - -// 관리자 화면에서 -Button("상태 변경: \(attendance.status.desc)") { - store.send(.view(.showAdminModal(currentStatus: attendance.status))) -} - -// 늦은 시간 체크 -if isLateTime { - store.send(.view(.showLateTimeModal)) -} -``` -*/ - -// MARK: - 실제 예제 코드 (참고용) - -/* -@Reducer -struct ExampleFeature { - @ObservableState - struct State: Equatable { - @Presents var attendanceModal: AttendanceModalState? - } - - enum Action: ViewAction, FeatureAction, BindableAction { - case view(View) - case scope(ScopeAction) - case binding(BindingAction) - } - - @CasePathable - enum View { - case showAttendanceModal - } - - @CasePathable - enum ScopeAction { - case attendanceModal(PresentationAction) - } - - var body: some Reducer { - BindingReducer() - - Reduce { state, action in - switch action { - case .view(.showAttendanceModal): - state.attendanceModal = .changeAttendanceStatus( - availableStatuses: [.attended, .late, .absent] - ) - return .none - - case .scope(.attendanceModal(let action)): - return handleAttendanceModalAction(state: &state, action: action) - - case .binding: - return .none - } - } - .ifLet(\.$attendanceModal, action: \.scope.attendanceModal) { - AttendanceModal() - } - } - - private func handleAttendanceModalAction( - state: inout State, - action: PresentationAction - ) -> Effect { - switch action { - case .presented(.confirmTapped(let status)): - // 실제 API 호출이나 상태 업데이트 - print("선택된 상태: \(status)") - state.attendanceModal = nil - return .none - - case .presented(.cancelTapped): - state.attendanceModal = nil - return .none - - case .dismiss: - return .none - } - } -} - -struct ExampleView: View { - @Bindable var store: StoreOf - - var body: some View { - VStack { - Button("출석 상태 변경") { - store.send(.view(.showAttendanceModal)) - } - .padding() - } - .attendanceModal($store.scope(state: \.attendanceModal, action: \.scope.attendanceModal)) - } -} -*/ diff --git a/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift b/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift index 1e65a395..1e2d6f1d 100644 --- a/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift +++ b/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift @@ -198,10 +198,9 @@ extension AttendanceCheck { .run { await $0(.async(.fetchStatus)) }, ) } else { - // 이미 데이터가 있는 경우 출석 현황과 출석 리스트만 조용히 새로고침 return .concatenate( - .run { await $0(.async(.fetchSchedule)) }, // 성공시 fetchAttendanceCount와 fetchTeams 자동 호출 - .run { await $0(.async(.fetchStatus)) }, // scheduleID 업데이트 후 attendanceCount와 teams 자동 호출 + .run { await $0(.async(.fetchSchedule)) }, + .run { await $0(.async(.fetchStatus)) }, ) } @@ -437,6 +436,15 @@ extension AttendanceCheck { case .success(let data): state.attendanceModel = data state.attendanceByTeam[teamId] = data + + // 특정 팀 데이터를 받았을 때 해당 팀으로 selectPart 변경 + if let firstAttendance = data.first, + let teamEntity = firstAttendance.selectTeamEntity, + teamEntity != .unknown { + state.selectPart = teamEntity + #logDebug("[AttendanceCheck] Updated selectPart to: \(teamEntity.rawValue)") + } + case .failure(let error): #logNetwork("기수 출석 현황 조회 실패", error.localizedDescription) } diff --git a/Projects/Presentation/Splash/Sources/Reducer/Splash.swift b/Projects/Presentation/Splash/Sources/Reducer/Splash.swift index 4eb9b2b2..c4a4ce49 100644 --- a/Projects/Presentation/Splash/Sources/Reducer/Splash.swift +++ b/Projects/Presentation/Splash/Sources/Reducer/Splash.swift @@ -11,6 +11,7 @@ import Shareds import Utill import UseCase import Entity +import DesignSystem import ComposableArchitecture import LogMacro @@ -26,6 +27,12 @@ public struct Splash { @Shared(.appStorage("staffRole")) var staffRole: Staff? var profileModel: ProfileEntity? + // 앱 업데이트 관련 + @Presents var customAlert: CustomAlertState? + var appStoreUrl: String = "" + var isUpdateCheckCompleted: Bool = false + var profileFetchCompleted: Bool = false + public init() { } @@ -37,6 +44,17 @@ public struct Splash { case async(AsyncAction) case inner(InnerAction) case navigation(NavigationAction) + case scope(ScopeAction) + + @CasePathable + public enum ScopeAction { + case alert(PresentationAction) + case customAlert(PresentationAction) + } + } + + public enum AlertAction: Equatable { + // 필요시 추가 } // MARK: - ViewAction @@ -48,15 +66,17 @@ public struct Splash { } // MARK: - AsyncAction 비동기 처리 액션 - + public enum AsyncAction: Equatable { case fetchUser + case checkAppUpdate } // MARK: - 앱내에서 사용하는 액션 - + public enum InnerAction: Equatable { case fetchUserResponse(Result) + case checkAppUpdateResponse(Result) } // MARK: - NavigationAction @@ -69,12 +89,15 @@ public struct Splash { nonisolated enum CancelID: Hashable { case fetchProfile + case checkAppUpdate } @Dependency(\.continuousClock) var clock @Dependency(\.profileUseCase) var profileUseCase + @Dependency(\.appUpdateUseCase) var appUpdateUseCase + @Dependency(\.openURL) var openURL @Dependency(\.mainQueue) var mainQueue @Dependency(\.keychainManager) var keychainManager @@ -84,7 +107,7 @@ public struct Splash { switch action { case .binding(_): return .none - + case .view(let viewAction): return handleViewAction(state: &state, action: viewAction) @@ -93,11 +116,32 @@ public struct Splash { case .inner(let innerAction): return handleInnerAction(state: &state, action: innerAction) - + case .navigation(let navigationAction): return handleNavigationAction(state: &state, action: navigationAction) + + case .scope(.customAlert(.presented(.confirmTapped))): + // 앱스토어로 이동 + return .run { [appStoreUrl = state.appStoreUrl] _ in + if let url = URL(string: appStoreUrl) { + await openURL(url) + } + } + + case .scope(.customAlert(.presented(.cancelTapped))): + // "나중에 할게요" 선택 시 화면 이동 + if state.profileFetchCompleted { + return navigateToNextScreen(state: &state) + } + return .none + + case .scope: + return .none } } + .ifLet(\.$customAlert, action: \.scope.customAlert) { + EmptyReducer() + } } } @@ -113,6 +157,9 @@ extension Splash { // 먼저 딜레이를 주고 try await clock.sleep(for: .seconds(0.5)) + // 앱 업데이트 체크 (백그라운드에서) + await send(.async(.checkAppUpdate)) + if staffRole == .manager { #logDebug("👔 [Splash] Redirecting to staff") await send(.async(.fetchUser)) @@ -144,6 +191,16 @@ extension Splash { return await send(.inner(.fetchUserResponse(fetchUserResult))) } .cancellable(id: CancelID.fetchProfile, cancelInFlight: true) + + case .checkAppUpdate: + return .run { send in + let updateResult = await Result { + try await appUpdateUseCase.checkForUpdate() + } + .mapError(AppUpdateError.from) + await send(.inner(.checkAppUpdateResponse(updateResult))) + } + .cancellable(id: CancelID.checkAppUpdate, cancelInFlight: true) } } @@ -155,22 +212,17 @@ extension Splash { case .fetchUserResponse(let result): switch result { case .success(let profileDTOData): - #logDebug("✅ [Splash] User profile fetched successfully") + #logDebug("[Splash] User profile fetched successfully") state.profileModel = profileDTOData + state.profileFetchCompleted = true - // 사용자 역할에 따라 적절한 화면으로 이동 - let staffRole = state.staffRole - if staffRole == .manager { - #logDebug("👔 [Splash] Navigation to staff after profile fetch") - return .send(.navigation(.presentStaff)) - } else if staffRole == .member { - #logDebug("👤 [Splash] Navigation to member after profile fetch") - return .send(.navigation(.presentMember)) - } else { - #logDebug("❓ [Splash] No staff role after profile fetch - redirecting to login") - return .send(.navigation(.presentLogin)) + // 업데이트 체크가 완료되고 팝업이 없는 경우에만 화면 이동 + if state.isUpdateCheckCompleted && state.customAlert == nil { + return navigateToNextScreen(state: &state) } + return .none + case .failure(let error): #logError("❌ [Splash] Failed to fetch user profile", error.localizedDescription) @@ -181,6 +233,51 @@ extension Splash { await send(.navigation(.presentLogin)) } } + + case .checkAppUpdateResponse(let result): + state.isUpdateCheckCompleted = true + + switch result { + case .success(let updateInfo): + // 업데이트가 필요한 경우에만 Alert 표시 + if let updateInfo = updateInfo { + #logDebug("[Splash] App update available: \(updateInfo.latestVersion)") + state.appStoreUrl = updateInfo.appStoreUrl + + // 릴리즈 노트에서 실제 버전 추출 + let actualVersion = extractVersionFromReleaseNotes( + releaseNotes: updateInfo.releaseNotes, + fallbackVersion: updateInfo.latestVersion + ) + + let message = "새로운 버전 \(actualVersion)이 출시되었습니다!\n\n더 나은 경험을 위해 지금 업데이트하세요!" + + state.customAlert = .alert( + title: "새로운 버전이 출시되었어요!", + message: message, + confirmTitle: "지금 업데이트", + cancelTitle: "나중에 할게요", + isDestructive: false + ) + } else { + #logDebug("[Splash] App is up to date") + + // 업데이트가 없고 프로필 fetch가 완료되었다면 화면 이동 + if state.profileFetchCompleted { + return navigateToNextScreen(state: &state) + } + } + return .none + + case .failure(let error): + #logError("[Splash] Failed to check app update", error.localizedDescription) + + // 에러가 발생해도 프로필 fetch가 완료되었다면 화면 이동 + if state.profileFetchCompleted { + return navigateToNextScreen(state: &state) + } + return .none + } } } @@ -199,4 +296,48 @@ extension Splash { return .none // fetchUser는 이미 onAppear에서 처리됨 } } + + private func navigateToNextScreen(state: inout State) -> Effect { + let staffRole = state.staffRole + + if staffRole == .manager { + #logDebug("[Splash] Navigation to staff after checks completed") + return .send(.navigation(.presentStaff)) + } else if staffRole == .member { + #logDebug("[Splash] Navigation to member after checks completed") + return .send(.navigation(.presentMember)) + } else { + #logDebug("[Splash] No staff role after checks completed - redirecting to login") + return .send(.navigation(.presentLogin)) + } + } + + private func extractVersionFromReleaseNotes( + releaseNotes: String?, + fallbackVersion: String + ) -> String { + guard let releaseNotes = releaseNotes else { + return fallbackVersion + } + + // "[v 1.0.2]" 또는 "v 1.0.2" 패턴에서 버전 추출 + let patterns = [ + #"\[v\s*([0-9]+\.[0-9]+\.[0-9]+)\]"#, // [v 1.0.2] + #"v\s*([0-9]+\.[0-9]+\.[0-9]+)"# // v 1.0.2 + ] + + for pattern in patterns { + if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) { + let range = NSRange(location: 0, length: releaseNotes.count) + if let match = regex.firstMatch(in: releaseNotes, range: range) { + let versionRange = Range(match.range(at: 1), in: releaseNotes) + if let versionRange = versionRange { + return String(releaseNotes[versionRange]) + } + } + } + } + + return fallbackVersion + } } diff --git a/Projects/Presentation/Splash/Sources/View/SplashView.swift b/Projects/Presentation/Splash/Sources/View/SplashView.swift index f366f101..0f90f6c1 100644 --- a/Projects/Presentation/Splash/Sources/View/SplashView.swift +++ b/Projects/Presentation/Splash/Sources/View/SplashView.swift @@ -41,6 +41,7 @@ public struct SplashView: View { .onAppear { store.send(.view(.onAppear)) } + .customAlert($store.scope(state: \.customAlert, action: \.scope.customAlert)) } } diff --git a/Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift b/Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift index 5b193b0a..a69fbce8 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift @@ -120,4 +120,23 @@ public extension CustomAlertState where Action == CustomAlertAction { checkboxTitle: "개인정보처리방침 동의" ) } + + static func appUpdate( + version: String, + releaseNotes: String? = nil + ) -> CustomAlertState { + let message = if let releaseNotes = releaseNotes, !releaseNotes.isEmpty { + "새로운 버전 \(version)이 출시되었습니다!\n\n\(releaseNotes)\n\n더 나은 경험을 위해 업데이트하세요!" + } else { + "새로운 버전 \(version)이 준비되었습니다!\n\n더 나은 경험을 위해 지금 업데이트하세요!" + } + + return .alert( + title: "새로운 버전이 출시되었어요!", + message: message, + confirmTitle: "지금 업데이트", + cancelTitle: "나중에 할게요", + isDestructive: false + ) + } } diff --git a/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift b/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift index 52131c01..ccc73744 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift @@ -47,6 +47,7 @@ public struct AttendanceCheckStatusCard: View { Text("\(selectTeam.attandanceCardDescription) / \(selectPart.desc) ") .pretendardCustomFont(textStyle: .body2NormalBold) .foregroundStyle(isDisabled ? .borderDisabled : .staticWhite) + .minimumScaleFactor(0.7) } Spacer() From 0112ab55d3383899918566ce37decddea76b47a8 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 10 Mar 2026 00:21:59 +0900 Subject: [PATCH 2/3] release: Bump version to 1.0.3 with app update popup --- .../Project+Templete/Extension+String.swift | 4 ++-- fastlane/metadata/en-US/release_notes.txt | 3 ++- fastlane/metadata/ko/release_notes.txt | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift index 42827fb3..3e1af73e 100644 --- a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift +++ b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift @@ -9,7 +9,7 @@ import Foundation import ProjectDescription extension String { - public static func appVersion(version: String = "1.0.2") -> String { + public static func appVersion(version: String = "1.0.3") -> String { return version } @@ -17,7 +17,7 @@ extension String { return Project.Environment.bundlePrefix } - public static func appBuildVersion(buildVersion: String = "72") -> String { + public static func appBuildVersion(buildVersion: String = "73") -> String { return buildVersion } diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index cc512a5b..cb326d98 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,2 +1,3 @@ -[v 1.0.2] +[v 1.0.3] - Bug fixes and performance improvements +- Add app store version check popup diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt index d385b91c..e75203e2 100644 --- a/fastlane/metadata/ko/release_notes.txt +++ b/fastlane/metadata/ko/release_notes.txt @@ -1,2 +1,3 @@ -[v 1.0.2] -- 버그 수정 +[v 1.0.3] +- 버그 수정 +- 앱 업데이트 팝업 추가 From fc03b5255f275cf452c463574eb7764e6d877962 Mon Sep 17 00:00:00 2001 From: Roy Date: Sat, 21 Mar 2026 15:03:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix=20=20:=20=EC=95=B1=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=EC=88=98=EC=A0=95=EB=B0=8F=20=20=EC=B6=9C=EC=84=9D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=20=EB=8C=80=EA=B8=B0=EC=83=81=ED=83=9C=20=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=201.0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Project+Templete/Extension+String.swift | 4 +-- .../Mapper/AttendanceDTOModel+.swift | 2 +- .../OnBoarding/OnBoardingService.swift | 2 +- .../Sources/Attendance/AttendanceStatus.swift | 3 ++ .../Reducer/AttendanceCheck.swift | 4 +-- .../Sources/Main/Reducer/ProfileReducer.swift | 29 ++++++++++--------- .../Default_icons.imageset/Contents.json | 12 ++++++++ .../Default_icons.imageset/Default_icons.svg | 9 ++++++ .../UI/Card/AttendanceCheckStatusCard.swift | 5 +++- fastlane/metadata/en-US/release_notes.txt | 3 +- fastlane/metadata/ko/release_notes.txt | 5 ++-- 11 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Default_icons.svg diff --git a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift index 3e1af73e..59f84f22 100644 --- a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift +++ b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Extension+String.swift @@ -9,7 +9,7 @@ import Foundation import ProjectDescription extension String { - public static func appVersion(version: String = "1.0.3") -> String { + public static func appVersion(version: String = "1.0.4") -> String { return version } @@ -17,7 +17,7 @@ extension String { return Project.Environment.bundlePrefix } - public static func appBuildVersion(buildVersion: String = "73") -> String { + public static func appBuildVersion(buildVersion: String = "74") -> String { return buildVersion } diff --git a/Projects/Data/Model/Sources/Attendance/Mapper/AttendanceDTOModel+.swift b/Projects/Data/Model/Sources/Attendance/Mapper/AttendanceDTOModel+.swift index 76d62aa4..8424a51a 100644 --- a/Projects/Data/Model/Sources/Attendance/Mapper/AttendanceDTOModel+.swift +++ b/Projects/Data/Model/Sources/Attendance/Mapper/AttendanceDTOModel+.swift @@ -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 ) } } diff --git a/Projects/Data/Service/Sources/OnBoarding/OnBoardingService.swift b/Projects/Data/Service/Sources/OnBoarding/OnBoardingService.swift index a9b575e8..c166abc7 100644 --- a/Projects/Data/Service/Sources/OnBoarding/OnBoardingService.swift +++ b/Projects/Data/Service/Sources/OnBoarding/OnBoardingService.swift @@ -19,7 +19,7 @@ public enum OnBoardingService { } -extension OnBoardingService: BaseTargetType { +extension OnBoardingService: BaseTargetType { public typealias Domain = AttendanceDomain public var domain: AttendanceDomain { diff --git a/Projects/Domain/Entity/Sources/Attendance/AttendanceStatus.swift b/Projects/Domain/Entity/Sources/Attendance/AttendanceStatus.swift index 4a61352c..7d082eb9 100644 --- a/Projects/Domain/Entity/Sources/Attendance/AttendanceStatus.swift +++ b/Projects/Domain/Entity/Sources/Attendance/AttendanceStatus.swift @@ -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 @@ -24,6 +25,8 @@ public enum AttendanceStatus: String, CaseIterable, Equatable, Identifiable { return "지각" case .absent: return "결석" + case .defaults: + return "대기" } } diff --git a/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift b/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift index 1e2d6f1d..ce327d70 100644 --- a/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift +++ b/Projects/Presentation/Management/Sources/AttendanceCheck/Reducer/AttendanceCheck.swift @@ -190,7 +190,6 @@ extension AttendanceCheck { case .onAppear: // 첫 진입 시에만 전체 데이터 로드, 이후에는 중요한 데이터만 새로고침 state.selectPart = state.userSession.selectTeam - print("선택 된 팀", state.selectPart, state.userSession.selectTeam) if !state.hasFetchedAttendance { state.hasFetchedAttendance = true return .concatenate( @@ -199,8 +198,7 @@ extension AttendanceCheck { ) } else { return .concatenate( - .run { await $0(.async(.fetchSchedule)) }, - .run { await $0(.async(.fetchStatus)) }, + .run { await $0(.view(.refreshData)) }, ) } diff --git a/Projects/Presentation/Profile/Sources/Main/Reducer/ProfileReducer.swift b/Projects/Presentation/Profile/Sources/Main/Reducer/ProfileReducer.swift index 307e017a..700170af 100644 --- a/Projects/Presentation/Profile/Sources/Main/Reducer/ProfileReducer.swift +++ b/Projects/Presentation/Profile/Sources/Main/Reducer/ProfileReducer.swift @@ -10,6 +10,7 @@ import Foundation import Shareds import UseCase + import AsyncMoya import ComposableArchitecture import Entity @@ -18,7 +19,7 @@ import DesignSystem @Reducer public struct ProfileReducer { public init() {} - + @ObservableState public struct State: Equatable { var isLoading: Bool = false @@ -46,12 +47,12 @@ public struct ProfileReducer { public init() {} } - + @Reducer(state: .equatable) public enum Destination { case createApp(CreateApp) } - + public enum Action: ViewAction, FeatureAction, BindableAction { case destination(PresentationAction) case binding(BindingAction) @@ -62,7 +63,7 @@ public struct ProfileReducer { case navigation(NavigationAction) } - + // MARK: - View action @CasePathable public enum View { @@ -71,7 +72,7 @@ public struct ProfileReducer { case showWithdrawAlert case showLogoutAlert } - + // MARK: - 비동기 처리 액션 @CasePathable public enum AsyncAction: Equatable { @@ -79,7 +80,7 @@ public struct ProfileReducer { case deleteUser case logout } - + // MARK: - 앱내에서 사용하는 액션 @CasePathable public enum InnerAction: Equatable { @@ -87,7 +88,7 @@ public struct ProfileReducer { case deleteUserResponse(Result) case logoutResponses(Result) } - + // MARK: - 네비게이션 연결 액션 @CasePathable public enum NavigationAction: Equatable { @@ -119,32 +120,32 @@ public struct ProfileReducer { @Dependency(\.profileUseCase) var profileUseCase @Dependency(\.mainQueue) var mainQueue @Dependency(\.continuousClock) var clock - + public var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(_): return .none - + case .destination(let destinationAction): return handleDestinationAction(state: &state, action: destinationAction) // MARK: - ViewAction - + case .view(let viewAction): return handleViewAction(state: &state, action: viewAction) - + // MARK: - AsyncAction case .async(let asyncAction): return handleAsyncAction(state: &state, action: asyncAction) - + // MARK: - InnerAction - + case .inner(let innerAction): return handleInnerAction(state: &state, action: innerAction) - + // MARK: - NavigationAction case .navigation(let navigationAction): return handleNavigationAction(state: &state, action: navigationAction) diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Contents.json new file mode 100644 index 00000000..6ca2a665 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Default_icons.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Default_icons.svg b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Default_icons.svg new file mode 100644 index 00000000..075aa7fe --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/ImageAssets.xcassets/Attandance/Default_icons.imageset/Default_icons.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift b/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift index ccc73744..58087075 100644 --- a/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift +++ b/Projects/Shared/DesignSystem/Sources/UI/Card/AttendanceCheckStatusCard.swift @@ -100,7 +100,7 @@ public struct AttendanceCheckStatusCard: View { } private var isDisabled: Bool { - attendanceStatus == .absent + attendanceStatus == .absent || attendanceStatus == .defaults } private var imageName: String { @@ -111,6 +111,9 @@ public struct AttendanceCheckStatusCard: View { return "Late_icons" case .absent: return "Abesent_icons" + case .defaults: + return "Default_icons" + } } } diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index cb326d98..07f1eaca 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,3 +1,2 @@ -[v 1.0.3] +[v 1.0.4] - Bug fixes and performance improvements -- Add app store version check popup diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt index e75203e2..f8eba098 100644 --- a/fastlane/metadata/ko/release_notes.txt +++ b/fastlane/metadata/ko/release_notes.txt @@ -1,3 +1,2 @@ -[v 1.0.3] -- 버그 수정 -- 앱 업데이트 팝업 추가 +[v 1.0.4] +- 버그 수정