Skip to content
20 changes: 20 additions & 0 deletions Domain/Domain.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@
6F3BCDD02CF3322C005F6642 /* ChatUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3BCDCF2CF3322C005F6642 /* ChatUseCase.swift */; };
6F3BCDD22CF45510005F6642 /* ChatUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3BCDD12CF45510005F6642 /* ChatUseCaseTests.swift */; };
6F47898F2CEB8D8F0016B7AB /* TextObjectUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F47898E2CEB8D8F0016B7AB /* TextObjectUseCaseInterface.swift */; };
6F5FC1562D2BF5100049A44F /* LWWRegister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5FC1552D2BF5100049A44F /* LWWRegister.swift */; };
6F5FC1582D2BF51E0049A44F /* Timestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5FC1572D2BF51E0049A44F /* Timestamp.swift */; };
6F5FC15A2D2C1BFF0049A44F /* WhiteboardObjectRegisters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5FC1592D2C1BFF0049A44F /* WhiteboardObjectRegisters.swift */; };
6F5FC15C2D2C1CF80049A44F /* WhiteboardObjectRegistersInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5FC15B2D2C1CF80049A44F /* WhiteboardObjectRegistersInterface.swift */; };
6F68E7852CEC200000945394 /* TextObjectUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F68E7842CEC200000945394 /* TextObjectUseCase.swift */; };
6F68E7872CEC593300945394 /* TextObjectUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F68E7862CEC593300945394 /* TextObjectUseCaseTests.swift */; };
6F6A89A72D2E9367008CF899 /* LWWRegisterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6A89A62D2E9367008CF899 /* LWWRegisterTests.swift */; };
A81E7BF32CF70C17007E8414 /* GameObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81E7BF22CF70C17007E8414 /* GameObject.swift */; };
A81E7BF92CF72503007E8414 /* GameObjectUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81E7BF82CF72503007E8414 /* GameObjectUseCase.swift */; };
A81E7BFB2CF725C2007E8414 /* GameObjectUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81E7BFA2CF725C2007E8414 /* GameObjectUseCaseInterface.swift */; };
Expand Down Expand Up @@ -99,8 +104,13 @@
6F3BCDCF2CF3322C005F6642 /* ChatUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUseCase.swift; sourceTree = "<group>"; };
6F3BCDD12CF45510005F6642 /* ChatUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUseCaseTests.swift; sourceTree = "<group>"; };
6F47898E2CEB8D8F0016B7AB /* TextObjectUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextObjectUseCaseInterface.swift; sourceTree = "<group>"; };
6F5FC1552D2BF5100049A44F /* LWWRegister.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LWWRegister.swift; sourceTree = "<group>"; };
6F5FC1572D2BF51E0049A44F /* Timestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timestamp.swift; sourceTree = "<group>"; };
6F5FC1592D2C1BFF0049A44F /* WhiteboardObjectRegisters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardObjectRegisters.swift; sourceTree = "<group>"; };
6F5FC15B2D2C1CF80049A44F /* WhiteboardObjectRegistersInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardObjectRegistersInterface.swift; sourceTree = "<group>"; };
6F68E7842CEC200000945394 /* TextObjectUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextObjectUseCase.swift; sourceTree = "<group>"; };
6F68E7862CEC593300945394 /* TextObjectUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextObjectUseCaseTests.swift; sourceTree = "<group>"; };
6F6A89A62D2E9367008CF899 /* LWWRegisterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LWWRegisterTests.swift; sourceTree = "<group>"; };
A81E7BF22CF70C17007E8414 /* GameObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameObject.swift; sourceTree = "<group>"; };
A81E7BF82CF72503007E8414 /* GameObjectUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameObjectUseCase.swift; sourceTree = "<group>"; };
A81E7BFA2CF725C2007E8414 /* GameObjectUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameObjectUseCaseInterface.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -197,6 +207,7 @@
0080E9192CE4B0880095B958 /* UseCase */,
0080E8CC2CE446270095B958 /* Repository */,
00036B852CF58D7C007D1244 /* WhiteboardObjectSetInterface.swift */,
6F5FC15B2D2C1CF80049A44F /* WhiteboardObjectRegistersInterface.swift */,
);
path = Interface;
sourceTree = "<group>";
Expand Down Expand Up @@ -239,13 +250,17 @@
6F68E7862CEC593300945394 /* TextObjectUseCaseTests.swift */,
007BCEDB2CEB852C009E6935 /* AddPhotoUseCaseTests.swift */,
6F3BCDD12CF45510005F6642 /* ChatUseCaseTests.swift */,
6F6A89A62D2E9367008CF899 /* LWWRegisterTests.swift */,
);
path = DomainTests;
sourceTree = "<group>";
};
00D2DD962CE8A4DB0089F0BA /* Model */ = {
isa = PBXGroup;
children = (
6F5FC1572D2BF51E0049A44F /* Timestamp.swift */,
6F5FC1552D2BF5100049A44F /* LWWRegister.swift */,
6F5FC1592D2C1BFF0049A44F /* WhiteboardObjectRegisters.swift */,
00D2DD8E2CE88C640089F0BA /* WhiteboardTool.swift */,
005BCC502CF01D29001B3623 /* WhiteboardObjectSet.swift */,
);
Expand Down Expand Up @@ -418,6 +433,7 @@
buildActionMask = 2147483647;
files = (
007BCEDC2CEB852C009E6935 /* AddPhotoUseCaseTests.swift in Sources */,
6F6A89A72D2E9367008CF899 /* LWWRegisterTests.swift in Sources */,
6F3BCDD22CF45510005F6642 /* ChatUseCaseTests.swift in Sources */,
00D2DD952CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift in Sources */,
0080E9582CE4D8760095B958 /* DrawObjectUseCaseTests.swift in Sources */,
Expand All @@ -434,9 +450,11 @@
A8E97C052CE5E3AB00B28063 /* ProfileUseCaseInterface.swift in Sources */,
004B217D2CEB2B2300A5BEB8 /* DomainError.swift in Sources */,
6F3BCDC82CF31CB4005F6642 /* ChatMessage.swift in Sources */,
6F5FC15C2D2C1CF80049A44F /* WhiteboardObjectRegistersInterface.swift in Sources */,
005BCC512CF01D29001B3623 /* WhiteboardObjectSet.swift in Sources */,
A8E97BDB2CE5A6D500B28063 /* ProfileRepositoryInterface.swift in Sources */,
A8E97BD92CE5A6B800B28063 /* ProfileIcon.swift in Sources */,
6F5FC1562D2BF5100049A44F /* LWWRegister.swift in Sources */,
00036B862CF58D7C007D1244 /* WhiteboardObjectSetInterface.swift in Sources */,
A81E7BF32CF70C17007E8414 /* GameObject.swift in Sources */,
A8E97BD82CE5A6B800B28063 /* Profile.swift in Sources */,
Expand All @@ -447,13 +465,15 @@
6F3BCDD02CF3322C005F6642 /* ChatUseCase.swift in Sources */,
5BDFD9342CE1F7DB00DA4F5B /* Whiteboard.swift in Sources */,
A81E7BFB2CF725C2007E8414 /* GameObjectUseCaseInterface.swift in Sources */,
6F5FC1582D2BF51E0049A44F /* Timestamp.swift in Sources */,
5BDFD9372CE2E6BC00DA4F5B /* WhiteboardRepositoryInterface.swift in Sources */,
5B6542482CE44631000168AD /* WhiteboardUseCaseInterface.swift in Sources */,
00D2DD842CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift in Sources */,
6F3BCDCE2CF3312A005F6642 /* ChatUseCaseInterface.swift in Sources */,
A81E7C012CF72A4C007E8414 /* GameRepositoryInterface.swift in Sources */,
6F47898F2CEB8D8F0016B7AB /* TextObjectUseCaseInterface.swift in Sources */,
00D2DD882CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift in Sources */,
6F5FC15A2D2C1BFF0049A44F /* WhiteboardObjectRegisters.swift in Sources */,
003D5A2B2CEB1FF0005F3D09 /* PhotoObject.swift in Sources */,
6F3BCDCA2CF31DBA005F6642 /* ChatRepositoryInterface.swift in Sources */,
0080E8CE2CE4463B0095B958 /* WhiteboardObjectRepositoryInterface.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// WhiteboardObjectRegistersInterface.swift
// Domain
//
// Created by 박승찬 on 1/6/25.
//

import Foundation

public protocol WhiteboardObjectRegistersInterface {
/// 집합에 레지스터가 있는지 확인합니다.
/// - Parameter register: 확인할 레지스터
/// - Returns: 오브젝트 존재 여부
func contains(register: LWWRegister) async -> Bool

/// 집합에 레지스터를 추가합니다.
/// - Parameter object: 추가할 레지스터
func insert(register: LWWRegister) async

/// 집합에서 레지스터를 삭제합니다.
/// - Parameter register: 삭제할 레지스터
func remove(register: LWWRegister) async

/// 모든 화이트보드 오브젝트 레지스터들을 삭제합니다.
func removeAll() async

/// 집합에 있는 레지스터를 업데이트 합니다.
/// - Parameter register: 업데이트할 레지스터
func update(register: LWWRegister) async

/// ID로 집합에있는 레지스터를 가져옵니다.
/// - Parameter id: 가져올 레지스터의 오브젝트 ID
/// - Returns: 레지스터
func fetchObjectByID(id: UUID) async -> LWWRegister?

/// 모든 화이트보드 오브젝트 레지스터들을 가져옵니다.
/// - Returns: 화이트보드 레지스터 배열
func fetchAll() async -> [LWWRegister]
}
34 changes: 34 additions & 0 deletions Domain/Domain/Sources/Model/LWWRegister.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// LWWRegister.swift
// Domain
//
// Created by 박승찬 on 1/6/25.
//

import Foundation

public struct LWWRegister {
public let whiteboardObject: WhiteboardObject
private let timestamp: Timestamp

public init(whiteboardObject: WhiteboardObject, timestamp: Timestamp) {
self.whiteboardObject = whiteboardObject
self.timestamp = timestamp
}

public func merge(register: LWWRegister) -> LWWRegister {
timestamp < register.timestamp ? register: self
}
}

extension LWWRegister: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(whiteboardObject)
}
}

extension LWWRegister: Comparable {
public static func < (lhs: LWWRegister, rhs: LWWRegister) -> Bool {
lhs.timestamp < rhs.timestamp
}
}
23 changes: 23 additions & 0 deletions Domain/Domain/Sources/Model/Timestamp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Timestamp.swift
// Domain
//
// Created by 박승찬 on 1/6/25.
//

import Foundation

public struct Timestamp: Comparable {
let updatedAt: Date
let updatedBy: UUID

public init(updatedAt: Date, updatedBy: UUID) {
self.updatedAt = updatedAt
self.updatedBy = updatedBy
}

public static func < (lhs: Timestamp, rhs: Timestamp) -> Bool {
if lhs.updatedAt == rhs.updatedAt { return lhs.updatedBy < rhs.updatedBy }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 요 부분 덕분에 updatedAt이 같더라도, 우선 순위가 생기겠군요 죠습니다 🦈

return lhs.updatedAt < rhs.updatedAt
}
}
49 changes: 49 additions & 0 deletions Domain/Domain/Sources/Model/WhiteboardObjectRegisters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// WhiteboardObjectRegisters.swift
// Domain
//
// Created by 박승찬 on 1/6/25.
//

import Foundation

actor WhiteboardObjectRegisters: WhiteboardObjectRegistersInterface {
private var registers: Set<LWWRegister>

init() {
registers = []
}

func contains(register: LWWRegister) async -> Bool {
registers.contains(register)
}

func insert(register: LWWRegister) async {
registers.insert(register)
}

func remove(register: LWWRegister) async {
registers.remove(register)
}

func removeAll() async {
registers.removeAll()
}

func update(register: LWWRegister) async {
if registers.contains(register) {
registers.remove(register)
await insert(register: register.merge(register: register))
} else {
await insert(register: register)
}
}

func fetchObjectByID(id: UUID) async -> LWWRegister? {
registers.first(where: { $0.whiteboardObject.id == id })
}

func fetchAll() async -> [LWWRegister] {
Array(registers.sorted { $0 < $1 })
}
}
134 changes: 134 additions & 0 deletions Domain/DomainTests/LWWRegisterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// LWWRegisterTests.swift
// DomainTests
//
// Created by 박승찬 on 1/8/25.
//

import Domain
import XCTest

final class LWWRegisterTests: XCTestCase {
private var register: LWWRegister!
private var defaultTimestamp: Timestamp!
private var defaultDate: Date!
private var defaultObject: WhiteboardObject!

override func setUp() {
super.setUp()
defaultObject = TextObject(
id: UUID(),
centerPosition: CGPoint(x: 0, y: 0),
size: CGSize(width: 100, height: 100),
text: "default")
defaultDate = Date()
defaultTimestamp = Timestamp(updatedAt: defaultDate, updatedBy: UUID())
register = LWWRegister(whiteboardObject: defaultObject, timestamp: defaultTimestamp)
}

override func tearDown() {
register = nil
defaultTimestamp = nil
defaultDate = nil
defaultObject = nil
}

// Timestamp가 같을 때
func testMergeWhenEqualTimestmap() {
// 준비
let textObject = TextObject(
id: UUID(),
centerPosition: CGPoint(x: 50, y: 50),
size: CGSize(width: 200, height: 200),
text: "equal")
let mockRegister = LWWRegister(whiteboardObject: textObject, timestamp: defaultTimestamp)

// 실행
let sut = register.merge(register: mockRegister)

// 검증
XCTAssertEqual(sut, register)
}

// 새로 들어온 updatedAt이 더 빠를 때
func testMergeWhenIncomingTimestampIsEarlier() {
// 준비
let earlierDate = defaultDate.addingTimeInterval(-10)
let earlierTimestamp = Timestamp(updatedAt: earlierDate, updatedBy: UUID())
let textObject = TextObject(
id: UUID(),
centerPosition: CGPoint(x: 50, y: 50),
size: CGSize(width: 200, height: 200),
text: "incoming")
let mockRegister = LWWRegister(whiteboardObject: textObject, timestamp: earlierTimestamp)

// 실행
let sut = register.merge(register: mockRegister)

// 검증
XCTAssertEqual(sut, register)
}

// updatedAt은 같지만 새로 들어온 UUID가 작을 때
func testMergeWhenIncomingTimestampHasSmallerUUID() {
// 준비
guard let smallerUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000") else {
XCTFail("Test UUID생성 실패")
return
}
let smallerTimestamp = Timestamp(updatedAt: defaultDate, updatedBy: smallerUUID)
let textObject = TextObject(
id: UUID(),
centerPosition: CGPoint(x: 50, y: 50),
size: CGSize(width: 200, height: 200),
text: "incoming")
let mockRegister = LWWRegister(whiteboardObject: textObject, timestamp: smallerTimestamp)

// 실행
let sut = register.merge(register: mockRegister)

// 검증
XCTAssertEqual(sut, register)
}

// 새로 들어온 updatedAt이 더 느릴 때
func testMergeWhenIncomingTimestampIsLater() {
// 준비
let laterDate = defaultDate.addingTimeInterval(10)
let laterTimestamp = Timestamp(updatedAt: laterDate, updatedBy: UUID())
let textObject = TextObject(
id: UUID(),
centerPosition: CGPoint(x: 50, y: 50),
size: CGSize(width: 200, height: 200),
text: "incoming")
let mockRegister = LWWRegister(whiteboardObject: textObject, timestamp: laterTimestamp)

// 실행
let sut = register.merge(register: mockRegister)

// 검증
XCTAssertEqual(sut, mockRegister)
}

// updatedAt은 같지만 새로 들어온 UUID가 클 때
func testMergeWhenIncomingTimestampHasLargerUUID() {
// 준비
guard let largerUUID = UUID(uuidString: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") else {
XCTFail("Test UUID생성 실패")
return
}
let largerTimestamp = Timestamp(updatedAt: defaultDate, updatedBy: largerUUID)
let textObject = TextObject(
id: UUID(),
centerPosition: CGPoint(x: 50, y: 50),
size: CGSize(width: 200, height: 200),
text: "incoming")
let mockRegister = LWWRegister(whiteboardObject: textObject, timestamp: largerTimestamp)

// 실행
let sut = register.merge(register: mockRegister)

// 검증
XCTAssertEqual(sut, mockRegister)
}
}