├── MemorialHouse ├── MHApplication │ ├── MHApplication │ │ ├── Resource │ │ │ ├── ko.lproj │ │ │ │ └── LaunchScreen.strings │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── icon-20@2x.png │ │ │ │ │ ├── icon-20@3x.png │ │ │ │ │ ├── icon-29@2x.png │ │ │ │ │ ├── icon-29@3x.png │ │ │ │ │ ├── icon-38@2x.png │ │ │ │ │ ├── icon-38@3x.png │ │ │ │ │ ├── icon-40@2x.png │ │ │ │ │ ├── icon-40@3x.png │ │ │ │ │ ├── icon-60@2x.png │ │ │ │ │ ├── icon-60@3x.png │ │ │ │ │ ├── icon-64@2x.png │ │ │ │ │ ├── icon-64@3x.png │ │ │ │ │ ├── icon-68@2x.png │ │ │ │ │ ├── icon-76@2x.png │ │ │ │ │ ├── icon-83_5@2x.png │ │ │ │ │ ├── ios-marketing.png │ │ │ │ │ └── Contents.json │ │ │ │ └── MemorialHouseAppIcon.imageset │ │ │ │ │ ├── MemorialHouseAppIcon.png │ │ │ │ │ ├── MemorialHouseAppIcon@2x.png │ │ │ │ │ ├── MemorialHouseAppIcon@3x.png │ │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ └── Base.lproj │ │ │ │ └── LaunchScreen.storyboard │ │ └── Source │ │ │ └── App │ │ │ └── AppDelegate.swift │ └── MHApplication.xcodeproj │ │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── MHPresentation │ ├── MHPresentation │ │ ├── Source │ │ │ ├── Audio │ │ │ │ ├── Player │ │ │ │ │ ├── Enum │ │ │ │ │ │ └── AudioPlayState.swift │ │ │ │ │ └── ViewModel │ │ │ │ │ │ └── AudioPlayerViewModel.swift │ │ │ │ └── Audio │ │ │ │ │ └── ViewModel │ │ │ │ │ ├── CreateAudioViewModelFactory.swift │ │ │ │ │ └── CreateAudioViewModel.swift │ │ │ ├── Extensions │ │ │ │ ├── String+localized.swift │ │ │ │ ├── UITableViewCell+Identifier.swift │ │ │ │ ├── Notification+MediaPlayback.swift │ │ │ │ ├── UICollectionViewCell+Identifier.swift │ │ │ │ ├── UISheetPresentationController+Detent+Identifier.swift │ │ │ │ ├── UIView │ │ │ │ │ ├── UIView+SnapshotImage.swift │ │ │ │ │ ├── UIView+CustomView.swift │ │ │ │ │ └── UIView+Background.swift │ │ │ │ ├── Date+convertToString.swift │ │ │ │ ├── Notification+Keyboard.swift │ │ │ │ ├── UIImage │ │ │ │ │ ├── UIImage+Resize.swift │ │ │ │ │ └── UIImage+Rotate.swift │ │ │ │ ├── MediaType+height.swift │ │ │ │ ├── BookColor+Image.swift │ │ │ │ ├── UIViewController │ │ │ │ │ ├── UIViewController+AudioSession.swift │ │ │ │ │ ├── UIViewController+HideKeyboard.swift │ │ │ │ │ ├── UIViewController+ErrorAlert.swift │ │ │ │ │ └── UIViewController+AuthorizationAlert.swift │ │ │ │ ├── UIFont+Ownglyph.swift │ │ │ │ ├── UIBarButtonItem+Create.swift │ │ │ │ ├── UILabel+Style.swift │ │ │ │ └── UIAlertController+Initializer.swift │ │ │ ├── Common │ │ │ │ └── ViewModelType.swift │ │ │ ├── EditBook │ │ │ │ ├── View │ │ │ │ │ ├── MediaAttachable.swift │ │ │ │ │ ├── MediaAttachmentViewProvider.swift │ │ │ │ │ └── MediaAttachment.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── EditBookViewModelFactory.swift │ │ │ ├── Setting │ │ │ │ └── ViewModel │ │ │ │ │ └── SettingViewModel.swift │ │ │ ├── ReadPage │ │ │ │ └── ViewModel │ │ │ │ │ ├── ReadPageViewModelFactory.swift │ │ │ │ │ └── ReadPageViewModel.swift │ │ │ ├── Book │ │ │ │ └── ViewModel │ │ │ │ │ ├── BookViewModelFactory.swift │ │ │ │ │ └── BookViewModel.swift │ │ │ ├── Register │ │ │ │ └── ViewModel │ │ │ │ │ ├── RegisterViewModelFactory.swift │ │ │ │ │ └── RegisterViewModel.swift │ │ │ ├── Onboarding │ │ │ │ └── OnboardingPageViewController.swift │ │ │ ├── BookCover │ │ │ │ └── ViewModel │ │ │ │ │ ├── ModifyBookCoverViewmodelFactory.swift │ │ │ │ │ └── CreateBookCoverViewModelFactory.swift │ │ │ ├── Home │ │ │ │ └── ViewModel │ │ │ │ │ └── HomeViewModelFactory.swift │ │ │ ├── CustomAlbum │ │ │ │ ├── View │ │ │ │ │ └── CustomAlbumCollectionViewCell.swift │ │ │ │ └── ViewModel │ │ │ │ │ ├── LocalPhotoManager.swift │ │ │ │ │ └── CustomAlbumViewModel.swift │ │ │ ├── Category │ │ │ │ ├── View │ │ │ │ │ └── BookCategoryTableViewCell.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── BookCategoryViewModelFactory.swift │ │ │ ├── Design │ │ │ │ ├── MHNavigationBar.swift │ │ │ │ ├── MHRegisterView.swift │ │ │ │ ├── MHVideoView.swift │ │ │ │ └── MHBookCover.swift │ │ │ └── EditVideo │ │ │ │ └── EditVideoViewController.swift │ │ └── Resource │ │ │ ├── Colors.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHBeigeColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHBlueColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHBorderColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHGreenColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHOrangeColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHPinkColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHSectionColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MHTitleColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── BaseBackgroundColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── DividedLineColor.colorset │ │ │ │ └── Contents.json │ │ │ └── CaptionPlaceHolderColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ ├── DrawImage.imageset │ │ │ │ ├── Draw.png │ │ │ │ ├── Draw@2x.png │ │ │ │ ├── Draw@3x.png │ │ │ │ └── Contents.json │ │ │ ├── RotateImage.imageset │ │ │ │ ├── Rotate.png │ │ │ │ ├── Rotate@2x.png │ │ │ │ ├── Rotate@3x.png │ │ │ │ └── Contents.json │ │ │ ├── BookMakeImage.imageset │ │ │ │ ├── Subject.png │ │ │ │ ├── Subject@2x.png │ │ │ │ ├── Subject@3x.png │ │ │ │ └── Contents.json │ │ │ ├── CheckImage.imageset │ │ │ │ ├── Checkmark.png │ │ │ │ ├── Checkmark@2x.png │ │ │ │ ├── Checkmark@3x.png │ │ │ │ └── Contents.json │ │ │ ├── DropDownImage.imageset │ │ │ │ ├── Vector 17.png │ │ │ │ ├── Vector 17@2x.png │ │ │ │ ├── Vector 17@3x.png │ │ │ │ └── Contents.json │ │ │ ├── likeEmptyImage.imageset │ │ │ │ ├── Favorite.png │ │ │ │ ├── Favorite@2x.png │ │ │ │ ├── Favorite@3x.png │ │ │ │ └── Contents.json │ │ │ ├── PhotoImage.imageset │ │ │ │ ├── Camera_duotone.png │ │ │ │ ├── Camera_duotone@2x.png │ │ │ │ ├── Camera_duotone@3x.png │ │ │ │ └── Contents.json │ │ │ ├── RegisterBookImage.imageset │ │ │ │ ├── Group 2.png │ │ │ │ ├── Group 2@2x.png │ │ │ │ ├── Group 2@3x.png │ │ │ │ └── Contents.json │ │ │ ├── BlueBookImage.imageset │ │ │ │ ├── BlueBookImage.png │ │ │ │ ├── BlueBookImage@2x.png │ │ │ │ ├── BlueBookImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── DotHorizontalImage.imageset │ │ │ │ ├── image 19.png │ │ │ │ ├── image 19@2x.png │ │ │ │ ├── image 19@3x.png │ │ │ │ └── Contents.json │ │ │ ├── PinkBookImage.imageset │ │ │ │ ├── PingBookImage.png │ │ │ │ ├── PingBookImage@2x.png │ │ │ │ ├── PingBookImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── BeigeBookImage.imageset │ │ │ │ ├── BeigeBookImage.png │ │ │ │ ├── BeigeBookImage@2x.png │ │ │ │ ├── BeigeBookImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── GreenBookImage.imageset │ │ │ │ ├── GreenBookImage.png │ │ │ │ ├── GreenBookImage@2x.png │ │ │ │ ├── GreenBookImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── LikeFillImage.imageset │ │ │ │ ├── Favorite_duotone.png │ │ │ │ ├── Favorite_duotone@2x.png │ │ │ │ ├── Favorite_duotone@3x.png │ │ │ │ └── Contents.json │ │ │ ├── OnboardingFourImage.imageset │ │ │ │ ├── onboarding4.png │ │ │ │ ├── onboarding4@2x.png │ │ │ │ ├── onboarding4@3x.png │ │ │ │ └── Contents.json │ │ │ ├── OnboardingOneImage.imageset │ │ │ │ ├── onboarding1.png │ │ │ │ ├── onboarding1@2x.png │ │ │ │ ├── onboarding1@3x.png │ │ │ │ └── Contents.json │ │ │ ├── OnboardingTwoImage.imageset │ │ │ │ ├── onboarding2.png │ │ │ │ ├── onboarding2@2x.png │ │ │ │ ├── onboarding2@3x.png │ │ │ │ └── Contents.json │ │ │ ├── OrangeBookImage.imageset │ │ │ │ ├── OrangeBookImage.png │ │ │ │ ├── OrangeBookImage@2x.png │ │ │ │ ├── OrangeBookImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── textButtonImage.imageset │ │ │ │ ├── textButtonImage.png │ │ │ │ ├── textButtonImage@2x.png │ │ │ │ ├── textButtonImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── OnboardingThreeImage.imageset │ │ │ │ ├── onboarding3.png │ │ │ │ ├── onboarding3@2x.png │ │ │ │ ├── onboarding3@3x.png │ │ │ │ └── Contents.json │ │ │ ├── audioButtonImage.imageset │ │ │ │ ├── audioButtonImage.png │ │ │ │ ├── audioButtonImage@2x.png │ │ │ │ ├── audioButtonImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── imageButtonImage.imageset │ │ │ │ ├── imageButtonImage.png │ │ │ │ ├── imageButtonImage@2x.png │ │ │ │ ├── imageButtonImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── videoButtonImage.imageset │ │ │ │ ├── videoButtonImage.png │ │ │ │ ├── videoButtonImage@2x.png │ │ │ │ ├── videoButtonImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── SettingLightImage.imageset │ │ │ │ ├── Setting_line_light.png │ │ │ │ ├── Setting_line_light@2x.png │ │ │ │ ├── Setting_line_light@3x.png │ │ │ │ └── Contents.json │ │ │ └── publishButtonImage.imageset │ │ │ │ ├── publishButtonImage.png │ │ │ │ ├── publishButtonImage@2x.png │ │ │ │ ├── publishButtonImage@3x.png │ │ │ │ └── Contents.json │ │ │ ├── Fonts │ │ │ └── OwnglyphBerry.ttf │ │ │ └── Info.plist │ └── MHPresentationTests │ │ ├── TestDoubles │ │ ├── StubMemorialHouseNameUseCase.swift │ │ └── StubBookCoverUseCase.swift │ │ ├── HomeViewModelTest.swift │ │ └── RegisterViewModelTest.swift ├── MHData │ ├── MHData │ │ ├── LocalStorage │ │ │ ├── MemorialHouseNameStorage.swift │ │ │ ├── BookStorage.swift │ │ │ ├── BookCoverStorage.swift │ │ │ ├── BookCategoryStorage.swift │ │ │ ├── UserDefaults │ │ │ │ └── UserDefaultsMemorialHouseNameStorage.swift │ │ │ ├── CoreData │ │ │ │ ├── CoreDataStorage.swift │ │ │ │ ├── CoreDataBookCategoryStorage.swift │ │ │ │ └── MemorialHouseModel.xcdatamodeld │ │ │ │ │ └── MemorialHouseModel.xcdatamodel │ │ │ │ │ └── contents │ │ │ └── FileStorage.swift │ │ ├── DTO │ │ │ ├── BookCategoryDTO.swift │ │ │ ├── BookDTO.swift │ │ │ ├── PageDTO.swift │ │ │ ├── MediaDescriptionDTO.swift │ │ │ └── BookCoverDTO.swift │ │ └── Repository │ │ │ ├── LocalMemorialHouseNameRepository.swift │ │ │ ├── LocalBookCategoryRepository.swift │ │ │ ├── LocalBookCoverRepository.swift │ │ │ └── LocalBookRepository.swift │ └── MHDataTests │ │ ├── TestDoubles │ │ └── MockCoreDataStorage.swift │ │ └── UserDefaultsMemorialHouseNameStorageTests.swift ├── MHDomain │ ├── MHDomain │ │ ├── Repository │ │ │ ├── MemorialHouseNameRepository.swift │ │ │ ├── BookRepository.swift │ │ │ ├── BookCategoryRepository.swift │ │ │ ├── BookCoverRepository.swift │ │ │ └── MediaRepository.swift │ │ ├── UseCase │ │ │ ├── Interface │ │ │ │ ├── MemorialHouseNameUseCase.swift │ │ │ │ ├── BookUseCase.swift │ │ │ │ ├── BookCategoryUseCase.swift │ │ │ │ ├── BookCoverUseCase.swift │ │ │ │ └── MediaUseCase.swift │ │ │ ├── DefaultMemorialHouseNameUseCase.swift │ │ │ ├── DefaultBookCategoryUseCase.swift │ │ │ ├── DefaultBookUseCase.swift │ │ │ └── DefaultBookCoverUseCase.swift │ │ └── Entity │ │ │ ├── BookCategory.swift │ │ │ ├── Book.swift │ │ │ ├── Page.swift │ │ │ ├── MediaDescription.swift │ │ │ ├── MediaType.swift │ │ │ ├── BookColor.swift │ │ │ └── BookCover.swift │ └── MHDomainTests │ │ ├── Stubs │ │ └── StubMemorialHouseRepository.swift │ │ └── MemorialHouseNameUseCaseTest.swift ├── MHCore │ └── MHCore │ │ ├── Constant.swift │ │ ├── MHCoreError.swift │ │ ├── DIContainer.swift │ │ ├── MHLogger.swift │ │ └── MHDataError.swift ├── MemorialHouse.xcworkspace │ ├── xcshareddata │ │ └── swiftpm │ │ │ └── Package.resolved │ └── contents.xcworkspacedata └── .swiftlint.yml ├── .github ├── ISSUE_TEMPLATE │ ├── issue-template.md │ └── mh-issue-template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── swiftlint.yml │ └── swift.yml └── .gitignore /MemorialHouse/MHApplication/MHApplication/Resource/ko.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | ## 📌 Issue Description 2 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Player/Enum/AudioPlayState.swift: -------------------------------------------------------------------------------- 1 | enum AudioPlayState { 2 | case play 3 | case pause 4 | } 5 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Fonts/OwnglyphBerry.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Fonts/OwnglyphBerry.ttf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mh-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: MH Issue Template 3 | about: 집주인들 이슈 템플릿 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 📌 Issue Description 11 | 12 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/String+localized.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | public func localized() -> String { 5 | NSLocalizedString(self, comment: "") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UITableViewCell+Identifier.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableViewCell { 4 | static var identifier: String { 5 | String(describing: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/Notification+MediaPlayback.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Notification.Name { 4 | static let mediaPlaybackStarted = Notification.Name("mediaPlaybackStarted") 5 | } 6 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UICollectionViewCell+Identifier.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UICollectionViewCell { 4 | static var identifier: String { 5 | String(describing: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Draw.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Draw@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Draw@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Draw@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Draw@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Rotate.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-83_5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/icon-83_5@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/ios-marketing.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Subject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Subject.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Checkmark.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Rotate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Rotate@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Rotate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Rotate@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/MemorialHouseNameStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol MemorialHouseNameStorage: Sendable { 5 | func create(with memorialHouseName: String) async 6 | func fetch() async throws -> String 7 | } 8 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Checkmark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Checkmark@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Checkmark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Checkmark@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Vector 17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Vector 17.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Favorite.png -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Repository/MemorialHouseNameRepository.swift: -------------------------------------------------------------------------------- 1 | import MHCore 2 | 3 | public protocol MemorialHouseNameRepository: Sendable { 4 | func createMemorialHouseName(with name: String) async 5 | func fetchMemorialHouseName() async throws -> String 6 | } 7 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Subject@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Subject@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Subject@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Subject@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Vector 17@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Vector 17@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Vector 17@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Vector 17@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Camera_duotone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Camera_duotone.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Group 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Group 2.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Favorite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Favorite@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Favorite@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Favorite@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/BlueBookImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/BlueBookImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/image 19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/image 19.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Camera_duotone@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Camera_duotone@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Camera_duotone@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Camera_duotone@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/PingBookImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/PingBookImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Group 2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Group 2@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Group 2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Group 2@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/BeigeBookImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/BeigeBookImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/BlueBookImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/BlueBookImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/BlueBookImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/BlueBookImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/image 19@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/image 19@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/image 19@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/image 19@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/GreenBookImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/GreenBookImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Favorite_duotone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Favorite_duotone.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/onboarding4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/onboarding4.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/onboarding1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/onboarding1.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/onboarding2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/onboarding2.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/OrangeBookImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/OrangeBookImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/PingBookImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/PingBookImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/PingBookImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/PingBookImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/textButtonImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/textButtonImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/BeigeBookImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/BeigeBookImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/BeigeBookImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/BeigeBookImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/GreenBookImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/GreenBookImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/GreenBookImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/GreenBookImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Favorite_duotone@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Favorite_duotone@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Favorite_duotone@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Favorite_duotone@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/onboarding1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/onboarding1@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/onboarding1@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/onboarding1@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/onboarding3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/onboarding3.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/onboarding2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/onboarding2@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/onboarding2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/onboarding2@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/audioButtonImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/audioButtonImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/imageButtonImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/imageButtonImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/videoButtonImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/videoButtonImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/onboarding4@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/onboarding4@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/onboarding4@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/onboarding4@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/onboarding3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/onboarding3@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/onboarding3@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/onboarding3@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/OrangeBookImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/OrangeBookImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/OrangeBookImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/OrangeBookImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Setting_line_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Setting_line_light.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/audioButtonImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/audioButtonImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/audioButtonImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/audioButtonImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/imageButtonImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/imageButtonImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/imageButtonImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/imageButtonImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/textButtonImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/textButtonImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/textButtonImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/textButtonImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/videoButtonImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/videoButtonImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/videoButtonImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/videoButtonImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UISheetPresentationController+Detent+Identifier.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UISheetPresentationController.Detent.Identifier { 4 | static let categorySheet = UISheetPresentationController.Detent.Identifier("categorySheet") 5 | } 6 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/publishButtonImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/publishButtonImage.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/MemorialHouseAppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/MemorialHouseAppIcon.png -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MemorialHouseNameUseCase.swift: -------------------------------------------------------------------------------- 1 | public protocol CreateMemorialHouseNameUseCase: Sendable { 2 | func execute(with name: String) async 3 | } 4 | 5 | public protocol FetchMemorialHouseNameUseCase: Sendable { 6 | func execute() async throws -> String 7 | } 8 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Setting_line_light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Setting_line_light@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Setting_line_light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Setting_line_light@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/publishButtonImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/publishButtonImage@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/publishButtonImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/publishButtonImage@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/MemorialHouseAppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/MemorialHouseAppIcon@2x.png -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/MemorialHouseAppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS10-MemorialHouse/HEAD/MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/MemorialHouseAppIcon@3x.png -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Common/ViewModelType.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | protocol ViewModelType { 4 | associatedtype Input 5 | associatedtype Output 6 | 7 | @MainActor 8 | func transform(input: AnyPublisher) -> AnyPublisher 9 | } 10 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/BookCategory.swift: -------------------------------------------------------------------------------- 1 | public struct BookCategory: Sendable { 2 | public let order: Int 3 | public let name: String 4 | 5 | public init( 6 | order: Int, 7 | name: String 8 | ) { 9 | self.order = order 10 | self.name = name 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | @MainActor 5 | protocol MediaAttachable { 6 | func configureSource(with mediaDescription: MediaDescription, data: Data) 7 | func configureSource(with mediaDescription: MediaDescription, url: URL) 8 | } 9 | -------------------------------------------------------------------------------- /MemorialHouse/MHCore/MHCore/Constant.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Constant { 4 | public static let houseNameUserDefaultKey = "houseName" 5 | public static let navigationBarHeight: CGFloat = 60 6 | public static let photoCaption: String = "photoCaption" 7 | public static let photoCreationDate: String = "photoCreationDate" 8 | } 9 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/BookStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol BookStorage: Sendable { 5 | func create(data: BookDTO) async throws 6 | func fetch(with id: UUID) async throws -> BookDTO 7 | func update(with id: UUID, data: BookDTO) async throws 8 | func delete(with id: UUID) async throws 9 | } 10 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIAppFonts 6 | 7 | OwnglyphBerry.ttf 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Repository/BookRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol BookRepository: Sendable { 5 | func create(book: Book) async throws 6 | func fetch(bookID id: UUID) async throws -> Book 7 | func update(bookID id: UUID, to book: Book) async throws 8 | func delete(bookID id: UUID) async throws 9 | } 10 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+SnapshotImage.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | func snapshotImage() -> UIImage? { 5 | let renderer = UIGraphicsImageRenderer(bounds: bounds) 6 | return renderer.image { context in 7 | layer.render(in: context.cgContext) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/BookCoverStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol BookCoverStorage: Sendable { 5 | func create(data: BookCoverDTO) async throws 6 | func fetch() async throws -> [BookCoverDTO] 7 | func update(with id: UUID, data: BookCoverDTO) async throws 8 | func delete(with id: UUID) async throws 9 | } 10 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/Date+convertToString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Date { 4 | /// Date 타입을 yyyy.MM.dd 형태의 String으로 변환하여 반환합니다. 5 | func toString() -> String { 6 | let formatter = DateFormatter() 7 | formatter.dateFormat = "yyyy.MM.dd" 8 | 9 | return formatter.string(from: self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/Notification+Keyboard.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | @objc 5 | func keyboardWillShow(_ sender: Notification) { 6 | self.view.frame.origin.y = -120 7 | } 8 | 9 | @objc 10 | func keyboardWillHide(_ sender: Notification) { 11 | self.view.frame.origin.y = 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MemorialHouse/MHCore/MHCore/MHCoreError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum MHCoreError: Error, CustomStringConvertible, Equatable { 4 | case DIContainerResolveFailure(key: String) 5 | 6 | public var description: String { 7 | switch self { 8 | case .DIContainerResolveFailure(let key): 9 | "\(key)에 대한 dependency resolve 실패" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage/UIImage+Resize.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIImage { 4 | static func resizedImage(image: UIImage, size: CGSize) -> UIImage { 5 | let renderer = UIGraphicsImageRenderer(size: size) 6 | 7 | return renderer.image { _ in 8 | image.draw(in: CGRect(origin: .zero, size: size)) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/BookCategoryStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol BookCategoryStorage: Sendable { 5 | func create(with category: BookCategoryDTO) async throws 6 | func fetch() async throws -> [BookCategoryDTO] 7 | func update(oldName: String, with category: BookCategoryDTO) async throws 8 | func delete(with categoryName: String) async throws 9 | } 10 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/MediaType+height.swift: -------------------------------------------------------------------------------- 1 | import MHDomain 2 | 3 | extension MediaType { 4 | var height: Double { 5 | switch self { 6 | case .image: 7 | return 300 8 | case .video: 9 | return 400 10 | case .audio: 11 | return 100 12 | default: 13 | return 100 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/Book.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Book: Identifiable, Sendable { 4 | public let id: UUID 5 | public let title: String 6 | public let pages: [Page] 7 | 8 | public init( 9 | id: UUID, 10 | title: String, 11 | pages: [Page] 12 | ) { 13 | self.id = id 14 | self.title = title 15 | self.pages = pages 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Repository/BookCategoryRepository.swift: -------------------------------------------------------------------------------- 1 | import MHCore 2 | 3 | public protocol BookCategoryRepository: Sendable { 4 | func createBookCategory(with category: BookCategory) async throws 5 | func fetchBookCategories() async throws -> [BookCategory] 6 | func updateBookCategory(oldName: String, with category: BookCategory) async throws 7 | func deleteBookCategory(with categoryName: String) async throws 8 | } 9 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/BookColor+Image.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MHDomain 3 | 4 | extension BookColor { 5 | var image: UIImage { 6 | switch self { 7 | case .blue: .blueBook 8 | case .beige: .beigeBook 9 | case .green: .greenBook 10 | case .orange: .orangeBook 11 | case .pink: .pinkBook 12 | default: .blueBook 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## #️⃣ 연관된 이슈 2 | 3 |
4 | 5 | ## ⏰ 작업 시간 6 | 7 | 8 | |예상 시간|실제 걸린 시간| 9 | |:-:|:-:| 10 | ||| 11 | 12 | ## 📝 작업 내용 13 | 14 | 15 |
16 | 17 | ## 📸 스크린샷 18 | 19 | 20 |
21 | 22 | ## 📒 리뷰 노트 23 | 24 | 25 |
26 | 27 | ## ⚽️ 트러블 슈팅 28 | 29 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Repository/BookCoverRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol BookCoverRepository: Sendable { 5 | func createBookCover(with bookCover: BookCover) async throws 6 | func fetchAllBookCovers() async throws -> [BookCover] 7 | func fetchBookCover(with id: UUID) async throws -> BookCover? 8 | func updateBookCover(id: UUID, with bookCover: BookCover) async throws 9 | func deleteBookCover(id: UUID) async throws 10 | } 11 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/Page.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Page: Identifiable, Sendable { 4 | public let id: UUID 5 | public let metadata: [Int: MediaDescription] 6 | public let text: String 7 | 8 | public init( 9 | id: UUID = .init(), 10 | metadata: [Int: MediaDescription] = [:], 11 | text: String = "" 12 | ) { 13 | self.id = id 14 | self.metadata = metadata 15 | self.text = text 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xDE", 9 | "green" : "0xDF", 10 | "red" : "0xFA" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHBeigeColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC5", 9 | "green" : "0xDD", 10 | "red" : "0xE5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHBlueColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xB2", 10 | "red" : "0x94" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHBorderColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x26", 10 | "red" : "0xB3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHGreenColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x8D", 9 | "green" : "0xB6", 10 | "red" : "0xA5" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHOrangeColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x99", 9 | "green" : "0xC7", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHPinkColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xDB", 9 | "green" : "0xDB", 10 | "red" : "0xF0" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHSectionColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF8", 9 | "green" : "0xFC", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/MHTitleColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x47", 9 | "green" : "0x47", 10 | "red" : "0x47" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/BaseBackgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF3", 9 | "green" : "0xFC", 10 | "red" : "0xFE" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/DividedLineColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.169", 9 | "green" : "0.169", 10 | "red" : "0.169" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/DTO/BookCategoryDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct BookCategoryDTO { 5 | let order: Int 6 | let name: String 7 | 8 | public init( 9 | order: Int, 10 | name: String 11 | ) { 12 | self.order = order 13 | self.name = name 14 | } 15 | 16 | func convertToBookCategory() -> BookCategory { 17 | BookCategory( 18 | order: order, 19 | name: name 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol CreateBookUseCase: Sendable { 4 | func execute(book: Book) async throws 5 | } 6 | 7 | public protocol FetchBookUseCase: Sendable { 8 | func execute(id: UUID) async throws -> Book 9 | } 10 | 11 | public protocol UpdateBookUseCase: Sendable { 12 | func execute(id: UUID, book: Book) async throws 13 | } 14 | 15 | public protocol DeleteBookUseCase: Sendable { 16 | func execute(id: UUID) async throws 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Colors.xcassets/CaptionPlaceHolderColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.529", 9 | "green" : "0.529", 10 | "red" : "0.529" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/MediaDescription.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MediaDescription: Identifiable, Sendable { 4 | public let id: UUID 5 | public let type: MediaType 6 | public let attributes: [String: any Sendable]? 7 | 8 | public init( 9 | id: UUID = .init(), 10 | type: MediaType, 11 | attributes: [String: any Sendable]? = nil 12 | ) { 13 | self.id = id 14 | self.type = type 15 | self.attributes = attributes 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift: -------------------------------------------------------------------------------- 1 | public enum MediaType: String, CaseIterable, Sendable { 2 | case image 3 | case video 4 | case audio 5 | 6 | /// 기본 파일 확장자를 반환합니다. 7 | /// 사진은 .png, 비디오는 .mp4, 오디오는 .m4a를 반환합니다. 8 | public var defaultFileExtension: String { 9 | switch self { 10 | case .image: 11 | return ".png" 12 | case .video: 13 | return ".mp4" 14 | case .audio: 15 | return ".m4a" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Setting/ViewModel/SettingViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SettingViewModel { 4 | let tableViewDataSource = [ 5 | "서비스 이용 약관", 6 | "개인정보 처리 방침", 7 | "불편신고 및 개선요청", 8 | "자주 묻는 질문 (FAQ)", 9 | "앱 버전" 10 | ] 11 | 12 | var appVersion: String? { 13 | guard let dictionary = Bundle.main.infoDictionary else { return nil} 14 | return dictionary["CFBundleShortVersionString"] as? String 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MemorialHouse/MemorialHouse.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "1fa961aa1dc717cea452f3389668f0f99a254f07e4eb11d190768a53798f744f", 3 | "pins" : [ 4 | { 5 | "identity" : "swiftlintplugins", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", 8 | "state" : { 9 | "revision" : "7c80ce6f142164b0201871e580b021d1b2c69804", 10 | "version" : "0.57.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DrawImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Draw.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Draw@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Draw@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AudioSession.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | extension UIViewController { 4 | func configureAudioSessionForPlayback() { 5 | do { 6 | let audioSession = AVAudioSession.sharedInstance() 7 | try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) 8 | try audioSession.setActive(true) 9 | } catch { 10 | print("Failed to configure audio session: \(error)") 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RotateImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Rotate.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Rotate@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Rotate@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+HideKeyboard.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | func hideKeyboardWhenTappedView() { 5 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(viewTapped(_:))) 6 | tapGestureRecognizer.cancelsTouchesInView = false 7 | view.addGestureRecognizer(tapGestureRecognizer) 8 | } 9 | 10 | @objc func viewTapped(_ sender: UITapGestureRecognizer) { 11 | view.endEditing(true) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BookMakeImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Subject.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Subject@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Subject@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/CheckImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Checkmark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Checkmark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Checkmark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DotHorizontalImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image 19.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "image 19@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "image 19@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/DropDownImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Vector 17.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Vector 17@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Vector 17@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/RegisterBookImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Group 2@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Group 2@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/likeEmptyImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Favorite.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Favorite@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Favorite@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BlueBookImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BlueBookImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "BlueBookImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "BlueBookImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingFourImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "onboarding4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "onboarding4@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "onboarding4@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingOneImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "onboarding1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "onboarding1@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "onboarding1@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingTwoImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "onboarding2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "onboarding2@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "onboarding2@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PhotoImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Camera_duotone.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Camera_duotone@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Camera_duotone@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/PinkBookImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "PingBookImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "PingBookImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "PingBookImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+ErrorAlert.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | func showErrorAlert(with errorMessage: String) { 5 | let alertController = UIAlertController( 6 | title: "에러", 7 | message: errorMessage, 8 | preferredStyle: .alert 9 | ) 10 | let okAction = UIAlertAction(title: "확인", style: .default) 11 | alertController.addAction(okAction) 12 | 13 | present(alertController, animated: true) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookCategoryUseCase.swift: -------------------------------------------------------------------------------- 1 | public protocol CreateBookCategoryUseCase: Sendable { 2 | func execute(with category: BookCategory) async throws 3 | } 4 | 5 | public protocol FetchBookCategoriesUseCase: Sendable { 6 | func execute() async throws -> [BookCategory] 7 | } 8 | 9 | public protocol UpdateBookCategoryUseCase: Sendable { 10 | func execute(oldName: String, with category: BookCategory) async throws 11 | } 12 | 13 | public protocol DeleteBookCategoryUseCase: Sendable { 14 | func execute(with categoryName: String) async throws 15 | } 16 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/BeigeBookImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BeigeBookImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "BeigeBookImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "BeigeBookImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/GreenBookImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "GreenBookImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "GreenBookImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "GreenBookImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OnboardingThreeImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "onboarding3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "onboarding3@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "onboarding3@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/LikeFillImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Favorite_duotone.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Favorite_duotone@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Favorite_duotone@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/OrangeBookImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "OrangeBookImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "OrangeBookImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "OrangeBookImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/textButtonImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "textButtonImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "textButtonImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "textButtonImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/audioButtonImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "audioButtonImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "audioButtonImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "audioButtonImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/imageButtonImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "imageButtonImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "imageButtonImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "imageButtonImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/videoButtonImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "videoButtonImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "videoButtonImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "videoButtonImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/SettingLightImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Setting_line_light.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Setting_line_light@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Setting_line_light@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Resource/Images.xcassets/publishButtonImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "publishButtonImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "publishButtonImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "publishButtonImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/MemorialHouseAppIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MemorialHouseAppIcon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "MemorialHouseAppIcon@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "MemorialHouseAppIcon@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentationTests/TestDoubles/StubMemorialHouseNameUseCase.swift: -------------------------------------------------------------------------------- 1 | import MHDomain 2 | 3 | struct StubCreateMemorialHouseNameUseCase: CreateMemorialHouseNameUseCase { 4 | func execute(with name: String) async throws { } 5 | } 6 | 7 | struct StubFetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase { 8 | private var dummyMemorialHouseName: String 9 | 10 | init(dummyMemorialHouseName: String) { 11 | self.dummyMemorialHouseName = dummyMemorialHouseName 12 | } 13 | 14 | func execute() async -> String { 15 | return dummyMemorialHouseName 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/Repository/LocalMemorialHouseNameRepository.swift: -------------------------------------------------------------------------------- 1 | import MHCore 2 | import MHDomain 3 | 4 | public struct LocalMemorialHouseNameRepository: MemorialHouseNameRepository { 5 | private let storage: MemorialHouseNameStorage 6 | 7 | public init(storage: MemorialHouseNameStorage) { 8 | self.storage = storage 9 | } 10 | 11 | public func createMemorialHouseName(with name: String) async { 12 | await storage.create(with: name) 13 | } 14 | 15 | public func fetchMemorialHouseName() async throws -> String { 16 | return try await storage.fetch() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MemorialHouse/MemorialHouse.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/DTO/BookDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct BookDTO { 5 | let id: UUID 6 | let title: String 7 | let pages: [PageDTO] 8 | 9 | public init( 10 | id: UUID, 11 | title: String, 12 | pages: [PageDTO] 13 | ) { 14 | self.id = id 15 | self.title = title 16 | self.pages = pages 17 | } 18 | 19 | func convertToBook() -> Book { 20 | return Book( 21 | id: self.id, 22 | title: self.title, 23 | pages: self.pages.map { $0.convertToPage() } 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/ViewModel/ReadPageViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct ReadPageViewModelFactory: Sendable { 5 | private let fetchMediaUseCase: FetchMediaUseCase 6 | 7 | public init(fetchMediaUseCase: FetchMediaUseCase) { 8 | self.fetchMediaUseCase = fetchMediaUseCase 9 | } 10 | 11 | public func make(bookID: UUID, page: Page) -> ReadPageViewModel { 12 | ReadPageViewModel( 13 | fetchMediaUseCase: fetchMediaUseCase, 14 | bookID: bookID, 15 | page: page 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Book/ViewModel/BookViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct BookViewModelFactory: Sendable { 5 | private let fetchBookUseCase: FetchBookUseCase 6 | 7 | public init( 8 | fetchBookUseCase: FetchBookUseCase 9 | ) { 10 | self.fetchBookUseCase = fetchBookUseCase 11 | } 12 | 13 | public func make(bookID: UUID, bookTitle: String) -> BookViewModel { 14 | BookViewModel( 15 | fetchBookUseCase: fetchBookUseCase, 16 | identifier: bookID, 17 | bookTitle: bookTitle 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomainTests/Stubs/StubMemorialHouseRepository.swift: -------------------------------------------------------------------------------- 1 | @testable import MHCore 2 | @testable import MHDomain 3 | 4 | struct StubMemorialHouseRepository: MemorialHouseNameRepository { 5 | private let dummyMemorialHouseName: String 6 | 7 | init(dummyMemorialHouseName: String) { 8 | self.dummyMemorialHouseName = dummyMemorialHouseName 9 | } 10 | 11 | func createMemorialHouseName(with name: String) async -> Result { 12 | .success(()) 13 | } 14 | 15 | func fetchMemorialHouseName() async -> Result { 16 | .success(dummyMemorialHouseName) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Register/ViewModel/RegisterViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import MHDomain 2 | 3 | public struct RegisterViewModelFactory: Sendable { 4 | private let createMemorialHouseNameUseCase: CreateMemorialHouseNameUseCase 5 | 6 | public init( 7 | createMemorialHouseNameUseCase: CreateMemorialHouseNameUseCase 8 | ) { 9 | self.createMemorialHouseNameUseCase = createMemorialHouseNameUseCase 10 | } 11 | 12 | public func make() -> RegisterViewModel { 13 | return RegisterViewModel( 14 | createMemorialHouseNameUseCase: createMemorialHouseNameUseCase 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct CreateAudioViewModelFactory: Sendable { 5 | private let temporaryStoreMediaUseCase: TemporaryStoreMediaUseCase 6 | 7 | public init(temporaryStoreMediaUseCase: TemporaryStoreMediaUseCase) { 8 | self.temporaryStoreMediaUseCase = temporaryStoreMediaUseCase 9 | } 10 | 11 | public func make(completion: @escaping (MediaDescription?) -> Void) -> CreateAudioViewModel { 12 | CreateAudioViewModel(temporaryStoreMediaUsecase: temporaryStoreMediaUseCase, completion: completion) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookCoverUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol CreateBookCoverUseCase: Sendable { 4 | func execute(with bookCover: BookCover) async throws 5 | } 6 | 7 | public protocol FetchBookCoverUseCase: Sendable { 8 | func execute(id: UUID) async throws -> BookCover? 9 | } 10 | 11 | public protocol FetchAllBookCoverUseCase: Sendable { 12 | func execute() async throws -> [BookCover] 13 | } 14 | 15 | public protocol UpdateBookCoverUseCase: Sendable { 16 | func execute(id: UUID, with bookCover: BookCover) async throws 17 | } 18 | 19 | public protocol DeleteBookCoverUseCase: Sendable { 20 | func execute(id: UUID) async throws 21 | } 22 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+CustomView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | static func dividedLine() -> UIView { 5 | let dividedLineView = UIView() 6 | dividedLineView.backgroundColor = .dividedLine 7 | dividedLineView.setHeight(1) 8 | 9 | return dividedLineView 10 | } 11 | 12 | static func dimmedView(opacity: Float, color: UIColor = .white) -> UIView { 13 | let dimmedView = UIView() 14 | dimmedView.backgroundColor = color 15 | dimmedView.layer.opacity = opacity 16 | dimmedView.isUserInteractionEnabled = false 17 | 18 | return dimmedView 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHCore/MHCore/DIContainer.swift: -------------------------------------------------------------------------------- 1 | public actor DIContainer { 2 | public static let shared = DIContainer() 3 | private var objects: [String: Any] = [:] 4 | 5 | private init() {} 6 | 7 | public func register(_ type: T.Type, object: Any) { 8 | let key = String(describing: type) 9 | objects[key] = object 10 | } 11 | 12 | public func resolve(_ type: T.Type) throws -> T { 13 | let key = String(describing: type) 14 | guard let object = objects[key] as? T else { 15 | MHLogger.error("\(#function): \(key)에 해당하는 object 없음") 16 | throw MHCoreError.DIContainerResolveFailure(key: key) 17 | } 18 | return object 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/BookColor.swift: -------------------------------------------------------------------------------- 1 | public enum BookColor: String, Sendable { 2 | case pink 3 | case green 4 | case blue 5 | case orange 6 | case beige 7 | 8 | public var index: Int { 9 | switch self { 10 | case .pink: 11 | return 0 12 | case .green: 13 | return 1 14 | case .blue: 15 | return 2 16 | case .orange: 17 | return 3 18 | case .beige: 19 | return 4 20 | } 21 | } 22 | 23 | public static func indexToColor(index: Int) -> BookColor { 24 | let colors: [BookColor] = [.pink, .green, .blue, .orange, .beige] 25 | return colors[index] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachmentViewProvider.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MHDomain 3 | 4 | final class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { 5 | // MARK: - Property 6 | var type: MediaType? 7 | private var height: CGFloat { 8 | type?.height ?? 100 9 | } 10 | 11 | override func attachmentBounds( 12 | for attributes: [NSAttributedString.Key: Any], 13 | location: NSTextLocation, 14 | textContainer: NSTextContainer?, 15 | proposedLineFragment: CGRect, 16 | position: CGPoint 17 | ) -> CGRect { 18 | return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: height) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/DTO/PageDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct PageDTO { 5 | let id: UUID 6 | let metadata: [Int: MediaDescriptionDTO] 7 | let text: String 8 | 9 | public init( 10 | id: UUID, 11 | metadata: [Int: MediaDescriptionDTO], 12 | text: String 13 | ) { 14 | self.id = id 15 | self.metadata = metadata 16 | self.text = text 17 | } 18 | 19 | func convertToPage() -> Page { 20 | let metadata = self.metadata 21 | .compactMapValues { $0.convertToMediaDescription() } 22 | 23 | return Page( 24 | id: self.id, 25 | metadata: metadata, 26 | text: self.text 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Source/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MHPresentation 3 | 4 | @main 5 | final class AppDelegate: UIResponder, UIApplicationDelegate { 6 | func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | UIFont.registerFont() 11 | return true 12 | } 13 | 14 | func application( 15 | _ application: UIApplication, 16 | configurationForConnecting connectingSceneSession: UISceneSession, 17 | options: UIScene.ConnectionOptions 18 | ) -> UISceneConfiguration { 19 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: swiftlint 🔨 2 | 3 | on: 4 | pull_request: 5 | branches: ["develop"] 6 | paths: 7 | - ".github/workflows/swiftlint.yml" 8 | - ".swiftlint.yml" 9 | - "**/*.swift" 10 | 11 | jobs: 12 | SwiftLint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: GitHub Action for SwiftLint 17 | uses: norio-nomura/action-swiftlint@3.2.1 18 | - name: GitHub Action for SwiftLint (Only files changed in the PR) 19 | uses: norio-nomura/action-swiftlint@3.2.1 20 | env: 21 | DIFF_BASE: ${{ github.base_ref }} 22 | - name: GitHub Action for SwiftLint (Different working directory) 23 | uses: norio-nomura/action-swiftlint@3.2.1 24 | env: 25 | WORKING_DIRECTORY: Source 26 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | UIApplicationSceneManifest 8 | 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | UIWindowSceneSessionRoleApplication 14 | 15 | 16 | UISceneConfigurationName 17 | Default Configuration 18 | UISceneDelegateClassName 19 | $(PRODUCT_MODULE_NAME).SceneDelegate 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIFont+Ownglyph.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | static func ownglyphBerry(size: CGFloat) -> UIFont { 5 | return UIFont(name: "Ownglyph_BERRY_RW-Rg", size: size) ?? UIFont.systemFont(ofSize: size) 6 | } 7 | 8 | public static func registerFont() { 9 | guard let url = Bundle(for: HomeViewController.self) 10 | .url(forResource: "OwnglyphBerry", withExtension: "ttf") else { 11 | // TODO: Logger로 처리 12 | print("폰트 파일을 찾을 수 없습니다.") 13 | return 14 | } 15 | 16 | var error: Unmanaged? 17 | if !CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) { 18 | // TODO: Logger로 처리 19 | print("폰트 등록 실패: \(error?.takeRetainedValue().localizedDescription ?? "알 수 없는 오류")") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/DTO/MediaDescriptionDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct MediaDescriptionDTO { 5 | let id: UUID 6 | let type: String 7 | let attributes: Data? 8 | 9 | public init( 10 | id: UUID, 11 | type: String, 12 | attributes: Data? 13 | ) { 14 | self.id = id 15 | self.type = type 16 | self.attributes = attributes 17 | } 18 | 19 | func convertToMediaDescription() -> MediaDescription? { 20 | guard let type = MediaType(rawValue: self.type) else { return nil } 21 | let attributes = try? JSONSerialization.jsonObject( 22 | with: attributes ?? Data(), 23 | options: [] 24 | ) as? [String: any Sendable] 25 | 26 | return MediaDescription( 27 | id: self.id, 28 | type: type, 29 | attributes: attributes 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Entity/BookCover.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct BookCover: Identifiable, Equatable, Sendable { 4 | public let id: UUID 5 | public let order: Int 6 | public let title: String 7 | public let imageData: Data? 8 | public let color: BookColor 9 | public let category: String? 10 | public let favorite: Bool 11 | 12 | public init( 13 | id: UUID = .init(), 14 | order: Int, 15 | title: String, 16 | imageData: Data?, 17 | color: BookColor, 18 | category: String?, 19 | favorite: Bool = false 20 | ) { 21 | self.id = id 22 | self.order = order 23 | self.title = title 24 | self.imageData = imageData 25 | self.color = color 26 | self.category = category 27 | self.favorite = favorite 28 | } 29 | 30 | public static func == (lhs: BookCover, rhs: BookCover) -> Bool { 31 | lhs.id == rhs.id 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/UserDefaults/UserDefaultsMemorialHouseNameStorage.swift: -------------------------------------------------------------------------------- 1 | import MHCore 2 | import Foundation 3 | 4 | public struct UserDefaultsMemorialHouseNameStorage: MemorialHouseNameStorage { 5 | private nonisolated(unsafe) let userDefaults: UserDefaults 6 | 7 | public init(userDefaults: UserDefaults = .standard) { 8 | self.userDefaults = userDefaults 9 | } 10 | 11 | public func create(with memorialHouseName: String) async { 12 | userDefaults.set(memorialHouseName, forKey: Constant.houseNameUserDefaultKey) 13 | } 14 | 15 | public func fetch() async throws -> String { 16 | guard let memorialHouseName = userDefaults.string(forKey: Constant.houseNameUserDefaultKey) else { 17 | MHLogger.error("MemorialHouseName을 찾을 수 없습니다: \(Constant.houseNameUserDefaultKey)") 18 | throw MHDataError.noSuchEntity(key: Constant.houseNameUserDefaultKey) 19 | } 20 | return memorialHouseName 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MemorialHouse/MHCore/MHCore/MHLogger.swift: -------------------------------------------------------------------------------- 1 | import OSLog 2 | 3 | public enum MHLogger { 4 | public static func debug(_ object: T?) { 5 | #if DEBUG 6 | let message = object != nil ? "[Debug] \(object!)" : "[Debug] nil" 7 | os_log(.debug, "%@", message) 8 | #endif 9 | } 10 | 11 | public static func info(_ object: T?) { 12 | #if DEBUG 13 | let message = object != nil ? "[Info] \(object!)" : "[Info] nil" 14 | os_log(.info, "%@", message) 15 | #endif 16 | } 17 | 18 | public static func error(_ object: T?) { 19 | #if DEBUG 20 | let message = object != nil ? "[Error] \(object!)" : "[Error] nil" 21 | os_log(.error, "%@", message) 22 | #endif 23 | } 24 | 25 | public static func network(_ object: T?) { 26 | #if DEBUG 27 | let message = object != nil ? "[Network] \(object!)" : "[Network] nil" 28 | os_log(.debug, "%@", message) 29 | #endif 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Onboarding/OnboardingPageViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class OnboardingPageViewController: UIViewController { 4 | private let imageView = UIImageView() 5 | 6 | // MARK: - Initializer 7 | init(image: UIImage) { 8 | super.init(nibName: nil, bundle: nil) 9 | 10 | imageView.image = image 11 | imageView.contentMode = .scaleAspectFit 12 | } 13 | 14 | required init?(coder: NSCoder) { 15 | super.init(coder: coder) 16 | 17 | imageView.image = .onboardingOne 18 | imageView.contentMode = .scaleAspectFit 19 | } 20 | 21 | // MARK: - View Life Cycle 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | configureAddSubview() 26 | } 27 | 28 | // MARK: - Setup & Configuration 29 | private func configureAddSubview() { 30 | view.addSubview(imageView) 31 | imageView.fillSuperview() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIBarButtonItem+Create.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIBarButtonItem { 4 | /// UIBarButtonItem 편의 생성자 5 | /// - Parameters: 6 | /// - title: 버튼 제목 7 | /// - normal: 기본 상태의 속성 (Optional, 기본값 제공 가능) 8 | /// - selected: 선택 상태의 속성 (Optional, 기본값 제공 가능) 9 | /// - action: 버튼 클릭 시 실행할 클로저 10 | convenience init( 11 | title: String, 12 | normal: [NSAttributedString.Key: Any]? = nil, 13 | selected: [NSAttributedString.Key: Any]? = nil, 14 | action: @escaping () -> Void 15 | ) { 16 | let uiAction = UIAction { _ in action() } 17 | self.init(title: title, primaryAction: uiAction) 18 | 19 | if let normalAttributes = normal { 20 | setTitleTextAttributes(normalAttributes, for: .normal) 21 | } 22 | 23 | if let selectedAttributes = selected { 24 | setTitleTextAttributes(selectedAttributes, for: .selected) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UILabel+Style.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UILabel { 4 | enum Style { 5 | case header1 6 | case header2 7 | case header3 8 | case body1 9 | case body2 10 | case body3 11 | } 12 | 13 | convenience init(style: Style) { 14 | self.init(frame: .zero) 15 | self.textColor = .mhTitle 16 | self.textAlignment = .center 17 | switch style { 18 | case .header1: 19 | self.font = UIFont.ownglyphBerry(size: 30) 20 | case .header2: 21 | self.font = UIFont.ownglyphBerry(size: 25) 22 | case .header3: 23 | self.font = UIFont.ownglyphBerry(size: 22) 24 | case .body1: 25 | self.font = UIFont.ownglyphBerry(size: 20) 26 | case .body2: 27 | self.font = UIFont.ownglyphBerry(size: 17) 28 | case .body3: 29 | self.font = UIFont.ownglyphBerry(size: 12) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol MediaRepository: Sendable { 5 | func create(media mediaDescription: MediaDescription, data: Data, to bookID: UUID?) async throws 6 | func create(media mediaDescription: MediaDescription, from: URL, to bookID: UUID?) async throws 7 | func fetch(media mediaDescription: MediaDescription, from bookID: UUID?) async throws -> Data 8 | func getURL(media mediaDescription: MediaDescription, from bookID: UUID?) async throws -> URL 9 | func makeTemporaryDirectory() async throws 10 | func delete(media mediaDescription: MediaDescription, at bookID: UUID?) async throws 11 | func moveTemporaryMedia(_ mediaDescription: MediaDescription, to bookID: UUID) async throws 12 | func moveAllTemporaryMedia(to bookID: UUID) async throws 13 | 14 | // MARK: - Snapshot 15 | func createSnapshot(for media: [MediaDescription], in bookID: UUID) async throws 16 | func deleteMediaBySnapshot(for bookID: UUID) async throws 17 | } 18 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHDataTests/TestDoubles/MockCoreDataStorage.swift: -------------------------------------------------------------------------------- 1 | @testable import MHData 2 | @testable import MHCore 3 | import CoreData 4 | 5 | final class MockCoreDataStorage: CoreDataStorage { 6 | override init() { 7 | super.init() 8 | 9 | /// 해당 Container는 InMemory 타입이라는 것을 명시해줍니다. 10 | let persistentStoreDescription = NSPersistentStoreDescription() 11 | persistentStoreDescription.type = NSInMemoryStoreType 12 | 13 | /// 실제 사용하는 CoreDataStorage의 Model명과 NSManagedObjectModel을 가진 Container를 생성합니다. 14 | let container = NSPersistentContainer( 15 | name: CoreDataStorage.modelName, 16 | managedObjectModel: CoreDataStorage.memorialHouseModel 17 | ) 18 | 19 | container.persistentStoreDescriptions = [persistentStoreDescription] 20 | 21 | container.loadPersistentStores { _, error in 22 | if let error = error as NSError? { 23 | MHLogger.error("\(#function): PersistentContainer 호출에 실패; \(error.localizedDescription)") 24 | } 25 | } 26 | persistentContainer = container 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/Repository/LocalBookCategoryRepository.swift: -------------------------------------------------------------------------------- 1 | import MHCore 2 | import MHDomain 3 | 4 | public struct LocalBookCategoryRepository: BookCategoryRepository { 5 | private let storage: BookCategoryStorage 6 | 7 | public init(storage: BookCategoryStorage) { 8 | self.storage = storage 9 | } 10 | 11 | public func createBookCategory(with category: BookCategory) async throws { 12 | try await storage.create(with: BookCategoryDTO(order: category.order, name: category.name)) 13 | } 14 | 15 | public func fetchBookCategories() async throws -> [BookCategory] { 16 | let bookCategoryEntities = try await storage.fetch() 17 | return bookCategoryEntities.compactMap { $0.convertToBookCategory() } 18 | } 19 | 20 | public func updateBookCategory(oldName: String, with category: BookCategory) async throws { 21 | try await storage.update( 22 | oldName: oldName, 23 | with: BookCategoryDTO(order: category.order, name: category.name) 24 | ) 25 | } 26 | 27 | public func deleteBookCategory(with categoryName: String) async throws { 28 | try await storage.delete(with: categoryName) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/ViewModel/ModifyBookCoverViewmodelFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct ModifyBookCoverViewModelFactory: Sendable { 5 | private let fetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase 6 | private let fetchBookCoverUseCase: FetchBookCoverUseCase 7 | private let updateBookCoverUseCase: UpdateBookCoverUseCase 8 | 9 | public init( 10 | fetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase, 11 | fetchBookCoverUseCase: FetchBookCoverUseCase, 12 | updateBookCoverUseCase: UpdateBookCoverUseCase 13 | ) { 14 | self.fetchMemorialHouseNameUseCase = fetchMemorialHouseNameUseCase 15 | self.fetchBookCoverUseCase = fetchBookCoverUseCase 16 | self.updateBookCoverUseCase = updateBookCoverUseCase 17 | } 18 | 19 | func make(bookID: UUID) -> ModifyBookCoverViewModel { 20 | return ModifyBookCoverViewModel( 21 | fetchMemorialHouseNameUseCase: fetchMemorialHouseNameUseCase, 22 | fetchBookCoverUseCase: fetchBookCoverUseCase, 23 | updateBookCoverUseCase: updateBookCoverUseCase, 24 | bookID: bookID 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/DTO/BookCoverDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct BookCoverDTO { 5 | let id: UUID 6 | let order: Int 7 | let title: String 8 | let imageData: Data? 9 | let color: String 10 | let category: String? 11 | let favorite: Bool 12 | 13 | public init( 14 | id: UUID, 15 | order: Int, 16 | title: String, 17 | imageData: Data?, 18 | color: String, 19 | category: String?, 20 | favorite: Bool 21 | ) { 22 | self.id = id 23 | self.order = order 24 | self.title = title 25 | self.imageData = imageData 26 | self.color = color 27 | self.category = category 28 | self.favorite = favorite 29 | } 30 | 31 | func convertToBookCover() -> BookCover? { 32 | guard let color = BookColor(rawValue: self.color) else { return nil } 33 | 34 | return BookCover( 35 | id: self.id, 36 | order: self.order, 37 | title: self.title, 38 | imageData: self.imageData, 39 | color: color, 40 | category: self.category, 41 | favorite: self.favorite 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Player/ViewModel/AudioPlayerViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | final public class AudioPlayerViewModel: ViewModelType { 5 | enum Input { 6 | case audioStateButtonTapped 7 | } 8 | enum Output { 9 | case getAudioState(AudioPlayState) 10 | } 11 | 12 | private let output = PassthroughSubject() 13 | private var cancellables = Set() 14 | private var audioPlayState: AudioPlayState = .pause 15 | 16 | func transform(input: AnyPublisher) -> AnyPublisher { 17 | input.sink { [weak self] event in 18 | switch event { 19 | case .audioStateButtonTapped: 20 | self?.audioStateChanged() 21 | } 22 | }.store(in: &cancellables) 23 | 24 | return output.eraseToAnyPublisher() 25 | } 26 | 27 | private func audioStateChanged() { 28 | switch audioPlayState { 29 | case .pause: 30 | audioPlayState = .play 31 | output.send(.getAudioState(.play)) 32 | case .play: 33 | audioPlayState = .pause 34 | output.send(.getAudioState(.pause)) 35 | } 36 | return 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Home/ViewModel/HomeViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import MHDomain 2 | 3 | public struct HomeViewModelFactory: Sendable { 4 | let fetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase 5 | let fetchAllBookCoverUseCase: FetchAllBookCoverUseCase 6 | let updateBookCoverUseCase: UpdateBookCoverUseCase 7 | let deleteBookCoverUseCase: DeleteBookCoverUseCase 8 | 9 | public init( 10 | fetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase, 11 | fetchAllBookCoverUseCase: FetchAllBookCoverUseCase, 12 | updateBookCoverUseCase: UpdateBookCoverUseCase, 13 | deleteBookCoverUseCase: DeleteBookCoverUseCase 14 | ) { 15 | self.fetchMemorialHouseNameUseCase = fetchMemorialHouseNameUseCase 16 | self.fetchAllBookCoverUseCase = fetchAllBookCoverUseCase 17 | self.updateBookCoverUseCase = updateBookCoverUseCase 18 | self.deleteBookCoverUseCase = deleteBookCoverUseCase 19 | } 20 | 21 | public func make() -> HomeViewModel { 22 | HomeViewModel( 23 | fetchMemorialHouseUseCase: fetchMemorialHouseNameUseCase, 24 | fetchAllBookCoverUseCase: fetchAllBookCoverUseCase, 25 | updateBookCoverUseCase: updateBookCoverUseCase, 26 | deleteBookCoverUseCase: deleteBookCoverUseCase 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol CreateMediaUseCase: Sendable { 4 | func execute(media: MediaDescription, data: Data, at bookID: UUID?) async throws 5 | func execute(media: MediaDescription, from url: URL, at bookID: UUID?) async throws 6 | } 7 | 8 | public protocol FetchMediaUseCase: Sendable { 9 | func execute(media: MediaDescription, in bookID: UUID) async throws -> Data 10 | func execute(media: MediaDescription, in bookID: UUID) async throws -> URL 11 | } 12 | 13 | public protocol DeleteMediaUseCase: Sendable { 14 | func execute(media: MediaDescription, in bookID: UUID) async throws 15 | } 16 | 17 | public protocol PersistentlyStoreMediaUseCase: Sendable { 18 | @available(*, deprecated, message: "temp를 더이상 사용하지 않습니다.") 19 | func execute(to bookID: UUID) async throws // TODO: - 없애야함 20 | /// mediaList가 없을 경우 현재 디렉토리의 스냅샷 기준으로 저장합니다. 21 | /// mediaList가 있을 경우 해당 목록을 기준으로 저장합니다. 22 | func execute(to bookID: UUID, mediaList: [MediaDescription]?) async throws 23 | 24 | func excute(media: MediaDescription, to bookID: UUID) async throws 25 | } 26 | 27 | public protocol TemporaryStoreMediaUseCase: Sendable { 28 | func execute(media: MediaDescription) async throws -> URL 29 | } 30 | 31 | public protocol DeleteTemporaryMediaUseCase: Sendable { 32 | func execute(media: MediaDescription) async throws 33 | } 34 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/View/CustomAlbumCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class CustomAlbumCollectionViewCell: UICollectionViewCell { 4 | // MARK: - Properties 5 | private let photoImageView: UIImageView = { 6 | let imageView = UIImageView(image: nil) 7 | imageView.contentMode = .scaleAspectFill 8 | imageView.clipsToBounds = true 9 | 10 | return imageView 11 | }() 12 | var representedAssetIdentifier: String? 13 | 14 | // MARK: - Initialize 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | setup() 19 | configureConstraints() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | 25 | setup() 26 | configureConstraints() 27 | } 28 | 29 | // MARK: - PrepareForReuse 30 | override func prepareForReuse() { 31 | super.prepareForReuse() 32 | 33 | photoImageView.image = nil 34 | } 35 | 36 | // MARK: - Configure 37 | private func setup() { 38 | contentView.backgroundColor = .lightGray 39 | } 40 | 41 | private func configureConstraints() { 42 | contentView.addSubview(photoImageView) 43 | photoImageView.fillSuperview() 44 | } 45 | 46 | // MARK: - Set Cell Image 47 | func setPhoto(_ photo: UIImage?) { 48 | photoImageView.image = photo 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AuthorizationAlert.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum AlertType: CustomStringConvertible { 4 | case camera 5 | case image 6 | case audio 7 | 8 | var description: String { 9 | switch self { 10 | case .camera: 11 | "카메라" 12 | case .image: 13 | "사진" 14 | case .audio: 15 | "마이크" 16 | } 17 | } 18 | } 19 | 20 | extension UIViewController { 21 | func showRedirectSettingAlert(with content: AlertType) { 22 | let alertController = UIAlertController( 23 | title: "\(content.description) 권한이 필요합니다.", 24 | message: "\(content.description) 권한을 허용해야만\n해당 기능을 사용하실 수 있습니다.", 25 | confirmTitle: "설정", 26 | cancelTitle: "취소", 27 | confirmHandler: { _ in 28 | if let appSetting = URL(string: UIApplication.openSettingsURLString) { 29 | UIApplication.shared.open(appSetting) 30 | } 31 | }, 32 | cancelHandler: { [weak self] in 33 | switch content { 34 | case .image, .camera: 35 | self?.navigationController?.popViewController(animated: true) 36 | case .audio: 37 | self?.dismiss(animated: true) 38 | } 39 | } 40 | ) 41 | 42 | self.present(alertController, animated: true, completion: nil) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMemorialHouseNameUseCase.swift: -------------------------------------------------------------------------------- 1 | import MHCore 2 | 3 | public struct DefaultCreateMemorialHouseNameUseCase: CreateMemorialHouseNameUseCase { 4 | private let repository: MemorialHouseNameRepository 5 | 6 | public init(repository: MemorialHouseNameRepository) { 7 | self.repository = repository 8 | } 9 | 10 | public func execute(with name: String) async { 11 | await repository.createMemorialHouseName(with: name) 12 | } 13 | } 14 | 15 | public struct DefaultFetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase { 16 | private let repository: MemorialHouseNameRepository 17 | 18 | public init(repository: MemorialHouseNameRepository) { 19 | self.repository = repository 20 | } 21 | 22 | public func execute() async throws -> String { 23 | let memorialHouseName = try await repository.fetchMemorialHouseName() 24 | let transformedName = transformHouseName(with: memorialHouseName) 25 | MHLogger.info("저장된 기록소 이름: \(transformedName)") 26 | 27 | return transformedName 28 | } 29 | 30 | /// 집 이름이 2글자인 경우, 각 글자 사이에 공백을 추가하여 변환합니다. 31 | /// 32 | /// - Parameter name: 원본 이름 문자열. 33 | /// - Returns: 글자 사이에 공백이 추가된 문자열(두 글자인 경우). 34 | /// 한 글자, 혹은 세 글자 이상라면 원본 문자열 그대로 반환합니다. 35 | private func transformHouseName(with name: String) -> String { 36 | guard name.count == 2 else { return name } 37 | return name.map { String($0) }.joined(separator: " ") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentationTests/TestDoubles/StubBookCoverUseCase.swift: -------------------------------------------------------------------------------- 1 | import MHDomain 2 | import Foundation 3 | 4 | struct StubFetchAllBookCoverUseCase: FetchAllBookCoverUseCase { 5 | func execute() async throws -> [BookCover] { 6 | return [ 7 | BookCover( 8 | id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!, 9 | order: 0, 10 | title: "title1", 11 | imageData: nil, 12 | color: .blue, 13 | category: "친구", 14 | favorite: false 15 | ), 16 | BookCover( 17 | id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, 18 | order: 1, 19 | title: "title2", 20 | imageData: nil, 21 | color: .blue, 22 | category: "가족", 23 | favorite: false 24 | ), 25 | BookCover( 26 | id: UUID(uuidString: "33333333-3333-3333-3333-333333333333")!, 27 | order: 2, 28 | title: "title3", 29 | imageData: nil, 30 | color: .green, 31 | category: "전체", 32 | favorite: false 33 | ) 34 | ] 35 | } 36 | } 37 | 38 | struct StubUpdateBookCoverUseCase: UpdateBookCoverUseCase { 39 | func execute(id: UUID, with bookCover: BookCover) async throws { } 40 | } 41 | 42 | struct StubDeleteBookCoverUseCase: DeleteBookCoverUseCase { 43 | func execute(id: UUID) async throws { } 44 | } 45 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage/UIImage+Rotate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIImage { 4 | func rotate(radians: Float) -> UIImage? { 5 | /// 회전된 이미지의 새로운 이미지 크기 계산 6 | var newSize = CGRect( 7 | origin: CGPoint.zero, 8 | size: self.size 9 | ).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size 10 | 11 | /// Core Graphics가 반올림으로 인한 오차를 발생시키지 않도록 소수점 이하 버림 12 | newSize.width = floor(newSize.width) 13 | newSize.height = floor(newSize.height) 14 | 15 | /// 위에서 설정한 새로운 크기의 GraphicContext 생성 16 | /// false: 배경을 투명하게 설정 17 | /// self.scale: 원본 이미지의 해상도 유지 18 | UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) 19 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 20 | 21 | /// Context의 원점을 이미지 중앙으로 이동 22 | context.translateBy(x: newSize.width/2, y: newSize.height/2) 23 | /// 중심점을 기준으로 이미지 회전 24 | context.rotate(by: CGFloat(radians)) 25 | /// 회전된 context에 그림 그리기 26 | /// 이때, 중앙을 기준으로 그리기 위해 x, y좌표를 넓이, 높이의 절반 음수 값을 넣어줌 27 | self.draw(in: CGRect( 28 | x: -self.size.width/2, 29 | y: -self.size.height/2, 30 | width: self.size.width, 31 | height: self.size.height)) 32 | 33 | /// 회전된 이미지를 새로운 UIImage로 생성 후 GraphicContext 종료 34 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 35 | UIGraphicsEndImageContext() 36 | 37 | return newImage 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Category/View/BookCategoryTableViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class BookCategoryTableViewCell: UITableViewCell { 4 | nonisolated static let height: CGFloat = 44 5 | 6 | // MARK: - UI Components 7 | private let categoryLabel = UILabel(style: .body1) 8 | private let checkImageView = UIImageView() 9 | 10 | // MARK: - Initializer 11 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 12 | super.init(style: style, reuseIdentifier: reuseIdentifier) 13 | 14 | setup() 15 | configureLayout() 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | 21 | setup() 22 | configureLayout() 23 | } 24 | 25 | // MARK: - Setup & Configuration 26 | func configure(category: String, isSelected: Bool) { 27 | categoryLabel.text = category 28 | checkImageView.image = isSelected ? .check : nil 29 | } 30 | 31 | private func setup() { 32 | contentView.backgroundColor = .baseBackground 33 | contentView.addSubview(categoryLabel) 34 | contentView.addSubview(checkImageView) 35 | checkImageView.contentMode = .scaleAspectFit 36 | } 37 | 38 | private func configureLayout() { 39 | categoryLabel.setLeading(anchor: contentView.leadingAnchor, constant: 16) 40 | categoryLabel.setCenterY(view: contentView) 41 | checkImageView.setTrailing(anchor: contentView.trailingAnchor, constant: 16) 42 | checkImageView.setCenterY(view: contentView) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/ViewModel/CreateBookCoverViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct CreateBookCoverViewModelFactory: Sendable { 5 | private let fetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase 6 | private let createBookCoverUseCase: CreateBookCoverUseCase 7 | private let deleteBookCoverUseCase: DeleteBookCoverUseCase 8 | private let createBookUseCase: CreateBookUseCase 9 | private let deleteBookUseCase: DeleteBookUseCase 10 | 11 | public init( 12 | fetchMemorialHouseNameUseCase: FetchMemorialHouseNameUseCase, 13 | createBookCoverUseCase: CreateBookCoverUseCase, 14 | deleteBookCoverUseCase: DeleteBookCoverUseCase, 15 | createBookUseCase: CreateBookUseCase, 16 | deleteBookUseCase: DeleteBookUseCase 17 | ) { 18 | self.fetchMemorialHouseNameUseCase = fetchMemorialHouseNameUseCase 19 | self.createBookCoverUseCase = createBookCoverUseCase 20 | self.deleteBookCoverUseCase = deleteBookCoverUseCase 21 | self.createBookUseCase = createBookUseCase 22 | self.deleteBookUseCase = deleteBookUseCase 23 | } 24 | 25 | func make(bookCount order: Int) -> CreateBookCoverViewModel { 26 | return CreateBookCoverViewModel( 27 | fetchMemorialHouseNameUseCase: fetchMemorialHouseNameUseCase, 28 | createBookCoverUseCase: createBookCoverUseCase, 29 | deleteBookCoverUseCase: deleteBookCoverUseCase, 30 | createBookUseCase: createBookUseCase, 31 | deleteBookUseCase: deleteBookUseCase, 32 | bookOrder: order 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookCategoryUseCase.swift: -------------------------------------------------------------------------------- 1 | public struct DefaultCreateBookCategoryUseCase: CreateBookCategoryUseCase { 2 | let repository: BookCategoryRepository 3 | 4 | public init(repository: BookCategoryRepository) { 5 | self.repository = repository 6 | } 7 | 8 | public func execute(with category: BookCategory) async throws { 9 | try await repository.createBookCategory(with: category) 10 | } 11 | } 12 | 13 | public struct DefaultFetchBookCategoriesUseCase: FetchBookCategoriesUseCase { 14 | let repository: BookCategoryRepository 15 | 16 | public init(repository: BookCategoryRepository) { 17 | self.repository = repository 18 | } 19 | 20 | public func execute() async throws -> [BookCategory] { 21 | try await repository.fetchBookCategories() 22 | } 23 | } 24 | 25 | public struct DefaultUpdateBookCategoryUseCase: UpdateBookCategoryUseCase { 26 | let repository: BookCategoryRepository 27 | 28 | public init(repository: BookCategoryRepository) { 29 | self.repository = repository 30 | } 31 | 32 | public func execute(oldName: String, with category: BookCategory) async throws { 33 | try await repository.updateBookCategory(oldName: oldName, with: category) 34 | } 35 | } 36 | 37 | public struct DefaultDeleteBookCategoryUseCase: DeleteBookCategoryUseCase { 38 | let repository: BookCategoryRepository 39 | 40 | public init(repository: BookCategoryRepository) { 41 | self.repository = repository 42 | } 43 | 44 | public func execute(with categoryName: String) async throws { 45 | try await repository.deleteBookCategory(with: categoryName) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DefaultCreateBookUseCase: CreateBookUseCase { 4 | private let repository: BookRepository 5 | private let mediaRepository: MediaRepository 6 | 7 | public init(repository: BookRepository, mediaRepository: MediaRepository) { 8 | self.repository = repository 9 | self.mediaRepository = mediaRepository 10 | } 11 | 12 | public func execute(book: Book) async throws { 13 | try await repository.create(book: book) 14 | try await mediaRepository.createSnapshot(for: [], in: book.id) 15 | } 16 | } 17 | 18 | public struct DefaultFetchBookUseCase: FetchBookUseCase { 19 | private let repository: BookRepository 20 | 21 | public init(repository: BookRepository) { 22 | self.repository = repository 23 | } 24 | 25 | public func execute(id: UUID) async throws -> Book { 26 | try await repository.fetch(bookID: id) 27 | } 28 | } 29 | 30 | public struct DefaultUpdateBookUseCase: UpdateBookUseCase { 31 | private let repository: BookRepository 32 | 33 | public init(repository: BookRepository) { 34 | self.repository = repository 35 | } 36 | 37 | public func execute(id: UUID, book: Book) async throws { 38 | try await repository.update(bookID: id, to: book) 39 | } 40 | } 41 | 42 | public struct DefaultDeleteBookUseCase: DeleteBookUseCase { 43 | private let repository: BookRepository 44 | 45 | public init(repository: BookRepository) { 46 | self.repository = repository 47 | } 48 | 49 | public func execute(id: UUID) async throws { 50 | try await repository.delete(bookID: id) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/ViewModel/LocalPhotoManager.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | struct LocalPhotoManager { 5 | private let imageManager = PHCachingImageManager() 6 | private let imageRequestOptions: PHImageRequestOptions = { 7 | let options = PHImageRequestOptions() 8 | options.isSynchronous = true 9 | options.isNetworkAccessAllowed = true 10 | options.deliveryMode = .highQualityFormat 11 | 12 | return options 13 | }() 14 | 15 | func requestThumbnailImage( 16 | with asset: PHAsset?, 17 | cellSize: CGSize = .zero, 18 | completion: @escaping @MainActor (UIImage?) -> Void 19 | ) { 20 | guard let asset else { return } 21 | 22 | imageManager.requestImage( 23 | for: asset, 24 | targetSize: cellSize, 25 | contentMode: .aspectFill, 26 | options: imageRequestOptions, 27 | resultHandler: { image, _ in 28 | Task { await completion(image) } 29 | }) 30 | 31 | imageManager.startCachingImages( 32 | for: [asset], 33 | targetSize: cellSize, 34 | contentMode: .aspectFill, 35 | options: nil 36 | ) 37 | } 38 | 39 | func requestVideoURL( 40 | with asset: PHAsset, 41 | completion: @escaping @MainActor (URL?) -> Void 42 | ) { 43 | let options = PHVideoRequestOptions() 44 | options.version = .current 45 | imageManager.requestAVAsset(forVideo: asset, options: options) { avAsset, _, _ in 46 | let url = (avAsset as? AVURLAsset)?.url 47 | Task { await completion(url) } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataStorage.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import MHCore 3 | 4 | /// Core Data 스택을 구성하는 기본 클래스이며, 다른 코어데이터 구현체의 프로퍼티로 사용됩니다. 5 | /// 또한, 테스트 환경에서 `MockCoreDataStorage`를 상속받아 확장할 수 있도록 설계되었습니다. 6 | /// 7 | /// 이 클래스를 프로토콜로 작성하지 않은 이유는, 8 | /// 테스트 코드에서도 `MemorialHouseModel`이라는 Core Data 모델을 동일하게 사용하기 위해서입니다. 9 | /// 이를 통해 테스트 환경에서도 실제 DB 모델 구조를 유지하며 간단히 확장할 수 있습니다. 10 | /// 11 | /// - 주요 특징: 12 | /// - `NSPersistentContainer`를 활용해 Core Data 스택을 구성합니다. 13 | /// - `saveContext` 메서드를 통해 변경된 컨텍스트를 저장합니다. 14 | public class CoreDataStorage: @unchecked Sendable { 15 | static let modelName: String = "MemorialHouseModel" 16 | 17 | nonisolated(unsafe) static let memorialHouseModel: NSManagedObjectModel = { 18 | guard let modelURL = Bundle(for: CoreDataStorage.self).url( 19 | forResource: CoreDataStorage.modelName, 20 | withExtension: "momd" 21 | ) else { 22 | fatalError("Error loading model from bundle") 23 | } 24 | return NSManagedObjectModel(contentsOf: modelURL)! 25 | }() 26 | 27 | var persistentContainer: NSPersistentContainer 28 | 29 | public init() { 30 | let container = NSPersistentContainer( 31 | name: CoreDataStorage.modelName, 32 | managedObjectModel: Self.memorialHouseModel 33 | ) 34 | container.loadPersistentStores { _, error in 35 | guard let error else { return } 36 | MHLogger.error("\(#function): PersistentContainer 호출에 실패: \(error.localizedDescription)") 37 | } 38 | self.persistentContainer = container 39 | } 40 | 41 | func createBackgroundContext() -> NSManagedObjectContext { 42 | return persistentContainer.newBackgroundContext() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## Obj-C/Swift specific 11 | *.hmap 12 | 13 | ## App packaging 14 | *.ipa 15 | *.dSYM.zip 16 | *.dSYM 17 | 18 | ## Playgrounds 19 | timeline.xctimeline 20 | playground.xcworkspace 21 | 22 | # Swift Package Manager 23 | # 24 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 25 | # Packages/ 26 | # Package.pins 27 | # Package.resolved 28 | # *.xcodeproj 29 | # 30 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 31 | # hence it is not needed unless you have added a package configuration file to your project 32 | # .swiftpm 33 | 34 | .build/ 35 | 36 | # CocoaPods 37 | # 38 | # We recommend against adding the Pods directory to your .gitignore. However 39 | # you should judge for yourself, the pros and cons are mentioned at: 40 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 41 | # 42 | # Pods/ 43 | # 44 | # Add this line if you want to avoid checking in source code from the Xcode workspace 45 | # *.xcworkspace 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build/ 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. 57 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 60 | 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots/**/*.png 64 | fastlane/test_output 65 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Category/ViewModel/BookCategoryViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import MHDomain 2 | 3 | public struct BookCategoryViewModelFactory: Sendable { 4 | let createBookCategoryUseCase: CreateBookCategoryUseCase 5 | let fetchBookCategoriesUseCase: FetchBookCategoriesUseCase 6 | let updateBookCategoryUseCase: UpdateBookCategoryUseCase 7 | let deleteBookCategoryUseCase: DeleteBookCategoryUseCase 8 | 9 | public init( 10 | createBookCategoryUseCase: CreateBookCategoryUseCase, 11 | fetchBookCategoriesUseCase: FetchBookCategoriesUseCase, 12 | updateBookCategoryUseCase: UpdateBookCategoryUseCase, 13 | deleteBookCategoryUseCase: DeleteBookCategoryUseCase 14 | ) { 15 | self.createBookCategoryUseCase = createBookCategoryUseCase 16 | self.fetchBookCategoriesUseCase = fetchBookCategoriesUseCase 17 | self.updateBookCategoryUseCase = updateBookCategoryUseCase 18 | self.deleteBookCategoryUseCase = deleteBookCategoryUseCase 19 | } 20 | 21 | func makeForHome() -> BookCategoryViewModel { 22 | BookCategoryViewModel( 23 | createBookCategoryUseCase: createBookCategoryUseCase, 24 | fetchBookCategoriesUseCase: fetchBookCategoriesUseCase, 25 | updateBookCategoryUseCase: updateBookCategoryUseCase, 26 | deleteBookCategoryUseCase: deleteBookCategoryUseCase 27 | ) 28 | } 29 | 30 | func makeForCreateBook() -> BookCategoryViewModel { 31 | BookCategoryViewModel( 32 | createBookCategoryUseCase: createBookCategoryUseCase, 33 | fetchBookCategoriesUseCase: fetchBookCategoriesUseCase, 34 | updateBookCategoryUseCase: updateBookCategoryUseCase, 35 | deleteBookCategoryUseCase: deleteBookCategoryUseCase, 36 | categories: [] 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHNavigationBar.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class MHNavigationBar: UIView { 4 | // MARK: - Property 5 | private let titleLabel = UILabel(style: .header1) 6 | private let settingButton = UIButton(type: .custom) 7 | 8 | // MARK: - Initializer 9 | init() { 10 | super.init(frame: .zero) 11 | 12 | setup() 13 | configureAddSubView() 14 | configureConstraints() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | 20 | setup() 21 | configureAddSubView() 22 | configureConstraints() 23 | } 24 | 25 | // MARK: - Setup & Configuration 26 | private func setup() { 27 | settingButton.setImage(.settingLight, for: .normal) 28 | backgroundColor = .baseBackground 29 | } 30 | 31 | private func configureAddSubView() { 32 | addSubview(titleLabel) 33 | addSubview(settingButton) 34 | } 35 | 36 | private func configureConstraints() { 37 | titleLabel.setTop(anchor: topAnchor) 38 | titleLabel.setLeading(anchor: leadingAnchor) 39 | titleLabel.setBottom(anchor: bottomAnchor) 40 | titleLabel.setCenterY(view: settingButton) 41 | 42 | settingButton.setTop(anchor: topAnchor) 43 | settingButton.setTrailing(anchor: trailingAnchor) 44 | settingButton.setCenterY(view: self) 45 | settingButton.setWidth(30) 46 | settingButton.setHeight(30) 47 | } 48 | 49 | func configureSettingAction(action: UIAction) { 50 | settingButton.addAction(action, for: .touchUpInside) 51 | } 52 | 53 | func configureTitle(with title: String) { 54 | let localizedKey = "UserName 기록소" 55 | titleLabel.text = String.localizedStringWithFormat(localizedKey.localized(), title) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIAlertController+Initializer.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIAlertController { 4 | /// UIAlertController의 기본 편의 생성자 5 | /// - Parameters: 6 | /// - title: Alert의 제목 7 | /// - message: Alert의 메시지 8 | /// - preferredStyle: Alert의 스타일 (기본값: .alert) 9 | /// - textFieldConfiguration: 텍스트 필드를 구성하기 위한 클로저 (기본값: nil) 10 | /// - confirmTitle: 확인 버튼의 제목 (기본값: "확인") 11 | /// - cancelTitle: 취소 버튼의 제목 (기본값: "취소") 12 | /// - confirmHandler: 확인 버튼 클릭 시 실행될 핸들러, 텍스트 필드 입력값(String?)을 매개변수로 전달 13 | convenience init( 14 | title: String?, 15 | message: String? = nil, 16 | preferredStyle: UIAlertController.Style = .alert, 17 | textFieldConfiguration: ((UITextField) -> Void)? = nil, 18 | confirmTitle: String = "확인", 19 | cancelTitle: String = "취소", 20 | confirmHandler: ((String?) -> Void)? = nil, 21 | cancelHandler: (() -> Void)? = nil 22 | ) { 23 | self.init(title: title, message: message, preferredStyle: preferredStyle) 24 | 25 | // 텍스트 필드 추가 (textFieldConfiguration이 nil이 아닐 때만) 26 | if let textFieldConfiguration = textFieldConfiguration { 27 | self.addTextField(configurationHandler: textFieldConfiguration) 28 | } 29 | 30 | // 확인 액션 추가 31 | let confirmAction = UIAlertAction(title: confirmTitle.localized(), style: .default) { [weak self] _ in 32 | let text = self?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines) 33 | confirmHandler?(textFieldConfiguration != nil ? text : nil) 34 | } 35 | self.addAction(confirmAction) 36 | 37 | // 취소 액션 추가 38 | let cancelAction = UIAlertAction(title: cancelTitle.localized(), style: .cancel) { _ in 39 | cancelHandler?() 40 | } 41 | self.addAction(cancelAction) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/Repository/LocalBookCoverRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | import MHCore 4 | 5 | public struct LocalBookCoverRepository: BookCoverRepository { 6 | private let storage: BookCoverStorage 7 | 8 | public init(storage: BookCoverStorage) { 9 | self.storage = storage 10 | } 11 | 12 | public func createBookCover(with bookCover: BookCover) async throws { 13 | let bookCoverDTO = BookCoverDTO( 14 | id: bookCover.id, 15 | order: bookCover.order, 16 | title: bookCover.title, 17 | imageData: bookCover.imageData, 18 | color: bookCover.color.rawValue, 19 | category: bookCover.category, 20 | favorite: bookCover.favorite 21 | ) 22 | try await storage.create(data: bookCoverDTO) 23 | } 24 | 25 | public func fetchBookCover(with id: UUID) async throws -> BookCover? { 26 | let bookCoverEntities = try await storage.fetch() 27 | let bookCoverEntity = bookCoverEntities.filter({ $0.id == id }).first 28 | return bookCoverEntity?.convertToBookCover() 29 | } 30 | 31 | public func fetchAllBookCovers() async throws -> [BookCover] { 32 | let bookCoverEntities = try await storage.fetch() 33 | return bookCoverEntities.compactMap { $0.convertToBookCover() } 34 | } 35 | 36 | public func updateBookCover(id: UUID, with bookCover: BookCover) async throws { 37 | let bookCoverDTO = BookCoverDTO( 38 | id: bookCover.id, 39 | order: bookCover.order, 40 | title: bookCover.title, 41 | imageData: bookCover.imageData, 42 | color: bookCover.color.rawValue, 43 | category: bookCover.category, 44 | favorite: bookCover.favorite 45 | ) 46 | try await storage.update(with: id, data: bookCoverDTO) 47 | } 48 | 49 | public func deleteBookCover(id: UUID) async throws { 50 | try await storage.delete(with: id) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | 4 | public struct EditBookViewModelFactory: Sendable { 5 | private let fetchBookUseCase: FetchBookUseCase 6 | private let updateBookUseCase: UpdateBookUseCase 7 | private let storeMediaUseCase: PersistentlyStoreMediaUseCase 8 | private let deleteTemporaryMediaUseCase: DeleteTemporaryMediaUseCase 9 | private let createMediaUseCase: CreateMediaUseCase 10 | private let fetchMediaUseCase: FetchMediaUseCase 11 | private let deleteMediaUseCase: DeleteMediaUseCase 12 | 13 | public init( 14 | fetchBookUseCase: FetchBookUseCase, 15 | updateBookUseCase: UpdateBookUseCase, 16 | storeMediaUseCase: PersistentlyStoreMediaUseCase, 17 | deleteTemporaryMediaUseCase: DeleteTemporaryMediaUseCase, 18 | createMediaUseCase: CreateMediaUseCase, 19 | fetchMediaUseCase: FetchMediaUseCase, 20 | deleteMediaUseCase: DeleteMediaUseCase 21 | ) { 22 | self.fetchBookUseCase = fetchBookUseCase 23 | self.updateBookUseCase = updateBookUseCase 24 | self.storeMediaUseCase = storeMediaUseCase 25 | self.deleteTemporaryMediaUseCase = deleteTemporaryMediaUseCase 26 | self.createMediaUseCase = createMediaUseCase 27 | self.fetchMediaUseCase = fetchMediaUseCase 28 | self.deleteMediaUseCase = deleteMediaUseCase 29 | } 30 | 31 | func make(bookID: UUID, bookTitle: String) -> EditBookViewModel { 32 | EditBookViewModel( 33 | fetchBookUseCase: fetchBookUseCase, 34 | updateBookUseCase: updateBookUseCase, 35 | storeMediaUseCase: storeMediaUseCase, 36 | deleteTemporaryMediaUsecase: deleteTemporaryMediaUseCase, 37 | createMediaUseCase: createMediaUseCase, 38 | fetchMediaUseCase: fetchMediaUseCase, 39 | deleteMediaUseCase: deleteMediaUseCase, 40 | bookID: bookID, 41 | bookTitle: bookTitle 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookCoverUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DefaultCreateBookCoverUseCase: CreateBookCoverUseCase { 4 | private let repository: BookCoverRepository 5 | 6 | public init(repository: BookCoverRepository) { 7 | self.repository = repository 8 | } 9 | 10 | public func execute(with bookCover: BookCover) async throws { 11 | try await repository.createBookCover(with: bookCover) 12 | } 13 | } 14 | 15 | public struct DefaultFetchBookCoverUseCase: FetchBookCoverUseCase { 16 | private let repository: BookCoverRepository 17 | 18 | public init(repository: BookCoverRepository) { 19 | self.repository = repository 20 | } 21 | 22 | public func execute(id: UUID) async throws -> BookCover? { 23 | try await repository.fetchBookCover(with: id) 24 | } 25 | } 26 | 27 | public struct DefaultFetchAllBookCoverUseCase: FetchAllBookCoverUseCase { 28 | private let repository: BookCoverRepository 29 | 30 | public init(repository: BookCoverRepository) { 31 | self.repository = repository 32 | } 33 | 34 | public func execute() async throws -> [BookCover] { 35 | try await repository.fetchAllBookCovers() 36 | } 37 | } 38 | 39 | public struct DefaultUpdateBookCoverUseCase: UpdateBookCoverUseCase { 40 | private let repository: BookCoverRepository 41 | 42 | public init(repository: BookCoverRepository) { 43 | self.repository = repository 44 | } 45 | 46 | public func execute(id: UUID, with bookCover: BookCover) async throws { 47 | try await repository.updateBookCover(id: id, with: bookCover) 48 | } 49 | } 50 | 51 | public struct DefaultDeleteBookCoverUseCase: DeleteBookCoverUseCase { 52 | private let repository: BookCoverRepository 53 | 54 | public init(repository: BookCoverRepository) { 55 | self.repository = repository 56 | } 57 | 58 | public func execute(id: UUID) async throws { 59 | try await repository.deleteBookCover(id: id) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Register/ViewModel/RegisterViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | import Combine 4 | 5 | public final class RegisterViewModel: ViewModelType { 6 | public enum Input { 7 | case registerTextFieldEdited(text: String?) 8 | case registerButtonTapped(memorialHouseName: String) 9 | } 10 | 11 | public enum Output: Equatable { 12 | case registerButtonEnabled(isEnabled: Bool) 13 | case moveToHome 14 | case createFailure(errorMessage: String) 15 | } 16 | 17 | private let createMemorialHouseNameUseCase: CreateMemorialHouseNameUseCase 18 | private let output = PassthroughSubject() 19 | private var cancellables = Set() 20 | public init( 21 | createMemorialHouseNameUseCase: CreateMemorialHouseNameUseCase 22 | ) { 23 | self.createMemorialHouseNameUseCase = createMemorialHouseNameUseCase 24 | } 25 | 26 | func transform(input: AnyPublisher) -> AnyPublisher { 27 | input.sink { [weak self] event in 28 | switch event { 29 | case .registerTextFieldEdited(let text): 30 | self?.validateTextField(text: text) 31 | case .registerButtonTapped(let memorialHouseName): 32 | Task { await self?.registerButtonTapped(with: memorialHouseName) } 33 | } 34 | }.store(in: &cancellables) 35 | 36 | return output.eraseToAnyPublisher() 37 | } 38 | 39 | private func validateTextField(text: String?) { 40 | guard let text else { 41 | output.send(.registerButtonEnabled(isEnabled: false)) 42 | return 43 | } 44 | output.send(.registerButtonEnabled(isEnabled: !text.isEmpty && text.count < 11)) 45 | } 46 | 47 | private func registerButtonTapped(with memorialHouseName: String) async { 48 | await createMemorialHouseNameUseCase.execute(with: memorialHouseName) 49 | self.output.send(.moveToHome) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/Repository/LocalBookRepository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | import MHCore 4 | 5 | public struct LocalBookRepository: BookRepository { 6 | private let storage: BookStorage 7 | 8 | public init(storage: BookStorage) { 9 | self.storage = storage 10 | } 11 | 12 | public func create(book: Book) async throws { 13 | let bookDTO = mappingBookToDTO(book) 14 | try await storage.create(data: bookDTO) 15 | } 16 | 17 | public func fetch(bookID id: UUID) async throws -> Book { 18 | let bookEntity = try await storage.fetch(with: id) 19 | return bookEntity.convertToBook() 20 | } 21 | 22 | public func update(bookID id: UUID, to book: Book) async throws { 23 | let bookDTO = mappingBookToDTO(book) 24 | try await storage.update(with: id, data: bookDTO) 25 | } 26 | 27 | public func delete(bookID id: UUID) async throws { 28 | try await storage.delete(with: id) 29 | } 30 | 31 | // MARK: - Mapping 32 | private func mappingBookToDTO(_ book: Book) -> BookDTO { 33 | let pages = book.pages.map { mappingPageToDTO($0) } 34 | return BookDTO( 35 | id: book.id, 36 | title: book.title, 37 | pages: pages 38 | ) 39 | } 40 | 41 | private func mappingPageToDTO(_ page: Page) -> PageDTO { 42 | let meatadata = page.metadata 43 | .compactMapValues { mappingMediaDescriptionToDTO($0) } 44 | 45 | return PageDTO( 46 | id: page.id, 47 | metadata: meatadata, 48 | text: page.text 49 | ) 50 | } 51 | 52 | private func mappingMediaDescriptionToDTO(_ description: MediaDescription) -> MediaDescriptionDTO { 53 | let attributes = try? JSONSerialization.data(withJSONObject: description.attributes ?? [:], options: []) 54 | 55 | return MediaDescriptionDTO( 56 | id: description.id, 57 | type: description.type.rawValue, 58 | attributes: attributes 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MemorialHouse/MHCore/MHCore/MHDataError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum MHDataError: Error, CustomStringConvertible, Equatable { 4 | case noSuchEntity(key: String) 5 | case createEntityFailure 6 | case convertDTOFailure 7 | case fetchEntityFaliure 8 | case updateEntityFailure 9 | case deleteEntityFailure 10 | case findEntityFailure 11 | case saveContextFailure 12 | case directorySettingFailure 13 | case fileCreationFailure 14 | case fileReadingFailure 15 | case fileDeletionFailure 16 | case fileMovingFailure 17 | case fileNotExists 18 | case snapshotEncodingFailure 19 | case snapshotDecodingFailure 20 | case generalFailure 21 | case setUserDefaultFailure 22 | 23 | public var description: String { 24 | switch self { 25 | case let .noSuchEntity(key): 26 | "\(key)에 대한 Entity가 존재하지 않습니다" 27 | case .createEntityFailure: 28 | "Entity 생성 실패" 29 | case .convertDTOFailure: 30 | "Entity에 대한 DTO 변환 실패" 31 | case .fetchEntityFaliure: 32 | "Entity 가져오기 실패" 33 | case .updateEntityFailure: 34 | "Entity 업데이트 실패" 35 | case .deleteEntityFailure: 36 | "Entity 삭제 실패" 37 | case .findEntityFailure: 38 | "Entity 찾기 실패" 39 | case .saveContextFailure: 40 | "Update된 Context 저장 실패" 41 | case .directorySettingFailure: 42 | "디렉토리 설정 실패" 43 | case .fileCreationFailure: 44 | "파일 생성 실패" 45 | case .fileReadingFailure: 46 | "파일 읽기 실패" 47 | case .fileDeletionFailure: 48 | "파일 삭제 실패" 49 | case .fileMovingFailure: 50 | "파일 이동 실패" 51 | case .fileNotExists: 52 | "파일이 존재하지 않습니다" 53 | case .snapshotEncodingFailure: 54 | "Snapshot 인코딩 실패" 55 | case .snapshotDecodingFailure: 56 | "Snapshot 디코딩 실패" 57 | case .generalFailure: 58 | "알 수 없는 에러입니다." 59 | case .setUserDefaultFailure: 60 | "UserDefault 설정 실패" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/ViewModel/CustomAlbumViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Photos 3 | import Combine 4 | 5 | final class CustomAlbumViewModel: ViewModelType { 6 | enum Input { 7 | case fetchPhotoAssets(mediaType: PHAssetMediaType) 8 | case photoDidChanged(to: PHChange) 9 | } 10 | 11 | enum Output { 12 | case fetchAssets 13 | case changedAssets(with: PHFetchResultChangeDetails) 14 | } 15 | 16 | private let output = PassthroughSubject() 17 | private var cancellables = Set() 18 | private(set) var photoAsset: PHFetchResult? 19 | 20 | func transform(input: AnyPublisher) -> AnyPublisher { 21 | input.sink { [weak self] event in 22 | switch event { 23 | case .fetchPhotoAssets(let mediaType): 24 | self?.fetchPhotoAssets(of: mediaType) 25 | case .photoDidChanged(let changeInstance): 26 | self?.updatePhotoAssets(changeInstance) 27 | } 28 | } 29 | .store(in: &cancellables) 30 | 31 | return output.eraseToAnyPublisher() 32 | } 33 | 34 | private func fetchPhotoAssets(of mediaType: PHAssetMediaType) { 35 | let fetchOptions = PHFetchOptions() 36 | 37 | fetchOptions.predicate = NSPredicate(format: "mediaType = %d", mediaType.rawValue) 38 | fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 39 | 40 | photoAsset = PHAsset.fetchAssets(with: fetchOptions) 41 | output.send(.fetchAssets) 42 | } 43 | 44 | private func updatePhotoAssets(_ changeInstance: PHChange) { 45 | guard let asset = self.photoAsset, 46 | let changes = changeInstance.changeDetails(for: asset) else { return } 47 | 48 | self.photoAsset = changes.fetchResultAfterChanges 49 | 50 | if changes.hasIncrementalChanges { 51 | output.send(.changedAssets(with: changes)) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MHDomain 3 | 4 | protocol MediaAttachmentDataSource: AnyObject { 5 | func mediaAttachmentDragingImage(_ mediaAttachment: MediaAttachment, about view: UIView?) -> UIImage? 6 | } 7 | 8 | final class MediaAttachment: NSTextAttachment { 9 | // MARK: - Property 10 | private let view: (UIView & MediaAttachable) 11 | let mediaDescription: MediaDescription 12 | weak var dataSource: MediaAttachmentDataSource? 13 | 14 | // MARK: - Initializer 15 | init(view: (UIView & MediaAttachable), description: MediaDescription) { 16 | self.view = view 17 | self.mediaDescription = description 18 | super.init(data: nil, ofType: nil) 19 | } 20 | @available(*, unavailable) 21 | required init?(coder: NSCoder) { 22 | fatalError() 23 | } 24 | 25 | // MARK: - ViewConfigures 26 | override func viewProvider( 27 | for parentView: UIView?, 28 | location: any NSTextLocation, 29 | textContainer: NSTextContainer? 30 | ) -> NSTextAttachmentViewProvider? { 31 | let provider = MediaAttachmentViewProvider( 32 | textAttachment: self, 33 | parentView: parentView, 34 | textLayoutManager: textContainer?.textLayoutManager, 35 | location: location 36 | ) 37 | provider.tracksTextAttachmentViewBounds = true 38 | provider.view = view 39 | provider.type = mediaDescription.type 40 | 41 | return provider 42 | } 43 | override func image( 44 | forBounds imageBounds: CGRect, 45 | textContainer: NSTextContainer?, 46 | characterIndex charIndex: Int 47 | ) -> UIImage? { 48 | return dataSource?.mediaAttachmentDragingImage(self, about: view) 49 | } 50 | 51 | // MARK: - Method 52 | @MainActor 53 | func configure(with data: Data) { 54 | view.configureSource(with: mediaDescription, data: data) 55 | } 56 | 57 | @MainActor 58 | func configure(with url: URL) { 59 | view.configureSource(with: mediaDescription, url: url) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHRegisterView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class MHRegisterView: UIView { 4 | // MARK: UI Components 5 | let registerTextField: UITextField = { 6 | let registerFont = UIFont.ownglyphBerry(size: 20) 7 | 8 | let textField = UITextField() 9 | textField.font = registerFont 10 | 11 | var attributedText = AttributedString(stringLiteral: "ex) 영현".localized()) 12 | attributedText.font = registerFont 13 | textField.textAlignment = .right 14 | textField.attributedPlaceholder = NSAttributedString(attributedText) 15 | 16 | return textField 17 | }() 18 | private let registerLabel = UILabel(style: .header2) 19 | 20 | // MARK: - Initializer 21 | init() { 22 | super.init(frame: .zero) 23 | 24 | setup() 25 | configureAddSubview() 26 | configureLayout() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | 32 | setup() 33 | configureAddSubview() 34 | configureLayout() 35 | } 36 | 37 | func configure(textFieldAction: @escaping (String?) -> Void) { 38 | registerTextField.addAction(UIAction { [weak self] _ in 39 | guard let self else { return } 40 | textFieldAction(self.registerTextField.text) 41 | }, for: .editingChanged) 42 | } 43 | 44 | // MARK: - Setup & Configuration 45 | private func setup() { 46 | backgroundColor = .baseBackground 47 | registerLabel.text = "기록소".localized() 48 | } 49 | 50 | private func configureAddSubview() { 51 | addSubview(registerTextField) 52 | addSubview(registerLabel) 53 | } 54 | 55 | private func configureLayout() { 56 | registerTextField.setAnchor( 57 | top: topAnchor, 58 | leading: leadingAnchor, 59 | bottom: bottomAnchor, 60 | trailing: registerLabel.leadingAnchor, constantTrailing: 8 61 | ) 62 | registerLabel.setAnchor( 63 | trailing: trailingAnchor, constantTrailing: 4 64 | ) 65 | registerLabel.setCenterY(view: self) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: My Swift Actions 🚗 2 | 3 | on: 4 | pull_request: 5 | branches: ["develop"] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-15 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: 🔖 certificate 14 | env: 15 | BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} 16 | P12_PASSWORD: ${{ secrets.P12_PASSWORD }} 17 | BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} 18 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 19 | run: | 20 | # create variables 21 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 22 | PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision 23 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 24 | 25 | # import certificate and provisioning profile from secrets 26 | echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH 27 | echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH 28 | 29 | # create temporary keychain 30 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 31 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 32 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 33 | 34 | # import certificate to keychain 35 | security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 36 | security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 37 | security list-keychain -d user -s $KEYCHAIN_PATH 38 | 39 | # apply provisioning profile 40 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 41 | cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles 42 | 43 | - name: 🔨 set up xcode version 44 | run: | 45 | sudo xcode-select -s /Applications/Xcode_16.2.app 46 | 47 | - name: ✅ check xcode version 48 | run: | 49 | sudo xcodebuild -version 50 | 51 | - name: Build 52 | run: | 53 | xcodebuild -workspace ./MemorialHouse/MemorialHouse.xcworkspace -scheme MemorialHouse -destination 'platform=iOS Simulator,name=iPhone 16 Pro' 54 | -------------------------------------------------------------------------------- /MemorialHouse/MHDomain/MHDomainTests/MemorialHouseNameUseCaseTest.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MHDomain 3 | 4 | struct MemorialHouseUseCaseTest { 5 | var sut: FetchMemorialHouseNameUseCase! 6 | 7 | @Test mutating func test기록소_이름_가져오기() async throws { 8 | // Arrange 준비 단계: 테스트 대상 시스템(SUT)와 의존성을 원하는 상태로 만들기 9 | let dummyMemorialHouseName = "집주인들" 10 | let stubMemorialHouseNameRepository = StubMemorialHouseRepository( 11 | dummyMemorialHouseName: dummyMemorialHouseName 12 | ) 13 | self.sut = DefaultFetchMemorialHouseNameUseCase(repository: stubMemorialHouseNameRepository) 14 | 15 | // Act 실행 단계: SUT 메소드를 호출하면서 의존성을 전달해서 결과를 저장하기 16 | let result = try await sut.execute() 17 | 18 | // Assert 검증 단계: 결과와 기대치를 비교해서 검증하기 19 | #expect(result == dummyMemorialHouseName) 20 | } 21 | 22 | // TODO: 기록소 이름이 아닌, 책 이름으로 변경 23 | @Test mutating func test세글자_이상인_경우_원본_문자열_그대로_반환() async throws { 24 | // Arrange 준비 단계: 테스트 대상 시스템(SUT)와 의존성을 원하는 상태로 만들기 25 | let dummyMemorialHouseName = "집주인들" 26 | let stubMemorialHouseNameRepository = StubMemorialHouseRepository( 27 | dummyMemorialHouseName: dummyMemorialHouseName 28 | ) 29 | self.sut = DefaultFetchMemorialHouseNameUseCase(repository: stubMemorialHouseNameRepository) 30 | 31 | // Act 실행 단계: SUT 메소드를 호출하면서 의존성을 전달해서 결과를 저장하기 32 | let result = try await sut.execute() 33 | 34 | // Assert 검증 단계: 결과와 기대치를 비교해서 검증하기 35 | #expect(result == dummyMemorialHouseName) 36 | } 37 | 38 | @Test mutating func test두글자_이하인_경우_글자_사이에_공백추가() async throws { 39 | // Arrange 준비 단계: 테스트 대상 시스템(SUT)와 의존성을 원하는 상태로 만들기 40 | let dummyMemorialHouseName = "집주인들" 41 | let stubMemorialHouseNameRepository = StubMemorialHouseRepository( 42 | dummyMemorialHouseName: dummyMemorialHouseName 43 | ) 44 | self.sut = DefaultFetchMemorialHouseNameUseCase(repository: stubMemorialHouseNameRepository) 45 | 46 | // Act 실행 단계: SUT 메소드를 호출하면서 의존성을 전달해서 결과를 저장하기 47 | let result = try await sut.execute() 48 | 49 | // Assert 검증 단계: 결과와 기대치를 비교해서 검증하기 50 | #expect(result == dummyMemorialHouseName) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHVideoView.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | import MHCore 3 | import MHDomain 4 | 5 | final class MHVideoView: UIView { 6 | // MARK: - Property 7 | let playerViewController = AVPlayerViewController() 8 | private var timeControlStatusObservation: NSKeyValueObservation? 9 | 10 | // MARK: - Initializer 11 | init() { 12 | super.init(frame: .zero) 13 | configureConstraint() 14 | NotificationCenter.default.addObserver(self, selector: #selector(stopPlayer), name: .mediaPlaybackStarted, object: nil) 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | configureConstraint() 20 | NotificationCenter.default.addObserver(self, selector: #selector(stopPlayer), name: .mediaPlaybackStarted, object: nil) 21 | } 22 | 23 | // MARK: - Configuration 24 | func configurePlayer(player: AVPlayer) { 25 | playerViewController.player = player 26 | playerViewController.showsPlaybackControls = true 27 | timeControlStatusObservation = player.observe(\.timeControlStatus, options: [.new]) { player, change in 28 | switch player.timeControlStatus { 29 | case .playing: 30 | NotificationCenter.default.post(name: .mediaPlaybackStarted, object: self) 31 | default: 32 | break 33 | } 34 | } 35 | } 36 | 37 | private func configureConstraint() { 38 | addSubview(playerViewController.view) 39 | playerViewController.view.fillSuperview() 40 | } 41 | 42 | @objc 43 | private func stopPlayer(_ notification: NSNotification) { 44 | guard notification.object as? MHVideoView !== self else { return } 45 | playerViewController.player?.pause() 46 | } 47 | 48 | // MARK: - LifeCycle 49 | override func didMoveToWindow() { 50 | super.didMoveToSuperview() 51 | 52 | if window == nil { 53 | playerViewController.player?.pause() 54 | } 55 | } 56 | 57 | deinit { 58 | timeControlStatusObservation?.invalidate() 59 | } 60 | } 61 | 62 | extension MHVideoView: MediaAttachable { 63 | func configureSource(with mediaDescription: MediaDescription, data: Data) { 64 | let player = AVPlayer() 65 | configurePlayer(player: player) 66 | } 67 | 68 | func configureSource(with mediaDescription: MediaDescription, url: URL) { 69 | let player = AVPlayer(url: url) 70 | configurePlayer(player: player) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHDataTests/UserDefaultsMemorialHouseNameStorageTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MHData 3 | @testable import MHCore 4 | import Foundation 5 | 6 | struct UserDefaultsMemorialHouseNameStorageTest { 7 | @Test func test저장소에_기록소_이름을_저장한다() async throws { 8 | // Arrange 9 | let suiteName = UUID().uuidString 10 | let userDefaults = UserDefaults(suiteName: suiteName)! 11 | let storage = UserDefaultsMemorialHouseNameStorage(userDefaults: userDefaults) 12 | let testName = "테스트 기록소" 13 | 14 | // Act 15 | let result = await storage.create(with: testName) 16 | 17 | // Assert 18 | switch result { 19 | case .success: 20 | // 비동기 작업 후 일정 시간 대기 (필요 시) 21 | let savedName = userDefaults.string(forKey: Constant.houseNameUserDefaultKey) 22 | #expect(savedName == testName) 23 | case .failure: 24 | throw MHDataError.noSuchEntity(key: suiteName) 25 | } 26 | 27 | userDefaults.removePersistentDomain(forName: suiteName) 28 | } 29 | 30 | @Test func test_fetch_저장소에서_기록소_이름을_불러온다() async throws { 31 | // Arrange 32 | let suiteName = UUID().uuidString 33 | let userDefaults = UserDefaults(suiteName: suiteName)! 34 | let storage = UserDefaultsMemorialHouseNameStorage(userDefaults: userDefaults) 35 | let testName = "테스트 기록소" 36 | userDefaults.set(testName, forKey: Constant.houseNameUserDefaultKey) 37 | 38 | // Act 39 | let result = await storage.fetch() 40 | 41 | // Assert 42 | switch result { 43 | case .success(let fetchedName): 44 | #expect(fetchedName == testName) 45 | case .failure: 46 | throw MHDataError.noSuchEntity(key: suiteName) 47 | } 48 | 49 | userDefaults.removePersistentDomain(forName: suiteName) 50 | } 51 | 52 | @Test func test_fetch_기록소_이름이_없을때_에러를_반환한다() async throws { 53 | // Arrange 54 | let suiteName = UUID().uuidString 55 | let userDefaults = UserDefaults(suiteName: suiteName)! 56 | userDefaults.removePersistentDomain(forName: suiteName) 57 | let storage = UserDefaultsMemorialHouseNameStorage(userDefaults: userDefaults) 58 | 59 | // Act 60 | let result = await storage.fetch() 61 | 62 | // Assert 63 | switch result { 64 | case .success: 65 | throw MHDataError.noSuchEntity(key: suiteName) 66 | case .failure(let error): 67 | #expect(error == .noSuchEntity(key: Constant.houseNameUserDefaultKey)) 68 | } 69 | 70 | userDefaults.removePersistentDomain(forName: suiteName) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/ViewModel/ReadPageViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHDomain 3 | import MHCore 4 | import Combine 5 | 6 | public final class ReadPageViewModel: ViewModelType { 7 | enum Input { 8 | case loadPage 9 | case didRequestMediaDataForData(media: MediaDescription) 10 | case didRequestMediaDataForURL(media: MediaDescription) 11 | } 12 | 13 | enum Output { 14 | case loadPage(page: Page?) 15 | case mediaLoadedWithData(media: MediaDescription, data: Data) 16 | case mediaLoadedWithURL(media: MediaDescription, url: URL) 17 | case error(message: String) 18 | } 19 | 20 | private let fetchMediaUseCase: FetchMediaUseCase 21 | private let output = PassthroughSubject() 22 | private var cancellables = Set() 23 | 24 | private let bookID: UUID 25 | private let page: Page 26 | 27 | init( 28 | fetchMediaUseCase: FetchMediaUseCase, 29 | bookID: UUID, 30 | page: Page 31 | ) { 32 | self.fetchMediaUseCase = fetchMediaUseCase 33 | self.bookID = bookID 34 | self.page = page 35 | } 36 | 37 | func transform(input: AnyPublisher) -> AnyPublisher { 38 | input.sink { [weak self] event in 39 | switch event { 40 | case .loadPage: 41 | self?.output.send(.loadPage(page: self?.page)) 42 | case .didRequestMediaDataForData(let media): 43 | Task { await self?.loadMediaForData(media: media) } 44 | case .didRequestMediaDataForURL(let media): 45 | Task { await self?.loadMediaForURL(media: media) } 46 | } 47 | }.store(in: &cancellables) 48 | 49 | return output.eraseToAnyPublisher() 50 | } 51 | 52 | private func loadMediaForData(media: MediaDescription) async { 53 | do { 54 | let mediaData: Data = try await fetchMediaUseCase.execute(media: media, in: bookID) 55 | output.send(.mediaLoadedWithData(media: media, data: mediaData)) 56 | } catch { 57 | MHLogger.error(error.localizedDescription + #function) 58 | output.send(.error(message: "미디어 로딩에 실패하였습니다.")) 59 | } 60 | } 61 | 62 | private func loadMediaForURL(media: MediaDescription) async { 63 | do { 64 | let mediaURL: URL = try await fetchMediaUseCase.execute(media: media, in: bookID) 65 | output.send(.mediaLoadedWithURL(media: media, url: mediaURL)) 66 | } catch { 67 | output.send(.error(message: "미디어 로딩에 실패하였습니다.")) 68 | MHLogger.error(error.localizedDescription + #function) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Book/ViewModel/BookViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | import MHDomain 4 | import Combine 5 | 6 | public final class BookViewModel: ViewModelType { 7 | enum Input { 8 | case loadBookTitle 9 | case loadBook 10 | case loadPreviousPage 11 | case loadNextPage 12 | case editBook 13 | } 14 | 15 | enum Output { 16 | case setBookTitle(with: String?) 17 | case loadFirstPage(page: Page?) 18 | case moveToEdit(bookID: UUID, bookTitle: String) 19 | } 20 | 21 | private let fetchBookUseCase: FetchBookUseCase 22 | private let output = PassthroughSubject() 23 | private var cancellables = Set() 24 | 25 | let identifier: UUID 26 | private var book: Book? 27 | private let bookTitle: String 28 | private var nowPageIndex: Int = 0 29 | var previousPage: Page? { nowPageIndex > 0 ? book?.pages[nowPageIndex - 1] : nil } 30 | var nextPage: Page? { nowPageIndex < (book?.pages.count ?? 0) - 1 ? book?.pages[nowPageIndex + 1] : nil } 31 | 32 | init( 33 | fetchBookUseCase: FetchBookUseCase, 34 | identifier: UUID, 35 | bookTitle: String 36 | ) { 37 | self.fetchBookUseCase = fetchBookUseCase 38 | self.identifier = identifier 39 | self.bookTitle = bookTitle 40 | } 41 | 42 | func transform(input: AnyPublisher) -> AnyPublisher { 43 | input.sink { [weak self] event in 44 | switch event { 45 | case .loadBookTitle: 46 | self?.output.send(.setBookTitle(with: self?.bookTitle)) 47 | case .loadBook: 48 | Task { await self?.fetchBook() } 49 | case .loadPreviousPage: 50 | if self?.nowPageIndex ?? 0 > 0 { 51 | self?.nowPageIndex -= 1 52 | } 53 | case .loadNextPage: 54 | if self?.nowPageIndex ?? 0 < (self?.book?.pages.count ?? 0) - 1 { 55 | self?.nowPageIndex += 1 56 | } 57 | case .editBook: 58 | guard let self else { return } 59 | self.output.send(.moveToEdit(bookID: self.identifier, bookTitle: bookTitle)) 60 | } 61 | } 62 | .store(in: &cancellables) 63 | 64 | return output.eraseToAnyPublisher() 65 | } 66 | 67 | private func fetchBook() async { 68 | do { 69 | book = try await fetchBookUseCase.execute(id: identifier) 70 | output.send(.loadFirstPage(page: book?.pages[nowPageIndex])) 71 | } catch { 72 | MHLogger.error("책을 불러오는 중에 에러 발생: \(error.localizedDescription)") 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookCategoryStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | import CoreData 4 | 5 | public final class CoreDataBookCategoryStorage { 6 | private let coreDataStorage: CoreDataStorage 7 | 8 | public init(coreDataStorage: CoreDataStorage) { 9 | self.coreDataStorage = coreDataStorage 10 | } 11 | } 12 | 13 | extension CoreDataBookCategoryStorage: BookCategoryStorage { 14 | public func create(with category: BookCategoryDTO) async throws { 15 | let context = coreDataStorage.createBackgroundContext() 16 | try await context.perform { 17 | guard let entity = NSEntityDescription.entity(forEntityName: "BookCategoryEntity", in: context) else { 18 | throw MHDataError.noSuchEntity(key: "BookCategoryEntity") 19 | } 20 | let bookCategory = NSManagedObject(entity: entity, insertInto: context) 21 | bookCategory.setValue(category.order, forKey: "order") 22 | bookCategory.setValue(category.name, forKey: "name") 23 | try context.save() 24 | } 25 | } 26 | 27 | public func fetch() async throws -> [BookCategoryDTO] { 28 | let context = coreDataStorage.createBackgroundContext() 29 | let bookCategoryEntities = try await context.perform { 30 | let request = BookCategoryEntity.fetchRequest() 31 | return try context.fetch(request) 32 | } 33 | 34 | return bookCategoryEntities.compactMap { coreBookCategoryToDTO($0) } 35 | } 36 | 37 | public func update(oldName: String, with category: BookCategoryDTO) async throws { 38 | let context = coreDataStorage.createBackgroundContext() 39 | try await context.perform { 40 | let request = BookCategoryEntity.fetchRequest() 41 | if let entity = try context.fetch(request).first(where: { $0.name == oldName }) { 42 | entity.setValue(category.name, forKey: "name") 43 | entity.setValue(category.order, forKey: "order") 44 | try context.save() 45 | } 46 | } 47 | } 48 | 49 | public func delete(with categoryName: String) async throws { 50 | let context = coreDataStorage.createBackgroundContext() 51 | try await context.perform { 52 | let request = BookCategoryEntity.fetchRequest() 53 | if let entity = try context.fetch(request).first(where: { $0.name == categoryName }) { 54 | context.delete(entity) 55 | try context.save() 56 | } 57 | } 58 | } 59 | } 60 | 61 | // MARK: - Mapper 62 | extension CoreDataBookCategoryStorage { 63 | func coreBookCategoryToDTO(_ bookCategory: BookCategoryEntity) -> BookCategoryDTO? { 64 | guard let name = bookCategory.name else { return nil } 65 | let order = Int(bookCategory.order) 66 | 67 | return BookCategoryDTO( 68 | order: order, 69 | name: name 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/EditVideo/EditVideoViewController.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | 3 | final class EditVideoViewController: UIViewController { 4 | // MARK: - Properties 5 | private let videoURL: URL 6 | private let completion: (URL) -> Void 7 | private let videoView = MHVideoView() 8 | 9 | // MARK: - Initializer 10 | init(videoURL: URL, videoSelectCompletionHandler: @escaping (URL) -> Void) { 11 | self.videoURL = videoURL 12 | self.completion = videoSelectCompletionHandler 13 | 14 | super.init(nibName: nil, bundle: nil) 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | self.videoURL = URL(fileURLWithPath: "") 19 | self.completion = { _ in } 20 | 21 | super.init(coder: coder) 22 | } 23 | 24 | // MARK: - Life Cycle 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | setup() 29 | configureAudioSessionForPlayback() 30 | configureVideoView() 31 | configureNavigationBar() 32 | } 33 | 34 | // MARK: - Setup & Configuration 35 | private func setup() { 36 | view.backgroundColor = .baseBackground 37 | let player = AVPlayer(url: videoURL) 38 | videoView.configurePlayer(player: player) 39 | } 40 | 41 | private func configureVideoView() { 42 | addChild(videoView.playerViewController) 43 | view.addSubview(videoView) 44 | videoView.fillSuperview() 45 | } 46 | 47 | private func configureNavigationBar() { 48 | navigationController?.navigationBar.isHidden = false 49 | navigationController?.navigationBar.titleTextAttributes = [ 50 | .font: UIFont.ownglyphBerry(size: 17), 51 | .foregroundColor: UIColor.black 52 | ] 53 | navigationItem.title = "동영상 업로드".localized() 54 | 55 | // 공통 스타일 정의 56 | let normalAttributes: [NSAttributedString.Key: Any] = [ 57 | .font: UIFont.ownglyphBerry(size: 17), 58 | .foregroundColor: UIColor.mhTitle 59 | ] 60 | let selectedAttributes: [NSAttributedString.Key: Any] = [ 61 | .font: UIFont.ownglyphBerry(size: 17), 62 | .foregroundColor: UIColor.mhTitle 63 | ] 64 | 65 | // 좌측 편집 버튼 66 | let editButton = UIBarButtonItem( 67 | title: "취소".localized(), 68 | normal: normalAttributes, 69 | selected: selectedAttributes 70 | ) { [weak self] in 71 | self?.navigationController?.popViewController(animated: true) 72 | } 73 | navigationItem.leftBarButtonItem = editButton 74 | 75 | // 우측 추가 버튼 76 | navigationItem.rightBarButtonItem = UIBarButtonItem( 77 | title: "추가".localized(), 78 | normal: normalAttributes, 79 | selected: selectedAttributes 80 | ) { [weak self] in 81 | self?.completion(self?.videoURL ?? URL(fileURLWithPath: "")) 82 | self?.dismiss(animated: true) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "icon-20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "icon-29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "icon-29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "icon-38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "icon-38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "icon-40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "icon-40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "icon-60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "icon-60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "icon-64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "icon-64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "icon-68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "icon-76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "icon-83_5@2x.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "ios-marketing.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import MHCore 4 | import MHDomain 5 | 6 | public final class CreateAudioViewModel: ViewModelType { 7 | // MARK: - Type 8 | enum Input { 9 | case prepareTemporaryAudio 10 | case audioButtonTapped 11 | case saveButtonTapped 12 | case recordCancelled 13 | } 14 | enum Output { 15 | case audioFileURL(url: URL) 16 | case audioStart 17 | case audioStop 18 | case recordCompleted 19 | } 20 | 21 | // MARK: - Property 22 | private let output = PassthroughSubject() 23 | private var cancellables = Set() 24 | private var audioIsRecoding: Bool = false 25 | private let completion: (MediaDescription?) -> Void 26 | private let temporaryStoreMediaUsecase: TemporaryStoreMediaUseCase 27 | private var mediaDescription: MediaDescription? 28 | 29 | // MARK: - Initializer 30 | init( 31 | temporaryStoreMediaUsecase: TemporaryStoreMediaUseCase, 32 | completion: @escaping (MediaDescription?) -> Void 33 | ) { 34 | self.temporaryStoreMediaUsecase = temporaryStoreMediaUsecase 35 | self.completion = completion 36 | } 37 | 38 | // MARK: - Method 39 | func transform(input: AnyPublisher) -> AnyPublisher { 40 | input.sink { [weak self] event in 41 | switch event { 42 | case .prepareTemporaryAudio: 43 | Task { await self?.prepareTemporaryAudio() } 44 | case .audioButtonTapped: 45 | self?.audioButtonTapped() 46 | case .saveButtonTapped: 47 | self?.completeRecord(withCompletion: true) 48 | case .recordCancelled: 49 | self?.completeRecord(withCompletion: false) 50 | } 51 | }.store(in: &cancellables) 52 | 53 | return output.eraseToAnyPublisher() 54 | } 55 | 56 | // MARK: - Helper 57 | private func prepareTemporaryAudio() async { 58 | let mediaDescription = MediaDescription(type: .audio) 59 | self.mediaDescription = mediaDescription 60 | do { 61 | let url = try await temporaryStoreMediaUsecase.execute(media: mediaDescription) 62 | output.send(.audioFileURL(url: url)) 63 | } catch { 64 | MHLogger.error(error.localizedDescription + #function) 65 | completion(nil) 66 | output.send(.recordCompleted) 67 | } 68 | } 69 | private func audioButtonTapped() { 70 | switch audioIsRecoding { 71 | case false: 72 | output.send(.audioStart) 73 | case true: 74 | output.send(.audioStop) 75 | } 76 | audioIsRecoding.toggle() 77 | } 78 | 79 | private func completeRecord(withCompletion: Bool) { 80 | if audioIsRecoding { 81 | output.send(.audioStop) 82 | } 83 | output.send(.recordCompleted) 84 | 85 | if withCompletion { 86 | completion(mediaDescription) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+Background.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | /// 뷰에 빨간 테두리와 배경을 추가한 뷰를 반환해줍니다. 5 | /// - Parameter insets: border와 content사이의 간격입니다. 6 | /// - Returns: 배경 스타일이 적용된 새로운 뷰 7 | func embededInDefaultBackground( 8 | with insets: UIEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 9 | ) -> UIView { 10 | let borderView = UIView() 11 | borderView.backgroundColor = .clear 12 | borderView.layer.cornerRadius = 17 13 | borderView.layer.borderWidth = 1.5 14 | borderView.layer.borderColor = UIColor.mhBorder.cgColor 15 | borderView.clipsToBounds = true 16 | borderView.addSubview(self) 17 | self.setAnchor( 18 | top: borderView.topAnchor, constantTop: insets.top, 19 | leading: borderView.leadingAnchor, constantLeading: insets.left, 20 | bottom: borderView.bottomAnchor, constantBottom: insets.bottom, 21 | trailing: borderView.trailingAnchor, constantTrailing: insets.right 22 | ) 23 | 24 | let backGroundView = UIView() 25 | backGroundView.backgroundColor = .mhSection 26 | backGroundView.layer.cornerRadius = 20 27 | backGroundView.clipsToBounds = true 28 | backGroundView.addSubview(borderView) 29 | borderView.setAnchor( 30 | top: backGroundView.topAnchor, constantTop: 6, 31 | leading: backGroundView.leadingAnchor, constantLeading: 7, 32 | bottom: backGroundView.bottomAnchor, constantBottom: 6, 33 | trailing: backGroundView.trailingAnchor, constantTrailing: 7 34 | ) 35 | 36 | return backGroundView 37 | } 38 | 39 | /// 뷰의 왼쪽 / 오른쪽에 String으로 입력받은 텍스트를 추가합니다. 40 | /// 글꼴은 default를 사용합니다. 41 | /// 5포인트의 간격을 갖습니다. 42 | /// - Parameters: 43 | /// - leftTitle: 왼쪽에 넣을 텍스트 44 | /// - rightTitle: 오른쪽에 넣을 텍스트 45 | /// - Returns: 해당 텍스트가 추가된 새로운 뷰 46 | func titledContentView(leftTitle: String? = nil, rightTitle: String? = nil) -> UIView { 47 | let wrappedView = UIStackView(arrangedSubviews: [self]) 48 | wrappedView.axis = .horizontal 49 | wrappedView.distribution = .fill 50 | wrappedView.alignment = .fill 51 | wrappedView.spacing = 5 52 | 53 | if let leftTitle { 54 | let leftLabel = UILabel(style: .body1) 55 | leftLabel.text = leftTitle 56 | leftLabel.setContentHuggingPriority(UILayoutPriority(751), for: .horizontal) 57 | leftLabel.setContentCompressionResistancePriority(UILayoutPriority(751), for: .horizontal) 58 | 59 | wrappedView.insertArrangedSubview(leftLabel, at: 0) 60 | } 61 | if let rightTitle { 62 | let rightLabel = UILabel(style: .body2) 63 | rightLabel.text = rightTitle 64 | rightLabel.setContentHuggingPriority(UILayoutPriority(752), for: .horizontal) 65 | rightLabel.setContentCompressionResistancePriority(UILayoutPriority(752), for: .horizontal) 66 | 67 | wrappedView.addArrangedSubview(rightLabel) 68 | } 69 | 70 | return wrappedView 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MemorialHouse/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # 사용하지 않는 룰 2 | disabled_rules: 3 | # 라인의 마지막에는 빈 여백이 있으면 안 됨 => 라인 마지막에 여백 있어도 됨 4 | - trailing_whitespace 5 | 6 | # 타입은 최대 1단계로 중첩되어야 함 => 타입여러개 중첩돼도 됨 7 | - nesting 8 | 9 | # 함수 안은 복잡하면 안 됨 (warning: 10, error: 20) 10 | - cyclomatic_complexity 11 | 12 | # Protocol은 class-only여야 함. 13 | - class_delegate_protocol 14 | 15 | # 함수 매개변수들이 여러 개일 경우 세로로 같은 줄에 있어야 함 16 | - vertical_parameter_alignment 17 | 18 | # Void 함수를 호출하기 위해 삼항연산자을 사용하는 것은 피해야 한다. 19 | - void_function_in_ternary 20 | 21 | # 스위치 문의 케이스는 스위치와 같은 줄에 있어야 함 22 | - switch_case_alignment 23 | 24 | # fileprivate보다 private을 선호함. => fileprivate에 워닝 띄우지 않을꺼다 25 | - private_over_fileprivate 26 | 27 | analyzer_rules: # => 적용on 28 | # 모든 선언은 한 번 이상 사용되어야 함 29 | - unused_declaration 30 | 31 | # 명시적 룰 활성화 32 | opt_in_rules: 33 | # type name은 영숫자만 포함, 대문자로 시작해 3-40자 사이어야 함 34 | - type_name 35 | 36 | # delegate protocol은 class-only로 참조해 weak로 참조되도록 권장 37 | - class_delegate_protocol 38 | 39 | # 닫는 괄호 ')'와 '}' 사이에는 공백이 없어야 함 40 | - closing_brace 41 | 42 | # 클로저 내용과 괄호 사이에 공백이 있어야 함 43 | - closure_spacing 44 | 45 | # collection elem은 vertically aligned 되어야 함 46 | - collection_alignment 47 | 48 | # colon 사용 시 앞 공백 필수, 뒷 공백이 있으면 안 됨 49 | - colon 50 | 51 | # comma 사용 시 앞 공백 필수, 뒷 공백이 있으면 안 됨 52 | - comma 53 | 54 | # first(where:) != nil, firstIndex(where:) != nil 대신 contains 사용을 권장 (https://realm.github.io/SwiftLint/contains_over_first_not_nil.html) 55 | - contains_over_first_not_nil 56 | 57 | # filter.count 사용 시 isEmpty 대신 contains 사용 권장 (https://realm.github.io/SwiftLint/contains_over_filter_is_empty.html) 58 | - contains_over_filter_is_empty 59 | 60 | # filter.count가 0인지 비교할 때 contains 사용 권장 (https://realm.github.io/SwiftLint/contains_over_filter_count.html) 61 | - contains_over_filter_count 62 | 63 | # range(of:) == nil 체크 대신 contains 사용 권장 (https://realm.github.io/SwiftLint/contains_over_range_nil_comparison.html) 64 | - contains_over_range_nil_comparison 65 | 66 | # if, for, guard, switch, while, catch 사용 시 () 사용 권장하지 않음 67 | - control_statement 68 | 69 | # deployment target 보다 낮은 버전의 @available 사용 시 warning 70 | - deployment_target 71 | 72 | # 중복 import 방지 73 | - duplicate_imports 74 | 75 | # count가 0인지 체크할 때는 isEmpty 사용 권장 76 | - empty_count 77 | 78 | # collection, array count 체크 시 isEmpty 사용 권장 79 | - empty_collection_literal 80 | 81 | # string empty 체크 시 isEmpty 사용 권장 82 | - empty_string 83 | 84 | # 강제 언래핑 사용 금지 85 | - force_try 86 | 87 | # array, dict 사용 시 동일한 indent로 표현 88 | - literal_expression_end_indentation 89 | 90 | # let, var 선언 시 다른 statments와 한 줄 공백이 필요함 91 | # - let_var_whitespace # private, let, var 별로 뗄 수도 있으니까 92 | 93 | # 수직 공백 2줄 이상 사용 지양 94 | - vertical_whitespace 95 | 96 | # 여는 괄호 앞에서 한 줄 이상의 공백 사용 지양 97 | - vertical_whitespace_opening_braces 98 | 99 | # 닫는 괄호 앞에서 한 줄 이상의 공백 사용 지양 100 | - vertical_whitespace_closing_braces 101 | 102 | # delegate는 약한 참조 사용 권장 103 | - weak_delegate 104 | 105 | force_try: 106 | severity: warning # 명시적으로 지정 107 | 108 | line_length: 109 | warning: 120 110 | ignores_urls: true 111 | ignores_comments: true 112 | 113 | # path 114 | included: 115 | excluded: 116 | - "*.xcodeproj" 117 | - "*.xcworkspace" 118 | - ".git" 119 | - ".gitignore" 120 | - ".github" 121 | - "README.md" 122 | reporter: "xcode" 123 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/CoreData/MemorialHouseModel.xcdatamodeld/MemorialHouseModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MHCore 3 | 4 | public protocol FileStorage: Sendable { 5 | /// 지정된 경로에 파일을 생성합니다. 6 | /// Documents폴더에 파일을 생성합니다. 7 | /// 중간 경로 폴더를 자동으로 생성합니다. 8 | /// - Parameters: 9 | /// - path: Documents/{path} 이런식으로 들어갑니다. 10 | /// - name: Documents/{path}/{name} 이런식으로 저장됩니다. (확장자 명시 필요) 11 | /// - data: 실제 저장될 데이터 12 | /// - Returns: 성공여부를 반환합니다. 13 | func create(at path: String, fileName name: String, data: Data) async throws 14 | 15 | /// 지정된 경로의 파일을 읽어옵니다. 16 | /// Documents폴더에서 파일을 읽어옵니다 17 | /// - Parameters: 18 | /// - path: Documents/{path} 이런식으로 들어갑니다. 19 | /// - name: Documents/{path}/{name} 이런식으로 읽어옵니다. (확장자 명시 필요) 20 | /// - Returns: 파일 데이터를 반환합니다. 21 | func read(at path: String, fileName name: String) async throws -> Data 22 | 23 | /// 지정된 경로의 파일을 삭제합니다. 24 | /// Documents폴더에서 파일을 삭제합니다. 25 | /// - Parameters: 26 | /// - path: Documents/{path} 이런식으로 들어갑니다. 27 | /// - name: Documents/{path}/{name} 이런식으로 삭제합니다. (확장자 명시 필요) 28 | /// - Returns: 성공여부를 반환합니다. 29 | func delete(at path: String, fileName name: String) async throws 30 | 31 | /// 지정된 경로의 파일을 새로운 파일 이름으로 복사합니다. 32 | /// 지정된 경로 -> Documents폴더로 파일을 복사합니다. 33 | /// - Parameters: 34 | /// - url: 내부 저장소의 파일 URL (AVURLAsset등의 URL호환) 35 | /// - newPath: Documents/{newPath} 이런식으로 들어갑니다. 36 | /// - name: Documents/{newPath}/{name} 이런식으로 저장됩니다. (확장자 명시 필요) 37 | /// - Returns: 성공여부를 반환합니다. 38 | func copy(at url: URL, to newPath: String, newFileName name: String) async throws 39 | 40 | /// 지정된 경로의 파일을 복사합니다. 41 | /// Documents폴더 -> Documents폴더로 파일을 복사합니다. 42 | /// Documents/{path}/{name} => Documents/{newPath}/{name} 이런식으로 복사합니다. 43 | /// - Parameters: 44 | /// - path: Documents/{path} 이런식으로 들어갑니다 45 | /// - name: Documents/{path}/{name} 이 파일을 복사합니다. (확장자 명시 필요) 46 | /// - newPath: Documents/{newPath}/{name} 으로 저장합니다. 47 | /// - Returns: 성공여부를 반환합니다. 48 | func copy(at path: String, fileName name: String, to newPath: String) async throws 49 | 50 | /// 지정된 경로의 파일을 이동합니다. 51 | /// - Parameters: 52 | /// - path: Documents/{path} 이런식으로 들어갑니다 53 | /// - name: Documents/{path}/{name} 이 파일을 이동합니다. (확장자 명시 필요) 54 | /// - newPath: Documents/{newPath}/{name} 으로 이동합니다. 55 | /// - Returns: 성공여부를 반환합니다. 56 | func move(at path: String, fileName name: String, to newPath: String) async throws 57 | 58 | /// 지정된 경로의 모든 파일을 이동합니다. 59 | /// - Parameters: 60 | /// - path: Documents/{path} 이런식으로 들어갑니다 61 | /// - newPath: Documents/{newPath} 으로 이동합니다. 62 | /// - Returns: 성공여부를 반환합니다. 63 | func moveAll(in path: String, to newPath: String) async throws 64 | 65 | /// 지정된 경로의 로컬 파일 URL을 반환합니다. 66 | /// Documents폴더기준으로 파일 URL을 반환합니다. 67 | /// - Parameters: 68 | /// - path: Documents/{path} 이런식으로 들어갑니다 69 | /// - name: Documents/{path}/{name} 이 파일 URL을 반환합니다. (확장자 명시 필요) 70 | /// - Returns: 파일 URL을 반환합니다. 71 | func getURL(at path: String, fileName name: String) async throws -> URL 72 | 73 | func makeDirectory(through path: String) async throws 74 | 75 | /// 지정된 경로의 파일 목록을 반환합니다. 76 | /// Documents폴더를 기준으로 파일 이름 목록을 반환합니다. 77 | /// path는 디렉토리여야 합니다. 78 | /// - Parameters: 79 | /// - path: Documents/{path} 이런식으로 들어갑니다 80 | /// - Returns: 파일 이름 목록을 반환합니다 81 | func getFileNames(at path: String) async throws -> [String] 82 | } 83 | -------------------------------------------------------------------------------- /MemorialHouse/MHApplication/MHApplication/Resource/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHBookCover.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Foundation 3 | import MHDomain 4 | 5 | final class MHBookCover: UIButton { 6 | // MARK: - Property 7 | private let bookCoverImageView = UIImageView() 8 | private let bookTitleLabel: UILabel = { 9 | let label = UILabel(style: .header2) 10 | label.adjustsFontSizeToFitWidth = true 11 | 12 | return label 13 | }() 14 | private let targetImageView: UIImageView = { 15 | let imageView = UIImageView(image: UIImage(systemName: "person.crop.square")) 16 | imageView.tintColor = .white 17 | imageView.contentMode = .scaleAspectFit 18 | 19 | imageView.layer.shadowRadius = 4 20 | imageView.layer.shadowOpacity = 0.4 21 | imageView.layer.shadowOffset = CGSize(width: 4, height: 4) 22 | 23 | return imageView 24 | }() 25 | private let houseLabel = UILabel(style: .body3) 26 | 27 | // MARK: - Initializer 28 | init() { 29 | super.init(frame: .zero) 30 | 31 | configureAddSubView() 32 | configureConstraints() 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | 38 | configureAddSubView() 39 | configureConstraints() 40 | } 41 | 42 | func resetProperties() { 43 | bookCoverImageView.image = nil 44 | bookTitleLabel.text = nil 45 | targetImageView.image = UIImage(systemName: "person.crop.square") 46 | } 47 | 48 | // MARK: - Configuration 49 | func configure( 50 | title: String? = nil, 51 | bookCoverImage: UIImage? = nil, 52 | targetImage: UIImage? = nil, 53 | houseName: String? = nil 54 | ) { 55 | if let title { 56 | bookTitleLabel.text = title 57 | } 58 | if let bookCoverImage { 59 | bookCoverImageView.image = bookCoverImage 60 | } 61 | if let targetImage { 62 | targetImageView.image = targetImage.withAlignmentRectInsets( 63 | UIEdgeInsets( 64 | top: -5, 65 | left: -5, 66 | bottom: -5, 67 | right: -5 68 | ) 69 | ) 70 | } 71 | if let houseName { 72 | houseLabel.text = houseName 73 | } 74 | } 75 | 76 | private func configureAddSubView() { 77 | addSubview(bookCoverImageView) 78 | addSubview(bookTitleLabel) 79 | addSubview(targetImageView) 80 | addSubview(houseLabel) 81 | } 82 | 83 | private func configureConstraints() { 84 | bookCoverImageView.fillSuperview() 85 | bookTitleLabel.setTop(anchor: topAnchor, constant: 16) 86 | bookTitleLabel.setLeading(anchor: leadingAnchor, constant: 25) 87 | bookTitleLabel.setTrailing(anchor: trailingAnchor, constant: 12) 88 | targetImageView.setTop(anchor: bookTitleLabel.bottomAnchor, constant: 14) 89 | targetImageView.setAnchor( 90 | leading: bookTitleLabel.leadingAnchor, constantLeading: 5, 91 | trailing: bookTitleLabel.trailingAnchor, constantTrailing: 5 92 | ) 93 | houseLabel.setBottom(anchor: bottomAnchor, constant: 12) 94 | houseLabel.setTrailing(anchor: trailingAnchor, constant: 12) 95 | NSLayoutConstraint.activate([ 96 | targetImageView.centerXAnchor.constraint(equalTo: bookTitleLabel.centerXAnchor, constant: 3), 97 | targetImageView.heightAnchor.constraint(equalTo: targetImageView.widthAnchor) 98 | ]) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentationTests/HomeViewModelTest.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Testing 3 | import Foundation 4 | @testable import MHPresentation 5 | @testable import MHDomain 6 | 7 | struct HomeViewModelTest { 8 | private var sut: HomeViewModel! 9 | private var cancellables = Set() 10 | private static let bookCovers = [ 11 | BookCover(id: UUID(), order: 0, title: "title1", imageData: nil, color: .blue, category: nil, favorite: false), 12 | BookCover(id: UUID(), order: 1, title: "title2", imageData: nil, color: .blue, category: nil, favorite: false), 13 | BookCover(id: UUID(), order: 2, title: "title3", imageData: nil, color: .blue, category: nil, favorite: false) 14 | ] 15 | 16 | @MainActor 17 | @Test mutating func test홈화면을_시작할때_MemorialHouse_이름과_책커버들을_가져온다() async throws { 18 | // Arrange 19 | let stubFetchMemorialHouseNameUseCase = StubFetchMemorialHouseNameUseCase(dummyMemorialHouseName: "효준") 20 | let stubFetchAllBookCoverUseCase = StubFetchAllBookCoverUseCase() 21 | let stubUpdateBookCoverUseCase = StubUpdateBookCoverUseCase() 22 | let stubDeleteBookCoverUseCase = StubDeleteBookCoverUseCase() 23 | let stubBookCovers = try await stubFetchAllBookCoverUseCase.execute() 24 | sut = HomeViewModel( 25 | fetchMemorialHouseUseCase: stubFetchMemorialHouseNameUseCase, 26 | fetchAllBookCoverUseCase: stubFetchAllBookCoverUseCase, 27 | updateBookCoverUseCase: stubUpdateBookCoverUseCase, 28 | deleteBookCoverUseCase: stubDeleteBookCoverUseCase 29 | ) 30 | 31 | let input = PassthroughSubject() 32 | var receivedOutput: [HomeViewModel.Output] = [] 33 | 34 | sut.transform(input: input.eraseToAnyPublisher()) 35 | .sink { output in 36 | receivedOutput.append(output) 37 | } 38 | .store(in: &cancellables) 39 | 40 | // Act 41 | receivedOutput.removeAll() 42 | input.send(.loadAllBookCovers) 43 | try await Task.sleep(nanoseconds: 500_000_000) 44 | 45 | // Assert 46 | #expect(receivedOutput.count == 2) 47 | #expect(receivedOutput.contains(.fetchedMemorialHouseName)) 48 | #expect(receivedOutput.contains(.reloadData)) 49 | #expect(sut.houseName == "효준") 50 | #expect(sut.bookCovers == stubBookCovers) 51 | } 52 | 53 | @MainActor 54 | @Test mutating func test카테고리_선택시_해당_카테고리에_맞는_책들로_필터링한다() async throws { 55 | // Arrange 56 | sut = HomeViewModel( 57 | fetchMemorialHouseUseCase: StubFetchMemorialHouseNameUseCase(dummyMemorialHouseName: "효준"), 58 | fetchAllBookCoverUseCase: StubFetchAllBookCoverUseCase(), 59 | updateBookCoverUseCase: StubUpdateBookCoverUseCase(), 60 | deleteBookCoverUseCase: StubDeleteBookCoverUseCase() 61 | ) 62 | let input = PassthroughSubject() 63 | var receivedOutput: [HomeViewModel.Output] = [] 64 | sut.transform(input: input.eraseToAnyPublisher()) 65 | .sink { output in 66 | receivedOutput.append(output) 67 | } 68 | .store(in: &cancellables) 69 | 70 | // Act 71 | input.send(.loadAllBookCovers) 72 | try await Task.sleep(nanoseconds: 500_000_000) 73 | receivedOutput.removeAll() 74 | 75 | input.send(.selectedCategory(category: "친구")) 76 | try await Task.sleep(nanoseconds: 500_000_000) 77 | 78 | // Assert 79 | #expect(receivedOutput.count == 1) 80 | #expect(receivedOutput.contains(.reloadData)) 81 | #expect(sut.currentBookCovers.count == 1) 82 | #expect(sut.currentBookCovers.first?.title == "title1") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /MemorialHouse/MHPresentation/MHPresentationTests/RegisterViewModelTest.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Testing 3 | @testable import MHPresentation 4 | 5 | struct RegisterViewModelTest { 6 | private var sut: RegisterViewModel! 7 | private var cancellables = Set() 8 | 9 | @MainActor 10 | @Test mutating func test입력받은_기록소_이름을_저장한다() async throws { 11 | // Arrange 12 | let stubUseCase = StubCreateMemorialHouseNameUseCase() // 스텁 설정 13 | sut = RegisterViewModel(createMemorialHouseNameUseCase: stubUseCase) 14 | let input = PassthroughSubject() 15 | var receivedOutput: [RegisterViewModel.Output] = [] 16 | sut.transform(input: input.eraseToAnyPublisher()) 17 | .sink { output in 18 | receivedOutput.append(output) 19 | } 20 | .store(in: &cancellables) 21 | 22 | // Act 23 | input.send(.registerButtonTapped(memorialHouseName: "집주인들")) 24 | try await Task.sleep(nanoseconds: 500_000_000) 25 | 26 | // Assert 27 | #expect(receivedOutput.count == 1) 28 | #expect(receivedOutput.contains(.moveToHome)) 29 | } 30 | 31 | @MainActor 32 | @Test mutating func test빈_텍스트필드_입력시_등록버튼이_비활성화된다() throws { 33 | // Arrange 34 | let stubUseCase = StubCreateMemorialHouseNameUseCase() 35 | sut = RegisterViewModel(createMemorialHouseNameUseCase: stubUseCase) 36 | 37 | let input = PassthroughSubject() 38 | var receivedOutput: [RegisterViewModel.Output] = [] 39 | sut.transform(input: input.eraseToAnyPublisher()) 40 | .sink { output in 41 | receivedOutput.append(output) 42 | } 43 | .store(in: &cancellables) 44 | 45 | // Act 46 | input.send(.registerTextFieldEdited(text: "")) 47 | 48 | // Assert 49 | #expect(receivedOutput.count == 1) 50 | #expect(receivedOutput.contains(.registerButtonEnabled(isEnabled: false))) 51 | } 52 | 53 | @MainActor 54 | @Test mutating func test기록소_이름이_11글자_이상이면_등록버튼이_비활성화된다() throws { 55 | // Arrange 56 | let stubUseCase = StubCreateMemorialHouseNameUseCase() 57 | sut = RegisterViewModel(createMemorialHouseNameUseCase: stubUseCase) 58 | 59 | let input = PassthroughSubject() 60 | var receivedOutput: [RegisterViewModel.Output] = [] 61 | sut.transform(input: input.eraseToAnyPublisher()) 62 | .sink { output in 63 | receivedOutput.append(output) 64 | } 65 | .store(in: &cancellables) 66 | 67 | // Act 68 | input.send(.registerTextFieldEdited(text: "이름이_너무너무너무너무_길어요")) 69 | 70 | // Assert 71 | #expect(receivedOutput.count == 1) 72 | #expect(receivedOutput.contains(.registerButtonEnabled(isEnabled: false))) 73 | } 74 | 75 | @MainActor 76 | @Test mutating func test기록소_이름_입력시_등록버튼이_활성화된다() throws { 77 | // Arrange 78 | let stubUseCase = StubCreateMemorialHouseNameUseCase() 79 | sut = RegisterViewModel(createMemorialHouseNameUseCase: stubUseCase) 80 | 81 | let input = PassthroughSubject() 82 | var receivedOutput: [RegisterViewModel.Output] = [] 83 | sut.transform(input: input.eraseToAnyPublisher()) 84 | .sink { output in 85 | receivedOutput.append(output) 86 | } 87 | .store(in: &cancellables) 88 | 89 | // Act 90 | input.send(.registerTextFieldEdited(text: "올바른 이름")) 91 | 92 | // Assert 93 | #expect(receivedOutput.count == 1) 94 | #expect(receivedOutput.contains(.registerButtonEnabled(isEnabled: true))) 95 | } 96 | } 97 | --------------------------------------------------------------------------------