diff --git a/Domain/Domain.xcodeproj/project.pbxproj b/Domain/Domain.xcodeproj/project.pbxproj index 7f56d05..9b5bd8e 100644 --- a/Domain/Domain.xcodeproj/project.pbxproj +++ b/Domain/Domain.xcodeproj/project.pbxproj @@ -14,6 +14,13 @@ 0080E91A2CE4B0880095B958 /* DrawObjectUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E9182CE4B0880095B958 /* DrawObjectUseCaseInterface.swift */; }; 0080E9502CE4D8700095B958 /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B7C6E262CDB6A010024704A /* Domain.framework */; platformFilter = ios; }; 0080E9582CE4D8760095B958 /* DrawObjectUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E9562CE4D8760095B958 /* DrawObjectUseCaseTests.swift */; }; + 00D2DD842CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD832CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift */; }; + 00D2DD862CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD852CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift */; }; + 00D2DD882CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD872CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift */; }; + 00D2DD8F2CE88C640089F0BA /* WhiteboardTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD8E2CE88C640089F0BA /* WhiteboardTool.swift */; }; + 00D2DD912CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD902CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift */; }; + 00D2DD932CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD922CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift */; }; + 00D2DD952CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD942CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift */; }; 5B6542482CE44631000168AD /* WhiteboardUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6542472CE44631000168AD /* WhiteboardUseCaseInterface.swift */; }; 5B7C6EB02CDB6C040024704A /* AirplaINFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B7C6EAF2CDB6C040024704A /* AirplaINFoundation.framework */; }; 5B7C6EB12CDB6C040024704A /* AirplaINFoundation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5B7C6EAF2CDB6C040024704A /* AirplaINFoundation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -62,6 +69,13 @@ 0080E9182CE4B0880095B958 /* DrawObjectUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawObjectUseCaseInterface.swift; sourceTree = ""; }; 0080E94C2CE4D8700095B958 /* DomainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DomainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0080E9562CE4D8760095B958 /* DrawObjectUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawObjectUseCaseTests.swift; sourceTree = ""; }; + 00D2DD832CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWhiteboardObjectUseCaseInterface.swift; sourceTree = ""; }; + 00D2DD852CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWhiteboardObjectUseCase.swift; sourceTree = ""; }; + 00D2DD872CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWhiteboardToolUseCaseInterface.swift; sourceTree = ""; }; + 00D2DD8E2CE88C640089F0BA /* WhiteboardTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WhiteboardTool.swift; path = Domain/Sources/Model/WhiteboardTool.swift; sourceTree = SOURCE_ROOT; }; + 00D2DD902CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWhiteboardToolUseCase.swift; sourceTree = ""; }; + 00D2DD922CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWhiteboardToolUseCaseTests.swift; sourceTree = ""; }; + 00D2DD942CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWhiteboardObjectsUseCaseTests.swift; sourceTree = ""; }; 5B6542472CE44631000168AD /* WhiteboardUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardUseCaseInterface.swift; sourceTree = ""; }; 5B7C6E262CDB6A010024704A /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5B7C6EAF2CDB6C040024704A /* AirplaINFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AirplaINFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -105,6 +119,8 @@ A8E97C062CE5E45500B28063 /* WhiteboardObjectSendUseCase.swift */, A85260222CE343B20089DA5E /* ProfileUseCase.swift */, 5BDFD9392CE2EE3100DA4F5B /* WhiteboardUseCase.swift */, + 00D2DD852CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift */, + 00D2DD902CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -112,6 +128,7 @@ 0080E8592CE19EBD0095B958 /* Sources */ = { isa = PBXGroup; children = ( + 00D2DD962CE8A4DB0089F0BA /* Model */, 0080E8CB2CE4461F0095B958 /* Interface */, 0080E8B92CE2ECB50095B958 /* Entity */, 00683D702CE3A72F000D28E4 /* UseCase */, @@ -166,6 +183,8 @@ A8E97C042CE5E3AB00B28063 /* ProfileUseCaseInterface.swift */, 6FBC909A2CE5A52B000FEB5A /* WhiteObjectSendUseCaseInterface.swift */, 5B6542472CE44631000168AD /* WhiteboardUseCaseInterface.swift */, + 00D2DD832CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift */, + 00D2DD872CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift */, ); path = UseCase; sourceTree = ""; @@ -174,10 +193,20 @@ isa = PBXGroup; children = ( 0080E9562CE4D8760095B958 /* DrawObjectUseCaseTests.swift */, + 00D2DD922CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift */, + 00D2DD942CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift */, ); path = DomainTests; sourceTree = ""; }; + 00D2DD962CE8A4DB0089F0BA /* Model */ = { + isa = PBXGroup; + children = ( + 00D2DD8E2CE88C640089F0BA /* WhiteboardTool.swift */, + ); + path = Model; + sourceTree = ""; + }; 5B7C6E1C2CDB6A010024704A = { isa = PBXGroup; children = ( @@ -344,7 +373,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 00D2DD952CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift in Sources */, 0080E9582CE4D8760095B958 /* DrawObjectUseCaseTests.swift in Sources */, + 00D2DD932CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -362,12 +393,17 @@ 5BDFD9342CE1F7DB00DA4F5B /* Whiteboard.swift in Sources */, 5BDFD9372CE2E6BC00DA4F5B /* WhiteboardRepositoryInterface.swift in Sources */, 5B6542482CE44631000168AD /* WhiteboardUseCaseInterface.swift in Sources */, + 00D2DD842CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift in Sources */, + 00D2DD882CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift in Sources */, 0080E8CE2CE4463B0095B958 /* WhiteboardObjectRepositoryInterface.swift in Sources */, 0080E8BB2CE2ECD80095B958 /* WhiteboardObject.swift in Sources */, 6FBC909B2CE5A6AE000FEB5A /* WhiteObjectSendUseCaseInterface.swift in Sources */, 6F21477B2CE4CEAE00B55E2C /* TextObject.swift in Sources */, 00683D722CE3A74A000D28E4 /* DrawObjectUseCase.swift in Sources */, + 00D2DD8F2CE88C640089F0BA /* WhiteboardTool.swift in Sources */, 0080E91A2CE4B0880095B958 /* DrawObjectUseCaseInterface.swift in Sources */, + 00D2DD912CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift in Sources */, + 00D2DD862CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift in Sources */, 00683D692CE37F2F000D28E4 /* DrawingObject.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Domain/Domain.xcodeproj/xcshareddata/xcschemes/DomainTests.xcscheme b/Domain/Domain.xcodeproj/xcshareddata/xcschemes/DomainTests.xcscheme index 6f11726..64afbed 100644 --- a/Domain/Domain.xcodeproj/xcshareddata/xcschemes/DomainTests.xcscheme +++ b/Domain/Domain.xcodeproj/xcshareddata/xcschemes/DomainTests.xcscheme @@ -37,6 +37,16 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + + + + + diff --git a/Domain/Domain/Sources/Entity/WhiteboardObject.swift b/Domain/Domain/Sources/Entity/WhiteboardObject.swift index d9137d2..0c0a8d9 100644 --- a/Domain/Domain/Sources/Entity/WhiteboardObject.swift +++ b/Domain/Domain/Sources/Entity/WhiteboardObject.swift @@ -6,7 +6,7 @@ // import Foundation -public class WhiteboardObject { +public class WhiteboardObject: Equatable { public let id: UUID public var position: CGPoint public var size: CGSize @@ -20,4 +20,8 @@ public class WhiteboardObject { self.position = position self.size = size } + + public static func == (lhs: WhiteboardObject, rhs: WhiteboardObject) -> Bool { + return lhs.id == rhs.id + } } diff --git a/Domain/Domain/Sources/Interface/UseCase/ManageWhiteboardObjectUseCaseInterface.swift b/Domain/Domain/Sources/Interface/UseCase/ManageWhiteboardObjectUseCaseInterface.swift new file mode 100644 index 0000000..4c24aed --- /dev/null +++ b/Domain/Domain/Sources/Interface/UseCase/ManageWhiteboardObjectUseCaseInterface.swift @@ -0,0 +1,33 @@ +// +// ManageWhiteboardObjectUseCaseInterface.swift +// Domain +// +// Created by 이동현 on 11/16/24. +// + +import Combine + +public protocol ManageWhiteboardObjectUseCaseInterface { + /// 화이트보드 객체가 추가될 때 이벤트를 방출합니다. + var addedObjectPublisher: AnyPublisher { get } + + /// 화이트보드 객체가 수정될 때 이벤트를 방출합니다. + var updatedObjectPublisher: AnyPublisher { get } + + /// 화이트보드 객체가 제거될 때 이벤트를 방출합니다. + var removedObjectPublisher: AnyPublisher { get } + + /// 화이트보드 객체를 추가하는 메서드 + /// - Parameter whiteboardObject: 추가할 화이트보드 객체 + /// - Returns: 추가 성공 여부 + func addObject(whiteboardObject: WhiteboardObject) -> Bool + + /// 화이트보드 객체를 수정하는 메서드 + /// - Parameter whiteboardObject: 수정할 화이트보드 객체 + /// - Returns: 추가 성공 여부 + func updateObject(whiteboardObject: WhiteboardObject) -> Bool + + /// 화이트보드를 제거하는 메서드 + /// - Returns: 추가 성공 여부 + func removeObject(whiteboardObject: WhiteboardObject) -> Bool +} diff --git a/Domain/Domain/Sources/Interface/UseCase/ManageWhiteboardToolUseCaseInterface.swift b/Domain/Domain/Sources/Interface/UseCase/ManageWhiteboardToolUseCaseInterface.swift new file mode 100644 index 0000000..f115220 --- /dev/null +++ b/Domain/Domain/Sources/Interface/UseCase/ManageWhiteboardToolUseCaseInterface.swift @@ -0,0 +1,24 @@ +// +// ManageWhiteboardToolUseCaseInterface.swift +// Domain +// +// Created by 이동현 on 11/16/24. +// + +import Combine + +public protocol ManageWhiteboardToolUseCaseInterface { + /// 화이트보드 도구 선택/선택 해제 시 이벤트를 방출합니다. + var currentToolPublisher: AnyPublisher { get } + + /// 현재 사용중인 화이트보드 도구를 반환합니다. + /// - Returns: 사용중인 화이트보드 도구 + func currentTool() -> WhiteboardTool? + + /// 화이트보드 도구를 선택합니다. + /// - Parameter tool: 선택할 화이트보드 도구 + func selectTool(tool: WhiteboardTool) + + /// 화이트보드 도구 사용을 완료합니다. + func finishUsingTool() +} diff --git a/Domain/Domain/Sources/Model/WhiteboardTool.swift b/Domain/Domain/Sources/Model/WhiteboardTool.swift new file mode 100644 index 0000000..f8a731a --- /dev/null +++ b/Domain/Domain/Sources/Model/WhiteboardTool.swift @@ -0,0 +1,15 @@ +// +// WhiteboardTool.swift +// Domain +// +// Created by 이동현 on 11/16/24. +// + +@frozen +public enum WhiteboardTool: CaseIterable { + case drawing + case text + case photo + case game + case chat +} diff --git a/Domain/Domain/Sources/UseCase/DrawObjectUseCase.swift b/Domain/Domain/Sources/UseCase/DrawObjectUseCase.swift index 8134ead..147fdf0 100644 --- a/Domain/Domain/Sources/UseCase/DrawObjectUseCase.swift +++ b/Domain/Domain/Sources/UseCase/DrawObjectUseCase.swift @@ -11,7 +11,7 @@ public final class DrawObjectUseCase: DrawObjectUseCaseInterface { public private(set) var points: [CGPoint] public private(set) var origin: CGPoint? - public init(repository: WhiteboardObjectRepositoryInterface) { + public init() { points = [] } diff --git a/Domain/Domain/Sources/UseCase/ManageWhiteboardObjectUseCase.swift b/Domain/Domain/Sources/UseCase/ManageWhiteboardObjectUseCase.swift new file mode 100644 index 0000000..311682a --- /dev/null +++ b/Domain/Domain/Sources/UseCase/ManageWhiteboardObjectUseCase.swift @@ -0,0 +1,52 @@ +// +// ManageWhiteboardObjectUseCase.swift +// Domain +// +// Created by 이동현 on 11/16/24. +// + +import Combine + +public final class ManageWhiteboardObjectUseCase: ManageWhiteboardObjectUseCaseInterface { + public var addedObjectPublisher: AnyPublisher + public var updatedObjectPublisher: AnyPublisher + public var removedObjectPublisher: AnyPublisher + private var whiteboardObjects: [WhiteboardObject] + + private let addedWhiteboardSubject: PassthroughSubject + private let updatedWhiteboardSubject: PassthroughSubject + private let removedWhiteboardSubject: PassthroughSubject + + public init() { + addedWhiteboardSubject = PassthroughSubject() + updatedWhiteboardSubject = PassthroughSubject() + removedWhiteboardSubject = PassthroughSubject() + + whiteboardObjects = [] + + addedObjectPublisher = addedWhiteboardSubject.eraseToAnyPublisher() + updatedObjectPublisher = updatedWhiteboardSubject.eraseToAnyPublisher() + removedObjectPublisher = removedWhiteboardSubject.eraseToAnyPublisher() + } + + public func addObject(whiteboardObject: WhiteboardObject) -> Bool { + guard !whiteboardObjects.contains(whiteboardObject) else { return false } + whiteboardObjects.append(whiteboardObject) + addedWhiteboardSubject.send(whiteboardObject) + return true + } + + public func updateObject(whiteboardObject: WhiteboardObject) -> Bool { + guard let index = whiteboardObjects.firstIndex(where: { $0 == whiteboardObject }) else { return false } + whiteboardObjects[index] = whiteboardObject + updatedWhiteboardSubject.send(whiteboardObject) + return true + } + + public func removeObject(whiteboardObject: WhiteboardObject) -> Bool { + guard whiteboardObjects.contains(whiteboardObject) else { return false } + whiteboardObjects.removeAll { $0 == whiteboardObject } + removedWhiteboardSubject.send(whiteboardObject) + return true + } +} diff --git a/Domain/Domain/Sources/UseCase/ManageWhiteboardToolUseCase.swift b/Domain/Domain/Sources/UseCase/ManageWhiteboardToolUseCase.swift new file mode 100644 index 0000000..d229c03 --- /dev/null +++ b/Domain/Domain/Sources/UseCase/ManageWhiteboardToolUseCase.swift @@ -0,0 +1,30 @@ +// +// ManageWhiteboardToolUseCase.swift +// Domain +// +// Created by 이동현 on 11/16/24. +// + +import Combine + +public final class ManageWhiteboardToolUseCase: ManageWhiteboardToolUseCaseInterface { + public var currentToolPublisher: AnyPublisher + private let currentToolSubject: CurrentValueSubject + + public init() { + currentToolSubject = CurrentValueSubject(nil) + currentToolPublisher = currentToolSubject.eraseToAnyPublisher() + } + + public func currentTool() -> WhiteboardTool? { + return currentToolSubject.value + } + + public func selectTool(tool: WhiteboardTool) { + currentToolSubject.send(tool) + } + + public func finishUsingTool() { + currentToolSubject.send(nil) + } +} diff --git a/Domain/DomainTests/DrawObjectUseCaseTests.swift b/Domain/DomainTests/DrawObjectUseCaseTests.swift index 22009db..3d44dab 100644 --- a/Domain/DomainTests/DrawObjectUseCaseTests.swift +++ b/Domain/DomainTests/DrawObjectUseCaseTests.swift @@ -9,15 +9,12 @@ import XCTest final class DrawObjectUseCaseTests: XCTestCase { var useCase: DrawObjectUseCaseInterface! - var mockRepository: WhiteboardObjectRepositoryInterface! override func setUpWithError() throws { - mockRepository = MockWhiteboardObjectRepository() - useCase = DrawObjectUseCase(repository: mockRepository) + useCase = DrawObjectUseCase() } override func tearDownWithError() throws { - mockRepository = nil useCase = nil } diff --git a/Domain/DomainTests/ManageWhiteboardObjectsUseCaseTests.swift b/Domain/DomainTests/ManageWhiteboardObjectsUseCaseTests.swift new file mode 100644 index 0000000..2b6b4cd --- /dev/null +++ b/Domain/DomainTests/ManageWhiteboardObjectsUseCaseTests.swift @@ -0,0 +1,162 @@ +// +// ManageWhiteboardObjectsUseCaseTests.swift +// DomainTests +// +// Created by 이동현 on 11/16/24. +// + +import Combine +import Domain +import XCTest + +final class ManageWhiteboardObjectsUseCaseTests: XCTestCase { + private var useCase: ManageWhiteboardObjectUseCaseInterface! + private var cancellables: Set! + + override func setUpWithError() throws { + useCase = ManageWhiteboardObjectUseCase() + cancellables = [] + } + + override func tearDownWithError() throws { + useCase = nil + } + + // 화이트보드 오브젝트 추가가 성공하는지 테스트 + func testAddWhiteboardObject() { + // 준비 + let targetObject = WhiteboardObject( + id: UUID(), + position: .zero, + size: CGSize(width: 100, height: 100)) + var receivedObject: WhiteboardObject? + + useCase.addedObjectPublisher + .sink { object in + receivedObject = object + } + .store(in: &cancellables) + + // 실행 + let result = useCase.addObject(whiteboardObject: targetObject) + + // 검증 + XCTAssertTrue(result) + XCTAssertEqual(receivedObject, targetObject) + } + + // 화이트보드 오브젝트 중복 추가가 실패하는지 테스트 + func testAddDuplicateObject() { + // 준비 + let targetObject = WhiteboardObject( + id: UUID(), + position: .zero, + size: CGSize(width: 100, height: 100)) + + // 실행 + let isSuccess = useCase.addObject(whiteboardObject: targetObject) + let isFailure = useCase.addObject(whiteboardObject: targetObject) + + // 검증 + XCTAssertTrue(isSuccess) + XCTAssertFalse(isFailure) + } + + // 화이트보드 오브젝트 업데이트 성공하는지 테스트 + func testUpdateObject() { + // 준비 + let uuid = UUID() + let object = WhiteboardObject( + id: uuid, + position: .zero, + size: CGSize(width: 100, height: 100)) + let updatedObject = WhiteboardObject( + id: uuid, + position: CGPoint(x: 50, y: 50), + size: CGSize(width: 200, height: 200)) + var receivedObject: WhiteboardObject? + + useCase.updatedObjectPublisher + .sink { receivedObject = $0 } + .store(in: &cancellables) + + // 실행 + _ = useCase.addObject(whiteboardObject: object) + let result = useCase.updateObject(whiteboardObject: updatedObject) + + // 검증 + XCTAssertTrue(result) + XCTAssertEqual(updatedObject, receivedObject) + } + + // 존재하지 않는 화이트보드 오브젝트 업데이트 실패하는지 테스트 + func testUpdateNonExistentObject() { + // 준비 + let targetObject = WhiteboardObject( + id: UUID(), + position: CGPoint(x: 50, y: 50), + size: CGSize(width: 200, height: 200)) + var receivedObject: WhiteboardObject? + + useCase.updatedObjectPublisher + .sink { receivedObject = $0 } + .store(in: &cancellables) + + // 실행 + let result = useCase.updateObject(whiteboardObject: targetObject) + + // 검증 + XCTAssertFalse(result) + XCTAssertNil(receivedObject) + } + + // 화이트보드 오브젝트 삭제 성공하는지 테스트 + func testRemoveObject() { + // 준비 + let object1 = WhiteboardObject( + id: UUID(), + position: .zero, + size: CGSize(width: 100, height: 100)) + let object2 = WhiteboardObject( + id: UUID(), + position: .zero, + size: CGSize(width: 100, height: 100)) + let targetObject = WhiteboardObject( + id: UUID(), + position: .zero, + size: CGSize(width: 100, height: 100)) + var receivedObject: WhiteboardObject? + + useCase.removedObjectPublisher + .sink { receivedObject = $0 } + .store(in: &cancellables) + + // 실행 + _ = useCase.addObject(whiteboardObject: object1) + _ = useCase.addObject(whiteboardObject: targetObject) + _ = useCase.addObject(whiteboardObject: object2) + let result = useCase.removeObject(whiteboardObject: targetObject) + + // 검증 + XCTAssertTrue(result) + XCTAssertEqual(targetObject, receivedObject) + } + + // 존재하지 않는 화이트보드 오브젝트 삭제 실패하는지 테스트 + func testRemoveNonExistentObject() { + // 준비 + let object = WhiteboardObject(id: UUID(), position: .zero, size: CGSize(width: 100, height: 100)) + var receivedObject: WhiteboardObject? + + useCase.removedObjectPublisher + .sink { receivedObject = $0 } + .store(in: &cancellables) + + // 실행 + let result = useCase.removeObject(whiteboardObject: object) + + // 검증 + XCTAssertFalse(result) + XCTAssertNil(receivedObject) + } +} diff --git a/Domain/DomainTests/ManageWhiteboardToolUseCaseTests.swift b/Domain/DomainTests/ManageWhiteboardToolUseCaseTests.swift new file mode 100644 index 0000000..28be78d --- /dev/null +++ b/Domain/DomainTests/ManageWhiteboardToolUseCaseTests.swift @@ -0,0 +1,81 @@ +// +// ManageWhiteboardToolUseCaseTests.swift +// DomainTests +// +// Created by 이동현 on 11/16/24. +// + +import Combine +import Domain +import XCTest + +final class ManageWhiteboardToolUseCaseTests: XCTestCase { + private var useCase: ManageWhiteboardToolUseCaseInterface! + private var cancellables: Set! + + override func setUpWithError() throws { + useCase = ManageWhiteboardToolUseCase() + cancellables = [] + } + + override func tearDownWithError() throws { + useCase = nil + } + + // 선택한 도구가 없을 때 화이트보드 도구 선택 성공 테스트 + func testSelectTool() { + // 준비 + let targetTool = WhiteboardTool.drawing + var receivedTool: WhiteboardTool? + + // 실행 + useCase.currentToolPublisher + .sink { receivedTool = $0 } + .store(in: &cancellables) + + useCase.selectTool(tool: targetTool) + + // 검증 + XCTAssertEqual(targetTool, receivedTool) + XCTAssertEqual(useCase.currentTool(), targetTool) + } + + // 선택한 도구가 있을 때, 새로운 도구 선택 성공 테스트 + func testSelectToolOverridesPrevious() { + // 준비 + let previousTool = WhiteboardTool.drawing + let targetTool = WhiteboardTool.text + var receivedTool: WhiteboardTool? + + useCase.currentToolPublisher + .sink { receivedTool = $0 } + .store(in: &cancellables) + + // 실행 + useCase.selectTool(tool: previousTool) + useCase.selectTool(tool: targetTool) + + // 검증 + XCTAssertEqual(targetTool, receivedTool) + XCTAssertEqual(useCase.currentTool(), targetTool) + } + + // 화이트보드 도구 사용 완료 성공 테스트 + func testFinishUsingTool() { + // 준비 + let tool = WhiteboardTool.drawing + var receivedTool: WhiteboardTool? + + useCase.currentToolPublisher + .sink { receivedTool = $0 } + .store(in: &cancellables) + + // 실행 + useCase.selectTool(tool: tool) + useCase.finishUsingTool() + + // 검증 + XCTAssertNil(receivedTool) + XCTAssertNil(useCase.currentTool()) + } +} diff --git a/Presentation/Presentation.xcodeproj/project.pbxproj b/Presentation/Presentation.xcodeproj/project.pbxproj index c7f5368..9ce9282 100644 --- a/Presentation/Presentation.xcodeproj/project.pbxproj +++ b/Presentation/Presentation.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 5B7C6EBB2CDB6C5C0024704A /* Domain.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5B7C6EB92CDB6C5C0024704A /* Domain.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6F199EF12CE31A05005DC40F /* WhiteboardToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F199EF02CE31A05005DC40F /* WhiteboardToolBar.swift */; }; 6F199EF32CE3203A005DC40F /* WhiteboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F199EF22CE3203A005DC40F /* WhiteboardViewController.swift */; }; - 6F199EF72CE3317E005DC40F /* WhiteboardTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F199EF62CE3317E005DC40F /* WhiteboardTool.swift */; }; 6F21477D2CE4EFCF00B55E2C /* TextObjectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F21477C2CE4EFCF00B55E2C /* TextObjectView.swift */; }; A8525DC32CE200230089DA5E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A8525DC22CE200230089DA5E /* Colors.xcassets */; }; A8525DCB2CE203D50089DA5E /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8525DCA2CE203D50089DA5E /* UIView+.swift */; }; @@ -58,7 +57,6 @@ 5B7C6EB92CDB6C5C0024704A /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6F199EF02CE31A05005DC40F /* WhiteboardToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardToolBar.swift; sourceTree = ""; }; 6F199EF22CE3203A005DC40F /* WhiteboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardViewController.swift; sourceTree = ""; }; - 6F199EF62CE3317E005DC40F /* WhiteboardTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardTool.swift; sourceTree = ""; }; 6F21477C2CE4EFCF00B55E2C /* TextObjectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextObjectView.swift; sourceTree = ""; }; A8525DC22CE200230089DA5E /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; A8525DC72CE201D50089DA5E /* AirplainFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirplainFont.swift; sourceTree = ""; }; @@ -94,14 +92,6 @@ path = ObjectView; sourceTree = ""; }; - 004645F32CE60AAB00C76EDB /* Model */ = { - isa = PBXGroup; - children = ( - 6F199EF62CE3317E005DC40F /* WhiteboardTool.swift */, - ); - path = Model; - sourceTree = ""; - }; 00683D742CE4B813000D28E4 /* Util */ = { isa = PBXGroup; children = ( @@ -136,7 +126,6 @@ 0080E8C82CE2FF3D0095B958 /* Factory */, 0080E8C42CE2F0430095B958 /* View */, 0080E8CF2CE49FE90095B958 /* ViewModel */, - 004645F32CE60AAB00C76EDB /* Model */, 00683D742CE4B813000D28E4 /* Util */, ); path = Whiteboard; @@ -399,7 +388,6 @@ A8525DCB2CE203D50089DA5E /* UIView+.swift in Sources */, 6F199EF32CE3203A005DC40F /* WhiteboardViewController.swift in Sources */, 6F199EF12CE31A05005DC40F /* WhiteboardToolBar.swift in Sources */, - 6F199EF72CE3317E005DC40F /* WhiteboardTool.swift in Sources */, 6F21477D2CE4EFCF00B55E2C /* TextObjectView.swift in Sources */, 0080E8D12CE4A00F0095B958 /* WhiteboardViewModel.swift in Sources */, 0080E8D32CE4A0840095B958 /* ViewModel.swift in Sources */, diff --git a/Presentation/Presentation/Sources/Whiteboard/Model/WhiteboardTool.swift b/Presentation/Presentation/Sources/Whiteboard/Model/WhiteboardTool.swift deleted file mode 100644 index e0071e7..0000000 --- a/Presentation/Presentation/Sources/Whiteboard/Model/WhiteboardTool.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// WhiteboardTool.swift -// Presentation -// -// Created by 박승찬 on 11/12/24. -// - -import Combine -import UIKit - -enum WhiteboardTool: CaseIterable { - case drawing - case text - case photo - case game - case chat -} diff --git a/Presentation/Presentation/Sources/Whiteboard/View/ObjectView/WhiteboardToolBar.swift b/Presentation/Presentation/Sources/Whiteboard/View/ObjectView/WhiteboardToolBar.swift index 6614aec..8b9551d 100644 --- a/Presentation/Presentation/Sources/Whiteboard/View/ObjectView/WhiteboardToolBar.swift +++ b/Presentation/Presentation/Sources/Whiteboard/View/ObjectView/WhiteboardToolBar.swift @@ -6,6 +6,7 @@ // import Combine +import Domain import UIKit protocol WhiteboardToolBarDelegate: AnyObject { diff --git a/Presentation/Presentation/Sources/Whiteboard/View/WhiteboardViewController.swift b/Presentation/Presentation/Sources/Whiteboard/View/WhiteboardViewController.swift index 5f783f2..b08176d 100644 --- a/Presentation/Presentation/Sources/Whiteboard/View/WhiteboardViewController.swift +++ b/Presentation/Presentation/Sources/Whiteboard/View/WhiteboardViewController.swift @@ -5,6 +5,7 @@ // Created by 박승찬 on 11/12/24. // +import Domain import UIKit public class WhiteboardViewController: UIViewController { diff --git a/Presentation/Presentation/Sources/Whiteboard/ViewModel/WhiteboardViewModel.swift b/Presentation/Presentation/Sources/Whiteboard/ViewModel/WhiteboardViewModel.swift index b4d1e65..e57cea1 100644 --- a/Presentation/Presentation/Sources/Whiteboard/ViewModel/WhiteboardViewModel.swift +++ b/Presentation/Presentation/Sources/Whiteboard/ViewModel/WhiteboardViewModel.swift @@ -10,35 +10,81 @@ import Foundation final class WhiteboardViewModel: ViewModel { enum Input { - // TODO: - tool을 바꾸는(선택하는) case 추가 + case startUsingTool(tool: WhiteboardTool) case startDrawing(startAt: CGPoint) case addDrawingPoint(point: CGPoint) - case finishDrawing + case finishUsingTool } struct Output { - let whiteboardObjectsPublisher: AnyPublisher<[WhiteboardObject], Never> + let whiteboardToolPublisher: AnyPublisher + let addedWhiteboardObjectPublisher: AnyPublisher + let updatedWhiteboardObjectPublisher: AnyPublisher + let removedWhiteboardObjectPublisher: AnyPublisher } let output: Output private let drawObjectUseCase: DrawObjectUseCaseInterface - private var whiteboardObjects = CurrentValueSubject<[WhiteboardObject], Never>([]) + private var whiteboardObjects: [WhiteboardObject] + private let currentTool: CurrentValueSubject + let addedWhiteboardObjectSubject: PassthroughSubject + let updatedWhiteboardObjectSubject: PassthroughSubject + let removedWhiteboardObjectSubject: PassthroughSubject init(drawObjectUseCase: DrawObjectUseCaseInterface) { self.drawObjectUseCase = drawObjectUseCase + whiteboardObjects = [] + currentTool = CurrentValueSubject(nil) + addedWhiteboardObjectSubject = PassthroughSubject() + updatedWhiteboardObjectSubject = PassthroughSubject() + removedWhiteboardObjectSubject = PassthroughSubject() - output = Output(whiteboardObjectsPublisher: whiteboardObjects.eraseToAnyPublisher()) + output = Output( + whiteboardToolPublisher: currentTool.eraseToAnyPublisher(), + addedWhiteboardObjectPublisher: addedWhiteboardObjectSubject.eraseToAnyPublisher(), + updatedWhiteboardObjectPublisher: updatedWhiteboardObjectSubject.eraseToAnyPublisher(), + removedWhiteboardObjectPublisher: removedWhiteboardObjectSubject.eraseToAnyPublisher()) } func action(input: Input) { switch input { + case .startUsingTool(let tool): + startUsingTool(with: tool) case .startDrawing(let point): startDrawing(at: point) case .addDrawingPoint(point: let point): addDrawingPoint(at: point) - case .finishDrawing: + case .finishUsingTool: + finishUsingTool() + } + } + + private func startUsingTool(with tool: WhiteboardTool) { + currentTool.send(tool) + } + + private func finishUsingTool() { + guard let currentTool = currentTool.value else { return } + + switch currentTool { + case .drawing: finishDrawing() + case .text: + break + case .photo: + break + case .game: + break + case .chat: + break } + + self.currentTool.send(nil) + } + + private func addWhiteboardObject(object: WhiteboardObject) { + guard !whiteboardObjects.contains(object) else { return } + whiteboardObjects.append(object) } private func startDrawing(at point: CGPoint) { @@ -51,8 +97,7 @@ final class WhiteboardViewModel: ViewModel { private func finishDrawing() { guard let drawingObject = drawObjectUseCase.finishDrawing() else { return } - var newObjects = whiteboardObjects.value - newObjects.append(drawingObject) - whiteboardObjects.send(newObjects) + addedWhiteboardObjectSubject.send(drawingObject) + addWhiteboardObject(object: drawingObject) } }