diff --git a/Projects/Domain/SelfStudyDomain/Interface/Enums/GenderType.swift b/Projects/Domain/BaseDomain/Interface/Enums/GenderType.swift similarity index 100% rename from Projects/Domain/SelfStudyDomain/Interface/Enums/GenderType.swift rename to Projects/Domain/BaseDomain/Interface/Enums/GenderType.swift diff --git a/Projects/Domain/MassageDomain/Interface/DataSource/RemoteMassageDataSource.swift b/Projects/Domain/MassageDomain/Interface/DataSource/RemoteMassageDataSource.swift index 4a75eaa3..1f8fa6a0 100644 --- a/Projects/Domain/MassageDomain/Interface/DataSource/RemoteMassageDataSource.swift +++ b/Projects/Domain/MassageDomain/Interface/DataSource/RemoteMassageDataSource.swift @@ -2,4 +2,5 @@ public protocol RemoteMassageDataSource { func fetchMassageInfo() async throws -> MassageInfoEntity func applyMassage() async throws func cancelMassage() async throws + func fetchMassageRankList() async throws -> [MassageRankEntity] } diff --git a/Projects/Domain/MassageDomain/Interface/Entity/MassageRankEntity.swift b/Projects/Domain/MassageDomain/Interface/Entity/MassageRankEntity.swift new file mode 100644 index 00000000..bcdb6b1c --- /dev/null +++ b/Projects/Domain/MassageDomain/Interface/Entity/MassageRankEntity.swift @@ -0,0 +1,18 @@ +import BaseDomainInterface +import Foundation + +public struct MassageRankEntity: Equatable { + public let id: Int + public let rank: Int + public let stuNum: String + public let memberName: String + public let gender: GenderType + + public init(id: Int, rank: Int, stuNum: String, memberName: String, gender: GenderType) { + self.id = id + self.rank = rank + self.stuNum = stuNum + self.memberName = memberName + self.gender = gender + } +} diff --git a/Projects/Domain/MassageDomain/Interface/Model/MassageRankModel.swift b/Projects/Domain/MassageDomain/Interface/Model/MassageRankModel.swift new file mode 100644 index 00000000..ce190127 --- /dev/null +++ b/Projects/Domain/MassageDomain/Interface/Model/MassageRankModel.swift @@ -0,0 +1 @@ +public typealias MassageRankModel = MassageRankEntity diff --git a/Projects/Domain/MassageDomain/Interface/Repository/MassageRepository.swift b/Projects/Domain/MassageDomain/Interface/Repository/MassageRepository.swift index 1537a46c..58d94827 100644 --- a/Projects/Domain/MassageDomain/Interface/Repository/MassageRepository.swift +++ b/Projects/Domain/MassageDomain/Interface/Repository/MassageRepository.swift @@ -2,4 +2,5 @@ public protocol MassageRepository { func fetchMassageInfo() async throws -> MassageInfoEntity func applyMassage() async throws func cancelMassage() async throws + func fetchMassageRankList() async throws -> [MassageRankEntity] } diff --git a/Projects/Domain/MassageDomain/Interface/UseCase/FetchMassageRankListUseCase.swift b/Projects/Domain/MassageDomain/Interface/UseCase/FetchMassageRankListUseCase.swift new file mode 100644 index 00000000..c4ba2980 --- /dev/null +++ b/Projects/Domain/MassageDomain/Interface/UseCase/FetchMassageRankListUseCase.swift @@ -0,0 +1,3 @@ +public protocol FetchMassageRankListUseCase { + func callAsFunction() async throws -> [MassageRankModel] +} diff --git a/Projects/Domain/MassageDomain/Project.swift b/Projects/Domain/MassageDomain/Project.swift index db84dc3d..e5c4132c 100644 --- a/Projects/Domain/MassageDomain/Project.swift +++ b/Projects/Domain/MassageDomain/Project.swift @@ -5,7 +5,9 @@ import DependencyPlugin let project = Project.module( name: ModulePaths.Domain.MassageDomain.rawValue, targets: [ - .interface(module: .domain(.MassageDomain)), + .interface(module: .domain(.MassageDomain), dependencies: [ + .domain(target: .BaseDomain, type: .interface) + ]), .implements(module: .domain(.MassageDomain), dependencies: [ .domain(target: .MassageDomain, type: .interface), .domain(target: .BaseDomain) diff --git a/Projects/Domain/MassageDomain/Sources/Assembly/MassageDomainAssembly.swift b/Projects/Domain/MassageDomain/Sources/Assembly/MassageDomainAssembly.swift index fa19b721..4abd8e72 100644 --- a/Projects/Domain/MassageDomain/Sources/Assembly/MassageDomainAssembly.swift +++ b/Projects/Domain/MassageDomain/Sources/Assembly/MassageDomainAssembly.swift @@ -26,5 +26,9 @@ public final class MassageDomainAssembly: Assembly { container.register(CancelMassageUseCase.self) { resolver in CancelMassageUseCaseImpl(massageRepository: resolver.resolve(MassageRepository.self)!) } + + container.register(FetchMassageRankListUseCase.self) { resolver in + FetchMassageRankListUseCaseImpl(massageRepository: resolver.resolve(MassageRepository.self)!) + } } } diff --git a/Projects/Domain/MassageDomain/Sources/DTO/Response/FetchMassageRankListResponseDTO.swift b/Projects/Domain/MassageDomain/Sources/DTO/Response/FetchMassageRankListResponseDTO.swift new file mode 100644 index 00000000..85e81ce9 --- /dev/null +++ b/Projects/Domain/MassageDomain/Sources/DTO/Response/FetchMassageRankListResponseDTO.swift @@ -0,0 +1,28 @@ +import BaseDomainInterface +import MassageDomainInterface +import Foundation + +struct FetchMassageRankListResponseDTO: Decodable { + let list: [MassageRankResponseDTO] + + struct MassageRankResponseDTO: Decodable { + let id: Int + let rank: Int + let stuNum: String + let memberName: String + let gender: GenderType + } +} + +extension FetchMassageRankListResponseDTO { + func toDomain() -> [MassageRankEntity] { + self.list + .map { $0.toDomain() } + } +} + +extension FetchMassageRankListResponseDTO.MassageRankResponseDTO { + func toDomain() -> MassageRankEntity { + MassageRankEntity(id: id, rank: rank, stuNum: stuNum, memberName: memberName, gender: gender) + } +} diff --git a/Projects/Domain/MassageDomain/Sources/DataSource/Remote/MassageEndpoint.swift b/Projects/Domain/MassageDomain/Sources/DataSource/Remote/MassageEndpoint.swift index 0f6aa081..2a6c5608 100644 --- a/Projects/Domain/MassageDomain/Sources/DataSource/Remote/MassageEndpoint.swift +++ b/Projects/Domain/MassageDomain/Sources/DataSource/Remote/MassageEndpoint.swift @@ -7,6 +7,7 @@ public enum MassageEndpoint { case fetchMassageInfo case applyMassage case cancelMassage + case fetchMassageRankList } extension MassageEndpoint: DotoriEndpoint { @@ -24,6 +25,9 @@ extension MassageEndpoint: DotoriEndpoint { case .cancelMassage: return .delete("") + + case .fetchMassageRankList: + return .get("/rank") } } diff --git a/Projects/Domain/MassageDomain/Sources/DataSource/Remote/RemoteMassageDataSourceImpl.swift b/Projects/Domain/MassageDomain/Sources/DataSource/Remote/RemoteMassageDataSourceImpl.swift index c1c1a4de..f2b7599c 100644 --- a/Projects/Domain/MassageDomain/Sources/DataSource/Remote/RemoteMassageDataSourceImpl.swift +++ b/Projects/Domain/MassageDomain/Sources/DataSource/Remote/RemoteMassageDataSourceImpl.swift @@ -23,4 +23,12 @@ final class RemoteMassageDataSourceImpl: RemoteMassageDataSource { func cancelMassage() async throws { try await networking.request(MassageEndpoint.cancelMassage) } + + func fetchMassageRankList() async throws -> [MassageRankEntity] { + try await networking.request( + MassageEndpoint.fetchMassageRankList, + dto: FetchMassageRankListResponseDTO.self + ) + .toDomain() + } } diff --git a/Projects/Domain/MassageDomain/Sources/Repository/MassageRepositoryImpl.swift b/Projects/Domain/MassageDomain/Sources/Repository/MassageRepositoryImpl.swift index 4c2f86f7..ea5cea3e 100644 --- a/Projects/Domain/MassageDomain/Sources/Repository/MassageRepositoryImpl.swift +++ b/Projects/Domain/MassageDomain/Sources/Repository/MassageRepositoryImpl.swift @@ -18,4 +18,8 @@ final class MassageRepositoryImpl: MassageRepository { func cancelMassage() async throws { try await remoteMassageDataSource.cancelMassage() } + + func fetchMassageRankList() async throws -> [MassageRankEntity] { + try await remoteMassageDataSource.fetchMassageRankList() + } } diff --git a/Projects/Domain/MassageDomain/Sources/UseCase/FetchMassageRankListUseCaseImpl.swift b/Projects/Domain/MassageDomain/Sources/UseCase/FetchMassageRankListUseCaseImpl.swift new file mode 100644 index 00000000..eb88ad0f --- /dev/null +++ b/Projects/Domain/MassageDomain/Sources/UseCase/FetchMassageRankListUseCaseImpl.swift @@ -0,0 +1,13 @@ +import MassageDomainInterface + +struct FetchMassageRankListUseCaseImpl: FetchMassageRankListUseCase { + private let massageRepository: any MassageRepository + + init(massageRepository: any MassageRepository) { + self.massageRepository = massageRepository + } + + func callAsFunction() async throws -> [MassageRankModel] { + try await massageRepository.fetchMassageRankList() + } +} diff --git a/Projects/Domain/MassageDomain/Testing/DataSource/RemoteMassageDataSourceSpy.swift b/Projects/Domain/MassageDomain/Testing/DataSource/RemoteMassageDataSourceSpy.swift index b32fe604..7d8cb06b 100644 --- a/Projects/Domain/MassageDomain/Testing/DataSource/RemoteMassageDataSourceSpy.swift +++ b/Projects/Domain/MassageDomain/Testing/DataSource/RemoteMassageDataSourceSpy.swift @@ -17,4 +17,11 @@ final class RemoteMassageDataSourceSpy: RemoteMassageDataSource { func cancelMassage() async throws { cancelMassageCallCount += 1 } + + var fetchMassageRankListCallCount = 0 + var fetchMassageRankListHandler: () async throws -> [MassageRankEntity] = { [] } + func fetchMassageRankList() async throws -> [MassageRankEntity] { + fetchMassageRankListCallCount += 1 + return try await fetchMassageRankListHandler() + } } diff --git a/Projects/Domain/MassageDomain/Testing/Repository/MassageRepositorySpy.swift b/Projects/Domain/MassageDomain/Testing/Repository/MassageRepositorySpy.swift index ebf674ee..51d1bdc4 100644 --- a/Projects/Domain/MassageDomain/Testing/Repository/MassageRepositorySpy.swift +++ b/Projects/Domain/MassageDomain/Testing/Repository/MassageRepositorySpy.swift @@ -17,4 +17,11 @@ final class MassageRepositorySpy: MassageRepository { func cancelMassage() async throws { cancelMassageCallCount += 1 } + + var fetchMassageRankListCallCount = 0 + var fetchMassageRankListHandler: () async throws -> [MassageRankEntity] = { [] } + func fetchMassageRankList() async throws -> [MassageRankEntity] { + fetchMassageRankListCallCount += 1 + return try await fetchMassageRankListHandler() + } } diff --git a/Projects/Domain/MassageDomain/Testing/UseCase/FetchMassageRankListUseCaseSpy.swift b/Projects/Domain/MassageDomain/Testing/UseCase/FetchMassageRankListUseCaseSpy.swift new file mode 100644 index 00000000..9c660bef --- /dev/null +++ b/Projects/Domain/MassageDomain/Testing/UseCase/FetchMassageRankListUseCaseSpy.swift @@ -0,0 +1,10 @@ +import MassageDomainInterface + +final class FetchMassageRankListUseCaseSpy: FetchMassageRankListUseCase { + var fetchMassageRankListCallCount = 0 + var fetchMassageRankListHandler: () async throws -> [MassageRankEntity] = { [] } + func callAsFunction() async throws -> [MassageRankModel] { + fetchMassageRankListCallCount += 1 + return try await fetchMassageRankListHandler() + } +} diff --git a/Projects/Domain/MassageDomain/Tests/FetchMassageRankListUseCaseTests.swift b/Projects/Domain/MassageDomain/Tests/FetchMassageRankListUseCaseTests.swift new file mode 100644 index 00000000..306a49ca --- /dev/null +++ b/Projects/Domain/MassageDomain/Tests/FetchMassageRankListUseCaseTests.swift @@ -0,0 +1,32 @@ +import MassageDomainInterface +import XCTest +@testable import MassageDomain +@testable import MassageDomainTesting + +final class FetchMassageRankListUseCaseTests: XCTestCase { + var massageRepository: MassageRepositorySpy! + var sut: FetchMassageRankListUseCaseImpl! + + override func setUp() { + massageRepository = .init() + sut = .init(massageRepository: massageRepository) + } + + override func tearDown() { + massageRepository = nil + sut = nil + } + + func testFetchMassageInfo() async throws { + XCTAssertEqual(massageRepository.fetchMassageRankListCallCount, 0) + let expected = [ + MassageRankModel(id: 1, rank: 2, stuNum: "1111", memberName: "김시훈", gender: .man) + ] + massageRepository.fetchMassageRankListHandler = { expected } + + let actual = try await sut() + + XCTAssertEqual(massageRepository.fetchMassageRankListCallCount, 1) + XCTAssertEqual(actual, expected) + } +} diff --git a/Projects/Domain/MassageDomain/Tests/MassageRepositoryTests.swift b/Projects/Domain/MassageDomain/Tests/MassageRepositoryTests.swift index f24dfe06..4570ea24 100644 --- a/Projects/Domain/MassageDomain/Tests/MassageRepositoryTests.swift +++ b/Projects/Domain/MassageDomain/Tests/MassageRepositoryTests.swift @@ -34,4 +34,24 @@ final class MassageRepositoryTests: XCTestCase { XCTAssertEqual(remoteMassageDataSource.applyMassageCallCount, 1) } + + func testCancelMassage() async throws { + XCTAssertEqual(remoteMassageDataSource.cancelMassageCallCount, 0) + try await sut.cancelMassage() + + XCTAssertEqual(remoteMassageDataSource.cancelMassageCallCount, 1) + } + + func testFetchMassageRankList() async throws { + XCTAssertEqual(remoteMassageDataSource.fetchMassageRankListCallCount, 0) + let expected = [ + MassageRankModel(id: 1, rank: 2, stuNum: "3218", memberName: "전승원", gender: .man) + ] + remoteMassageDataSource.fetchMassageRankListHandler = { expected } + + let actual = try await sut.fetchMassageRankList() + + XCTAssertEqual(remoteMassageDataSource.fetchMassageRankListCallCount, 1) + XCTAssertEqual(actual, expected) + } } diff --git a/Projects/Domain/SelfStudyDomain/Interface/DTO/Request/FetchSelfStudyRankSearchRequestDTO.swift b/Projects/Domain/SelfStudyDomain/Interface/DTO/Request/FetchSelfStudyRankSearchRequestDTO.swift index 18291f52..5c3740a4 100644 --- a/Projects/Domain/SelfStudyDomain/Interface/DTO/Request/FetchSelfStudyRankSearchRequestDTO.swift +++ b/Projects/Domain/SelfStudyDomain/Interface/DTO/Request/FetchSelfStudyRankSearchRequestDTO.swift @@ -1,3 +1,4 @@ +import BaseDomainInterface import Foundation public struct FetchSelfStudyRankSearchRequestDTO { diff --git a/Projects/Domain/SelfStudyDomain/Interface/Entity/SelfStudyRankEntity.swift b/Projects/Domain/SelfStudyDomain/Interface/Entity/SelfStudyRankEntity.swift index 840e8265..d625807a 100644 --- a/Projects/Domain/SelfStudyDomain/Interface/Entity/SelfStudyRankEntity.swift +++ b/Projects/Domain/SelfStudyDomain/Interface/Entity/SelfStudyRankEntity.swift @@ -1,3 +1,4 @@ +import BaseDomainInterface import Foundation public struct SelfStudyRankEntity: Equatable { diff --git a/Projects/Domain/SelfStudyDomain/Project.swift b/Projects/Domain/SelfStudyDomain/Project.swift index a82d5572..b817463c 100644 --- a/Projects/Domain/SelfStudyDomain/Project.swift +++ b/Projects/Domain/SelfStudyDomain/Project.swift @@ -5,7 +5,9 @@ import DependencyPlugin let project = Project.module( name: ModulePaths.Domain.SelfStudyDomain.rawValue, targets: [ - .interface(module: .domain(.SelfStudyDomain)), + .interface(module: .domain(.SelfStudyDomain), dependencies: [ + .domain(target: .BaseDomain, type: .interface) + ]), .implements(module: .domain(.SelfStudyDomain), dependencies: [ .domain(target: .SelfStudyDomain, type: .interface), .domain(target: .BaseDomain) diff --git a/Projects/Domain/SelfStudyDomain/Sources/DTO/Response/FecthSelfStudyRankListResponseDTO.swift b/Projects/Domain/SelfStudyDomain/Sources/DTO/Response/FecthSelfStudyRankListResponseDTO.swift index 7e15e5ef..dd5b7035 100644 --- a/Projects/Domain/SelfStudyDomain/Sources/DTO/Response/FecthSelfStudyRankListResponseDTO.swift +++ b/Projects/Domain/SelfStudyDomain/Sources/DTO/Response/FecthSelfStudyRankListResponseDTO.swift @@ -1,3 +1,4 @@ +import BaseDomainInterface import Foundation import SelfStudyDomainInterface diff --git a/Projects/Feature/BaseFeature/Project.swift b/Projects/Feature/BaseFeature/Project.swift index d31e8a8b..3c64319d 100644 --- a/Projects/Feature/BaseFeature/Project.swift +++ b/Projects/Feature/BaseFeature/Project.swift @@ -6,7 +6,6 @@ let project = Project.module( name: ModulePaths.Feature.BaseFeature.rawValue, targets: [ .implements(module: .feature(.BaseFeature), product: .framework, dependencies: [ - .SPM.MSGLayout, .SPM.Moordinator, .SPM.Store, .SPM.IQKeyboardManagerSwift, diff --git a/Projects/Feature/MassageFeature/Demo/Resources/LaunchScreen.storyboard b/Projects/Feature/MassageFeature/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Projects/Feature/MassageFeature/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Feature/MassageFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/MassageFeature/Demo/Sources/AppDelegate.swift new file mode 100644 index 00000000..9286fd66 --- /dev/null +++ b/Projects/Feature/MassageFeature/Demo/Sources/AppDelegate.swift @@ -0,0 +1,35 @@ +import UIKit +import Inject +@testable import MassageFeature +@testable import MassageDomainTesting +@testable import UserDomainTesting + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let fetchMassageRankListUseCase = FetchMassageRankListUseCaseSpy() + fetchMassageRankListUseCase.fetchMassageRankListHandler = { + [ + .init(id: 1, rank: 1, stuNum: "1234", memberName: "대충이름", gender: .man), + .init(id: 2, rank: 2, stuNum: "1235", memberName: "대이충름", gender: .man), + .init(id: 3, rank: 3, stuNum: "1236", memberName: "이름대충", gender: .woman) + ] + } + let store = MassageStore( + fetchMassageRankListUseCase: fetchMassageRankListUseCase + ) + let viewController = Inject.ViewControllerHost( + UINavigationController(rootViewController: MassageViewController(store: store)) + ) + window?.rootViewController = viewController + window?.makeKeyAndVisible() + + return true + } +} diff --git a/Projects/Feature/MassageFeature/Project.swift b/Projects/Feature/MassageFeature/Project.swift index 4a304664..5ce9455f 100644 --- a/Projects/Feature/MassageFeature/Project.swift +++ b/Projects/Feature/MassageFeature/Project.swift @@ -7,10 +7,18 @@ let project = Project.module( targets: [ .implements(module: .feature(.MassageFeature), dependencies: [ .feature(target: .BaseFeature), - .domain(target: .AuthDomain, type: .interface) + .domain(target: .MassageDomain, type: .interface), + .domain(target: .UserDomain, type: .interface) ]), .tests(module: .feature(.MassageFeature), dependencies: [ - .feature(target: .MassageFeature) + .feature(target: .MassageFeature), + .domain(target: .MassageDomain, type: .testing), + .domain(target: .UserDomain, type: .testing) + ]), + .demo(module: .feature(.MassageFeature), dependencies: [ + .feature(target: .MassageFeature), + .domain(target: .MassageDomain, type: .testing), + .domain(target: .UserDomain, type: .testing) ]) ] ) diff --git a/Projects/Feature/MassageFeature/Sources/Assembly/MassageAssembly.swift b/Projects/Feature/MassageFeature/Sources/Assembly/MassageAssembly.swift index 39e15888..7fcc24da 100644 --- a/Projects/Feature/MassageFeature/Sources/Assembly/MassageAssembly.swift +++ b/Projects/Feature/MassageFeature/Sources/Assembly/MassageAssembly.swift @@ -1,10 +1,14 @@ +import MassageDomainInterface import Swinject +import UserDomainInterface public final class MassageAssembly: Assembly { public init() {} public func assemble(container: Container) { - container.register(MassageFactory.self) { _ in - MassageFactoryImpl() + container.register(MassageFactory.self) { resolver in + MassageFactoryImpl( + fetchMassageRankListUseCase: resolver.resolve(FetchMassageRankListUseCase.self)! + ) } } } diff --git a/Projects/Feature/MassageFeature/Sources/Factory/MassageFactoryImpl.swift b/Projects/Feature/MassageFeature/Sources/Factory/MassageFactoryImpl.swift index 17e1be07..2e8488ef 100644 --- a/Projects/Feature/MassageFeature/Sources/Factory/MassageFactoryImpl.swift +++ b/Projects/Feature/MassageFeature/Sources/Factory/MassageFactoryImpl.swift @@ -1,7 +1,21 @@ +import MassageDomainInterface import Moordinator +import UserDomainInterface struct MassageFactoryImpl: MassageFactory { + private let fetchMassageRankListUseCase: any FetchMassageRankListUseCase + + init( + fetchMassageRankListUseCase: any FetchMassageRankListUseCase + ) { + self.fetchMassageRankListUseCase = fetchMassageRankListUseCase + } + func makeMoordinator() -> Moordinator { - MassageMoordinator() + let store = MassageStore( + fetchMassageRankListUseCase: fetchMassageRankListUseCase + ) + let viewController = MassageViewController(store: store) + return MassageMoordinator(massageViewController: viewController) } } diff --git a/Projects/Feature/MassageFeature/Sources/Moordinator/MassageMoordinator.swift b/Projects/Feature/MassageFeature/Sources/Moordinator/MassageMoordinator.swift index d8bea064..8e809d68 100644 --- a/Projects/Feature/MassageFeature/Sources/Moordinator/MassageMoordinator.swift +++ b/Projects/Feature/MassageFeature/Sources/Moordinator/MassageMoordinator.swift @@ -5,11 +5,15 @@ import UIKit final class MassageMoordinator: Moordinator { private let rootVC = UINavigationController() - + private let massageViewController: any StoredViewControllable var root: Presentable { rootVC } + init(massageViewController: any StoredViewControllable) { + self.massageViewController = massageViewController + } + func route(to path: RoutePath) -> MoordinatorContributors { guard let path = path.asDotori else { return .none } switch path { @@ -25,8 +29,12 @@ final class MassageMoordinator: Moordinator { private extension MassageMoordinator { func coordinateToMassage() -> MoordinatorContributors { - let noticeWebViewController = DWebViewController(urlString: "https://www.dotori-gsm.com/massage") - self.rootVC.setViewControllers([noticeWebViewController], animated: true) - return .none + self.rootVC.setViewControllers([self.massageViewController], animated: true) + return .one( + .contribute( + withNextPresentable: self.massageViewController, + withNextRouter: self.massageViewController.store + ) + ) } } diff --git a/Projects/Feature/MassageFeature/Sources/Scene/MassageStore.swift b/Projects/Feature/MassageFeature/Sources/Scene/MassageStore.swift new file mode 100644 index 00000000..e54c349a --- /dev/null +++ b/Projects/Feature/MassageFeature/Sources/Scene/MassageStore.swift @@ -0,0 +1,97 @@ +import BaseDomainInterface +import BaseFeature +import Combine +import MassageDomainInterface +import Moordinator +import Store +import UserDomainInterface + +final class MassageStore: BaseStore { + var route: PassthroughSubject = .init() + var subscription: Set = .init() + var initialState: State + var stateSubject: CurrentValueSubject + private let fetchMassageRankListUseCase: any FetchMassageRankListUseCase + + init( + fetchMassageRankListUseCase: any FetchMassageRankListUseCase + ) { + self.initialState = .init() + self.stateSubject = .init(initialState) + self.fetchMassageRankListUseCase = fetchMassageRankListUseCase + } + + struct State { + var massageRankList: [MassageRankModel] = [] + var isRefreshing = false + } + enum Action { + case viewDidLoad + case fetchMassageRankList + } + enum Mutation { + case updateMassageRankList([MassageRankModel]) + case updateIsRefreshing(Bool) + } +} + +extension MassageStore { + func mutate(state: State, action: Action) -> SideEffect { + switch action { + case .viewDidLoad: + return viewDidLoad() + + case .fetchMassageRankList: + return fetchMassageRankList() + } + return .none + } +} + +extension MassageStore { + func reduce(state: State, mutate: Mutation) -> State { + var newState = state + switch mutate { + case let .updateMassageRankList(rankList): + newState.massageRankList = rankList + + case let .updateIsRefreshing(isRefreshing): + newState.isRefreshing = isRefreshing + } + return newState + } +} + +// MARK: - Mutate +private extension MassageStore { + func viewDidLoad() -> SideEffect { + return self.fetchMassageRankList() + } + + func fetchMassageRankList() -> SideEffect { + let massageRankListEffect = SideEffect<[MassageRankModel], Error> + .tryAsync { [fetchMassageRankListUseCase] in + try await fetchMassageRankListUseCase() + } + .map(Mutation.updateMassageRankList) + .eraseToSideEffect() + .catchToNever() + return self.makeRefreshingSideEffect(massageRankListEffect) + } +} + +// MARK: - Reusable +private extension MassageStore { + func makeRefreshingSideEffect( + _ publisher: SideEffect + ) -> SideEffect { + let startLoadingPublisher = SideEffect + .just(Mutation.updateIsRefreshing(true)) + let endLoadingPublisher = SideEffect + .just(Mutation.updateIsRefreshing(false)) + return startLoadingPublisher + .append(publisher) + .append(endLoadingPublisher) + .eraseToSideEffect() + } +} diff --git a/Projects/Feature/MassageFeature/Sources/Scene/MassageViewController.swift b/Projects/Feature/MassageFeature/Sources/Scene/MassageViewController.swift new file mode 100644 index 00000000..e9d9a532 --- /dev/null +++ b/Projects/Feature/MassageFeature/Sources/Scene/MassageViewController.swift @@ -0,0 +1,104 @@ +import BaseFeature +import CombineUtility +import DesignSystem +import Localization +import MSGLayout +import MassageDomainInterface +import UIKit +import UIKitUtil + +final class MassageViewController: BaseStoredViewController { + private let massageNavigationBarLabel = DotoriNavigationBarLabel(text: L10n.Massage.massageTitle) + private let massageTableView = UITableView() + .set(\.backgroundColor, .clear) + .set(\.isHidden, true) + .set(\.separatorStyle, .none) + .set(\.sectionHeaderHeight, 0) + .then { + $0.register(cellType: MassageCell.self) + } + private let massageRefreshContorol = UIRefreshControl() + private lazy var massageTableAdapter = TableViewAdapter>( + tableView: massageTableView + ) { tableView, indexPath, item in + let cell: MassageCell = tableView.dequeueReusableCell(for: indexPath) + cell.adapt(model: item) + return cell + } + private let emptySelfStudyStackView = VStackView(spacing: 8) { + DotoriIconView( + size: .custom(.init(width: 73.5, height: 120)), + image: .Dotori.coffee + ) + + DotoriLabel(L10n.Massage.emptyMassageTitle, font: .subtitle2) + + DotoriLabel(L10n.Massage.applyFromHomeMassageTitle, textColor: .neutral(.n20), font: .caption) + }.alignment(.center) + + override func addView() { + view.addSubviews { + massageTableView + emptySelfStudyStackView + } + massageTableView.refreshControl = massageRefreshContorol + } + + override func setLayout() { + MSGLayout.buildLayout { + massageTableView.layout + .top(.toSuperview(), .equal(24)) + .horizontal(.toSuperview()) + .bottom(.to(view.safeAreaLayoutGuide)) + + emptySelfStudyStackView.layout + .center(.toSuperview()) + } + } + + override func configureNavigation() { + self.navigationItem.setLeftBarButton(massageNavigationBarLabel, animated: true) + } + + override func bindAction() { + viewDidLoadPublisher + .map { Store.Action.viewDidLoad } + .sink(receiveValue: store.send(_:)) + .store(in: &subscription) + + massageRefreshContorol.controlPublisher(for: .valueChanged) + .map { _ in Store.Action.fetchMassageRankList } + .sink(receiveValue: store.send(_:)) + .store(in: &subscription) + } + + override func bindState() { + let sharedState = store.state.share() + .receive(on: DispatchQueue.main) + + sharedState + .map(\.massageRankList) + .map { [GenericSectionModel(items: $0)] } + .sink(receiveValue: massageTableAdapter.updateSections(sections:)) + .store(in: &subscription) + + sharedState + .map(\.isRefreshing) + .removeDuplicates() + .dropFirst(2) + .sink(with: massageRefreshContorol, receiveValue: { refreshControl, isRefreshing in + isRefreshing ? refreshControl.beginRefreshing() : refreshControl.endRefreshing() + }) + .store(in: &subscription) + + sharedState + .map(\.massageRankList) + .map(\.isEmpty) + .removeDuplicates() + .sink(with: self, receiveValue: { owner, massageIsEmpty in + owner.massageTableView.isHidden = massageIsEmpty + owner.emptySelfStudyStackView.isHidden = !massageIsEmpty + }) + .store(in: &subscription) + } +} diff --git a/Projects/Feature/MassageFeature/Sources/Scene/View/MassageCell.swift b/Projects/Feature/MassageFeature/Sources/Scene/View/MassageCell.swift new file mode 100644 index 00000000..106b19e4 --- /dev/null +++ b/Projects/Feature/MassageFeature/Sources/Scene/View/MassageCell.swift @@ -0,0 +1,43 @@ +import BaseDomainInterface +import BaseFeature +import Combine +import DesignSystem +import MSGLayout +import MassageDomainInterface +import UIKit + +final class MassageCell: BaseTableViewCell { + private let appliedStudentCardView = AppliedStudentCardView() + + override func addView() { + contentView.addSubviews { + appliedStudentCardView + } + } + + override func setLayout() { + MSGLayout.buildLayout { + appliedStudentCardView.layout + .horizontal(.toSuperview(), .equal(20)) + .vertical(.toSuperview(), .equal(12)) + } + } + + override func configureView() { + self.backgroundColor = .clear + self.selectionStyle = .none + } + + override func adapt(model: MassageRankModel) { + super.adapt(model: model) + self.appliedStudentCardView.updateContent( + with: .init( + rank: model.rank, + name: model.memberName, + gender: model.gender == .man ? .man : .woman, + stuNum: model.stuNum, + isChecked: false + ) + ) + } +} diff --git a/Projects/Feature/MassageFeature/Tests/MassageFeatureTest.swift b/Projects/Feature/MassageFeature/Tests/MassageFeatureTest.swift index 68263acd..97e288e5 100644 --- a/Projects/Feature/MassageFeature/Tests/MassageFeatureTest.swift +++ b/Projects/Feature/MassageFeature/Tests/MassageFeatureTest.swift @@ -1,11 +1,49 @@ +import Combine +import MassageDomainInterface import XCTest +@testable import MassageFeature +@testable import MassageDomainTesting +@testable import UserDomainTesting final class MassageFeatureTests: XCTestCase { - override func setUpWithError() throws {} + var fetchMassageRankListUseCase: FetchMassageRankListUseCaseSpy! + var sut: MassageStore! + var subscription: Set! - override func tearDownWithError() throws {} + override func setUp() { + fetchMassageRankListUseCase = .init() + sut = .init( + fetchMassageRankListUseCase: fetchMassageRankListUseCase + ) + subscription = [] + } + + override func tearDown() { + fetchMassageRankListUseCase = nil + sut = nil + subscription = nil + } + + func testFetchMassageRankList() { + let expectation = XCTestExpectation(description: "Test_Fetch_Massage_Rank_List") + expectation.expectedFulfillmentCount = 2 + let expectedMassageRankList = [ + MassageRankModel(id: 1, rank: 1, stuNum: "2222", memberName: "익명", gender: .woman) + ] + fetchMassageRankListUseCase.fetchMassageRankListHandler = { expectedMassageRankList } + + sut.state.map(\.massageRankList).removeDuplicates().sink { _ in + expectation.fulfill() + } + .store(in: &subscription) + + XCTAssertEqual(fetchMassageRankListUseCase.fetchMassageRankListCallCount, 0) + + sut.send(.fetchMassageRankList) + + wait(for: [expectation], timeout: 1.0) - func testExample() { - XCTAssertEqual(1, 1) + XCTAssertEqual(fetchMassageRankListUseCase.fetchMassageRankListCallCount, 1) + XCTAssertEqual(sut.currentState.massageRankList, expectedMassageRankList) } } diff --git a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift index 65fa177c..de9186b9 100644 --- a/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift +++ b/Projects/Feature/NoticeFeature/Sources/Scene/NoticeViewController.swift @@ -164,9 +164,11 @@ final class NoticeViewController: BaseStoredViewController { sharedState .map(\.isRefreshing) - .sink(with: noticeRefreshControl) { refreshControl, isRefreshing in + .removeDuplicates() + .dropFirst(2) + .sink(with: noticeRefreshControl, receiveValue: { refreshControl, isRefreshing in isRefreshing ? refreshControl.beginRefreshing() : refreshControl.endRefreshing() - } + }) .store(in: &subscription) } } diff --git a/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyStore.swift b/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyStore.swift index 7e1d10f5..9709360e 100644 --- a/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyStore.swift +++ b/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyStore.swift @@ -57,7 +57,7 @@ extension SelfStudyStore { return .none } } -import Foundation + extension SelfStudyStore { func reduce(state: State, mutate: Mutation) -> State { var newState = state @@ -81,6 +81,7 @@ extension SelfStudyStore: SelfStudyCellDelegate { } } +// MARK: - Mutate private extension SelfStudyStore { func fetchSelfStudyRank() -> SideEffect { let selfStudyEffect = SideEffect<[SelfStudyRankModel], Error> @@ -114,6 +115,7 @@ private extension SelfStudyStore { } } +// MARK: - Reduce private extension SelfStudyStore { func updateSelfStudyCheck(id: Int, isChecked: Bool) -> [SelfStudyRankModel] { currentState.selfStudyRankList.map { @@ -130,6 +132,7 @@ private extension SelfStudyStore { } } +// MARK: - Reusable private extension SelfStudyStore { func makeRefreshingSideEffect( _ publisher: SideEffect diff --git a/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyViewController.swift b/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyViewController.swift index c2d365a7..7585b7a4 100644 --- a/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyViewController.swift +++ b/Projects/Feature/SelfStudyFeature/Sources/Scene/SelfStudyViewController.swift @@ -11,7 +11,7 @@ import UIKit import UIKitUtil final class SelfStudyViewController: BaseStoredViewController { - private let dotoriNavigationBarLabel = DotoriNavigationBarLabel(text: L10n.SelfStudy.selfStudyTitle) + private let selfStudyNavigationBarLabel = DotoriNavigationBarLabel(text: L10n.SelfStudy.selfStudyTitle) private let filterBarButton = UIBarButtonItem( image: .Dotori.filter.tintColor(color: .dotori(.neutral(.n20))), style: .done, @@ -69,7 +69,7 @@ final class SelfStudyViewController: BaseStoredViewController { } override func configureNavigation() { - self.navigationItem.setLeftBarButton(dotoriNavigationBarLabel, animated: true) + self.navigationItem.setLeftBarButton(selfStudyNavigationBarLabel, animated: true) self.navigationItem.setRightBarButton(filterBarButton, animated: true) } @@ -94,8 +94,7 @@ final class SelfStudyViewController: BaseStoredViewController { sharedState .map(\.selfStudyRankList) - .map(\.count) - .map { $0 == 0 } + .map(\.isEmpty) .removeDuplicates() .sink(with: self, receiveValue: { owner, selfStudyIsEmpty in owner.selfStudyTableView.isHidden = selfStudyIsEmpty @@ -105,6 +104,8 @@ final class SelfStudyViewController: BaseStoredViewController { sharedState .map(\.isRefreshing) + .removeDuplicates() + .dropFirst(2) .sink(with: selfStudyRefreshContorol, receiveValue: { refreshControl, isRefreshing in isRefreshing ? refreshControl.beginRefreshing() : refreshControl.endRefreshing() }) diff --git a/Projects/Feature/SelfStudyFeature/Sources/Scene/View/SelfStudyCell.swift b/Projects/Feature/SelfStudyFeature/Sources/Scene/View/SelfStudyCell.swift index a3bde516..d3f69d76 100644 --- a/Projects/Feature/SelfStudyFeature/Sources/Scene/View/SelfStudyCell.swift +++ b/Projects/Feature/SelfStudyFeature/Sources/Scene/View/SelfStudyCell.swift @@ -12,32 +12,7 @@ protocol SelfStudyCellDelegate: AnyObject { final class SelfStudyCell: BaseTableViewCell { weak var delegate: (any SelfStudyCellDelegate)? - private let containerView = UIView() - private let rankLabel = DotoriLabel(textColor: .neutral(.n20), font: .caption) - private let selfStudyCheckBox = DotoriCheckBox() - .set(\.isHidden, true) - private let userImageView = DotoriIconView( - size: .custom(.init(width: 64, height: 64)), - image: .Dotori.personRectangle - ) - private let nameLabel = DotoriLabel(textColor: .neutral(.n10), font: .body1) - private let genderImageView = DotoriIconView( - size: .custom(.init(width: 16, height: 16)), - image: .Dotori.men - ) - private let stuNumLabel = DotoriLabel(textColor: .neutral(.n20), font: .caption) - private lazy var profileStackView = VStackView(spacing: 8) { - userImageView - - HStackView(spacing: 2) { - nameLabel - - genderImageView - } - .alignment(.center) - - stuNumLabel - }.alignment(.center) + private let appliedStudentCardView = AppliedStudentCardView() private let medalImageView = DotoriIconView( size: .custom(.init(width: 56, height: 80)), image: nil @@ -60,36 +35,19 @@ final class SelfStudyCell: BaseTableViewCell { override func addView() { contentView.addSubviews { - containerView + appliedStudentCardView } - containerView.addSubviews { - rankLabel - selfStudyCheckBox - profileStackView + appliedStudentCardView.addSubviews { medalImageView } } override func setLayout() { MSGLayout.buildLayout { - containerView.layout + appliedStudentCardView.layout .horizontal(.toSuperview(), .equal(20)) .vertical(.toSuperview(), .equal(12)) - rankLabel.layout - .top(.toSuperview(), .equal(12)) - .leading(.toSuperview(), .equal(16)) - - selfStudyCheckBox.layout - .top(.toSuperview(), .equal(12)) - .trailing(.toSuperview(), .equal(-12)) - .size(24) - - profileStackView.layout - .centerX(.toSuperview()) - .top(.toSuperview(), .equal(28)) - .bottom(.toSuperview(), .equal(-28)) - medalImageView.layout .trailing(.toSuperview(), .equal(-8)) .bottom(.toSuperview(), .equal(20)) @@ -97,32 +55,32 @@ final class SelfStudyCell: BaseTableViewCell { } override func configureView() { - self.containerView.backgroundColor = .dotori(.background(.card)) - self.containerView.cornerRadius = 16 - DotoriShadow.cardShadow(card: self.containerView) self.backgroundColor = .clear self.selectionStyle = .none } override func adapt(model: SelfStudyRankModel) { super.adapt(model: model) - self.rankLabel.text = "\(model.rank)" - self.nameLabel.text = model.memberName - self.genderImageView.image = (model.gender == .man ? UIImage.Dotori.men : .Dotori.women) - .tintColor(color: .dotori(.neutral(.n10))) - self.stuNumLabel.text = model.stuNum - self.selfStudyCheckBox.isChecked = model.selfStudyCheck + self.appliedStudentCardView.updateContent( + with: .init( + rank: model.rank, + name: model.memberName, + gender: model.gender == .man ? .man : .woman, + stuNum: model.stuNum, + isChecked: model.selfStudyCheck + ) + ) self.medalImageView.image = self.rankToImage(rank: model.rank) } func setUserRole(userRole: UserRoleType) { - selfStudyCheckBox.isHidden = userRole == .member + appliedStudentCardView.setIsHiddenAttendanceCheckBox(userRole == .member) } } private extension SelfStudyCell { func bindAction() { - selfStudyCheckBox.checkBoxDidTapPublisher + appliedStudentCardView.attendanceCheckBoxPublisher .throttle(for: 1, scheduler: RunLoop.main, latest: true) .compactMap { [weak self] (checked) -> (Int, Bool)? in guard let id = self?.model?.id else { return nil } diff --git a/Projects/UserInterface/DesignSystem/Project.swift b/Projects/UserInterface/DesignSystem/Project.swift index 4fafa9e7..ba7c8817 100644 --- a/Projects/UserInterface/DesignSystem/Project.swift +++ b/Projects/UserInterface/DesignSystem/Project.swift @@ -12,6 +12,7 @@ let project = Project.module( resources: ["Resources/**"], dependencies: [ .SPM.Anim, + .SPM.MSGLayout, .userInterface(target: .DWebKit), .shared(target: .GlobalThirdPartyLibrary), .shared(target: .UIKitUtil) diff --git a/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Coffee-dark.svg b/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Coffee-dark.svg new file mode 100644 index 00000000..c6a0ab17 --- /dev/null +++ b/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Coffee-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Coffee.svg b/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Coffee.svg new file mode 100644 index 00000000..1cd33e60 --- /dev/null +++ b/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Coffee.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Contents.json b/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Contents.json new file mode 100644 index 00000000..fa199fef --- /dev/null +++ b/Projects/UserInterface/DesignSystem/Resources/DotoriIcon/DotoriIcon.xcassets/Coffee.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Coffee.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Coffee-dark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/UserInterface/DesignSystem/Sources/CardView/AppliedStudentCardView.swift b/Projects/UserInterface/DesignSystem/Sources/CardView/AppliedStudentCardView.swift new file mode 100644 index 00000000..68a2d280 --- /dev/null +++ b/Projects/UserInterface/DesignSystem/Sources/CardView/AppliedStudentCardView.swift @@ -0,0 +1,116 @@ +import Combine +import MSGLayout +import UIKit + +public struct AppliedStudentViewModel: Equatable { + public let rank: Int + public let name: String + public let gender: Gender + public let stuNum: String + public let isChecked: Bool + + public enum Gender { + case man + case woman + } + + public init(rank: Int, name: String, gender: Gender, stuNum: String, isChecked: Bool) { + self.rank = rank + self.name = name + self.gender = gender + self.stuNum = stuNum + self.isChecked = isChecked + } +} + +public protocol AppliedStudentCardViewActionProtocol { + var attendanceCheckBoxPublisher: AnyPublisher { get } +} + +public final class AppliedStudentCardView: UIView { + private let rankLabel = DotoriLabel(textColor: .neutral(.n20), font: .caption) + private let attendanceCheckBox = DotoriCheckBox() + .set(\.isHidden, true) + private let userImageView = DotoriIconView( + size: .custom(.init(width: 64, height: 64)), + image: .Dotori.personRectangle + ) + private let nameLabel = DotoriLabel(textColor: .neutral(.n10), font: .body1) + private let genderImageView = DotoriIconView( + size: .custom(.init(width: 16, height: 16)), + image: .Dotori.men + ) + private let stuNumLabel = DotoriLabel(textColor: .neutral(.n20), font: .caption) + private lazy var profileStackView = VStackView(spacing: 8) { + userImageView + + HStackView(spacing: 2) { + nameLabel + + genderImageView + } + .alignment(.center) + + stuNumLabel + }.alignment(.center) + + public init() { + super.init(frame: .zero) + configureView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func updateContent( + with viewModel: AppliedStudentViewModel + ) { + rankLabel.text = "\(viewModel.rank)" + nameLabel.text = viewModel.name + genderImageView.image = (viewModel.gender == .man ? UIImage.Dotori.men : .Dotori.women) + .withTintColor(.dotori(.neutral(.n10)), renderingMode: .alwaysOriginal) + stuNumLabel.text = viewModel.stuNum + attendanceCheckBox.isChecked = viewModel.isChecked + } + + public func setIsHiddenAttendanceCheckBox(_ isHidden: Bool) { + attendanceCheckBox.isHidden = isHidden + } +} + +private extension AppliedStudentCardView { + func configureView() { + self.addSubviews { + rankLabel + attendanceCheckBox + profileStackView + } + + MSGLayout.buildLayout { + rankLabel.layout + .top(.toSuperview(), .equal(12)) + .leading(.toSuperview(), .equal(16)) + + attendanceCheckBox.layout + .top(.toSuperview(), .equal(12)) + .trailing(.toSuperview(), .equal(-12)) + .size(24) + + profileStackView.layout + .centerX(.toSuperview()) + .top(.toSuperview(), .equal(28)) + .bottom(.toSuperview(), .equal(-28)) + } + + self.backgroundColor = .dotori(.background(.card)) + self.cornerRadius = 16 + DotoriShadow.cardShadow(card: self) + } +} + +extension AppliedStudentCardView: AppliedStudentCardViewActionProtocol { + public var attendanceCheckBoxPublisher: AnyPublisher { + attendanceCheckBox.checkBoxDidTapPublisher + } +} diff --git a/Projects/UserInterface/DesignSystem/Sources/Icons/DotoriIcon.swift b/Projects/UserInterface/DesignSystem/Sources/Icons/DotoriIcon.swift index 97a2456d..873597f9 100644 --- a/Projects/UserInterface/DesignSystem/Sources/Icons/DotoriIcon.swift +++ b/Projects/UserInterface/DesignSystem/Sources/Icons/DotoriIcon.swift @@ -8,6 +8,7 @@ public extension UIImage { public static let chevronDown = DesignSystemAsset.DotoriIcon.chevronDown.image public static let chevronLeft = DesignSystemAsset.DotoriIcon.chevronLeft.image public static let chevronRight = DesignSystemAsset.DotoriIcon.chevronRight.image + public static let coffee = DesignSystemAsset.DotoriIcon.coffee.image public static let dotori = DesignSystemAsset.DotoriIcon.dotori.image public static let dotoriSigninLogo = DesignSystemAsset.DotoriIcon.dotoriSigninLogo.image public static let dotoriHomeLogo = DesignSystemAsset.DotoriIcon.dotoriHomeLogo.image diff --git a/Projects/UserInterface/Localization/Resources/en.lproj/Massage.strings b/Projects/UserInterface/Localization/Resources/en.lproj/Massage.strings index d5d0213e..73894a87 100644 --- a/Projects/UserInterface/Localization/Resources/en.lproj/Massage.strings +++ b/Projects/UserInterface/Localization/Resources/en.lproj/Massage.strings @@ -5,3 +5,7 @@ Created by 최형우 on 2023/06/07. Copyright © 2023 com.msg. All rights reserved. */ + +"massage_title" = "Massage"; +"empty_massage_title" = "No one has applied for the Massage.."; +"apply_from_home_massage_title" = "Apply for the Massage from Home!"; diff --git a/Projects/UserInterface/Localization/Resources/ko.lproj/Massage.strings b/Projects/UserInterface/Localization/Resources/ko.lproj/Massage.strings index d5d0213e..c7c1ee42 100644 --- a/Projects/UserInterface/Localization/Resources/ko.lproj/Massage.strings +++ b/Projects/UserInterface/Localization/Resources/ko.lproj/Massage.strings @@ -5,3 +5,7 @@ Created by 최형우 on 2023/06/07. Copyright © 2023 com.msg. All rights reserved. */ + +"massage_title" = "안마의자"; +"empty_massage_title" = "안마의자를 신청한 인원이 없습니다.."; +"apply_from_home_massage_title" = "홈에서 안마의자 신청을 해보세요!";