diff --git a/MyLuxury/Data/Sources/Data/Repository/ExampleRepositoryImpl.swift b/MyLuxury/Data/Sources/Data/Repository/ExampleRepositoryImpl.swift deleted file mode 100644 index 8044bf0..0000000 --- a/MyLuxury/Data/Sources/Data/Repository/ExampleRepositoryImpl.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ExampleRepositoryImpl.swift -// MyLuxury -// -// Created by KoSungmin on 10/3/24. -// - -import Foundation -import Combine -import Domain - -//public class ExampleRepositoryImpl: ExampleRepository { -// -// /// NetworkManager 사용 예정 -// func fetchData() -> AnyPublisher { -// -// let url = URL(string: "localhost:8080/")! -// -// return URLSession.shared.dataTaskPublisher(for: url) -// /// 네트워크 실패 시 실행되는 연산자 -// .catch { error in -// return Fail(error: error).eraseToAnyPublisher() -// } -// .map { $0.data } -// .decode(type: ExampleRespDTO.self, decoder: JSONDecoder()) -// .map { $0.toExampleEntity() } -// .eraseToAnyPublisher() -// } -//} - - diff --git a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift index 21eade6..c7c432b 100644 --- a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift +++ b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift @@ -16,49 +16,44 @@ public class PostRepositoryImpl: PostRepository { } public func getHomeViewData() -> AnyPublisher { - let homePostData: HomePostData = ( - todayPickPostData: Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), + let homePostData: HomePostData = HomePostData( + todayPickPostData: [Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1")], newPostData: [ Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2") + Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "9", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "10", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "11", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "12", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), ], weeklyTopPostData: [ - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7") + Post(post_id: "13", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "14", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "15", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "16", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "17", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "18", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "19", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "20", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "21", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "22", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7") ], customizedPostData: [ - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - ], - gridData: [ - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "23", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "24", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "25", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "26", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), ], editorRecommendationPostData: [ - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") + Post(post_id: "27", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "28", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "29", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "30", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") ] ) return Just(homePostData).eraseToAnyPublisher() diff --git a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift index a247d12..2a29633 100644 --- a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift +++ b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift @@ -15,50 +15,47 @@ public class PostRepositoryMockImpl: PostRepository { } public func getHomeViewData() -> AnyPublisher { - let homePostData: HomePostData = ( - todayPickPostData: Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), + let homePostData: HomePostData = HomePostData( + sectionOrder: [.todayPick, .new, .weeklyTop, .customized, .editorRecommendation], + todayPickPostData: [Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1")], newPostData: [ Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2") + Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "9", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "10", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "11", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "12", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), ], weeklyTopPostData: [ - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7") + Post(post_id: "13", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "14", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "15", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "16", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "17", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "18", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "19", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "20", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "21", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "22", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7") ], customizedPostData: [ - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - ], - gridData: [ - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "23", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "24", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "25", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "26", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), ], editorRecommendationPostData: [ - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") + Post(post_id: "27", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "28", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "29", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "30", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") ] + ) return Just(homePostData).eraseToAnyPublisher() } @@ -94,6 +91,146 @@ public class PostRepositoryMockImpl: PostRepository { postUpdatedAt: Date() ), Post(post_id: "3", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "4", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "5", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "6", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "7", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "8", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "9", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "10", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "11", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "12", + postUIType: .normal, + postCategory: .culture, + postTitle: "그때 그 시절, 무엇을 하며 놀았을까요?", + postThumbnailImage: "testImage2", + postImages: ["testImage2", "testImage2", "testImage2", "testImage2"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage2testImage21", "testImage2", "testImage2", "testImage2"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "13", postUIType: .normal, postCategory: .culture, postTitle: "그래서 이 아저씨가 누군데??", @@ -107,7 +244,133 @@ public class PostRepositoryMockImpl: PostRepository { postCreatedAt: Date(), postUpdatedAt: Date() ), - Post(post_id: "4", + Post(post_id: "14", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "15", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "16", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "17", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "18", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "19", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "20", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "21", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "22", + postUIType: .normal, + postCategory: .culture, + postTitle: "그래서 이 아저씨가 누군데??", + postThumbnailImage: "testImage7", + postImages: ["testImage7", "testImage7", "testImage7", "testImage7"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage7", "testImage7", "testImage7", "testImage7"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "23", postUIType: .normal, postCategory: .culture, postTitle: "흑백 사진은 언제 처음 사용됐을까?", @@ -121,7 +384,49 @@ public class PostRepositoryMockImpl: PostRepository { postCreatedAt: Date(), postUpdatedAt: Date() ), - Post(post_id: "5", + Post(post_id: "24", + postUIType: .normal, + postCategory: .culture, + postTitle: "흑백 사진은 언제 처음 사용됐을까?", + postThumbnailImage: "testImage3", + postImages: ["testImage3", "testImage3", "testImage3", "testImage3"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3", "testImage3", "testImage3", "testImage3"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "25", + postUIType: .normal, + postCategory: .culture, + postTitle: "흑백 사진은 언제 처음 사용됐을까?", + postThumbnailImage: "testImage3", + postImages: ["testImage3", "testImage3", "testImage3", "testImage3"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3", "testImage3", "testImage3", "testImage3"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "26", + postUIType: .normal, + postCategory: .culture, + postTitle: "흑백 사진은 언제 처음 사용됐을까?", + postThumbnailImage: "testImage3", + postImages: ["testImage3", "testImage3", "testImage3", "testImage3"], + postImageSources: ["이미지 출처", "이미지 출처", "이미지 출처", "이미지 출처"], + postContents: ["testImage3", "testImage3", "testImage3", "testImage3"], + postEditor: "에디터 이름", + postEditorProfileImage: "profileImage", + postView: 1132, + postCreatedAt: Date(), + postUpdatedAt: Date() + ), + Post(post_id: "27", postUIType: .normal, postCategory: .culture, postTitle: "흑백 사진은 언제 처음 사용됐을까?", @@ -135,7 +440,7 @@ public class PostRepositoryMockImpl: PostRepository { postCreatedAt: Date(), postUpdatedAt: Date() ), - Post(post_id: "6", + Post(post_id: "28", postUIType: .normal, postCategory: .culture, postTitle: "흑백 사진은 언제 처음 사용됐을까?", @@ -149,7 +454,7 @@ public class PostRepositoryMockImpl: PostRepository { postCreatedAt: Date(), postUpdatedAt: Date() ), - Post(post_id: "7", + Post(post_id: "29", postUIType: .normal, postCategory: .culture, postTitle: "흑백 사진은 언제 처음 사용됐을까?", @@ -163,7 +468,7 @@ public class PostRepositoryMockImpl: PostRepository { postCreatedAt: Date(), postUpdatedAt: Date() ), - Post(post_id: "8", + Post(post_id: "30", postUIType: .normal, postCategory: .culture, postTitle: "흑백 사진은 언제 처음 사용됐을까?", @@ -186,44 +491,12 @@ public class PostRepositoryMockImpl: PostRepository { let posts: [Post] = [ Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), - Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), - Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), - Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), - Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), - Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") + Post(post_id: "13", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "23", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "27", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "28", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "29", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "30", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") ] return Just(posts).eraseToAnyPublisher() } @@ -232,16 +505,12 @@ public class PostRepositoryMockImpl: PostRepository { let posts: [Post] = [ Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), - Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), - Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), - Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), - Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), - Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), - Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") + Post(post_id: "13", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "23", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "27", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "28", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "29", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "30", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") ] return Just(posts).eraseToAnyPublisher() } diff --git a/MyLuxury/Domain/Sources/Domain/Entity/Post.swift b/MyLuxury/Domain/Sources/Domain/Entity/Post.swift index 95e2ba8..b55893d 100644 --- a/MyLuxury/Domain/Sources/Domain/Entity/Post.swift +++ b/MyLuxury/Domain/Sources/Domain/Entity/Post.swift @@ -7,7 +7,7 @@ import Foundation -public struct Post { +public struct Post: @unchecked Sendable { /// 게시물 식별 아이디 public let post_id: String /// 게시물 UI 타입. 추후 종류가 추가될 예정 @@ -57,14 +57,48 @@ public struct Post { self.postView = postView self.postCreatedAt = postCreatedAt self.postUpdatedAt = postUpdatedAt - }} + } +} + +extension Post: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(post_id) + } + + public static func ==(lhs: Post, rhs: Post) -> Bool { + return lhs.post_id == rhs.post_id + } +} + +public enum HomeSection: String, CaseIterable, @unchecked Sendable { + case todayPick = "오늘의 Pick" + case new = "새로 게시된 지식" + case weeklyTop = "이번 주 TOP10" + case customized = "회원님이 좋아할만한" + case editorRecommendation = "에디터 추천 지식" +} -/// 홈 메인 화면 데이터 모음을 정의한 typealias -public typealias HomePostData = ( - todayPickPostData: Post, - newPostData: [Post], - weeklyTopPostData: [Post], - customizedPostData: [Post], - gridData: [Post], - editorRecommendationPostData: [Post] -) +public struct HomePostData { + public var sectionOrder: [HomeSection]? + public var todayPickPostData: [Post]? + public var newPostData: [Post]? + public var weeklyTopPostData: [Post]? + public var customizedPostData: [Post]? + public var editorRecommendationPostData: [Post]? + + public init( + sectionOrder: [HomeSection]? = nil, + todayPickPostData: [Post]? = nil, + newPostData: [Post]? = nil, + weeklyTopPostData: [Post]? = nil, + customizedPostData: [Post]? = nil, + editorRecommendationPostData: [Post]? = nil + ) { + self.sectionOrder = sectionOrder + self.todayPickPostData = todayPickPostData + self.newPostData = newPostData + self.weeklyTopPostData = weeklyTopPostData + self.customizedPostData = customizedPostData + self.editorRecommendationPostData = editorRecommendationPostData + } +} diff --git a/MyLuxury/MyLuxury/App/AppComponent.swift b/MyLuxury/MyLuxury/App/AppComponent.swift index b20324a..ee0bf5d 100644 --- a/MyLuxury/MyLuxury/App/AppComponent.swift +++ b/MyLuxury/MyLuxury/App/AppComponent.swift @@ -60,7 +60,7 @@ public class AppComponent: CoordinatorDependency { print("AppComponent init") self.window = window self.memberRepository = MemberRepositoryImpl() - self.postRepository = PostRepositoryImpl() + self.postRepository = PostRepositoryMockImpl() self.memberUseCase = MemberUseCaseImpl(memberRepository: memberRepository) self.postUseCase = PostUseCaseImpl(postRepository: postRepository) } diff --git a/MyLuxury/MyLuxury/CodeConvention.txt b/MyLuxury/MyLuxury/CodeConvention.txt new file mode 100644 index 0000000..d64f005 --- /dev/null +++ b/MyLuxury/MyLuxury/CodeConvention.txt @@ -0,0 +1,6 @@ +코드 컨벤션 + +1. +후미가 -Template 인 객체들은 프레젠테이션(뷰) 레이어에서 쓰이는 객체들입니다. +이 객체들은 도메인 레이어에서 쓰이는 객체들을 매핑하여 뷰 객체들(뷰모델 제외)이 +도메인 레이어를 알 수 없도록 하는데에 목적이 있습니다. diff --git a/MyLuxury/Presentation/Sources/Presentation/Example/View/ExampleView.swift b/MyLuxury/Presentation/Sources/Presentation/Example/View/ExampleView.swift deleted file mode 100644 index 9511b1f..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Example/View/ExampleView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ExampleView.swift -// MyLuxury -// -// Created by KoSungmin on 10/3/24. -// - -//import UIKit -// -//final class ExampleView: UIView { -// -// let myView = UIView() -// let exampleUILabel = UILabel() -// -// override init(frame: CGRect) { -// super.init(frame: frame) -// setUpUI() -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// private func setUpUI() { -// addSubview(exampleUILabel) -// -// exampleUILabel.translatesAutoresizingMaskIntoConstraints = false -// -// NSLayoutConstraint.activate([ -// exampleUILabel.centerXAnchor.constraint(equalTo: self.centerXAnchor), -// exampleUILabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) -// ]) -// -// exampleUILabel.text = "안녕하세요" -// exampleUILabel.textColor = .white -// } -//} diff --git a/MyLuxury/Presentation/Sources/Presentation/Example/ViewController/ExampleViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Example/ViewController/ExampleViewController.swift deleted file mode 100644 index 5f251ee..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Example/ViewController/ExampleViewController.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ExampleViewController.swift -// MyLuxury -// -// Created by KoSungmin on 10/3/24. -// - -//import UIKit -//import Combine -// -//class ExampleViewController: UIViewController { -// -// private let rootView = ExampleView() -// -// private let exampleVM = ExampleViewModel() -// private let input: PassthroughSubject = .init() -// private var cancellables = Set() -// -// override func viewDidLoad() { -// super.viewDidLoad() -// bindData() -// } -// -// override func viewDidAppear(_ animated: Bool) { -// super.viewDidAppear(animated) -// input.send(.viewDidAppear) -// } -// -// override func loadView() { -// -// self.view = rootView -// } -// -// private func bindData() { -// -// let output = exampleVM.transform(input: input.eraseToAnyPublisher()) -// -// output -// .receive(on: DispatchQueue.main) -// .sink { outputEvent in -// -// switch outputEvent { -// -// case .fetchData(let value): -// self.rootView.exampleUILabel.text = value -// } -// } -// .store(in: &cancellables) -// } -//} diff --git a/MyLuxury/Presentation/Sources/Presentation/Example/ViewModel/ExampleViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Example/ViewModel/ExampleViewModel.swift deleted file mode 100644 index 010da47..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Example/ViewModel/ExampleViewModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ExampleViewModel.swift -// MyLuxury -// -// Created by KoSungmin on 10/3/24. -// - -import Combine -import Domain - -//final class ExampleViewModel { -// -// enum Input { -// -// case viewDidAppear -// } -// -// enum Output { -// -// case fetchData(value: String) -// } -// -// private let exampleUseCase: ExampleUseCase -// private let output: PassthroughSubject = .init() -// private var cancellables = Set() -// -// init(exampleUseCase: ExampleUseCase) { -// self.exampleUseCase = exampleUseCase -// } -// -// func transform(input: AnyPublisher) -> AnyPublisher { -// -// input.sink { event in -// -// switch event { -// -// case .viewDidAppear: -// self.fetchData() -// } -// }.store(in: &cancellables) -// -// return output.eraseToAnyPublisher() -// } -// -// private func fetchData() { -// -// exampleUseCase.fetchData().sink { entity in -// -// self.output.send(.fetchData(value: entity.value)) -// }.store(in: &cancellables) -// } -//} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift index 5de583a..77cbf50 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/HomeCoordinator.swift @@ -51,11 +51,11 @@ public class HomeCoordinatorImpl: HomeCoordinator, @preconcurrency HomeViewModel } @MainActor - func goToPost(post: Post) { + func goToPost(postId: String) { let postCoordinator = self.dependency.postCoordinator childCoordinators.append(postCoordinator) postCoordinator.delegate = self - let postVC = postCoordinator.start(post: post) + let postVC = postCoordinator.start(postId: postId) self.navigationController.pushViewController(postVC, animated: true) } diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeEditorRecommendCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeEditorRecommendCVC.swift new file mode 100644 index 0000000..de3be55 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeEditorRecommendCVC.swift @@ -0,0 +1,93 @@ +// +// HomeEditorRecommendCVC.swift +// Presentation +// +// Created by KoSungmin on 10/30/24. +// + +import UIKit +import Domain + +final class HomeEditorRecommendCVC: UICollectionViewCell { + struct ViewModel: Hashable { + let uuid: String + let homePostTemplate: HomePostTemplate + + func hash(into hasher: inout Hasher) { + hasher.combine(uuid) + } + + static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { + lhs.uuid == rhs.uuid + } + } + + static let identifier = "HomeEditorRecommendCVC" + var viewModel: ViewModel? + + /// 게시물의 카테고리 + private let contentCategoryLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.extrabold, size: 36) + label.textColor = .white + return label + }() + /// 게시물의 제목 + private let contentTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.extrabold, size: 36) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + /// 게시물의 썸네일 + private let contentThumbnailImageView: UIImageView = { + let image = UIImageView() + image.clipsToBounds = true + image.layer.cornerRadius = 15 + return image + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + /// 그림자 레이어 추가 + contentThumbnailImageView.addTopBottomShadow(shadowHeight: homeEditorRecommendCVCLength/2) + } + + private func setUpHierarchy() { + self.addSubview(contentThumbnailImageView) + self.addSubview(contentTitleLabel) + self.addSubview(contentCategoryLabel) + } + + private func setUpLayout() { + contentThumbnailImageView.translatesAutoresizingMaskIntoConstraints = false + contentTitleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + contentThumbnailImageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + contentThumbnailImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + contentThumbnailImageView.widthAnchor.constraint(equalToConstant: homeEditorRecommendCVCLength), + contentThumbnailImageView.heightAnchor.constraint(equalToConstant: homeEditorRecommendCVCLength), + contentTitleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15), + contentTitleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), + contentTitleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 15) + ]) + } + + func configure(viewModel: ViewModel) { + self.viewModel = viewModel + self.contentTitleLabel.text = viewModel.homePostTemplate.postTitle + self.contentThumbnailImageView.image = UIImage(named: viewModel.homePostTemplate.postThumbnailImage) + self.contentCategoryLabel.text = viewModel.homePostTemplate.postCategory + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeGrid/HomeGridCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeGridCVC.swift similarity index 100% rename from MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeGrid/HomeGridCVC.swift rename to MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeGridCVC.swift diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeHorizontalCollection/HomeHorizontalCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeHorizontalCVC.swift similarity index 73% rename from MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeHorizontalCollection/HomeHorizontalCVC.swift rename to MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeHorizontalCVC.swift index 32c3c43..6f3ee3c 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeHorizontalCollection/HomeHorizontalCVC.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeHorizontalCVC.swift @@ -8,7 +8,20 @@ import UIKit final class HomeHorizontalCVC: UICollectionViewCell { + struct ViewModel: Hashable { + let uuid: String + let homePostTemplate: HomePostTemplate + + func hash(into hasher: inout Hasher) { + hasher.combine(uuid) + } + static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { + lhs.uuid == rhs.uuid + } + } + static let identifier = "HomeHorizontalCVC" + private var viewModel: ViewModel? private var contentImage: UIImageView = { let image = UIImageView() @@ -22,18 +35,7 @@ final class HomeHorizontalCVC: UICollectionViewCell { title.font = UIFont.pretendard(.light, size: 14) return title }() - - var image: String? { - didSet { - contentImage.image = UIImage(named: image!) - } - } - var title: String? { - didSet { - contentTitle.text = title - } - } - + override init(frame: CGRect) { super.init(frame: frame) addSubview(contentImage) @@ -53,4 +55,10 @@ final class HomeHorizontalCVC: UICollectionViewCell { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func configure(viewModel: ViewModel) { + self.viewModel = viewModel + self.contentTitle.text = viewModel.homePostTemplate.postTitle + self.contentImage.image = UIImage(named: viewModel.homePostTemplate.postThumbnailImage) + } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeTodayPickCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeTodayPickCVC.swift new file mode 100644 index 0000000..0355435 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/Cell/HomeTodayPickCVC.swift @@ -0,0 +1,80 @@ +// +// HomeTodayPickCVC.swift +// Presentation +// +// Created by KoSungmin on 12/11/24. +// + +import UIKit + +final class HomeTodayPickCVC: UICollectionViewCell { + struct ViewModel: Hashable { + let uuid: String + let homePostTemplate: HomePostTemplate + + // DiffableDataSource는 내부적으로 Hashable과 Equtable을 사용하여 + // 이전 스냅샷과 새로운 스냅샷을 비교함. + + // hash와 == 메소드는 항상 일관성을 유지해야함. + func hash(into hasher: inout Hasher) { + hasher.combine(uuid) + } + + // title이 업데이트 될 수 있는 상황이라면 + // equtable에서 title도 비교를 해줘야 함. + static func == (lhs: ViewModel, rhs: ViewModel) -> Bool { + lhs.uuid == rhs.uuid + } + } + + static let identifier = "HomeTodayPickCVC" + + private let postThumbnailImageView: UIImageView = { + let content = UIImageView() + content.layer.cornerRadius = 10 + content.isUserInteractionEnabled = true + return content + }() + + private let postTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.extrabold, size: 22) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpHierarchy() { + self.addSubview(postThumbnailImageView) + postThumbnailImageView.addSubview(postTitleLabel) + } + + private func setUpLayout() { + postThumbnailImageView.translatesAutoresizingMaskIntoConstraints = false + postTitleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + postThumbnailImageView.topAnchor.constraint(equalTo: self.topAnchor), + postThumbnailImageView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + postThumbnailImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + postThumbnailImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + postTitleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15), + postTitleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), + postTitleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) + ]) + } + + func configure(viewModel: ViewModel) { + self.postTitleLabel.text = viewModel.homePostTemplate.postTitle + self.postThumbnailImageView.image = UIImage(named: viewModel.homePostTemplate.postThumbnailImage) + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsSectionHeaderView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsSectionHeaderView.swift new file mode 100644 index 0000000..f8bc830 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsSectionHeaderView.swift @@ -0,0 +1,41 @@ +// +// HomeSectionHeaderView.swift +// Presentation +// +// Created by KoSungmin on 12/11/24. +// + +import UIKit + +final class HomeSectionHeaderView: UICollectionReusableView { + struct ViewModel { + let sectionTitle: String + } + + static let identifier = "HomeSectionHeaderView" + + let sectionTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.extrabold, size: 24) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.addSubview(sectionTitleLabel) + sectionTitleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + sectionTitleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + sectionTitleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(viewModel: ViewModel) { + self.sectionTitleLabel.text = viewModel.sectionTitle + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsSectionView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsSectionView.swift deleted file mode 100644 index f4bf394..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsSectionView.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// HomeContentsSectionView.swift -// Presentation -// -// Created by KoSungmin on 11/10/24. -// - -import UIKit - -@MainActor -protocol HomeContentsSectionView: UIView { - associatedtype PostData - var sectionTitle: String { get set } - var postData: PostData { get set } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsView.swift index 53b9995..a1ed36c 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsView.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeContentsView.swift @@ -9,62 +9,123 @@ import UIKit import Combine import Domain -final class HomeContentsView: UIView { - private var homeVM: HomeViewModel +struct HomeSectionCompositeViewModel { + let headerVM: HomeSectionHeaderViewModel + let cellVMs: [HomeSectionCellViewModel] +} + +enum HomeSectionHeaderViewModel: Hashable, Identifiable { + case todayPick(headerVM: HomeSectionHeaderView.ViewModel) + case newPost(headerVM: HomeSectionHeaderView.ViewModel) + case weeklyTop(headerVM: HomeSectionHeaderView.ViewModel) + case customized(headerVM: HomeSectionHeaderView.ViewModel) + case editorRecommend(headerVM: HomeSectionHeaderView.ViewModel) - private let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.showsVerticalScrollIndicator = false - return scrollView - }() + var id: String { + switch self { + case .todayPick: + return "todayPick" + case .newPost: + return "newPost" + case .weeklyTop: + return "weeklyTop" + case .customized: + return "customized" + case .editorRecommend: + return "editorRecommend" + } + } - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.distribution = .fill - stackView.alignment = .fill - stackView.spacing = 30 - return stackView - }() + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } - var homePostData: HomePostData? { - didSet { - updateContentsSections() - } + static func == (lhs: HomeSectionHeaderViewModel, rhs: HomeSectionHeaderViewModel) -> Bool { + return lhs.id == rhs.id } +} + +enum HomeSectionCellViewModel: Hashable, Sendable { + case todayPick(cellVM: HomeTodayPickCVC.ViewModel) + case newPost(cellVM: HomeHorizontalCVC.ViewModel) + case weeklyTop(cellVM: HomeHorizontalCVC.ViewModel) + case customized(cellVM: HomeHorizontalCVC.ViewModel) + case editorRecommend(cellVM: HomeEditorRecommendCVC.ViewModel) +} - private var contentsSections: [any HomeContentsSectionView] = [] + +final class HomeContentsView: UIView { + struct ViewModel { + let sections: [HomeSectionCompositeViewModel] + } - /// 섹션 추가는 이 곳에서 하시면 됩니다. - private func updateContentsSections() { - if let data = homePostData?.todayPickPostData { - contentsSections.append(HomeTodayPickView(homeVM: homeVM, sectionTitle: "오늘의 PICK", postData: data)) - } - if let data = homePostData?.newPostData { - contentsSections.append(HomeHorizontalCollectionView(homeVM: homeVM, sectionTitle: "새로 게시된 지식", postData: data)) - } - if let data = homePostData?.weeklyTopPostData { - contentsSections.append(HomeHorizontalCollectionView(homeVM: homeVM, sectionTitle: "이번 주 TOP10", postData: data)) - } - if let data = homePostData?.customizedPostData { - contentsSections.append(HomeHorizontalCollectionView(homeVM: homeVM, sectionTitle: "회원님이 좋아할 만한", postData: data)) - } - if let data = homePostData?.editorRecommendationPostData { - contentsSections.append(HomeEditorRecommendCollectionView(homeVM: homeVM, sectionTitle: "에디터 추천 지식", postData: data)) + private var homeVM: HomeViewModel + + private lazy var collectionView: UICollectionView = { + // collectionView의 레이아웃을 하단에서 생성한 CompositionalLayout 방식으로 지정 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()) + collectionView.backgroundColor = .clear + return collectionView + }() + + private lazy var dataSource: UICollectionViewDiffableDataSource = { + var dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { + collectionView, indexPath, cellViewModel in + switch cellViewModel { + case .todayPick(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeTodayPickCVC.identifier, for: indexPath) as! HomeTodayPickCVC + cell.configure(viewModel: cellVM) + return cell + case .newPost(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeHorizontalCVC.identifier, for: indexPath) as! HomeHorizontalCVC + cell.configure(viewModel: cellVM) + return cell + case .weeklyTop(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeHorizontalCVC.identifier, for: indexPath) as! HomeHorizontalCVC + cell.configure(viewModel: cellVM) + return cell + case .customized(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeHorizontalCVC.identifier, for: indexPath) as! HomeHorizontalCVC + cell.configure(viewModel: cellVM) + return cell + case .editorRecommend(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeEditorRecommendCVC.identifier, for: indexPath) as! HomeEditorRecommendCVC + cell.configure(viewModel: cellVM) + return cell + } } - - for section in contentsSections { - stackView.addArrangedSubview(section) + + dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in + guard kind == UICollectionView.elementKindSectionHeader else { return UICollectionReusableView() } + let headerView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: HomeSectionHeaderView.identifier, + for: indexPath) as! HomeSectionHeaderView + + let section = dataSource.snapshot().sectionIdentifiers[indexPath.section] + switch section { + case .todayPick(headerVM: let headerVM): + headerView.configure(viewModel: headerVM) + case .newPost(headerVM: let headerVM): + headerView.configure(viewModel: headerVM) + case .weeklyTop(headerVM: let headerVM): + headerView.configure(viewModel: headerVM) + case .customized(headerVM: let headerVM): + headerView.configure(viewModel: headerVM) + case .editorRecommend(headerVM: let headerVM): + headerView.configure(viewModel: headerVM) + } + return headerView } - } - + return dataSource + }() + init(homeVM: HomeViewModel) { self.homeVM = homeVM super.init(frame: .zero) + setUpCollectionView() setUpHierarchy() setUpLayout() - scrollView.backgroundColor = .black - stackView.backgroundColor = .clear } override init(frame: CGRect) { @@ -75,25 +136,126 @@ final class HomeContentsView: UIView { fatalError("init(coder:) has not been implemented") } + private func setUpCollectionView() { + collectionView.delegate = self + collectionView.register(HomeSectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HomeSectionHeaderView.identifier) + collectionView.register(HomeTodayPickCVC.self, forCellWithReuseIdentifier: HomeTodayPickCVC.identifier) + collectionView.register(HomeHorizontalCVC.self, forCellWithReuseIdentifier: HomeHorizontalCVC.identifier) + collectionView.register(HomeEditorRecommendCVC.self, forCellWithReuseIdentifier: HomeEditorRecommendCVC.identifier) + } + + func configureSnapshot(viewModel: ViewModel) { + var snapshot = NSDiffableDataSourceSnapshot() + viewModel.sections.forEach { viewModel in + snapshot.appendSections([viewModel.headerVM]) + snapshot.appendItems(viewModel.cellVMs, toSection: viewModel.headerVM) + } + dataSource.apply(snapshot, animatingDifferences: true) + } + private func setUpHierarchy() { - self.addSubview(scrollView) - scrollView.addSubview(stackView) + self.addSubview(collectionView) } private func setUpLayout() { - scrollView.translatesAutoresizingMaskIntoConstraints = false - stackView.translatesAutoresizingMaskIntoConstraints = false - + collectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: self.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), - stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + collectionView.topAnchor.constraint(equalTo: self.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor) ]) } + + private func createLayout() -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout { sectionIndex, _ in + let section = self.dataSource.snapshot().sectionIdentifiers[sectionIndex] + switch section { + case .todayPick: + return self.createOneItemSection() + case .newPost: + return self.createHorizontalSection() + case .weeklyTop: + return self.createHorizontalSection() + case .customized: + return self.createHorizontalSection() + case .editorRecommend: + return self.createVerticalSection() + } + } + } + + // 셀간 간격: group.interItemSpacing + // 그룹간 간격: section.interGroupSpacing + + private func createOneItemSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(hometodayPickViewWidth), heightDimension: .absolute(hometodayPickViewHeight)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(hometodayPickViewWidth), heightDimension: .absolute(hometodayPickViewHeight)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .none + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 15, bottom: 35, trailing: 15) + let headerSize = NSCollectionLayoutSize(widthDimension: .absolute(screenWidth), heightDimension: .absolute(50)) + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top) + section.boundarySupplementaryItems = [header] + return section + } + + private func createHorizontalSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(homeHorizontalCVCLength), heightDimension: .absolute(homeHorizontalCVCLength)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(homeHorizontalCVCLength), heightDimension: .absolute(homeHorizontalCVCLength)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 15, bottom: 60, trailing: 15) + section.interGroupSpacing = 15 + let headerSize = NSCollectionLayoutSize(widthDimension: .absolute(screenWidth), heightDimension: .absolute(50)) + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top) + section.boundarySupplementaryItems = [header] + return section + } + + private func createVerticalSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(homeEditorRecommendCVCLength), heightDimension: .absolute(homeEditorRecommendCVCLength)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(homeEditorRecommendCVCLength), heightDimension: .absolute(homeEditorRecommendCVCLength)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15) + section.interGroupSpacing = 20 + let headerSize = NSCollectionLayoutSize(widthDimension: .absolute(screenWidth), heightDimension: .absolute(50)) + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top) + section.boundarySupplementaryItems = [header] + return section + } +} + +extension HomeContentsView: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if let post = dataSource.itemIdentifier(for: indexPath) { + switch post { + case .todayPick(cellVM: let cellVM): + homeVM.sendInputEvent(input: .postTapped(cellVM.homePostTemplate.postId)) + case .newPost(cellVM: let cellVM): + homeVM.sendInputEvent(input: .postTapped(cellVM.homePostTemplate.postId)) + case .weeklyTop(cellVM: let cellVM): + homeVM.sendInputEvent(input: .postTapped(cellVM.homePostTemplate.postId)) + case .customized(cellVM: let cellVM): + homeVM.sendInputEvent(input: .postTapped(cellVM.homePostTemplate.postId)) + case .editorRecommend(cellVM: let cellVM): + homeVM.sendInputEvent(input: .postTapped(cellVM.homePostTemplate.postId)) + } + } + } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeEditorRecommend/HomeEditorRecommendCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeEditorRecommend/HomeEditorRecommendCVC.swift deleted file mode 100644 index 7b91999..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeEditorRecommend/HomeEditorRecommendCVC.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// HomeEditorRecommendCVC.swift -// Presentation -// -// Created by KoSungmin on 10/30/24. -// - -import UIKit -import Domain - -final class HomeEditorRecommendCVC: UICollectionViewCell { - static let identifier = "HomeEditorRecommendCVC" - - /// 게시물의 카테고리 - private let contentCategory: UILabel = { - let label = UILabel() - label.font = UIFont.pretendard(.extrabold, size: 36) - label.textColor = .white - return label - }() - /// 게시물의 제목 - private let contentTitle: UILabel = { - let label = UILabel() - label.font = UIFont.pretendard(.extrabold, size: 36) - label.textColor = .white - label.numberOfLines = 2 - return label - }() - /// 게시물의 썸네일 - private let contentThumbnail: UIImageView = { - let image = UIImageView() - image.clipsToBounds = true - image.layer.cornerRadius = 15 - return image - }() - - var category: KnowledgeCategory? { - didSet { - contentCategory.text = category?.name - } - } - var title: String? { - didSet { - contentTitle.text = title - } - } - var thumbnailImage: String? { - didSet { - contentThumbnail.image = UIImage(named: thumbnailImage!) - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setUpHierarchy() - setUpLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - /// 그림자 레이어 추가 - contentThumbnail.addTopBottomShadow(shadowHeight: homeEditorRecommendCVCLength/2) - } - - private func setUpHierarchy() { - self.addSubview(contentThumbnail) - self.addSubview(contentTitle) - self.addSubview(contentCategory) - } - - private func setUpLayout() { - contentThumbnail.translatesAutoresizingMaskIntoConstraints = false - contentTitle.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - contentThumbnail.centerXAnchor.constraint(equalTo: self.centerXAnchor), - contentThumbnail.centerYAnchor.constraint(equalTo: self.centerYAnchor), - contentThumbnail.widthAnchor.constraint(equalToConstant: homeEditorRecommendCVCLength), - contentThumbnail.heightAnchor.constraint(equalToConstant: homeEditorRecommendCVCLength), - contentTitle.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15), - contentTitle.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), - contentTitle.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 15) - ]) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeEditorRecommend/HomeEditorRecommendCollectionView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeEditorRecommend/HomeEditorRecommendCollectionView.swift deleted file mode 100644 index 50dda10..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeEditorRecommend/HomeEditorRecommendCollectionView.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// HomeEditorRecommendCollectionView.swift -// Presentation -// -// Created by KoSungmin on 10/30/24. -// - -import UIKit -import Domain -import Combine - -final class HomeEditorRecommendCollectionView: UIView, HomeContentsSectionView { - typealias PostData = [Post] - var sectionTitle: String - var postData: [Post] { - didSet { - updateCollectionViewHeight() - } - } - private var homeVM: HomeViewModel - - /// 해당 뷰의 제목 - private let sectionTitleLabel: UILabel = { - let label = UILabel() - label.font = UIFont.pretendard(.extrabold, size: 24) - label.textColor = .white - return label - }() - - private let collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.itemSize = CGSize(width: homeEditorRecommendCVCLength, height: homeEditorRecommendCVCLength) - layout.minimumLineSpacing = 30 - layout.sectionInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.backgroundColor = .clear - collectionView.isScrollEnabled = false - return collectionView - }() - - private var heightConstraint: NSLayoutConstraint? - - init(homeVM: HomeViewModel, sectionTitle: String, postData: [Post]) { - self.sectionTitle = sectionTitle - self.homeVM = homeVM - self.postData = postData - super.init(frame: .zero) - setUpUI() - setUpHierarchy() - setUpCollectionView() - setUpLayout() - } - - override init(frame: CGRect) { - fatalError("init(coder:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setUpHierarchy() { - addSubview(sectionTitleLabel) - addSubview(collectionView) - } - - private func setUpUI() { - self.sectionTitleLabel.text = sectionTitle - } - - private func setUpCollectionView() { - collectionView.dataSource = self - collectionView.delegate = self - collectionView.register(HomeEditorRecommendCVC.self, forCellWithReuseIdentifier: "HomeEditorRecommendCVC") - } - - private func setUpLayout() { - sectionTitleLabel.translatesAutoresizingMaskIntoConstraints = false - collectionView.translatesAutoresizingMaskIntoConstraints = false - /// 높이 초기값 설정 - heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: (homeEditorRecommendCVCLength + 30) * CGFloat(postData.count)) - /// 커스텀 제약 조건 활성화 - heightConstraint?.isActive = true - NSLayoutConstraint.activate([ - sectionTitleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), - sectionTitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15), - collectionView.topAnchor.constraint(equalTo: sectionTitleLabel.bottomAnchor, constant: 10), - collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - ]) - } - - /// collectionView의 동적 높이를 설정하기 위한 메소드 - private func updateCollectionViewHeight() { - heightConstraint?.constant = (homeEditorRecommendCVCLength + 30) * CGFloat(postData.count) - } -} - -extension HomeEditorRecommendCollectionView: UICollectionViewDataSource, UICollectionViewDelegate { - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - postData.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let post = self.postData[indexPath.row] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeEditorRecommendCVC", for: indexPath) as! HomeEditorRecommendCVC - cell.category = post.postCategory - cell.title = post.postTitle - cell.thumbnailImage = post.postThumbnailImage - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let post = postData[indexPath.row] - homeVM.sendInputEvent(input: .postTapped(post)) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeGrid/HomeGridCollectionView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeGrid/HomeGridCollectionView.swift deleted file mode 100644 index 7a62c04..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeGrid/HomeGridCollectionView.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// HomeGridCollectionView.swift -// Presentation -// -// Created by KoSungmin on 10/30/24. -// - -import UIKit -import Domain - -// MARK: 현재 아직 레이아웃에 넣지 않은 컬렉션뷰입니다. -// 추후 디자인에 따라 삽입 혹은 삭제될 수 있습니다. - -final class HomeGridCollectionView: UIView, HomeContentsSectionView { - typealias PostData = [Post] - var sectionTitle: String - var postData: [Post] - - private var homeVM: HomeViewModel - - private let titleLabel: UILabel = { - let label = UILabel() - label.font = UIFont.pretendard(.extrabold, size: 24) - label.textColor = .white - return label - }() - - private let collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.itemSize = CGSize(width: homeGridCVCWidth, height: homeGridCVCHeight) - layout.minimumLineSpacing = 15 - layout.minimumInteritemSpacing = 15 - layout.sectionInset = UIEdgeInsets(top: 10, left: 15, bottom: 0, right: 15) - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.backgroundColor = .clear - collectionView.isScrollEnabled = false - return collectionView - }() - - init(homeVM: HomeViewModel, sectionTitle: String, postData: [Post]) { - self.sectionTitle = sectionTitle - self.homeVM = homeVM - self.postData = postData - super.init(frame: .zero) - setUpUI() - setUpHierarchy() - setUpCollectionView() - setUpLayout() - } - - override init(frame: CGRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setUpUI() { - titleLabel.text = sectionTitle - } - - private func setUpHierarchy() { - addSubview(titleLabel) - addSubview(collectionView) - } - - private func setUpCollectionView() { - collectionView.dataSource = self - collectionView.delegate = self - collectionView.register(HomeGridCVC.self, forCellWithReuseIdentifier: "HomeGridCVC") - } - - private func setUpLayout() { - titleLabel.translatesAutoresizingMaskIntoConstraints = false - collectionView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15), - collectionView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), - collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), - collectionView.heightAnchor.constraint(equalToConstant: homeGridCVCHeight*2 + 25) - ]) - } -} - -extension HomeGridCollectionView: UICollectionViewDataSource, UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - /// 항상 4개로 고정 - return postData.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let post = self.postData[indexPath.row] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeGridCVC", for: indexPath) as! HomeGridCVC - cell.image = post.postThumbnailImage - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let post = postData[indexPath.row] - homeVM.sendInputEvent(input: .postTapped(post)) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeHorizontalCollection/HomeHorizontalCollectionView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeHorizontalCollection/HomeHorizontalCollectionView.swift deleted file mode 100644 index a0f159b..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeHorizontalCollection/HomeHorizontalCollectionView.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// HomeHorizontalCollectionView.swift -// Presentation -// -// Created by KoSungmin on 10/28/24. -// - -import UIKit -import Combine -import Domain - -class HomeHorizontalCollectionView: UIView, HomeContentsSectionView { - typealias PostData = [Post] - - var sectionTitle: String - var postData: [Post] - - private var homeVM: HomeViewModel - - private let sectionTitleLabel: UILabel = { - let label = UILabel() - label.font = UIFont.pretendard(.extrabold, size: 24) - label.textColor = .white - return label - }() - - private let collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.itemSize = CGSize(width: homeHorizontalCVCLength, height: homeHorizontalCVCLength+30) - layout.minimumLineSpacing = 15 - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.backgroundColor = .clear - collectionView.showsHorizontalScrollIndicator = false - collectionView.contentInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) - return collectionView - }() - - init(homeVM: HomeViewModel, sectionTitle: String, postData: [Post]) { - self.sectionTitle = sectionTitle - self.homeVM = homeVM - self.postData = postData - super.init(frame: .zero) - setUpUI() - setUpHierarchy() - setUpCollectionView() - setUpLayout() - } - - override init(frame: CGRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setUpUI() { - sectionTitleLabel.text = sectionTitle - } - - private func setUpHierarchy() { - addSubview(sectionTitleLabel) - addSubview(collectionView) - } - - private func setUpCollectionView() { - collectionView.dataSource = self - collectionView.delegate = self - collectionView.register(HomeHorizontalCVC.self, forCellWithReuseIdentifier: "HomeHorizontalCVC") - } - - private func setUpLayout() { - sectionTitleLabel.translatesAutoresizingMaskIntoConstraints = false - collectionView.translatesAutoresizingMaskIntoConstraints = false - // 레이블 제약 조건 - NSLayoutConstraint.activate([ - sectionTitleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), - sectionTitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15), - collectionView.topAnchor.constraint(equalTo: sectionTitleLabel.bottomAnchor, constant: 5), - collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), - collectionView.heightAnchor.constraint(equalToConstant: homeHorizontalCVCLength+45) - ]) - } -} - -extension HomeHorizontalCollectionView: UICollectionViewDataSource, UICollectionViewDelegate { - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return postData.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let post = self.postData[indexPath.row] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeHorizontalCVC", for: indexPath) as! HomeHorizontalCVC - cell.image = post.postThumbnailImage - cell.title = post.postTitle - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let post = postData[indexPath.row] - homeVM.sendInputEvent(input: .postTapped(post)) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeTodayPick/HomeTodayPickView.swift b/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeTodayPick/HomeTodayPickView.swift deleted file mode 100644 index 07e44c3..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Home/View/HomeContents/HomeTodayPick/HomeTodayPickView.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// HomeTodayPickView.swift -// Presentation -// -// Created by KoSungmin on 10/28/24. -// - -import UIKit -import Domain -import Combine - -final class HomeTodayPickView: UIView, HomeContentsSectionView { - typealias PostData = Post - var sectionTitle: String - var postData: Post { - didSet { - contentTitle.text = postData.postTitle - contentThumbnail.image = UIImage(named: postData.postThumbnailImage) - } - } - private var homeVM: HomeViewModel - - private let sectionTitleLabel: UILabel = { - let title = UILabel() - title.font = UIFont.pretendard(.extrabold, size: 24) - title.textColor = .white - return title - }() - - private let contentThumbnail: UIImageView = { - let content = UIImageView() - content.layer.cornerRadius = 10 - content.isUserInteractionEnabled = true - return content - }() - - private let contentTitle: UILabel = { - let label = UILabel() - label.font = UIFont.pretendard(.extrabold, size: 22) - label.textColor = .white - label.numberOfLines = 2 - return label - }() - - init(homeVM: HomeViewModel, sectionTitle: String, postData: Post) { - self.sectionTitle = sectionTitle - self.homeVM = homeVM - self.postData = postData - super.init(frame: .zero) - setUpUI() - setUpHierarchy() - setUpLayout() - setUpGesture() - } - - override init(frame: CGRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - contentThumbnail.addTopBottomShadow(shadowHeight: 80) - } - - private func setUpUI() { - self.sectionTitleLabel.text = sectionTitle - self.contentTitle.text = postData.postTitle - self.contentThumbnail.image = UIImage(named: postData.postThumbnailImage) - } - - private func setUpHierarchy() { - self.addSubview(sectionTitleLabel) - self.addSubview(contentThumbnail) - self.addSubview(contentTitle) - } - - private func setUpLayout() { - sectionTitleLabel.translatesAutoresizingMaskIntoConstraints = false - contentThumbnail.translatesAutoresizingMaskIntoConstraints = false - contentTitle.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - sectionTitleLabel.topAnchor.constraint(equalTo: self.topAnchor), - sectionTitleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), - contentThumbnail.topAnchor.constraint(equalTo: sectionTitleLabel.bottomAnchor, constant: 10), - contentThumbnail.centerXAnchor.constraint(equalTo: self.centerXAnchor), - contentThumbnail.widthAnchor.constraint(equalToConstant: hometodayPickViewWidth), - contentThumbnail.heightAnchor.constraint(equalToConstant: hometodayPickViewHeight), - contentThumbnail.bottomAnchor.constraint(equalTo: self.bottomAnchor), - contentTitle.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 30), - contentTitle.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -30), - contentTitle.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15) - ]) - } - - private func setUpGesture() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(postTapped)) - contentThumbnail.addGestureRecognizer(tapGesture) - } - - @objc func postTapped() { - homeVM.sendInputEvent(input: .postTapped(postData)) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift index 051e531..fd99073 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -37,7 +37,6 @@ final class HomeViewController: UIViewController { /// 두 번째로 호출 override func viewDidLoad() { super.viewDidLoad() - bindData() homeVM.sendInputEvent(input: .viewLoaded) } @@ -57,10 +56,10 @@ final class HomeViewController: UIViewController { .sink { [weak self] event in guard let self = self else { return } switch event { - case .getHomePostData: - self.rootView.contentView.homePostData = self.homeVM.homePostData + case .getHomePostData(let viewModel): + self.rootView.contentView.configureSnapshot(viewModel: viewModel) case .goToPost(let post): - self.homeVM.delegate?.goToPost(post: post) + self.homeVM.delegate?.goToPost(postId: post) } }.store(in: &cancellabes) } diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/.swift b/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/.swift new file mode 100644 index 0000000..279c7f2 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/.swift @@ -0,0 +1,7 @@ +// +// HomeVMDIContainer.swift +// Presentation +// +// Created by KoSungmin on 12/24/24. +// + diff --git a/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift index 71eab26..9015bf1 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Home/ViewModel/HomeViewModel.swift @@ -10,7 +10,7 @@ import Combine import Domain protocol HomeViewModelDelegate: AnyObject { - func goToPost(post: Post) + func goToPost(postId: String) } class HomeViewModel { @@ -18,8 +18,8 @@ class HomeViewModel { private let output: PassthroughSubject = .init() private let input: PassthroughSubject = .init() private var cancellables = Set() - var homePostData: HomePostData? = nil weak var delegate: HomeViewModelDelegate? + private var homePostTemplateGroup: [HomePostTemplateGroup] = [] init(postUseCase: PostUseCase) { print("HomeViewModel init") @@ -36,9 +36,9 @@ class HomeViewModel { guard let self = self else { return } switch event { case .viewLoaded: - self.getHomeViewData() + self.getHomePostData() case .viewReload: - self.getHomeViewData() + self.getHomePostData() case .postTapped(let post): self.output.send(.goToPost(post)) } @@ -56,14 +56,113 @@ class HomeViewModel { self.input.send(.postTapped(post)) } } - - func getHomeViewData() { + + func getHomePostData() { postUseCase.getHomeViewData() - .sink { [weak self] homeData in + .map { homePostData -> [HomePostTemplateGroup] in + var homePostTemplateGroup: [HomePostTemplateGroup] = [] + if let sectionOrder = homePostData.sectionOrder { + for section in sectionOrder { + switch section { + case .todayPick: + if let todayPickPostData = homePostData.todayPickPostData { + homePostTemplateGroup.append(HomePostTemplateGroup( + type: .todayPick, + homePosts: todayPickPostData.map { $0.toHomePostTemplate() })) + } + case .new: + if let newPostData = homePostData.newPostData { + homePostTemplateGroup.append(HomePostTemplateGroup( + type: .newPost, + homePosts: newPostData.map { $0.toHomePostTemplate() })) + } + case .weeklyTop: + if let weeklyTopPostData = homePostData.weeklyTopPostData { + homePostTemplateGroup.append(HomePostTemplateGroup( + type: .weeklyTop, + homePosts: weeklyTopPostData.map { $0.toHomePostTemplate() })) + } + case .customized: + if let customizedPostData = homePostData.customizedPostData { + homePostTemplateGroup.append(HomePostTemplateGroup( + type: .customized, + homePosts: customizedPostData.map { $0.toHomePostTemplate() })) + } + case .editorRecommendation: + if let editorRecommendationPostData = homePostData.editorRecommendationPostData { + homePostTemplateGroup.append(HomePostTemplateGroup( + type: .editorRecommend, + homePosts: editorRecommendationPostData.map { $0.toHomePostTemplate() })) + } + } + } + } + return homePostTemplateGroup + } + .sink { [weak self] homePostTemplateGroup in guard let self = self else { return } - self.homePostData = homeData - self.output.send(.getHomePostData) - }.store(in: &cancellables) + self.homePostTemplateGroup = homePostTemplateGroup + self.output.send(.getHomePostData(vm: makeViewModel())) + } + .store(in: &cancellables) + } + + private func makeViewModel() -> HomeContentsView.ViewModel { + let sections = homePostTemplateGroup.map { group -> HomeSectionCompositeViewModel in + switch group.type { + case .todayPick: + return .init(headerVM: .todayPick(headerVM: .init(sectionTitle: group.type.title)), + cellVMs: group.homePosts.map { + .todayPick(cellVM: .init(uuid: $0.uuid.uuidString, + homePostTemplate: .init( + postId: $0.postId, + postTitle: $0.postTitle, + postThumbnailImage: $0.postThumbnailImage, + postCategory: $0.postCategory))) + }) + case .newPost: + return .init(headerVM: .newPost(headerVM: .init(sectionTitle: group.type.title)), + cellVMs: group.homePosts.map { + .newPost(cellVM: .init(uuid: $0.uuid.uuidString, + homePostTemplate: .init( + postId: $0.postId, + postTitle: $0.postTitle, + postThumbnailImage: $0.postThumbnailImage, + postCategory: $0.postCategory))) + }) + case .weeklyTop: + return .init(headerVM: .weeklyTop(headerVM: .init(sectionTitle: group.type.title)), + cellVMs: group.homePosts.map { + .weeklyTop(cellVM: .init(uuid: $0.uuid.uuidString, + homePostTemplate: .init( + postId: $0.postId, + postTitle: $0.postTitle, + postThumbnailImage: $0.postThumbnailImage, + postCategory: $0.postCategory))) + }) + case .customized: + return .init(headerVM: .customized(headerVM: .init(sectionTitle: group.type.title)), + cellVMs: group.homePosts.map { + .customized(cellVM: .init(uuid: $0.uuid.uuidString, + homePostTemplate: .init( + postId: $0.postId, + postTitle: $0.postTitle, + postThumbnailImage: $0.postThumbnailImage, + postCategory: $0.postCategory))) + }) + case .editorRecommend: + return .init(headerVM: .editorRecommend(headerVM: .init(sectionTitle: group.type.title)), + cellVMs: group.homePosts.map { + .editorRecommend(cellVM: .init(uuid: $0.uuid.uuidString, + homePostTemplate: .init( + postId: $0.postId, + postTitle: $0.postTitle, + postThumbnailImage: $0.postThumbnailImage, + postCategory: $0.postCategory))) + }) + } + } + return .init(sections: sections) } } @@ -71,10 +170,55 @@ extension HomeViewModel { enum Input { case viewLoaded case viewReload - case postTapped(Post) + case postTapped(String) } enum Output { - case getHomePostData - case goToPost(Post) + case getHomePostData(vm: HomeContentsView.ViewModel) + case goToPost(String) + } +} + +struct HomePostTemplate: Sendable { + let uuid = UUID() + let postId: String + let postTitle: String + let postThumbnailImage: String + let postCategory: String +} + +struct HomePostTemplateGroup { + enum GroupType { + case todayPick + case newPost + case weeklyTop + case customized + case editorRecommend + + var title: String { + switch self { + case .todayPick: + return "오늘의 PICK" + case .newPost: + return "새로 게시된 지식" + case .weeklyTop: + return "이번 주 TOP10" + case .customized: + return "회원님이 좋아할 만한" + case .editorRecommend: + return "에디터 추천 지식" + } + } + } + let type: GroupType + let homePosts: [HomePostTemplate] +} + +extension Post { + func toHomePostTemplate() -> HomePostTemplate { + return HomePostTemplate( + postId: self.post_id, + postTitle: self.postTitle, + postThumbnailImage: self.postThumbnailImage, + postCategory: self.postCategory.name) } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift index a40a6e5..87fd8bb 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/PostCoordinator.swift @@ -13,7 +13,7 @@ public protocol PostCoordinatorDelegate: AnyObject { } public protocol PostCoordinator: Coordinator { - func start(post: Post) -> UIViewController + func start(postId: String) -> UIViewController var delegate: PostCoordinatorDelegate? { get set } } @@ -29,9 +29,13 @@ public class PostCoordinatorImpl: PostCoordinator, @preconcurrency PostViewModel print("PostCoordinatorImpl init") self.dependency = dependency } + + deinit { + print("PostCoordinatorImpl deinit") + } - public func start(post: Post) -> UIViewController { - let postVM = PostViewModel(post: post, postUseCase: self.dependency.postUseCase) + public func start(postId: String) -> UIViewController { + let postVM = PostViewModel(postId: postId, postUseCase: self.dependency.postUseCase) postVM.delegate = self let postVC = PostViewController(postVM: postVM) return postVC diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/View/PostContentCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Post/View/PostContentCVC.swift index 57f09cf..579455b 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/View/PostContentCVC.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/View/PostContentCVC.swift @@ -10,6 +10,21 @@ import Domain import Combine final class PostContentCVC: UICollectionViewCell { + struct ViewModel: Hashable { + let uuid: String + let postContentImage: String? + let postContentImageSource: String? + let postContentText: String? + + func hash(into hasher: inout Hasher) { + hasher.combine(uuid) + } + + static func == (lhs: PostContentCVC.ViewModel, rhs: PostContentCVC.ViewModel) -> Bool { + lhs.uuid == rhs.uuid + } + } + static let identifier = "postContentCVC" private let backImageView: UIImageView = { @@ -54,25 +69,6 @@ final class PostContentCVC: UICollectionViewCell { return label }() - var postContentImage: String? { - didSet { - backImageView.image = UIImage(named: postContentImage ?? "blackScreen") - postContentImageView.image = UIImage(named: postContentImage ?? "blackScreen") - } - } - - var postContentImageSource: String? { - didSet { - postContentImageSourceLabel.text = postContentImageSource - } - } - - var postContent: String? { - didSet { - postContentLabel.text = postContent - } - } - override init(frame: CGRect) { super.init(frame: frame) setUpHierarchy() @@ -125,4 +121,11 @@ final class PostContentCVC: UICollectionViewCell { postContentLabel.widthAnchor.constraint(equalTo: pageContentScrollView.widthAnchor) ]) } + + func configure(viewModel: ViewModel) { + self.backImageView.image = UIImage(named: viewModel.postContentImage ?? "blackScreen") + self.postContentImageView.image = UIImage(named: viewModel.postContentImage ?? "blackScreen") + self.postContentImageSourceLabel.text = viewModel.postContentImageSource + self.postContentLabel.text = viewModel.postContentText + } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/View/PostTitleCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Post/View/PostTitleCVC.swift index c7fc34e..1ddc60e 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/View/PostTitleCVC.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/View/PostTitleCVC.swift @@ -10,6 +10,24 @@ import Domain import Combine final class PostTitleCVC: UICollectionViewCell { + struct ViewModel: Hashable { + let uuid: String + let title: String? + let editorProfileImage: String? + let thumbnailImage: String? + let editorName: String? + let postCreatedAt: String? + let postCategory: String? + + func hash(into hasher: inout Hasher) { + hasher.combine(uuid) + } + + static func == (lhs: PostTitleCVC.ViewModel, rhs: PostTitleCVC.ViewModel) -> Bool { + lhs.uuid == rhs.uuid + } + } + static let identifier = "postTitleCVC" let backImageView: UIImageView = { @@ -62,42 +80,6 @@ final class PostTitleCVC: UICollectionViewCell { return label }() - var title: String? { - didSet { - self.titleLabel.text = title - } - } - - var editorProfileImage: String? { - didSet { - self.editorProfileImageView.image = UIImage(named: editorProfileImage ?? "blackScreen") - } - } - - var thumbnailImage: String? { - didSet { - self.backImageView.image = UIImage(named: thumbnailImage ?? "blackScreen") - } - } - - var editorName: String? { - didSet { - self.editorNameLabel.text = editorName - } - } - - var postCreatedAt: String? { - didSet { - self.postCreatedAtLabel.text = postCreatedAt - } - } - - var postCategory: String? { - didSet { - self.postCategoryLabel.text = postCategory - } - } - override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .black @@ -151,4 +133,13 @@ final class PostTitleCVC: UICollectionViewCell { postCategoryLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15) ]) } + + func configure(viewModel: ViewModel) { + self.titleLabel.text = viewModel.title + self.editorProfileImageView.image = UIImage(named: viewModel.editorProfileImage ?? "blackScreen") + self.backImageView.image = UIImage(named: viewModel.thumbnailImage ?? "blackScreen") + self.editorNameLabel.text = viewModel.editorName + self.postCreatedAtLabel.text = viewModel.postCreatedAt + self.postCategoryLabel.text = viewModel.postCategory + } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/View/PostView.swift b/MyLuxury/Presentation/Sources/Presentation/Post/View/PostView.swift index bbecec2..babb5a5 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/View/PostView.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/View/PostView.swift @@ -8,8 +8,17 @@ import UIKit import Domain +enum PostCellViewModel: Hashable { + case title(cellVM: PostTitleCVC.ViewModel) + case content(cellVM: PostContentCVC.ViewModel) +} + final class PostView: UIView { - let postVM: PostViewModel + private let postVM: PostViewModel + + struct ViewModel { + let viewModels: [PostCellViewModel] + } private let header: UIView = { let view = UIView() @@ -46,11 +55,21 @@ final class PostView: UIView { return pageControl }() - var post: Post? { - didSet { - contentCollectionView.reloadData() + private lazy var dataSource: UICollectionViewDiffableDataSource = { + var datasource = UICollectionViewDiffableDataSource(collectionView: contentCollectionView) { collectionView, indexPath, cellViewModel in + switch cellViewModel { + case .title(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostTitleCVC.identifier, for: indexPath) as! PostTitleCVC + cell.configure(viewModel: cellVM) + return cell + case .content(cellVM: let cellVM): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostContentCVC.identifier, for: indexPath) as! PostContentCVC + cell.configure(viewModel: cellVM) + return cell + } } - } + return datasource + }() init(postVM: PostViewModel) { self.postVM = postVM @@ -69,6 +88,14 @@ final class PostView: UIView { fatalError("init(coder:) has not been implemented") } + func configureSnapshot(viewModel: ViewModel) { + var snapshot = NSDiffableDataSourceSnapshot() + // 섹션이 하나밖에 없는 뷰입니다. + snapshot.appendSections([0]) + snapshot.appendItems(viewModel.viewModels, toSection: 0) + dataSource.apply(snapshot, animatingDifferences: true) + } + private func setUpHierarchy() { addSubview(contentCollectionView) addSubview(pageControl) @@ -77,10 +104,9 @@ final class PostView: UIView { } private func setUpCollectionView() { - self.contentCollectionView.dataSource = self self.contentCollectionView.delegate = self - self.contentCollectionView.register(PostTitleCVC.self, forCellWithReuseIdentifier: "postTitleCVC") - self.contentCollectionView.register(PostContentCVC.self, forCellWithReuseIdentifier: "postContentCVC") + self.contentCollectionView.register(PostTitleCVC.self, forCellWithReuseIdentifier: PostTitleCVC.identifier) + self.contentCollectionView.register(PostContentCVC.self, forCellWithReuseIdentifier: PostContentCVC.identifier) } private func setUpLayout() { @@ -110,32 +136,7 @@ final class PostView: UIView { } } -extension PostView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{ - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - let count = (post?.postImages?.count ?? 1) + 1 - self.pageControl.numberOfPages = count - return count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let row = indexPath.row - if row == 0 { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postTitleCVC", for: indexPath) as! PostTitleCVC - cell.thumbnailImage = post?.postThumbnailImage - cell.title = post?.postTitle - cell.editorProfileImage = post?.postEditorProfileImage - cell.editorName = post?.postEditor - cell.postCreatedAt = convertDateToString(date: post?.postCreatedAt ?? nil) - cell.postCategory = "\(post?.postCategory.tagName ?? "")" - return cell - } else { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postContentCVC", for: indexPath) as! PostContentCVC - cell.postContentImage = post?.postImages?[row-1] - cell.postContentImageSource = post?.postImageSources?[row-1] - cell.postContent = post?.postContents?[row-1] - return cell - } - } +extension PostView: UICollectionViewDelegateFlowLayout{ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: collectionView.frame.width, height: collectionView.frame.height) diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift index 041ef43..3689862 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/ViewController/PostViewController.swift @@ -63,8 +63,8 @@ final class PostViewController: UIViewController { switch event { case .goToBackScreen: self.postVM.delegate?.goToBackScreen() - case .getPostOneData: - self.rootView.post = self.postVM.post + case .getPostOneData(let viewModel): + self.rootView.configureSnapshot(viewModel: viewModel) } }.store(in: &cancellable) } diff --git a/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift index fdaf8fe..f2570f0 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Post/ViewModel/PostViewModel.swift @@ -21,12 +21,12 @@ class PostViewModel { weak var delegate: PostViewModelDelegate? let postId: String - var post: Post? = nil + var post: PostTemplate? - init(post: Post, postUseCase: PostUseCase) { + init(postId: String, postUseCase: PostUseCase) { print("PostViewModel init") self.postUseCase = postUseCase - self.postId = post.post_id + self.postId = postId } deinit { @@ -60,11 +60,37 @@ class PostViewModel { postUseCase.getPostOneData(postId: postId) .sink { [weak self] postData in guard let self = self else { return } - self.post = postData - self.output.send(.getPostOneData) + self.post = postData.toPostTemplate() + self.output.send(.getPostOneData(vm: makeViewModel())) } .store(in: &cancellables) } + + private func makeViewModel() -> PostView.ViewModel { + var viewModels: [PostCellViewModel] = [] + + if let post = self.post { + let titleViewModel = PostTitleCVC.ViewModel( + uuid: UUID().uuidString, + title: post.postTitle, + editorProfileImage: post.postEditorProfileImage, + thumbnailImage: post.postThumbnailImage, + editorName: post.postEditor, + postCreatedAt: convertDateToString(date: post.postCreatedAt), + postCategory: post.postCategory) + viewModels.append(.title(cellVM: titleViewModel)) + + for i in 0..<(post.postContents?.count ?? 0) { + let postContentVM: PostContentCVC.ViewModel = .init( + uuid: UUID().uuidString, + postContentImage: self.post?.postImages?[i], + postContentImageSource: self.post?.postImageSources?[i], + postContentText: self.post?.postContents?[i]) + viewModels.append(.content(cellVM: postContentVM)) + } + } + return .init(viewModels: viewModels) + } } extension PostViewModel { @@ -74,6 +100,40 @@ extension PostViewModel { } enum Output { case goToBackScreen - case getPostOneData + case getPostOneData(vm: PostView.ViewModel) + } +} + +struct PostTemplate: Sendable { + let uuid = UUID() + let postId: String + let postCategory: String + let postTitle: String + let postThumbnailImage: String + let postImages: [String]? + let postImageSources: [String]? + let postContents: [String]? + let postEditor: String? + let postEditorProfileImage: String? + let postView: Int? + let postCreatedAt: Date? + let postUpdatedAt: Date? +} + +extension Post { + func toPostTemplate() -> PostTemplate { + return PostTemplate( + postId: self.post_id, + postCategory: self.postCategory.name, + postTitle: self.postTitle, + postThumbnailImage: self.postThumbnailImage, + postImages: self.postImages, + postImageSources: self.postImageSources, + postContents: self.postContents, + postEditor: self.postEditor, + postEditorProfileImage: self.postEditorProfileImage, + postView: self.postView, + postCreatedAt: self.postCreatedAt, + postUpdatedAt: self.postUpdatedAt) } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift index ceb9419..9ae64ef 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/SearchCoordinator.swift @@ -61,11 +61,11 @@ public class SearchCoordinatorImpl: SearchCoordinator, @preconcurrency SearchVie } @MainActor - func goToPostView(post: Post) { + func goToPostView(postId: String) { let postCoordinator = self.dependency.postCoordinator postCoordinator.delegate = self childCoordinators.append(postCoordinator) - let postVC = postCoordinator.start(post: post) + let postVC = postCoordinator.start(postId: postId) self.navigationController.pushViewController(postVC, animated: true) } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridCVC.swift index 94682f4..9b9c551 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridCVC.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridCVC.swift @@ -24,16 +24,11 @@ final class SearchGridCVC: UICollectionViewCell { label.textColor = .white return label }() - - var postImage: String? { - didSet { - postImageView.image = UIImage(named: postImage ?? "blackScreen") - } - } - var postTitle: String? { + var searchGridPost: SearchGridPostTemplate? { didSet { - postTitleLabel.text = postTitle + self.postImageView.image = UIImage(named: searchGridPost?.postThumbnailImage ?? "blackScreen") + self.postTitleLabel.text = searchGridPost?.postTitle } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift index 8c31351..4221b79 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift @@ -39,7 +39,7 @@ final class SearchGridView: UIView { return collectionView }() - var posts: [Post] = [] { + var posts: [SearchGridPostTemplate] = [] { didSet { self.postGridCollectionView.reloadData() } @@ -113,14 +113,13 @@ extension SearchGridView: UICollectionViewDataSource, UICollectionViewDelegateFl func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let post = self.posts[indexPath.row] let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchGridCVC", for: indexPath) as! SearchGridCVC - cell.postImage = post.postThumbnailImage - cell.postTitle = post.postTitle + cell.searchGridPost = post return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let post = posts[indexPath.row] - searchVM.sendInputEvent(input: .postTappedFromGrid(post)) + searchVM.sendInputEvent(input: .postTappedFromGrid(post.postId)) } /// 각 셀의 크기를 동적으로 지정 diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift index bd6ec0e..e60c3f9 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift @@ -38,23 +38,14 @@ final class SearchResultCVC: UICollectionViewCell { return btn }() - var thumbnailImage: String? { + var searchResultPost: RecentSearchPostTemplate? { didSet { - postThumbnailImageView.image = UIImage(named: thumbnailImage ?? "blackScreen") + self.postThumbnailImageView.image = UIImage(named: searchResultPost?.postThumbnailImage ?? "blackScreen") + self.postTitleLabel.text = searchResultPost?.postTitle + self.postCategoryLabel.text = searchResultPost?.postCategory?.name } } - var postTitle: String? { - didSet { - postTitleLabel.text = postTitle - } - } - - var postCategory: String? { - didSet { - postCategoryLabel.text = postCategory - } - } /// 최근 검색일 경우에는 삭제 버튼이 보이지 않도록 var isRecentPost: Bool = true { diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift index f189d7f..a121328 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift @@ -16,13 +16,13 @@ final class SearchResultView: UIView { @Published private var isRecentSearch: Bool = true /// 최근 검색 - var recentSearchPosts: [Post] = [] { + var recentSearchPosts: [RecentSearchPostTemplate] = [] { didSet { resultCollectionView.reloadData() } } /// 검색 결과 - var searchResultPosts: [Post] = [] { + var searchResultPosts: [RecentSearchPostTemplate] = [] { didSet { resultCollectionView.reloadData() } @@ -185,21 +185,17 @@ extension SearchResultView: UICollectionViewDataSource, UICollectionViewDelegate func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchResultCVC", for: indexPath) as! SearchResultCVC + let post = recentSearchPosts[indexPath.row] if isRecentSearch { - let post = recentSearchPosts[indexPath.row] cell.isRecentPost = true - cell.postCategory = post.postCategory.tagName - cell.thumbnailImage = post.postThumbnailImage - cell.postTitle = post.postTitle + cell.searchResultPost = post cell.onDeleteRecentSearchPost = { [weak self] in guard let self = self else { return } self.searchVM.sendInputEvent(input: .deleteRecentSearchPostBtnTapped(indexPath.row)) } } else { cell.isRecentPost = false - cell.postCategory = "#인문" - cell.thumbnailImage = "testImage3" - cell.postTitle = "그래서 이 아저씨가 누군데?" + cell.searchResultPost = post } return cell } @@ -207,7 +203,7 @@ extension SearchResultView: UICollectionViewDataSource, UICollectionViewDelegate func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if isRecentSearch { let post = recentSearchPosts[indexPath.row] - searchVM.sendInputEvent(input: .postTappedFromRecentSearch(post)) + searchVM.sendInputEvent(input: .postTappedFromRecentSearch(post.postId)) } else { } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift index 2d79981..4ff9f5c 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift @@ -54,10 +54,8 @@ final class SearchGridViewController: UIViewController { self.searchVM.delegate?.goToSearchResultView(searchVM: searchVM) case .getSearchGridPosts: self.rootView.posts = self.searchVM.searchGridPosts - case .goToPostViewFromGrid(let post): - self.searchVM.delegate?.goToPostView(post: post) -// case .goToPostView(let post): -// self.searchVM.delegate?.goToPostView(post: post) + case .goToPostViewFromGrid(let postId): + self.searchVM.delegate?.goToPostView(postId: postId) default: break } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift index 4869583..23609d8 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift @@ -62,7 +62,7 @@ class SearchResultViewController: UIViewController { case .getRecentSearchPosts: self.rootView.recentSearchPosts = self.searchVM.recentSearchPosts case .goToPostViewFromSearch(let post): - self.searchVM.delegate?.goToPostView(post: post) + self.searchVM.delegate?.goToPostView(postId: post) case .removeRecentSearchPost(let index): self.rootView.recentSearchPosts.remove(at: index) default: diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift index 44ad656..54270e5 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewModel/SearchViewModel.swift @@ -11,7 +11,7 @@ import Domain protocol SearchViewModelDelegate: AnyObject { func goToSearchResultView(searchVM: SearchViewModel) - func goToPostView(post: Post) + func goToPostView(postId: String) func goBackToResultGridView() } @@ -22,8 +22,8 @@ class SearchViewModel { var cancellables = Set() weak var delegate: SearchViewModelDelegate? - var searchGridPosts: [Post] = [] - var recentSearchPosts: [Post] = [] + var searchGridPosts: [SearchGridPostTemplate] = [] + var recentSearchPosts: [RecentSearchPostTemplate] = [] init(postUseCase: PostUseCase) { print("SearchViewModel init") @@ -90,20 +90,46 @@ class SearchViewModel { private func getSearchGridPosts() { postUseCase.getSearchGridPostsData() - .sink { [weak self] searchGridPostData in + .map { searchGridPostData -> [SearchGridPostTemplate] in + var posts: [SearchGridPostTemplate] = [] + for post in searchGridPostData { + posts.append( + SearchGridPostTemplate(postId: post.post_id, + postTitle: post.postTitle, + postThumbnailImage: post.postThumbnailImage, + postCategory: post.postCategory)) + } + return posts + } + .sink { [weak self] posts in guard let self = self else { return } - self.searchGridPosts = searchGridPostData + self.searchGridPosts = posts self.output.send(.getSearchGridPosts) - }.store(in: &cancellables) + } + .store(in: &cancellables) } private func getRecentSearchPosts() { postUseCase.getRecentSearchPostData() - .sink { [weak self] recentSearchPostData in + .map { recentSearchPostData -> [RecentSearchPostTemplate] in + var posts: [RecentSearchPostTemplate] = [] + for post in recentSearchPostData { + posts.append( + RecentSearchPostTemplate( + postId: post.post_id, + postTitle: post.postTitle, + postThumbnailImage: post.postThumbnailImage, + postCategory: post.postCategory) + ) + } + return posts + } + .sink { [weak self] posts in guard let self = self else { return } - self.recentSearchPosts = recentSearchPostData + self.recentSearchPosts = posts self.output.send(.getRecentSearchPosts) - }.store(in: &cancellables) + } + .store(in: &cancellables) } private func saveRecentSearchPosts() { @@ -117,9 +143,9 @@ extension SearchViewModel { case searchBarTapped case searchBarCancelTapped case searchGridViewLoaded - case postTappedFromGrid(Post) + case postTappedFromGrid(String) case searchResultViewLoaded - case postTappedFromRecentSearch(Post) + case postTappedFromRecentSearch(String) case deleteRecentSearchPostBtnTapped(Int) case searchResultViewDisappeared } @@ -127,10 +153,25 @@ extension SearchViewModel { case goToSearchResultView case goBackToSearchResultView case getSearchGridPosts - case goToPostViewFromGrid(Post) + case goToPostViewFromGrid(String) case getRecentSearchPosts - case goToPostViewFromSearch(Post) + case goToPostViewFromSearch(String) case removeRecentSearchPost(Int) case saveRecentSearchPosts } } + +/// Domain의 엔티티와 대응되는 뷰계층용 구조체입니다. +struct SearchGridPostTemplate { + let postId: String + let postTitle: String + let postThumbnailImage: String + let postCategory: KnowledgeCategory? +} + +struct RecentSearchPostTemplate { + let postId: String + let postTitle: String + let postThumbnailImage: String + let postCategory: KnowledgeCategory? +} diff --git a/README.md b/README.md index 4653460..c098f60 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,88 @@ -# MyLuxury -클래식 음악과 고전 명화들을 즐길 수 있는 앱 서비스 +# MyLuxury: 상식과 지식 사이 + +## 프로젝트 소개(개인 프로젝트) +짧은 상식 및 지식을 모아놓은 캐주얼 교육 컨텐츠 앱 서비스 + +## 📆 개발 기간 +- 2024.10 ~ 진행중 + +## ⚡ 담당 개발 기능 +- 앱 내 모든 화면 UI 구현 +- MVVM-C 디자인 패턴 구현 +- 의존성 주입 컨테이너 구현 +- Firestore를 활용한 사용자 인증 기능 구현 +- 앱 아키텍처 구조 설계 + +## 📁 디렉토리 구조 +```plaintext +. +├── Data +│   ├── Package.swift +│   ├── Sources +│   │   └── Data +│   │   ├── Local +│   │   ├── Network +│   │   └── Repository +│   │   ├── Member +│   │   │   +│   │   └── Post   +│   └── Tests +│ +├── Domain +│   ├── Package.swift +│   ├── Sources +│   │   └── Domain +│   │   ├── Entity +│   │   ├── Enums +│   │   ├── RepositoryInterface +│   │   └── UseCase +│   └── Tests +│  +├── Presentation +│   ├── Package.swift +│   ├── Sources +│   │   └── Presentation +│   │   ├── Base +│   │   │   ├── AppCoordinator.swift +│   │   │   ├── Login +│   │   │   └── Tab +│   │   ├── Home +│   │   │   ├── HomeCoordinator.swift +│   │   │   ├── View +│   │   │   ├── ViewController +│   │   │   └── ViewModel +│   │   ├── Library +│   │   │   ├── LibraryCoordinator.swift +│   │   │   ├── View +│   │   │   ├── ViewController +│   │   │   └── ViewModel +│   │   ├── Post +│   │   │   ├── PostCoordinator.swift +│   │   │   ├── View +│   │   │   ├── ViewController +│   │   │   └── ViewModel +│   │   ├── Search +│   │   │   ├── SearchCoordinator.swift +│   │   │   ├── View +│   │   │   ├── ViewController +│   │   │   └── ViewModel +│   │   └── Utils +│   │   ├── Constants.swift +│   │   ├── DateFormatter.swift +│   │   └── Extensions +│   └── Tests +│  +├── MyLuxury +│   ├── App +│   │   ├── AppComponent.swift +│   │   ├── AppDelegate.swift +│   │   ├── SceneDelegate.swift +│   │   └── SplashView.swift +│   ├── Assets.xcassets +│   ├── CodeConvention.txt +│   ├── GoogleService-Info.plist +│   ├── Info.plist +│   ├── MyLuxury.entitlements +│   ├── Utils +├── MyLuxuryTests +├── MyLuxuryUITests