From 6b8a8e2d2b5b558656992fae628d7f9a595fb65f Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Sat, 20 Jul 2024 17:25:04 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[IDLE-180]=20=EC=84=BC=ED=84=B0=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=EC=A0=95=EB=B3=B4=20API=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 3 + .../DefaultUserProfileRepository.swift | 25 +++ .../APITesting/TokenTesting.swift | 12 +- .../API/{Auth => }/AuthAPI.swift | 0 .../Data/NetworkDataSource/API/BaseAPI.swift | 7 +- .../API/UserInformationAPI.swift | 51 +++++++ .../DTO/UserInfo/CenterProfileDTO.swift | 38 +++++ .../{Auth => }/CenterRegisterService.swift | 0 .../Service/UserInformationService.swift | 17 +++ .../DefaultCenterProfileUseCase.swift | 30 ++++ .../Entity/Error/UserInfo/UserInfoError.swift | 22 +++ .../Domain/Entity/State/User/UserType.swift | 13 ++ .../Entity/VO/UserInfo/CenterProfileVO.swift | 32 ++++ .../UserInfo/UserProfileRepository.swift | 15 ++ .../UserInfo/CenterProfileUseCase.swift | 21 +++ .../ExampleApp/Sources/SceneDelegate.swift | 15 +- .../Profile/CenterProfileViewController.swift | 15 +- .../Profile/CenterProfileViewModel.swift | 142 +++++++++++++----- .../Extensions/Rx+UIViewController.swift | 17 +++ 19 files changed, 416 insertions(+), 59 deletions(-) create mode 100644 project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift rename project/Projects/Data/NetworkDataSource/API/{Auth => }/AuthAPI.swift (100%) create mode 100644 project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift create mode 100644 project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift rename project/Projects/Data/NetworkDataSource/Service/{Auth => }/CenterRegisterService.swift (100%) create mode 100644 project/Projects/Data/NetworkDataSource/Service/UserInformationService.swift create mode 100644 project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift create mode 100644 project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift create mode 100644 project/Projects/Domain/Entity/State/User/UserType.swift create mode 100644 project/Projects/Domain/Entity/VO/UserInfo/CenterProfileVO.swift create mode 100644 project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift create mode 100644 project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift create mode 100644 project/Projects/Presentation/PresentationCore/Sources/Extensions/Rx+UIViewController.swift diff --git a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift index e7737ab5..2cf88964 100644 --- a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -30,6 +30,9 @@ public enum IdleInfoPlist { public static let exampleAppDefault: InfoPlist = .extendingDefault(with: [ "Privacy - Photo Library Usage Description" : "프로필 사진 설정을 위해 사진 라이브러리에 접근합니다.", + "NSAppTransportSecurity" : [ + "NSAllowsArbitraryLoads" : true + ], "UILaunchStoryboardName": "LaunchScreen.storyboard", "CFBundleDisplayName" : "$(BUNDLE_DISPLAY_NAME)", "UIApplicationSceneManifest": [ diff --git a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift new file mode 100644 index 00000000..b9b160b2 --- /dev/null +++ b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift @@ -0,0 +1,25 @@ +// +// DefaultUserProfileRepository.swift +// ConcreteRepository +// +// Created by choijunios on 7/20/24. +// + +import RxSwift +import Entity +import RepositoryInterface +import NetworkDataSource + +public class DefaultUserProfileRepository: UserProfileRepository { + + let service = UserInformationService() + + public init() { } + + public func getMyProfile(_ userType: UserType) -> Single { + + service + .requestDecodable(api: .getCenterProfile, with: .withToken) + .map { (dto: CenterProfileDTO) in dto.toEntity() } + } +} diff --git a/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift b/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift index 5d380e0a..9e3a5dfc 100644 --- a/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift +++ b/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift @@ -10,9 +10,9 @@ import RxSwift @testable import NetworkDataSource // TestKeyValueStore -class TestKeyValueStore: KeyValueStore { +public class TestKeyValueStore: KeyValueStore { - init(testStore: [String : String] = [:]) { + public init(testStore: [String : String] = [:]) { self.testStore = [ Key.Auth.kaccessToken: "access_token", Key.Auth.krefreshToken: "refresh_token", @@ -21,22 +21,22 @@ class TestKeyValueStore: KeyValueStore { var testStore: [String: String] = [:] - func save(key: String, value: String) throws { + public func save(key: String, value: String) throws { testStore[key] = value } - func get(key: String) -> String? { + public func get(key: String) -> String? { return testStore[key] } - func delete(key: String) throws { + public func delete(key: String) throws { testStore.removeValue(forKey: key) } - func removeAll() throws { + public func removeAll() throws { testStore.removeAll() } diff --git a/project/Projects/Data/NetworkDataSource/API/Auth/AuthAPI.swift b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift similarity index 100% rename from project/Projects/Data/NetworkDataSource/API/Auth/AuthAPI.swift rename to project/Projects/Data/NetworkDataSource/API/AuthAPI.swift diff --git a/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift b/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift index 57634184..f0496b92 100644 --- a/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift @@ -11,6 +11,7 @@ import Moya public enum APIType { case auth + case users } // MARK: BaseAPI @@ -25,13 +26,11 @@ public extension BaseAPI { var baseStr = NetworkConfig.baseUrl - let apiVersion = "v1" - - baseStr += "/api/\(apiVersion)" - switch apiType { case .auth: baseStr += "/auth" + case .users: + baseStr += "/users" } return URL(string: baseStr)! diff --git a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift new file mode 100644 index 00000000..b623553e --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift @@ -0,0 +1,51 @@ +// +// UserInformationAPI.swift +// NetworkDataSource +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import Moya +import Alamofire + +public enum UserInformationAPI { + + enum UserType { + case center, worker + } + + // 프로필 조회 + case getCenterProfile +// case getPreSignedUrlForProfile(type: UserType) +// case callbackForUpdateProfileImage(type: UserType) +} + +extension UserInformationAPI: BaseAPI { + + public var apiType: APIType { + .users + } + + public var path: String { + switch self { + case .getCenterProfile: + "/center/my/profile" + } + } + + public var method: Moya.Method { + switch self { + case .getCenterProfile: + .get + } + } + + public var task: Moya.Task { + switch self { + case .getCenterProfile: + return .requestPlain + } + } + +} diff --git a/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift new file mode 100644 index 00000000..331f35b5 --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift @@ -0,0 +1,38 @@ +// +// CenterProfileDTO.swift +// NetworkDataSource +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import Entity + +public struct CenterProfileDTO: Codable { + let centerName: String? + let officeNumber: String? + let roadNameAddress: String? + let lotNumberAddress: String? + let detailedAddress: String? + let longitude: String? + let latitude: String? + let introduce: String? + let profileImageUrl: String? +} + +public extension CenterProfileDTO { + + func toEntity() -> CenterProfileVO { + CenterProfileVO( + centerName: centerName ?? "", + officeNumber: officeNumber ?? "", + roadNameAddress: roadNameAddress ?? "", + lotNumberAddress: lotNumberAddress ?? "", + detailedAddress: detailedAddress ?? "", + longitude: longitude ?? "", + latitude: latitude ?? "", + introduce: introduce ?? "", + profileImageURL: URL(string: profileImageUrl ?? "") + ) + } +} diff --git a/project/Projects/Data/NetworkDataSource/Service/Auth/CenterRegisterService.swift b/project/Projects/Data/NetworkDataSource/Service/CenterRegisterService.swift similarity index 100% rename from project/Projects/Data/NetworkDataSource/Service/Auth/CenterRegisterService.swift rename to project/Projects/Data/NetworkDataSource/Service/CenterRegisterService.swift diff --git a/project/Projects/Data/NetworkDataSource/Service/UserInformationService.swift b/project/Projects/Data/NetworkDataSource/Service/UserInformationService.swift new file mode 100644 index 00000000..286d5ae3 --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/Service/UserInformationService.swift @@ -0,0 +1,17 @@ +// +// UserInformationService.swift +// NetworkDataSource +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public class UserInformationService: BaseNetworkService { + + public init() { } + + public override init(keyValueStore: KeyValueStore) { + super.init(keyValueStore: keyValueStore) + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift new file mode 100644 index 00000000..8162673f --- /dev/null +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift @@ -0,0 +1,30 @@ +// +// DefaultCenterProfileUseCase.swift +// ConcreteUseCase +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import RxSwift +import Entity +import UseCaseInterface +import RepositoryInterface + +public class DefaultCenterProfileUseCase: CenterProfileUseCase { + + let repository: UserProfileRepository + + public init(repository: UserProfileRepository) { + self.repository = repository + } + + public func getProfile() -> Single> { + + convert(task: repository + .getMyProfile(.center)) { [unowned self] error in + toDomainError(error: error) + } + + } +} diff --git a/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift b/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift new file mode 100644 index 00000000..5ea047df --- /dev/null +++ b/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift @@ -0,0 +1,22 @@ +// +// UserInfoError.swift +// Entity +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public enum UserInfoError: String, DomainError { + + + // undefinedError + case undefinedError="Err-000" + + public var message: String { + switch self { + case .undefinedError: + "❌ \(String(describing: Self.self)) 정의되지 않은 에러타입입니다. ❌" + } + } +} diff --git a/project/Projects/Domain/Entity/State/User/UserType.swift b/project/Projects/Domain/Entity/State/User/UserType.swift new file mode 100644 index 00000000..355e912a --- /dev/null +++ b/project/Projects/Domain/Entity/State/User/UserType.swift @@ -0,0 +1,13 @@ +// +// UserType.swift +// Entity +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public enum UserType { + case center + case worker +} diff --git a/project/Projects/Domain/Entity/VO/UserInfo/CenterProfileVO.swift b/project/Projects/Domain/Entity/VO/UserInfo/CenterProfileVO.swift new file mode 100644 index 00000000..c34d6d11 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/UserInfo/CenterProfileVO.swift @@ -0,0 +1,32 @@ +// +// CenterProfileVO.swift +// Entity +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public class CenterProfileVO { + public let centerName: String + public let officeNumber: String + public let roadNameAddress: String + public let lotNumberAddress: String + public let detailedAddress: String + public let longitude: String + public let latitude: String + public let introduce: String + public let profileImageURL: URL? + + public init(centerName: String, officeNumber: String, roadNameAddress: String, lotNumberAddress: String, detailedAddress: String, longitude: String, latitude: String, introduce: String, profileImageURL: URL?) { + self.centerName = centerName + self.officeNumber = officeNumber + self.roadNameAddress = roadNameAddress + self.lotNumberAddress = lotNumberAddress + self.detailedAddress = detailedAddress + self.longitude = longitude + self.latitude = latitude + self.introduce = introduce + self.profileImageURL = profileImageURL + } +} diff --git a/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift b/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift new file mode 100644 index 00000000..31e38ae1 --- /dev/null +++ b/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift @@ -0,0 +1,15 @@ +// +// UserProfileRepository.swift +// RepositoryInterface +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import RxSwift +import Entity + +public protocol UserProfileRepository: RepositoryBase { + + func getMyProfile(_ userType: UserType) -> Single +} diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift new file mode 100644 index 00000000..611d091f --- /dev/null +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift @@ -0,0 +1,21 @@ +// +// CenterProfileUseCase.swift +// UseCaseInterface +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import RxSwift +import Entity + +/// 1. 센터 프로필 정보 조회 +/// 2. 센터 프로필 정보 업데이트(전화번호, 센터소개글) +/// 3. 센터 프로필 정보 업데이트(이미지, pre-signed-url) +/// 4. 센터 프로필 정보 업데이트(이미지, pre-signed-url-callback) + + +public protocol CenterProfileUseCase: UseCaseBase { + + func getProfile() -> Single> +} diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index 29606e91..596fda1b 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -7,6 +7,8 @@ import UIKit import CenterFeature +import ConcreteUseCase +import ConcreteRepository class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -16,15 +18,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = scene as? UIWindowScene else { return } - window = UIWindow(windowScene: windowScene) - let viewModel = CenterProfileViewModel() + let viewModel = CenterProfileViewModel( + useCase: DefaultCenterProfileUseCase( + repository: DefaultUserProfileRepository() + ) + ) + let viewController = CenterProfileViewController() viewController.bind(viewModel: viewModel) - window?.rootViewController = viewController - window?.makeKeyAndVisible() + self.window?.rootViewController = viewController + self.window?.makeKeyAndVisible() + } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift index e9bb3a8b..25a19c32 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -17,11 +17,10 @@ public protocol CenterProfileViewModelable where Input: CenterProfileInputable, associatedtype Output var input: Input { get } var output: Output? { get } - - func requestData() } public protocol CenterProfileInputable { + var readyToFetch: PublishRelay { get } var editingButtonPressed: PublishRelay { get } var editingFinishButtonPressed: PublishRelay { get } var editingPhoneNumber: BehaviorRelay { get } @@ -34,7 +33,7 @@ public protocol CenterProfileOutputable { var centerLocation: Driver { get } var centerPhoneNumber: Driver { get } var centerIntroduction: Driver { get } - var centerImage: Driver { get } + var centerImage: Driver { get } var isEditingMode: Driver { get } var editingValidation: Driver { get } var alert: Driver { get } @@ -122,6 +121,7 @@ public class CenterProfileViewController: DisposableViewController { /// 센터 소개가 표시되는 라벨 let centerIntroductionLabel: IdleLabel = { let label = IdleLabel(typography: .Body3) + label.numberOfLines = 0 return label }() /// 센터 소개를 수정하는 텍스트 필드 @@ -358,6 +358,12 @@ public class CenterProfileViewController: DisposableViewController { // input let input = viewModel.input + let bindFinished = PublishRelay() + + bindFinished + .bind(to: input.readyToFetch) + .disposed(by: disposeBag) + profileEditButton .eventPublisher .bind(to: input.editingButtonPressed) @@ -452,7 +458,8 @@ public class CenterProfileViewController: DisposableViewController { } .disposed(by: disposeBag) - viewModel.requestData() + // 바인딩 종료 + bindFinished.accept(()) } public func showAlert(vo: DefaultAlertContentVO) { diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift index 0c2ec4f4..00aaf784 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -10,6 +10,7 @@ import Entity import RxSwift import RxCocoa import PresentationCore +import UseCaseInterface public struct ChangeCenterInformation { let phoneNumber: String? @@ -19,9 +20,15 @@ public struct ChangeCenterInformation { public class CenterProfileViewModel: CenterProfileViewModelable { + let profileUseCase: CenterProfileUseCase + public var input: Input public var output: Output? = nil + private var currentPhoneNumber: String? + private var currentIntroduction: String? + private var currentImage: UIImage? + func checkModification( prev_phoneNumber: String, prev_introduction: String, @@ -34,17 +41,75 @@ public class CenterProfileViewModel: CenterProfileViewModelable { ) } - public init() { + public init(useCase: CenterProfileUseCase) { + + self.profileUseCase = useCase + self.input = Input() + let profileRequestResult = input + .readyToFetch + .flatMap { [unowned self] _ in + self.profileUseCase.getProfile() + } + .share() + + let profileRequestSuccess = profileRequestResult + .compactMap { $0.value } + + let profileRequestFailure = profileRequestResult + .compactMap { $0.error } + .map { error in + DefaultAlertContentVO(title: "프로필 정보 불러오기 실패", message: error.message) + } + + let centerNameDriver = profileRequestSuccess + .map { [weak self] in + let name = $0.centerName + self?.currentPhoneNumber = name + return name + } + .asDriver(onErrorJustReturn: "") + + let centerAddressDriver = profileRequestSuccess + .map { $0.roadNameAddress } + .asDriver(onErrorJustReturn: "") + + let centerIntroductionDriver = profileRequestSuccess + .map { [weak self] in + let introduce = $0.introduce + self?.currentIntroduction = introduce + return introduce + } + .asDriver(onErrorJustReturn: "") + + let centerPhoneNumberDriver = profileRequestSuccess + .map { $0.officeNumber } + .asDriver(onErrorJustReturn: "") + + let centerImageDriver = profileRequestSuccess + .map { $0.profileImageURL } + .compactMap { $0 } + .observe(on: OperationQueueScheduler.init(operationQueue: .init(), queuePriority: .high)) + .map({ [weak self] imageUrl in + if let data = try? Data(contentsOf: imageUrl) { + let image = UIImage(data: data) + self?.currentImage = image + return image + } + return nil + }) + .asDriver(onErrorJustReturn: nil) + + // 최신 값들 + 버튼이 눌릴 경우 변경 로직이 실행된다. let editingRequestResult = input .editingFinishButtonPressed .map({ [unowned self] _ in self.checkModification( - prev_phoneNumber: self.input.centerPhoneNumber.value, - prev_introduction: self.input.centerIntroduction.value, - prev_image: self.input.centerImage.value + prev_phoneNumber: self.currentPhoneNumber ?? "", + prev_introduction: self.currentIntroduction ?? "", + prev_image: self.currentImage ?? .init() ) }) .flatMap { (inputs) in @@ -67,27 +132,42 @@ public class CenterProfileViewModel: CenterProfileViewModelable { // 스트림을 유지하기위해 생성한 Driver로 필수적으로 사용되지 않는다. let editingValidation = editingRequestResult .compactMap { $0.value } - .map { [weak input] info in + .map { [weak self] info in + + guard let self else { return () } if let phoneNumber = info.phoneNumber { printIfDebug("✅ 전화번호 변경 반영되었음") - input?.centerPhoneNumber.accept(phoneNumber) + currentPhoneNumber = phoneNumber } if let introduction = info.introduction { printIfDebug("✅ 센터소개 반영되었음") - input?.centerIntroduction.accept(introduction) + currentIntroduction = introduction } if let image = info.image { printIfDebug("✅ 센터 이미지 변경 반영되었음") - input?.centerImage.accept(image) + currentImage = image } + // 업데이트된 정보 요청 + input.readyToFetch.accept(()) + return () } .asDriver(onErrorJustReturn: ()) + let editingRequestFailure = editingRequestResult + .compactMap({ $0.error }) + .map({ error in + // 변경 실패 Alert + return DefaultAlertContentVO( + title: "변경 실패", + message: "변경 싪패 이유" + ) + }) + enum Mode { case editing, display } @@ -116,38 +196,24 @@ public class CenterProfileViewModel: CenterProfileViewModelable { .asDriver(onErrorJustReturn: false) - let alertDriver = editingRequestResult - .compactMap({ $0.error }) - .map({ error in - // 변경 실패 Alert - return DefaultAlertContentVO( - title: "변경 실패", - message: "변경 싪패 이유" - ) - }) + let alertDriver = Observable + .merge( + profileRequestFailure, + editingRequestFailure + ) .asDriver(onErrorJustReturn: .default) self.output = .init( - centerName: input.centerName.asDriver(onErrorJustReturn: ""), - centerLocation: input.centerLocation.asDriver(onErrorJustReturn: ""), - centerPhoneNumber: input.centerPhoneNumber.asDriver(onErrorJustReturn: ""), - centerIntroduction: input.centerIntroduction.asDriver(onErrorJustReturn: ""), - centerImage: input.centerImage.asDriver(onErrorJustReturn: UIImage()), + centerName: centerNameDriver, + centerLocation: centerAddressDriver, + centerPhoneNumber: centerPhoneNumberDriver, + centerIntroduction: centerIntroductionDriver, + centerImage: centerImageDriver, isEditingMode: isEditingMode, editingValidation: editingValidation, alert: alertDriver ) } - - public func requestData() { - - // 서버로 부터 데이터를 요청하는 API - input.centerName.accept("네 얼간이 방문요양센터") - input.centerLocation.accept("강남구 삼성동 512-3") - input.centerPhoneNumber.accept("(02) 123-4567") - input.centerIntroduction.accept("안녕하세요 반갑습니다!") - input.centerImage.accept(UIImage()) - } } @@ -155,14 +221,8 @@ public extension CenterProfileViewModel { class Input: CenterProfileInputable { - // 서버에서 받아오는데이터 - public var centerName = BehaviorRelay(value: "") - public var centerLocation = BehaviorRelay(value: "") - public var centerPhoneNumber = BehaviorRelay(value: "") - public var centerIntroduction = BehaviorRelay(value: "") - public var centerImage = BehaviorRelay(value: .init()) - // ViewController에서 받아오는 데이터 + public var readyToFetch: PublishRelay = .init() public var editingButtonPressed: PublishRelay = .init() public var editingFinishButtonPressed: PublishRelay = .init() public var editingPhoneNumber: BehaviorRelay = .init(value: "") @@ -176,7 +236,7 @@ public extension CenterProfileViewModel { public var centerLocation: Driver public var centerPhoneNumber: Driver public var centerIntroduction: Driver - public var centerImage: Driver + public var centerImage: Driver // 수정 상태 여부 public var isEditingMode: Driver @@ -186,7 +246,7 @@ public extension CenterProfileViewModel { public var alert: Driver - init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, centerImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { + init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, centerImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { self.centerName = centerName self.centerLocation = centerLocation self.centerPhoneNumber = centerPhoneNumber diff --git a/project/Projects/Presentation/PresentationCore/Sources/Extensions/Rx+UIViewController.swift b/project/Projects/Presentation/PresentationCore/Sources/Extensions/Rx+UIViewController.swift new file mode 100644 index 00000000..335c3c12 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/Extensions/Rx+UIViewController.swift @@ -0,0 +1,17 @@ +// +// Rx+UIViewController.swift +// PresentationCore +// +// Created by choijunios on 7/20/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public extension Reactive where Base: UIViewController { + var viewDidLoad: ControlEvent { + let source = self.methodInvoked(#selector(Base.viewDidLoad)).map { _ in } + return ControlEvent(events: source) + } +} From f277f26195d0ccf87932a02bf71bab85e517fba6 Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Sun, 21 Jul 2024 12:02:29 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[IDLE-180]=20=EC=84=BC=ED=84=B0=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C/=EC=88=98=EC=A0=95=20AP?= =?UTF-8?q?I=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultUserProfileRepository.swift | 63 +++++++- .../Data/NetworkDataSource/API/BaseAPI.swift | 8 +- .../NetworkDataSource/API/ExtenalUrlAPI.swift | 48 ++++++ .../API/UserInformationAPI.swift | 60 +++++++- .../UserInfo/ProfileImageUploadInfoDTO.swift | 15 ++ .../Service/BaseNetworkService.swift | 3 +- .../Service/ExternalRequestService.swift | 17 +++ .../DefaultCenterProfileUseCase.swift | 66 +++++++- .../Entity/Error/UserInfo/UserInfoError.swift | 6 + .../Entity/Transport/ImageUploadInfo.swift | 18 +++ .../UserInfo/UserProfileRepository.swift | 6 +- .../UserInfo/CenterProfileUseCase.swift | 2 +- .../ExampleApp/Sources/SceneDelegate.swift | 25 +++- .../Center/ExampleApp/Sources/Testing.swift | 28 ++++ .../Profile/CenterProfileViewController.swift | 10 +- .../Profile/CenterProfileViewModel.swift | 141 ++++++++++-------- .../ExampleApp/Sources/SceneDelegate.swift | 2 - .../Presentation/Feature/Worker/Project.swift | 4 + 18 files changed, 430 insertions(+), 92 deletions(-) create mode 100644 project/Projects/Data/NetworkDataSource/API/ExtenalUrlAPI.swift create mode 100644 project/Projects/Data/NetworkDataSource/DTO/UserInfo/ProfileImageUploadInfoDTO.swift create mode 100644 project/Projects/Data/NetworkDataSource/Service/ExternalRequestService.swift create mode 100644 project/Projects/Domain/Entity/Transport/ImageUploadInfo.swift create mode 100644 project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift diff --git a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift index b9b160b2..4cad3a36 100644 --- a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift +++ b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift @@ -5,6 +5,7 @@ // Created by choijunios on 7/20/24. // +import Foundation import RxSwift import Entity import RepositoryInterface @@ -12,14 +13,68 @@ import NetworkDataSource public class DefaultUserProfileRepository: UserProfileRepository { - let service = UserInformationService() + let userInformationService: UserInformationService + let externalRequestService: ExternalRequestService - public init() { } + public init(_ keyValueStore: KeyValueStore? = nil) { + + if let keyValueStore { + self.userInformationService = .init(keyValueStore: keyValueStore) + self.externalRequestService = .init(keyValueStore: keyValueStore) + } else { + self.userInformationService = .init() + self.externalRequestService = .init() + } + } - public func getMyProfile(_ userType: UserType) -> Single { + public func getCenterProfile() -> Single { - service + userInformationService .requestDecodable(api: .getCenterProfile, with: .withToken) .map { (dto: CenterProfileDTO) in dto.toEntity() } } + + public func updateCenterProfileForText(phoneNumber: String, introduction: String?) -> Single { + userInformationService + .request(api: .updateCenterProfile( + officeNumber: phoneNumber, + introduce: introduction + ), with: .withToken) + .map { _ in return () } + } + + /// 이미지 업로드 + public func uploadImage(_ userType: UserType, imageInfo: ImageUploadInfo) -> Single { + + getPreSignedUrl(userType, ext: imageInfo.ext) + .flatMap { [unowned self] dto in + self.uploadImageToPreSignedUrl(url: dto.uploadUrl, data: imageInfo.data) + .map { _ in (id: dto.imageId, ext: dto.imageFileExtension) } + } + .flatMap { (id, ext) in + self.callbackToServerForUploadImageSuccess(userType, imageId: id, ext: ext) + } + } + + private func getPreSignedUrl(_ userType: UserType, ext: String) -> Single { + userInformationService + .request(api: .getPreSignedUrl(userType: userType, imageExt: ext), with: .withToken) + .map(ProfileImageUploadInfoDTO.self) + } + + private func uploadImageToPreSignedUrl(url: String, data: Data) -> Single { + externalRequestService + .request(api: .uploadImageToS3(url: url, data: data), with: .plain) + .map { _ in () } + } + + private func callbackToServerForUploadImageSuccess(_ userType: UserType, imageId: String, ext: String) -> Single { + userInformationService + .request(api: .imageUploadSuccessCallback( + userType: userType, + imageId: imageId, + imageExt: ext), with: .withToken + ) + .map { _ in () } + } } diff --git a/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift b/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift index f0496b92..338c636c 100644 --- a/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift @@ -12,6 +12,7 @@ public enum APIType { case auth case users + case external(url: String) } // MARK: BaseAPI @@ -31,6 +32,8 @@ public extension BaseAPI { baseStr += "/auth" case .users: baseStr += "/users" + case .external(let url): + baseStr = url } return URL(string: baseStr)! @@ -39,7 +42,10 @@ public extension BaseAPI { /// Default header var headers: [String : String]? { - return ["Content-Type": "application/json"] + switch apiType { + default: + ["Content-Type": "application/json"] + } } var validationType: ValidationType { .successCodes } diff --git a/project/Projects/Data/NetworkDataSource/API/ExtenalUrlAPI.swift b/project/Projects/Data/NetworkDataSource/API/ExtenalUrlAPI.swift new file mode 100644 index 00000000..3946bffa --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/API/ExtenalUrlAPI.swift @@ -0,0 +1,48 @@ +// +// ExtenalUrlAPI.swift +// NetworkDataSource +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import Moya +import Alamofire + +public enum ExtenalUrlAPI { + + case uploadImageToS3(url: String, data: Data) +} + +extension ExtenalUrlAPI: BaseAPI { + + public var apiType: APIType { + var baseUrl: String! + switch self { + case .uploadImageToS3(let url, _): + baseUrl = url + } + return .external(url: baseUrl) + } + + public var path: String { + switch self { + default: + "" + } + } + + public var method: Moya.Method { + switch self { + case .uploadImageToS3: + .put + } + } + + public var task: Moya.Task { + switch self { + case .uploadImageToS3(_, let data): + .requestData(data) + } + } +} diff --git a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift index b623553e..5bb732b7 100644 --- a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift @@ -7,16 +7,30 @@ import Foundation import Moya +import Entity import Alamofire -public enum UserInformationAPI { - - enum UserType { - case center, worker +extension UserType { + var pathUri: String { + switch self { + case .center: + "center" + case .worker: + "carer" + } } +} + +public enum UserInformationAPI { // 프로필 조회 case getCenterProfile + case updateCenterProfile(officeNumber: String, introduce: String?) + + // 프로필 사진 업로드 + case getPreSignedUrl(userType: UserType, imageExt: String) + case imageUploadSuccessCallback(userType: UserType, imageId: String, imageExt: String) + // case getPreSignedUrlForProfile(type: UserType) // case callbackForUpdateProfileImage(type: UserType) } @@ -30,7 +44,13 @@ extension UserInformationAPI: BaseAPI { public var path: String { switch self { case .getCenterProfile: - "/center/my/profile" + "center/my/profile" + case .updateCenterProfile: + "center/my/profile" + case .getPreSignedUrl(let type, _): + "\(type.pathUri)/my/profile-image/upload-url" + case .imageUploadSuccessCallback(let type, _, _): + "\(type.pathUri)/my/profile-image/upload-callback" } } @@ -38,6 +58,19 @@ extension UserInformationAPI: BaseAPI { switch self { case .getCenterProfile: .get + case .updateCenterProfile: + .post + case .getPreSignedUrl: + .get + case .imageUploadSuccessCallback: + .post + } + } + + var parameterEncoding: ParameterEncoding { + switch self { + default: + return JSONEncoding.default } } @@ -45,6 +78,23 @@ extension UserInformationAPI: BaseAPI { switch self { case .getCenterProfile: return .requestPlain + case .updateCenterProfile(let officeNumber, let introduce): + var bodyData: [String: String] = ["officeNumber": officeNumber] + if let introduce { + bodyData["introduce"] = introduce + } + return .requestParameters(parameters: bodyData, encoding: parameterEncoding) + case .getPreSignedUrl(_, let imageExt): + let params: [String: String] = [ + "imageFileExtension": imageExt + ] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + case.imageUploadSuccessCallback(_, let imageId, let imageExt): + let params: [String: String] = [ + "imageId": imageId, + "imageFileExtension": imageExt + ] + return .requestParameters(parameters: params, encoding: parameterEncoding) } } diff --git a/project/Projects/Data/NetworkDataSource/DTO/UserInfo/ProfileImageUploadInfoDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/ProfileImageUploadInfoDTO.swift new file mode 100644 index 00000000..6e64241f --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/ProfileImageUploadInfoDTO.swift @@ -0,0 +1,15 @@ +// +// ProfileImageUploadInfoDTO.swift +// NetworkDataSource +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public struct ProfileImageUploadInfoDTO: Decodable { + + public let imageId: String + public let imageFileExtension: String + public let uploadUrl: String +} diff --git a/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift b/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift index 1d759558..d9d0f9b6 100644 --- a/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift +++ b/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift @@ -195,7 +195,7 @@ public extension BaseNetworkService { _request( api: api, - provider: with == .plain ? self.providerWithoutToken : self.providerWithoutToken + provider: with == .plain ? self.providerWithoutToken : self.providerWithToken ) } @@ -243,6 +243,7 @@ public extension BaseNetworkService { // } } +// MARK: HTTPResponseException+Extension extension HTTPResponseException { init(response: Response) { diff --git a/project/Projects/Data/NetworkDataSource/Service/ExternalRequestService.swift b/project/Projects/Data/NetworkDataSource/Service/ExternalRequestService.swift new file mode 100644 index 00000000..5aa17e1f --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/Service/ExternalRequestService.swift @@ -0,0 +1,17 @@ +// +// ExternalRequestService.swift +// NetworkDataSource +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public class ExternalRequestService: BaseNetworkService { + + public init() { } + + public override init(keyValueStore: KeyValueStore) { + super.init(keyValueStore: keyValueStore) + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift index 8162673f..632847ed 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift @@ -20,11 +20,73 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { } public func getProfile() -> Single> { - convert(task: repository - .getMyProfile(.center)) { [unowned self] error in + .getCenterProfile()) { [unowned self] error in toDomainError(error: error) } + } + + public func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> { + + var updateText: Single! + var updateImage: Single! + + if let phoneNumber { + updateText = repository.updateCenterProfileForText( + phoneNumber: phoneNumber, + introduction: introduction + ) + } else { + updateText = .just(()) + } + + if let imageInfo { + updateImage = repository.uploadImage( + .center, + imageInfo: imageInfo + ) + } else { + updateImage = .just(()) + } + + let updateTextResult = updateText + .catch { error in + if let httpExp = error as? HTTPResponseException { + let newError = HTTPResponseException( + status: httpExp.status, + rawCode: "Err-001", + timeStamp: httpExp.timeStamp + ) + + return .error(newError) + } + return .error(error) + } + + let updateImageResult = updateImage + .catch { error in + if let httpExp = error as? HTTPResponseException { + let newError = HTTPResponseException( + status: httpExp.status, + rawCode: "Err-002", + timeStamp: httpExp.timeStamp + ) + + return .error(newError) + } + return .error(error) + } + + let task = Observable + .zip( + updateTextResult.asObservable(), + updateImageResult.asObservable() + ) + .map { _ in () } + .asSingle() + return convert(task: task) { [unowned self] error in + toDomainError(error: error) + } } } diff --git a/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift b/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift index 5ea047df..137dee3c 100644 --- a/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift +++ b/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift @@ -9,6 +9,8 @@ import Foundation public enum UserInfoError: String, DomainError { + case textUpdateFailed = "Err-001" + case imageUpdateFailed = "Err-002" // undefinedError case undefinedError="Err-000" @@ -17,6 +19,10 @@ public enum UserInfoError: String, DomainError { switch self { case .undefinedError: "❌ \(String(describing: Self.self)) 정의되지 않은 에러타입입니다. ❌" + case .imageUpdateFailed: + "이미지 업로드에 실패했습니다." + case .textUpdateFailed: + "프로필 업로드에 실패했습니다." } } } diff --git a/project/Projects/Domain/Entity/Transport/ImageUploadInfo.swift b/project/Projects/Domain/Entity/Transport/ImageUploadInfo.swift new file mode 100644 index 00000000..2faf980e --- /dev/null +++ b/project/Projects/Domain/Entity/Transport/ImageUploadInfo.swift @@ -0,0 +1,18 @@ +// +// ImageUploadInfo.swift +// Entity +// +// Created by choijunios on 7/20/24. +// + +import Foundation + +public struct ImageUploadInfo { + public let data: Data + public let ext: String + + public init(data: Data, ext: String) { + self.data = data + self.ext = ext + } +} diff --git a/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift b/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift index 31e38ae1..cbc6ca84 100644 --- a/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift @@ -11,5 +11,9 @@ import Entity public protocol UserProfileRepository: RepositoryBase { - func getMyProfile(_ userType: UserType) -> Single + func getCenterProfile() -> Single + func updateCenterProfileForText(phoneNumber: String, introduction: String?) -> Single + + // ImageUpload + func uploadImage(_ userType: UserType, imageInfo: ImageUploadInfo) -> Single } diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift index 611d091f..7809cfc0 100644 --- a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift @@ -14,8 +14,8 @@ import Entity /// 3. 센터 프로필 정보 업데이트(이미지, pre-signed-url) /// 4. 센터 프로필 정보 업데이트(이미지, pre-signed-url-callback) - public protocol CenterProfileUseCase: UseCaseBase { func getProfile() -> Single> + func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> } diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index 596fda1b..5106809f 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -20,18 +20,27 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) - let viewModel = CenterProfileViewModel( - useCase: DefaultCenterProfileUseCase( - repository: DefaultUserProfileRepository() - ) + let store = TestStore() + + try! store.saveAuthToken( + accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMTUzMDYwMCwibmJmIjoxNzIxNTMwNjAwLCJleHAiOjE3MjE1MzEyMDAsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkwZDMzOC0zZjg0LTc3M2MtOTZhYy01MzZlODg2ZjBkMjUiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIifQ.dA9TrFJFDL715ram0uaShCjqRPI8t8iZ39ZJn7oHu6E", + refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMTQ4OTczMCwibmJmIjoxNzIxNDg5NzMwLCJleHAiOjE3MjI2OTkzMzAsInR5cGUiOiJSRUZSRVNIX1RPS0VOIiwidXNlcklkIjoiMDE5MGNmNDgtM2RjNi03ZWVkLTk4OGUtYTA5N2EwZDEwYjMzIn0.Hx4t09U3ra5RbYvwjl3flQccw6-hBMWUxY6zI_eVpiQ" ) - let viewController = CenterProfileViewController() + let useCase = DefaultCenterProfileUseCase( + repository: DefaultUserProfileRepository(store) + ) - viewController.bind(viewModel: viewModel) + let viewModel = CenterProfileViewModel( + useCase: useCase + ) + + let vc = CenterProfileViewController() - self.window?.rootViewController = viewController - self.window?.makeKeyAndVisible() + vc.bind(viewModel: viewModel) + window = UIWindow(windowScene: windowScene) + window?.rootViewController = vc + window?.makeKeyAndVisible() } } diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift new file mode 100644 index 00000000..c8aff834 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift @@ -0,0 +1,28 @@ +// +// Testing.swift +// Center_ExampleApp +// +// Created by choijunios on 7/20/24. +// + +import Foundation +import NetworkDataSource + +class TestStore: KeyValueStore { + func save(key: String, value: String) throws { + UserDefaults.standard.setValue(value, forKey: key) + } + + func get(key: String) -> String? { + UserDefaults.standard.string(forKey: key) + } + + func delete(key: String) throws { + + } + + func removeAll() throws { + + } + +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift index 25a19c32..61ea6fd1 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -25,7 +25,7 @@ public protocol CenterProfileInputable { var editingFinishButtonPressed: PublishRelay { get } var editingPhoneNumber: BehaviorRelay { get } var editingInstruction: BehaviorRelay { get } - var editingImage: BehaviorRelay { get } + var selectedImage: PublishRelay { get } } public protocol CenterProfileOutputable { @@ -33,7 +33,7 @@ public protocol CenterProfileOutputable { var centerLocation: Driver { get } var centerPhoneNumber: Driver { get } var centerIntroduction: Driver { get } - var centerImage: Driver { get } + var displayingImage: Driver { get } var isEditingMode: Driver { get } var editingValidation: Driver { get } var alert: Driver { get } @@ -385,7 +385,7 @@ public class CenterProfileViewController: DisposableViewController { .disposed(by: disposeBag) edtingImage - .bind(to: input.editingImage) + .bind(to: input.selectedImage) .disposed(by: disposeBag) // output @@ -420,7 +420,7 @@ public class CenterProfileViewController: DisposableViewController { .disposed(by: disposeBag) output - .centerImage + .displayingImage .drive(centerImageView.rx.image) .disposed(by: disposeBag) @@ -505,8 +505,6 @@ extension CenterProfileViewController: UIImagePickerControllerDelegate, UINaviga if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { edtingImage.accept(image) - centerImageView.image = image - picker.dismiss(animated: true) } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift index 00aaf784..7026d87e 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -25,19 +25,21 @@ public class CenterProfileViewModel: CenterProfileViewModelable { public var input: Input public var output: Output? = nil - private var currentPhoneNumber: String? - private var currentIntroduction: String? - private var currentImage: UIImage? + private var fetchedPhoneNumber: String? + private var fetchedIntroduction: String? + private var fetchedImage: UIImage? - func checkModification( - prev_phoneNumber: String, - prev_introduction: String, - prev_image: UIImage) -> (String?, String?, UIImage?) - { - ( - input.editingPhoneNumber.value == prev_phoneNumber ? nil : input.editingPhoneNumber.value, - input.editingInstruction.value == prev_introduction ? nil : input.editingInstruction.value, - input.editingImage.value == prev_image ? nil : input.editingImage.value + private var editingImageInfo: ImageUploadInfo? + + func checkModification() -> (String?, String?, ImageUploadInfo?) { + + let phoneNumber = input.editingPhoneNumber.value + let instruction = input.editingInstruction.value + + return ( + phoneNumber == fetchedPhoneNumber ? nil : phoneNumber, + instruction == fetchedIntroduction ? nil : instruction, + editingImageInfo ) } @@ -47,6 +49,7 @@ public class CenterProfileViewModel: CenterProfileViewModelable { self.input = Input() + // MARK: fetch from server let profileRequestResult = input .readyToFetch .flatMap { [unowned self] _ in @@ -64,11 +67,7 @@ public class CenterProfileViewModel: CenterProfileViewModelable { } let centerNameDriver = profileRequestSuccess - .map { [weak self] in - let name = $0.centerName - self?.currentPhoneNumber = name - return name - } + .map { $0.centerName } .asDriver(onErrorJustReturn: "") let centerAddressDriver = profileRequestSuccess @@ -78,78 +77,88 @@ public class CenterProfileViewModel: CenterProfileViewModelable { let centerIntroductionDriver = profileRequestSuccess .map { [weak self] in let introduce = $0.introduce - self?.currentIntroduction = introduce + self?.fetchedIntroduction = introduce return introduce } .asDriver(onErrorJustReturn: "") let centerPhoneNumberDriver = profileRequestSuccess - .map { $0.officeNumber } + .map { [weak self] in + let phoneNumber = $0.officeNumber + self?.fetchedPhoneNumber = phoneNumber + return phoneNumber + } .asDriver(onErrorJustReturn: "") - let centerImageDriver = profileRequestSuccess + let fetchCenterImage = profileRequestSuccess .map { $0.profileImageURL } .compactMap { $0 } .observe(on: OperationQueueScheduler.init(operationQueue: .init(), queuePriority: .high)) .map({ [weak self] imageUrl in if let data = try? Data(contentsOf: imageUrl) { let image = UIImage(data: data) - self?.currentImage = image + self?.fetchedImage = image return image } return nil }) - .asDriver(onErrorJustReturn: nil) + + // MARK: image validation + let imageValidationResult = input + .selectedImage + .map { [unowned self] image -> UIImage? in + guard let imageInfo = self.validateSelectedImage(image: image) else { return nil } + printIfDebug("✅ 업로드 가능한 이미지 타입 \(imageInfo.ext)") + self.editingImageInfo = imageInfo + return image + } + .share() + + let imageValidationFailure = imageValidationResult + .filter { $0 == nil } + .map { _ in + DefaultAlertContentVO( + title: "이미지 선택 오류", + message: "지원하지 않는 이미지 형식입니다." + ) + } + + let displayingImageDriver = Observable + .merge( + fetchCenterImage, + imageValidationResult.compactMap { $0 } + ) + .asDriver(onErrorJustReturn: .init()) // 최신 값들 + 버튼이 눌릴 경우 변경 로직이 실행된다. let editingRequestResult = input .editingFinishButtonPressed .map({ [unowned self] _ in - self.checkModification( - prev_phoneNumber: self.currentPhoneNumber ?? "", - prev_introduction: self.currentIntroduction ?? "", - prev_image: self.currentImage ?? .init() - ) + checkModification() }) - .flatMap { (inputs) in + .flatMap { [useCase] (inputs) in - let (phoneNumber, introduction, image) = inputs + let (phoneNumber, introduction, imageInfo) = inputs // 변경이 발생하지 않은 곳은 nil값이 전달된다. + if let _ = phoneNumber { printIfDebug("✅ 전화번호 변경되었음") } + if let _ = introduction { printIfDebug("✅ 센터소개 변경되었음") } + if let _ = imageInfo { printIfDebug("✅ 센터 이미지 변경되었음") } - // API 호출 - return Single.just(Result.success( - ChangeCenterInformation( - phoneNumber: phoneNumber, - introduction: introduction, - image: image - ) - )) + return useCase.updateProfile( + phoneNumber: phoneNumber, + introduction: introduction, + imageInfo: imageInfo + ) } .share() - // 스트림을 유지하기위해 생성한 Driver로 필수적으로 사용되지 않는다. let editingValidation = editingRequestResult .compactMap { $0.value } - .map { [weak self] info in + .map { [input] info in - guard let self else { return () } - - if let phoneNumber = info.phoneNumber { - printIfDebug("✅ 전화번호 변경 반영되었음") - currentPhoneNumber = phoneNumber - } - - if let introduction = info.introduction { - printIfDebug("✅ 센터소개 반영되었음") - currentIntroduction = introduction - } - - if let image = info.image { - printIfDebug("✅ 센터 이미지 변경 반영되었음") - currentImage = image - } + printIfDebug("✅ 정보가 성공적으로 업데이트됨") // 업데이트된 정보 요청 input.readyToFetch.accept(()) @@ -199,7 +208,8 @@ public class CenterProfileViewModel: CenterProfileViewModelable { let alertDriver = Observable .merge( profileRequestFailure, - editingRequestFailure + editingRequestFailure, + imageValidationFailure ) .asDriver(onErrorJustReturn: .default) @@ -208,12 +218,21 @@ public class CenterProfileViewModel: CenterProfileViewModelable { centerLocation: centerAddressDriver, centerPhoneNumber: centerPhoneNumberDriver, centerIntroduction: centerIntroductionDriver, - centerImage: centerImageDriver, + displayingImage: displayingImageDriver, isEditingMode: isEditingMode, editingValidation: editingValidation, alert: alertDriver ) } + + func validateSelectedImage(image: UIImage) -> ImageUploadInfo? { + if let pngData = image.pngData() { + return .init(data: pngData, ext: "PNG") + } else if let jpegData = image.jpegData(compressionQuality: 1) { + return .init(data: jpegData, ext: "JPEG") + } + return nil + } } @@ -227,7 +246,7 @@ public extension CenterProfileViewModel { public var editingFinishButtonPressed: PublishRelay = .init() public var editingPhoneNumber: BehaviorRelay = .init(value: "") public var editingInstruction: BehaviorRelay = .init(value: "") - public var editingImage: BehaviorRelay = .init(value: .init()) + public var selectedImage: PublishRelay = .init() } class Output: CenterProfileOutputable { @@ -236,7 +255,7 @@ public extension CenterProfileViewModel { public var centerLocation: Driver public var centerPhoneNumber: Driver public var centerIntroduction: Driver - public var centerImage: Driver + public var displayingImage: Driver // 수정 상태 여부 public var isEditingMode: Driver @@ -246,12 +265,12 @@ public extension CenterProfileViewModel { public var alert: Driver - init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, centerImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { + init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, displayingImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { self.centerName = centerName self.centerLocation = centerLocation self.centerPhoneNumber = centerPhoneNumber self.centerIntroduction = centerIntroduction - self.centerImage = centerImage + self.displayingImage = displayingImage self.isEditingMode = isEditingMode self.editingValidation = editingValidation self.alert = alert diff --git a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift index 015452b5..df94dbb6 100644 --- a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift @@ -16,8 +16,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = scene as? UIWindowScene else { return } - window = UIWindow(windowScene: windowScene) - window?.rootViewController = ViewController() window?.makeKeyAndVisible() } } diff --git a/project/Projects/Presentation/Feature/Worker/Project.swift b/project/Projects/Presentation/Feature/Worker/Project.swift index a21d9bfe..67367400 100644 --- a/project/Projects/Presentation/Feature/Worker/Project.swift +++ b/project/Projects/Presentation/Feature/Worker/Project.swift @@ -56,6 +56,10 @@ let project = Project( resources: ["ExampleApp/Resources/**"], dependencies: [ .target(name: "WorkerFeature"), + + D.Domain.ConcreteUseCase, + D.Data.ConcreteRepository, + D.Data.NetworkDataSource, ], settings: .settings( configurations: IdleConfiguration.presentationConfigurations From 2c16a8ea280a88cd6f80a674ca00ae2e0e0d90db Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Sun, 21 Jul 2024 12:35:09 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[IDLE-000]=20GenderEntity=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EB=B0=8F=20SVG=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVG형식의 이미지파일은 픽셀기반이 아닌 수학적 벡터를 사용하여 이미지를 랜더링한다. 따라서 Single scale모드로 사용해도 이미지의 해상도가 저하되지 않는다는 장점이 있다. 해당 방식을 적용한 이유는 화면 scale에(1x, 2x, 3x) 따라 이미지를 각각 할당해야하는 작업이 필요하지 않다는 것에서도 의의가 있다고 판단하여 사용하였다. UIImage는 랜더링 모드를 사용할 수 있다. 랜더링모드가 template일 경우 UIImageView의 tintColor프로퍼티로 전달한 색상값이 이미지 랜더링에 적용된다. 이미지가 단조로운 경우 여러 이미지를 사용할 필요없이 해당 방식을 사용하여 하나의 이미지를 범용적으로 쓸 수 있다는 장점이 있다. --- .../Auth/Worker/WorkerRegisterState.swift | 17 -------- .../Domain/Entity/State/User/Gender.swift | 25 ++++++++++++ .../activestar.imageset/Contents.json | 23 ----------- .../activestar.imageset/activestar 1.png | Bin 861 -> 0 bytes .../activestar.imageset/activestar 2.png | Bin 861 -> 0 bytes .../activestar.imageset/activestar.png | Bin 861 -> 0 bytes .../star.imageset/Contents.json | 23 ----------- .../Icons.xcassets/star.imageset/start 1.png | Bin 893 -> 0 bytes .../Icons.xcassets/star.imageset/start 2.png | Bin 893 -> 0 bytes .../Icons.xcassets/star.imageset/start.png | Bin 893 -> 0 bytes .../subscribeStar.imageset/Contents.json | 12 ++++++ .../subscribeStar.imageset/subscribeStar.svg | 3 ++ .../CommonUI/Card/WorkerEmployCard.swift | 12 +++--- .../Sources/CommonUI/IconStateButton.swift | 38 +++++++++--------- 14 files changed, 65 insertions(+), 88 deletions(-) create mode 100644 project/Projects/Domain/Entity/State/User/Gender.swift delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/Contents.json delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar 1.png delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar 2.png delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar.png delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/Contents.json delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/start 1.png delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/start 2.png delete mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/start.png create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/subscribeStar.imageset/Contents.json create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/subscribeStar.imageset/subscribeStar.svg diff --git a/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift b/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift index 1c0bbdf3..abbc0331 100644 --- a/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift +++ b/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift @@ -7,23 +7,6 @@ import Foundation -public enum Gender { - case notDetermined - case male - case female - - public var str: String { - switch self { - case .notDetermined: - "무결" - case .male: - "남성" - case .female: - "여성" - } - } -} - public class WorkerRegisterState { public var name: String = "" public var gender: Gender = .notDetermined diff --git a/project/Projects/Domain/Entity/State/User/Gender.swift b/project/Projects/Domain/Entity/State/User/Gender.swift new file mode 100644 index 00000000..fd130d28 --- /dev/null +++ b/project/Projects/Domain/Entity/State/User/Gender.swift @@ -0,0 +1,25 @@ +// +// Gender.swift +// Entity +// +// Created by choijunios on 7/21/24. +// + +import Foundation + +public enum Gender { + case notDetermined + case male + case female + + public var twoLetterKoreanWord: String { + switch self { + case .notDetermined: + "무결" + case .male: + "남성" + case .female: + "여성" + } + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/Contents.json deleted file mode 100644 index 77971f97..00000000 --- a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "activestar.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "activestar 1.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "activestar 2.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar 1.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar 1.png deleted file mode 100644 index a73e66a32b31c7df545f5f01f980c9e57b7298aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 861 zcmV-j1ETziP)oZ{Gs{DTf`MAIX6oK?sZ;y*dWW1)n`zel_O5V~Q^) z2uM(2c61g2vs>R2(i5?W7uzSlfER_4ji zI`D`$$I7{tU>^BOwLCiJW1je%YhE04i7ws33w|PGDPe9I@`LAwJ>QZO7J(CF^jP3^ znPHhJ%-#H51pE|NA`m@FgF42|qo!da7Df{^4w{It zD>M$eG8`U1vDe~OL<#?-RC_6IO_Z=P zSKKRc8$=0Pli@;eIG9*e2rnSj_p5fyj@p%%2wd$_M~+`M^5@u%yx ze+=btOCj08%>B?JO(V@hGs?NQJavL}%Zp+Hv-heM!X){x+6c1%%Ev*c`+*&>4r#R>q5#{<3hv~#!D`gH64WYT7rqR+hS!k|qIhArK ny}!lq@2>R+7l`i1n*M$RcQ_igQ&WG900000NkvXXu0mjf+GB*N diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar 2.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar 2.png deleted file mode 100644 index a73e66a32b31c7df545f5f01f980c9e57b7298aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 861 zcmV-j1ETziP)oZ{Gs{DTf`MAIX6oK?sZ;y*dWW1)n`zel_O5V~Q^) z2uM(2c61g2vs>R2(i5?W7uzSlfER_4ji zI`D`$$I7{tU>^BOwLCiJW1je%YhE04i7ws33w|PGDPe9I@`LAwJ>QZO7J(CF^jP3^ znPHhJ%-#H51pE|NA`m@FgF42|qo!da7Df{^4w{It zD>M$eG8`U1vDe~OL<#?-RC_6IO_Z=P zSKKRc8$=0Pli@;eIG9*e2rnSj_p5fyj@p%%2wd$_M~+`M^5@u%yx ze+=btOCj08%>B?JO(V@hGs?NQJavL}%Zp+Hv-heM!X){x+6c1%%Ev*c`+*&>4r#R>q5#{<3hv~#!D`gH64WYT7rqR+hS!k|qIhArK ny}!lq@2>R+7l`i1n*M$RcQ_igQ&WG900000NkvXXu0mjf+GB*N diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/activestar.imageset/activestar.png deleted file mode 100644 index a73e66a32b31c7df545f5f01f980c9e57b7298aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 861 zcmV-j1ETziP)oZ{Gs{DTf`MAIX6oK?sZ;y*dWW1)n`zel_O5V~Q^) z2uM(2c61g2vs>R2(i5?W7uzSlfER_4ji zI`D`$$I7{tU>^BOwLCiJW1je%YhE04i7ws33w|PGDPe9I@`LAwJ>QZO7J(CF^jP3^ znPHhJ%-#H51pE|NA`m@FgF42|qo!da7Df{^4w{It zD>M$eG8`U1vDe~OL<#?-RC_6IO_Z=P zSKKRc8$=0Pli@;eIG9*e2rnSj_p5fyj@p%%2wd$_M~+`M^5@u%yx ze+=btOCj08%>B?JO(V@hGs?NQJavL}%Zp+Hv-heM!X){x+6c1%%Ev*c`+*&>4r#R>q5#{<3hv~#!D`gH64WYT7rqR+hS!k|qIhArK ny}!lq@2>R+7l`i1n*M$RcQ_igQ&WG900000NkvXXu0mjf+GB*N diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/Contents.json deleted file mode 100644 index 941788f9..00000000 --- a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "start.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "start 1.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "start 2.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/start 1.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/star.imageset/start 1.png deleted file mode 100644 index c07908cdfa7b1ef47d1d00e29cb8e5348565b24d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 893 zcmV-@1A_dCP)Y;6MxAhgcRCmp8k2?x4?hOA?3Jb9~=mVz)1PCjfh`D`=oX^%5&;S3*upK`W;RkVu61q@fN+jE zw>-2dND#+JZmkI9S{TRjI1$J-?~dhiYefLp{5w{}fdH;~b*zwED^zsq^IY@mSeZ8; zr9F7Wn`5QiTA}dVSE%LBF(2#1<686LmDVe-c>JDsl31UUNQO=rO5_t6LA(ib!d#k+R9e~+dS zsUCN0s%aQa+^n)GnugKDk>V?whS6k7MV+BJ?FVQIN)4>0wR3FLH(O<}8pJ_xqF(py z$9s{-QHxs>WqA-%9i_MpQI@T-;!%m)BFeHe2tEpNJ49JBqx)Km%MfKDq%9_vsTytd%%|C=03PvHYXz5Ao&UOKD0k=BRlM@-;rBw#-%Ykl#ic6(Y_$7SWU99F<^xhKga}FQ{#E2-B6XF|AcYry_Vl zZb@k*BeO(5=!G+~U9ccaiycSfRP_Hv@|NN((% z7gYRo@!;HiJ4HdLJL3sR6q<@tQ$xQH;??!W_e1+EQz4tgCddVCXQU*WIq#Ou^^Rf} z)byf|K*C)$L6`}C`QzLD+r1O?8Hlg;UCM3mhc1v3Y;6MxAhgcRCmp8k2?x4?hOA?3Jb9~=mVz)1PCjfh`D`=oX^%5&;S3*upK`W;RkVu61q@fN+jE zw>-2dND#+JZmkI9S{TRjI1$J-?~dhiYefLp{5w{}fdH;~b*zwED^zsq^IY@mSeZ8; zr9F7Wn`5QiTA}dVSE%LBF(2#1<686LmDVe-c>JDsl31UUNQO=rO5_t6LA(ib!d#k+R9e~+dS zsUCN0s%aQa+^n)GnugKDk>V?whS6k7MV+BJ?FVQIN)4>0wR3FLH(O<}8pJ_xqF(py z$9s{-QHxs>WqA-%9i_MpQI@T-;!%m)BFeHe2tEpNJ49JBqx)Km%MfKDq%9_vsTytd%%|C=03PvHYXz5Ao&UOKD0k=BRlM@-;rBw#-%Ykl#ic6(Y_$7SWU99F<^xhKga}FQ{#E2-B6XF|AcYry_Vl zZb@k*BeO(5=!G+~U9ccaiycSfRP_Hv@|NN((% z7gYRo@!;HiJ4HdLJL3sR6q<@tQ$xQH;??!W_e1+EQz4tgCddVCXQU*WIq#Ou^^Rf} z)byf|K*C)$L6`}C`QzLD+r1O?8Hlg;UCM3mhc1v3Y;6MxAhgcRCmp8k2?x4?hOA?3Jb9~=mVz)1PCjfh`D`=oX^%5&;S3*upK`W;RkVu61q@fN+jE zw>-2dND#+JZmkI9S{TRjI1$J-?~dhiYefLp{5w{}fdH;~b*zwED^zsq^IY@mSeZ8; zr9F7Wn`5QiTA}dVSE%LBF(2#1<686LmDVe-c>JDsl31UUNQO=rO5_t6LA(ib!d#k+R9e~+dS zsUCN0s%aQa+^n)GnugKDk>V?whS6k7MV+BJ?FVQIN)4>0wR3FLH(O<}8pJ_xqF(py z$9s{-QHxs>WqA-%9i_MpQI@T-;!%m)BFeHe2tEpNJ49JBqx)Km%MfKDq%9_vsTytd%%|C=03PvHYXz5Ao&UOKD0k=BRlM@-;rBw#-%Ykl#ic6(Y_$7SWU99F<^xhKga}FQ{#E2-B6XF|AcYry_Vl zZb@k*BeO(5=!G+~U9ccaiycSfRP_Hv@|NN((% z7gYRo@!;HiJ4HdLJL3sR6q<@tQ$xQH;??!W_e1+EQz4tgCddVCXQU*WIq#Ou^^Rf} z)byf|K*C)$L6`}C`QzLD+r1O?8Hlg;UCM3mhc1v3 + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/WorkerEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/WorkerEmployCard.swift index 582f365b..649de83b 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/WorkerEmployCard.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/WorkerEmployCard.swift @@ -13,11 +13,11 @@ import Entity public class WorkerEmployCard: UITableViewCell { // View - let starButton: IconStateButton = { - let button = IconStateButton( - normal: DSKitAsset.Icons.star.image, - accent: DSKitAsset.Icons.activestar.image, - initial: .normal + let starButton: IconWithColorStateButton = { + let button = IconWithColorStateButton( + representImage: DSKitAsset.Icons.subscribeStar.image, + normalColor: DSKitAsset.Colors.gray200.color, + accentColor: DSKitAsset.Colors.orange300.color ) return button }() @@ -254,7 +254,7 @@ public class WorkerEmployCard: UITableViewCell { dayLeftTag.textString = "D-\(mock.dayLeft)" titleLabel.textString = mock.title timeTakenForWalkLabel.textString = mock.timeTakenForWalk - serviceTargetInfoLabel.textString = "\(mock.targetLevel)등급 \(mock.targetAge)세 \(mock.targetGender.str)" + serviceTargetInfoLabel.textString = "\(mock.targetLevel)등급 \(mock.targetAge)세 \(mock.targetGender.twoLetterKoreanWord)" workDaysLabel.textString = mock.days.map({ $0.rawValue }).joined(separator: ",") workTimeLabel.textString = "\(mock.startTime) - \(mock.endTime)" payPerHourLabel.textString = "시급 \(mock.payPerHour) 원" diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/IconStateButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/IconStateButton.swift index 957bdb11..fc794fec 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/IconStateButton.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/IconStateButton.swift @@ -8,32 +8,27 @@ import UIKit import RxCocoa -public class IconStateButton: UIImageView { +public class IconWithColorStateButton: UIImageView { // Init values public private(set) var state: State - public var normalImage: UIImage - public var accentImage: UIImage + public var representImage: UIImage + public var normalColor: UIColor + public var accentColor: UIColor public let eventPublisher: PublishRelay = .init() - // View - let label: IdleLabel = { - - let view = IdleLabel(typography: .Body3) - - return view - }() - public init( - normal: UIImage, - accent: UIImage, - initial: State) + representImage: UIImage, + normalColor: UIColor, + accentColor: UIColor, + initial: State = .normal) { self.state = initial - self.normalImage = normal - self.accentImage = accent + self.representImage = representImage + self.normalColor = normalColor + self.accentColor = accentColor super.init(frame: .zero) @@ -48,6 +43,11 @@ public class IconStateButton: UIImageView { private func setAppearance() { self.contentMode = .scaleAspectFit + + // 이미지를 템플릿 모드로 변경 + let templateImage = self.representImage.withRenderingMode(.alwaysTemplate) + self.image = templateImage + setState(.normal) } @@ -73,17 +73,17 @@ public class IconStateButton: UIImageView { eventPublisher.accept(state) - let nextImage = state == .normal ? normalImage : accentImage + let nextColor = state == .normal ? normalColor : accentColor // UIView.animate - 뷰 속성 변화에 적합 // UIView.tranistion - 뷰컨텐츠변화 혹은 뷰자체에 대한 변화, 이미지의 경우 여기 해당한다. UIView.transition(with: self, duration: withAnimation ? 0.1 : 0.0, options: .transitionCrossDissolve, animations: { [weak self] in - self?.image = nextImage + self?.tintColor = nextColor }, completion: nil) } } -public extension IconStateButton { +public extension IconWithColorStateButton { enum State { case normal, accent