diff --git a/Domain/Domain.xcodeproj/project.pbxproj b/Domain/Domain.xcodeproj/project.pbxproj index 9b5bd8e..e743d3d 100644 --- a/Domain/Domain.xcodeproj/project.pbxproj +++ b/Domain/Domain.xcodeproj/project.pbxproj @@ -7,8 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 003D5A2B2CEB1FF0005F3D09 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003D5A2A2CEB1FEA005F3D09 /* PhotoObject.swift */; }; + 003D5A2D2CEB21AA005F3D09 /* AddPhotoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003D5A2C2CEB217E005F3D09 /* AddPhotoUseCase.swift */; }; + 003D5A2F2CEB21B6005F3D09 /* AddPhotoUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003D5A2E2CEB21B2005F3D09 /* AddPhotoUseCaseInterface.swift */; }; + 004B217D2CEB2B2300A5BEB8 /* DomainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 004B217C2CEB2B2300A5BEB8 /* DomainError.swift */; }; 00683D692CE37F2F000D28E4 /* DrawingObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00683D682CE37F2F000D28E4 /* DrawingObject.swift */; }; 00683D722CE3A74A000D28E4 /* DrawObjectUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00683D712CE3A74A000D28E4 /* DrawObjectUseCase.swift */; }; + 007BCEDC2CEB852C009E6935 /* AddPhotoUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007BCEDB2CEB852C009E6935 /* AddPhotoUseCaseTests.swift */; }; 0080E8BB2CE2ECD80095B958 /* WhiteboardObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E8BA2CE2ECC60095B958 /* WhiteboardObject.swift */; }; 0080E8CE2CE4463B0095B958 /* WhiteboardObjectRepositoryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E8CD2CE4462E0095B958 /* WhiteboardObjectRepositoryInterface.swift */; }; 0080E91A2CE4B0880095B958 /* DrawObjectUseCaseInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E9182CE4B0880095B958 /* DrawObjectUseCaseInterface.swift */; }; @@ -62,8 +67,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 003D5A2A2CEB1FEA005F3D09 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = ""; }; + 003D5A2C2CEB217E005F3D09 /* AddPhotoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPhotoUseCase.swift; sourceTree = ""; }; + 003D5A2E2CEB21B2005F3D09 /* AddPhotoUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPhotoUseCaseInterface.swift; sourceTree = ""; }; + 004B217C2CEB2B2300A5BEB8 /* DomainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainError.swift; sourceTree = ""; }; 00683D682CE37F2F000D28E4 /* DrawingObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingObject.swift; sourceTree = ""; }; 00683D712CE3A74A000D28E4 /* DrawObjectUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawObjectUseCase.swift; sourceTree = ""; }; + 007BCEDB2CEB852C009E6935 /* AddPhotoUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPhotoUseCaseTests.swift; sourceTree = ""; }; 0080E8BA2CE2ECC60095B958 /* WhiteboardObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardObject.swift; sourceTree = ""; }; 0080E8CD2CE4462E0095B958 /* WhiteboardObjectRepositoryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardObjectRepositoryInterface.swift; sourceTree = ""; }; 0080E9182CE4B0880095B958 /* DrawObjectUseCaseInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawObjectUseCaseInterface.swift; sourceTree = ""; }; @@ -112,6 +122,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 004B217B2CEB2B0B00A5BEB8 /* Common */ = { + isa = PBXGroup; + children = ( + 004B217C2CEB2B2300A5BEB8 /* DomainError.swift */, + ); + path = Common; + sourceTree = ""; + }; 00683D702CE3A72F000D28E4 /* UseCase */ = { isa = PBXGroup; children = ( @@ -121,6 +139,7 @@ 5BDFD9392CE2EE3100DA4F5B /* WhiteboardUseCase.swift */, 00D2DD852CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift */, 00D2DD902CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift */, + 003D5A2C2CEB217E005F3D09 /* AddPhotoUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -128,6 +147,7 @@ 0080E8592CE19EBD0095B958 /* Sources */ = { isa = PBXGroup; children = ( + 004B217B2CEB2B0B00A5BEB8 /* Common */, 00D2DD962CE8A4DB0089F0BA /* Model */, 0080E8CB2CE4461F0095B958 /* Interface */, 0080E8B92CE2ECB50095B958 /* Entity */, @@ -153,6 +173,7 @@ 5BDFD9332CE1F7DB00DA4F5B /* Whiteboard.swift */, 0080E8BA2CE2ECC60095B958 /* WhiteboardObject.swift */, 00683D682CE37F2F000D28E4 /* DrawingObject.swift */, + 003D5A2A2CEB1FEA005F3D09 /* PhotoObject.swift */, ); path = Entity; sourceTree = ""; @@ -185,6 +206,7 @@ 5B6542472CE44631000168AD /* WhiteboardUseCaseInterface.swift */, 00D2DD832CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift */, 00D2DD872CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift */, + 003D5A2E2CEB21B2005F3D09 /* AddPhotoUseCaseInterface.swift */, ); path = UseCase; sourceTree = ""; @@ -195,6 +217,7 @@ 0080E9562CE4D8760095B958 /* DrawObjectUseCaseTests.swift */, 00D2DD922CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift */, 00D2DD942CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift */, + 007BCEDB2CEB852C009E6935 /* AddPhotoUseCaseTests.swift */, ); path = DomainTests; sourceTree = ""; @@ -373,6 +396,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 007BCEDC2CEB852C009E6935 /* AddPhotoUseCaseTests.swift in Sources */, 00D2DD952CE88EDA0089F0BA /* ManageWhiteboardObjectsUseCaseTests.swift in Sources */, 0080E9582CE4D8760095B958 /* DrawObjectUseCaseTests.swift in Sources */, 00D2DD932CE88EC70089F0BA /* ManageWhiteboardToolUseCaseTests.swift in Sources */, @@ -383,7 +407,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 003D5A2F2CEB21B6005F3D09 /* AddPhotoUseCaseInterface.swift in Sources */, A8E97C052CE5E3AB00B28063 /* ProfileUseCaseInterface.swift in Sources */, + 004B217D2CEB2B2300A5BEB8 /* DomainError.swift in Sources */, A8E97BDB2CE5A6D500B28063 /* ProfileRepositoryInterface.swift in Sources */, A8E97BD92CE5A6B800B28063 /* ProfileIcon.swift in Sources */, A8E97C072CE5E45500B28063 /* WhiteboardObjectSendUseCase.swift in Sources */, @@ -395,6 +421,7 @@ 5B6542482CE44631000168AD /* WhiteboardUseCaseInterface.swift in Sources */, 00D2DD842CE8864B0089F0BA /* ManageWhiteboardObjectUseCaseInterface.swift in Sources */, 00D2DD882CE88BD80089F0BA /* ManageWhiteboardToolUseCaseInterface.swift in Sources */, + 003D5A2B2CEB1FF0005F3D09 /* PhotoObject.swift in Sources */, 0080E8CE2CE4463B0095B958 /* WhiteboardObjectRepositoryInterface.swift in Sources */, 0080E8BB2CE2ECD80095B958 /* WhiteboardObject.swift in Sources */, 6FBC909B2CE5A6AE000FEB5A /* WhiteObjectSendUseCaseInterface.swift in Sources */, @@ -402,6 +429,7 @@ 00683D722CE3A74A000D28E4 /* DrawObjectUseCase.swift in Sources */, 00D2DD8F2CE88C640089F0BA /* WhiteboardTool.swift in Sources */, 0080E91A2CE4B0880095B958 /* DrawObjectUseCaseInterface.swift in Sources */, + 003D5A2D2CEB21AA005F3D09 /* AddPhotoUseCase.swift in Sources */, 00D2DD912CE88D260089F0BA /* ManageWhiteboardToolUseCase.swift in Sources */, 00D2DD862CE887540089F0BA /* ManageWhiteboardObjectUseCase.swift in Sources */, 00683D692CE37F2F000D28E4 /* DrawingObject.swift in Sources */, diff --git a/Domain/Domain/Sources/Common/DomainError.swift b/Domain/Domain/Sources/Common/DomainError.swift new file mode 100644 index 0000000..17ed15b --- /dev/null +++ b/Domain/Domain/Sources/Common/DomainError.swift @@ -0,0 +1,27 @@ +// +// DomainError.swift +// Domain +// +// Created by 이동현 on 11/18/24. +// + +import Foundation + +public enum DomainError { + case cannotWriteFile + case cannotCreateDirectory + case cannotFindDirectory +} + +extension DomainError: LocalizedError { + public var errorDescription: String? { + switch self { + case .cannotWriteFile: + return "파일을 쓸 수 없습니다." + case .cannotCreateDirectory: + return "디렉터리를 생성할 수 없습니다." + case .cannotFindDirectory: + return "디렉터리를 찾을 수 없습니다." + } + } +} diff --git a/Domain/Domain/Sources/Entity/DrawingObject.swift b/Domain/Domain/Sources/Entity/DrawingObject.swift index d542cee..9e3520d 100644 --- a/Domain/Domain/Sources/Entity/DrawingObject.swift +++ b/Domain/Domain/Sources/Entity/DrawingObject.swift @@ -6,7 +6,7 @@ // import Foundation -public class DrawingObject: WhiteboardObject { +public final class DrawingObject: WhiteboardObject { public private(set) var points: [CGPoint] public let lineWidth: CGFloat diff --git a/Domain/Domain/Sources/Entity/PhotoObject.swift b/Domain/Domain/Sources/Entity/PhotoObject.swift new file mode 100644 index 0000000..debd21f --- /dev/null +++ b/Domain/Domain/Sources/Entity/PhotoObject.swift @@ -0,0 +1,25 @@ +// +// PhotoObject.swift +// Domain +// +// Created by 이동현 on 11/18/24. +// + +import Foundation + +public final class PhotoObject: WhiteboardObject { + public let photoURL: URL + + public init( + id: UUID, + position: CGPoint, + size: CGSize, + photoURL: URL + ) { + self.photoURL = photoURL + super.init( + id: id, + position: position, + size: size) + } +} diff --git a/Domain/Domain/Sources/Interface/UseCase/AddPhotoUseCaseInterface.swift b/Domain/Domain/Sources/Interface/UseCase/AddPhotoUseCaseInterface.swift new file mode 100644 index 0000000..c2777e2 --- /dev/null +++ b/Domain/Domain/Sources/Interface/UseCase/AddPhotoUseCaseInterface.swift @@ -0,0 +1,23 @@ +// +// AddPhotoUseCaseInterface.swift +// Domain +// +// Created by 이동현 on 11/18/24. +// + +import Foundation + +public protocol AddPhotoUseCaseInterface { + + /// 사진 오브젝트를 추가합니다. + /// - Parameters: + /// - imageData: 추가할 사진의 데이터 + /// - position: 사진을 추가할 위치 (origin) + /// - size: 사진 객체 + /// - Returns: + func addPhoto( + imageData: Data, + position: CGPoint, + size: CGSize + ) throws -> PhotoObject +} diff --git a/Domain/Domain/Sources/UseCase/AddPhotoUseCase.swift b/Domain/Domain/Sources/UseCase/AddPhotoUseCase.swift new file mode 100644 index 0000000..18f9fdd --- /dev/null +++ b/Domain/Domain/Sources/UseCase/AddPhotoUseCase.swift @@ -0,0 +1,55 @@ +// +// AddPhotoUseCase.swift +// Domain +// +// Created by 이동현 on 11/18/24. +// + +import Foundation + +public final class AddPhotoUseCase: AddPhotoUseCaseInterface { + private let photoDirectory: URL + private let fileManager: FileManager + + public init() throws { + fileManager = FileManager.default + guard + let documentDirectory = fileManager + .urls(for: .documentDirectory, in: .userDomainMask) + .first + else { throw DomainError.cannotCreateDirectory } + photoDirectory = documentDirectory.appending(path: "photos") + + if !fileManager.fileExists(atPath: photoDirectory.path()) { + do { + try fileManager.createDirectory(at: photoDirectory, withIntermediateDirectories: true) + } catch { + throw DomainError.cannotCreateDirectory + } + } + } + + public func addPhoto( + imageData: Data, + position: CGPoint, + size: CGSize + ) throws -> PhotoObject { + let uuid = UUID() + let photoname = "\(uuid.uuidString).jpg" + let photoURL = photoDirectory.appending(path: photoname) + + do { + try imageData.write(to: photoURL) + } catch { + throw DomainError.cannotWriteFile + } + + let photoObject = PhotoObject( + id: uuid, + position: position, + size: size, + photoURL: photoURL) + + return photoObject + } +} diff --git a/Domain/DomainTests/AddPhotoUseCaseTests.swift b/Domain/DomainTests/AddPhotoUseCaseTests.swift new file mode 100644 index 0000000..c983afd --- /dev/null +++ b/Domain/DomainTests/AddPhotoUseCaseTests.swift @@ -0,0 +1,53 @@ +// +// AddPhotoUseCaseTests.swift +// DomainTests +// +// Created by 이동현 on 11/18/24. +// + +import Domain +import XCTest + +final class AddPhotoUseCaseTests: XCTestCase { + private var useCase: AddPhotoUseCase! + + override func setUp() { + super.setUp() + + do { + useCase = try AddPhotoUseCase() + } catch { + XCTFail("init photoUseCase should success.") + } + } + + override func tearDown() { + useCase = nil + super.tearDown() + } + + // 사진 객체 생성 성공 테스트 + func testAddPhotoSuccess() throws { + // 준비 + let dummyImageData = Data() + let position = CGPoint(x: 100, y: 100) + let size = CGSize(width: 200, height: 200) + let photoObject: PhotoObject + + // 실행 + do { + photoObject = try useCase.addPhoto( + imageData: dummyImageData, + position: position, + size: size) + } catch { + XCTFail("photoObject should not fail.") + return + } + + // 검증 + XCTAssertEqual(photoObject.photoURL.lastPathComponent.suffix(4), ".jpg") + XCTAssertEqual(photoObject.position, position) + XCTAssertEqual(photoObject.size, size) + } +}