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" = "홈에서 안마의자 신청을 해보세요!";