diff --git a/.gitignore b/.gitignore index ae7b5f9..6453281 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,4 @@ Derived/ *.xcworkspace *.xcodeproj master.key +Package.resolved diff --git a/.mise.toml b/.mise.toml index 6770e5b..10c31de 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,3 +1,3 @@ [tools] -tuist = "4.9.0" +tuist = "4.32.0" diff --git a/Projects/App/Resources/Assets.xcassets/first.imageset/Contents.json b/Projects/App/Resources/Assets.xcassets/first.imageset/Contents.json new file mode 100644 index 0000000..0dcb4da --- /dev/null +++ b/Projects/App/Resources/Assets.xcassets/first.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "rank1.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/App/Resources/Assets.xcassets/first.imageset/rank1.svg b/Projects/App/Resources/Assets.xcassets/first.imageset/rank1.svg new file mode 100644 index 0000000..4926dc7 --- /dev/null +++ b/Projects/App/Resources/Assets.xcassets/first.imageset/rank1.svg @@ -0,0 +1,3 @@ +<svg width="120" height="48" viewBox="0 0 120 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 10C0 4.47715 4.47715 0 10 0H110C115.523 0 120 4.47715 120 10V48H0V10Z" fill="#202326"/> +</svg> diff --git a/Projects/App/Resources/Assets.xcassets/second.imageset/Contents.json b/Projects/App/Resources/Assets.xcassets/second.imageset/Contents.json new file mode 100644 index 0000000..4e53344 --- /dev/null +++ b/Projects/App/Resources/Assets.xcassets/second.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "rank2.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/App/Resources/Assets.xcassets/second.imageset/rank2.svg b/Projects/App/Resources/Assets.xcassets/second.imageset/rank2.svg new file mode 100644 index 0000000..5bbe0d1 --- /dev/null +++ b/Projects/App/Resources/Assets.xcassets/second.imageset/rank2.svg @@ -0,0 +1,3 @@ +<svg width="120" height="32" viewBox="0 0 120 32" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 10C0 4.47715 4.47715 0 10 0H120V32H0V10Z" fill="#202326"/> +</svg> diff --git a/Projects/App/Resources/Assets.xcassets/third.imageset/Contents.json b/Projects/App/Resources/Assets.xcassets/third.imageset/Contents.json new file mode 100644 index 0000000..dfc47b2 --- /dev/null +++ b/Projects/App/Resources/Assets.xcassets/third.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "rank3.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/App/Resources/Assets.xcassets/third.imageset/rank3.svg b/Projects/App/Resources/Assets.xcassets/third.imageset/rank3.svg new file mode 100644 index 0000000..3414c44 --- /dev/null +++ b/Projects/App/Resources/Assets.xcassets/third.imageset/rank3.svg @@ -0,0 +1,3 @@ +<svg width="120" height="20" viewBox="0 0 120 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 0H110C115.523 0 120 4.47715 120 10V20H0V0Z" fill="#202326"/> +</svg> diff --git a/Projects/App/Sources/Feature/DetailFeature/Sources/DetailView.swift b/Projects/App/Sources/Feature/DetailFeature/Sources/DetailView.swift index 08cc9ec..9a2d8ae 100644 --- a/Projects/App/Sources/Feature/DetailFeature/Sources/DetailView.swift +++ b/Projects/App/Sources/Feature/DetailFeature/Sources/DetailView.swift @@ -1,12 +1,9 @@ import SwiftUI struct DetailView: View { - @StateObject var viewModel: DetailViewModel @State private var topNavigationState: Bool = false @State private var emojiName: [String] = ["heart", "congrats", "thumbsUp", "thinking", "poop", "china"] @State private var emojiServerName: [String] = ["HEART", "CONGRATUATION", "THUMBSUP", "THINKING", "POOP", "CHINA"] - @State private var emojiStates: [Int] = [0, 2, 3, 400, 500, 600] - @State private var test: [Bool] = [false, false, false, false, false, false] @State private var graySmileState: Bool = false @Environment(\.dismiss) private var dismiss @StateObject var postViewModel: PostViewModel @@ -18,11 +15,12 @@ struct DetailView: View { @State public var name: String = "" @State public var grade: Int = 0 @State public var imageUrl: [String] = [] - @State public var tagList: [(name: String, id: Int)] + @State public var tagList: [(name: String, id: Int)] = [] @State public var emojiList: [Int] = [] @State public var checkEmojiList: [Bool] = [] @State public var createTime: String = "" @State public var topNavigationBar: Bool = true + @State private var selectedIndex = 0 var body: some View { NavigationStack { @@ -66,12 +64,11 @@ struct DetailView: View { .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) .foregroundStyle(GPleAsset.Color.gray800.swiftUIColor) - Spacer() } .padding(.top, 8) - TabView(selection: $viewModel.imageCount) { + TabView(selection: $selectedIndex) { ForEach(imageUrl.indices, id: \.self) { index in if let imageUrl = URL(string: imageUrl[index]) { AsyncImage(url: imageUrl) { image in @@ -96,7 +93,6 @@ struct DetailView: View { .padding(.top, 16) .padding(.leading, 16) - HStack(spacing: 8) { ForEach(tagList.indices, id: \.self) { tag in Text("@\(tagList[tag].name)") @@ -107,7 +103,6 @@ struct DetailView: View { .padding(.top, 6) .padding(.leading, 16) - let dateString = createTime.split(separator: "T").first if let dateString = dateString { let components = dateString.split(separator: "-") @@ -143,46 +138,48 @@ struct DetailView: View { } .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 8) - } - } - - Spacer() - } - .padding(.top, 8) - if graySmileState { - HStack(spacing: 25) { - ForEach(0..<6) { tag in - Button(action: { - Haptic.impact(style: .soft) - postViewModel.setupPostId(postId: postId) - postViewModel.setupEmojiType(emojiType: emojiServerName[tag]) - - postViewModel.postEmoji { success in - print("\(emojiName[tag]) 성공") - - if checkEmojiList[tag] == true { - emojiList[tag] -= 1 - checkEmojiList[tag].toggle() - } else { - emojiList[tag] = 1 - checkEmojiList[tag].toggle() + if graySmileState { + HStack(spacing: 25) { + ForEach(0..<6) { tag in + Button(action: { + Haptic.impact(style: .soft) + postViewModel.setupPostId(postId: postId) + postViewModel.setupEmojiType(emojiType: emojiServerName[tag]) + + postViewModel.postEmoji { success in + print("\(emojiName[tag]) 성공") + + if success { + if checkEmojiList[tag] == true { + emojiList[tag] -= 1 + checkEmojiList[tag].toggle() + } else { + emojiList[tag] = 1 + checkEmojiList[tag].toggle() + } + } + } + }) { + Image(emojiName[tag]) + } } } - }) { - Image(emojiName[tag]) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 5) + .foregroundStyle(GPleAsset.Color.gray1000.swiftUIColor) + ) + .padding(.leading, 20) + .padding(.top, 10) } } } - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background( - RoundedRectangle(cornerRadius: 5) - .foregroundStyle(GPleAsset.Color.gray1000.swiftUIColor) - ) - .padding(.leading, 20) - .padding(.top, 490) + + Spacer() } + .padding(.top, 8) } } .navigationBarBackButtonHidden(true) diff --git a/Projects/App/Sources/Feature/DetailFeature/Sources/DetailViewModel.swift b/Projects/App/Sources/Feature/DetailFeature/Sources/DetailViewModel.swift deleted file mode 100644 index 763d72c..0000000 --- a/Projects/App/Sources/Feature/DetailFeature/Sources/DetailViewModel.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -final class DetailViewModel: ObservableObject { - @Published var name: String = "한재형" - @Published var schoolYear: Int = 2 - @Published var imageCount: Int = 3 - @Published var title: String = "유성이 없이 찍은 7기" - @Published var tagUser: [String] = ["박미리", "장예슬"] - @Published var wwDay: String = "6" - @Published var ddDay: String = "7" -} diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostView.swift b/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostView.swift new file mode 100644 index 0000000..7a442cb --- /dev/null +++ b/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostView.swift @@ -0,0 +1,202 @@ +import SwiftUI + +struct LocationPostView: View { + @StateObject var viewModel: LocationPostViewModel + @State private var topNavigationState: Bool = false + @Environment(\.dismiss) private var dismiss + + var locationType: String + + var body: some View { + ZStack { + GPleAsset.Color.back.swiftUIColor + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + ZStack { + HStack { + Button { + dismiss() + } label: { + GPleAsset.Assets.chevronRight.swiftUIImage + .padding(.leading, 20) + } + Spacer() + } + Text(locationTypeText()) + .foregroundStyle(.white) + .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 18)) + } + .padding(.bottom, 16) + + ScrollView { + switch locationType { + case "GYM": + ForEach(viewModel.gymPostList) { post in + DetailView( + postViewModel: PostViewModel(), + postId: post.id, + title: post.title, + name: post.author.name, + grade: post.author.grade, + imageUrl: post.imageUrl, + tagList: post.tagList.map{ ($0.name, $0.id) }, + emojiList: [ + post.emojiList.chinaCount, + post.emojiList.congCount, + post.emojiList.heartCount, + post.emojiList.poopCount, + post.emojiList.thinkCount, + post.emojiList.thumbsCount + ], + checkEmojiList: post.checkEmoji, + createTime: post.createdTime + ) + + Rectangle() + .foregroundStyle(GPleAsset.Color.gray900.swiftUIColor) + .frame(height: 3) + .padding(.vertical, 10) + } + case "HOME": + ForEach(viewModel.homePostList) { post in + DetailView( + postViewModel: PostViewModel(), + postId: post.id, + title: post.title, + name: post.author.name, + grade: post.author.grade, + imageUrl: post.imageUrl, + tagList: post.tagList.map{ ($0.name, $0.id) }, + emojiList: [ + post.emojiList.chinaCount, + post.emojiList.congCount, + post.emojiList.heartCount, + post.emojiList.poopCount, + post.emojiList.thinkCount, + post.emojiList.thumbsCount + ], + checkEmojiList: post.checkEmoji, + createTime: post.createdTime + ) + + Rectangle() + .foregroundStyle(GPleAsset.Color.gray900.swiftUIColor) + .frame(height: 3) + .padding(.vertical, 10) + } + case "PLAYGROUND": + ForEach(viewModel.playgroundPostList) { post in + DetailView( + postViewModel: PostViewModel(), + postId: post.id, + title: post.title, + name: post.author.name, + grade: post.author.grade, + imageUrl: post.imageUrl, + tagList: post.tagList.map{ ($0.name, $0.id) }, + emojiList: [ + post.emojiList.chinaCount, + post.emojiList.congCount, + post.emojiList.heartCount, + post.emojiList.poopCount, + post.emojiList.thinkCount, + post.emojiList.thumbsCount + ], + checkEmojiList: post.checkEmoji, + createTime: post.createdTime + ) + + Rectangle() + .foregroundStyle(GPleAsset.Color.gray900.swiftUIColor) + .frame(height: 3) + .padding(.vertical, 10) + } + case "DOMITORY": + ForEach(viewModel.domitoryPostList) { post in + DetailView( + postViewModel: PostViewModel(), + postId: post.id, + title: post.title, + name: post.author.name, + grade: post.author.grade, + imageUrl: post.imageUrl, + tagList: post.tagList.map{ ($0.name, $0.id) }, + emojiList: [ + post.emojiList.chinaCount, + post.emojiList.congCount, + post.emojiList.heartCount, + post.emojiList.poopCount, + post.emojiList.thinkCount, + post.emojiList.thumbsCount + ], + checkEmojiList: post.checkEmoji, + createTime: post.createdTime + ) + + Rectangle() + .foregroundStyle(GPleAsset.Color.gray900.swiftUIColor) + .frame(height: 3) + .padding(.vertical, 10) + } + case "WALKING_TRAIL": + ForEach(viewModel.walkingTrailPostList) { post in + DetailView( + postViewModel: PostViewModel(), + postId: post.id, + title: post.title, + name: post.author.name, + grade: post.author.grade, + imageUrl: post.imageUrl, + tagList: post.tagList.map{ ($0.name, $0.id) }, + emojiList: [ + post.emojiList.chinaCount, + post.emojiList.congCount, + post.emojiList.heartCount, + post.emojiList.poopCount, + post.emojiList.thinkCount, + post.emojiList.thumbsCount + ], + checkEmojiList: post.checkEmoji, + createTime: post.createdTime + ) + + Rectangle() + .foregroundStyle(GPleAsset.Color.gray900.swiftUIColor) + .frame(height: 3) + .padding(.vertical, 10) + } + default: + EmptyView() + } + } + .padding(.top, 8) + } + .navigationBarBackButtonHidden() + .onAppear { + viewModel.fetchGymList() + viewModel.fetchDomitoryList() + viewModel.fetchHomeList() + viewModel.fetchPlayGroundList() + viewModel.fetchWalkingTrailList() + } + } + } + + func locationTypeText() -> String { + switch locationType { + case "GYM": + return "금봉관" + case "DOMITORY": + return "동행관" + case "HOME": + return "본관" + case "PLAYGROUND": + return "운동장" + case "WALKING_TRAIL": + return "산책로" + default: + return "" + } + } +} diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift b/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift new file mode 100644 index 0000000..1e96938 --- /dev/null +++ b/Projects/App/Sources/Feature/MainFeature/Sources/LocationPostViewModel.swift @@ -0,0 +1,129 @@ +import Moya +import Domain +import Foundation + +final class LocationPostViewModel: ObservableObject { + private let authProvider = MoyaProvider<MainAPI>() + private var accessToken: String = "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzM0NjYyNTg4LCJleHAiOjE3NDQ2NjI1ODh9.FG4FVQ4oikC4HNy5h7gq0QyCIjVZtceIOKwAMnkULAt4y0lX5gGIF1s2Mdj9qr1H" + + @Published public var gymPostList: [Post] = [] + @Published public var homePostList: [Post] = [] + @Published public var playgroundPostList: [Post] = [] + @Published public var domitoryPostList: [Post] = [] + @Published public var walkingTrailPostList: [Post] = [] + + @MainActor + public func fetchGymList() { + authProvider.request(.fetchGymPostList(authorization: accessToken)) { result in + switch result { + case let .success(res): + do { + if let responseString = String(data: res.data, encoding: .utf8) { + print("서버 응답 데이터: \(responseString)") + } + + let responseModel = try JSONDecoder().decode([Post].self, from: res.data) + self.gymPostList = responseModel + + print("게시물 불러오기 성공") + } catch { + print("JSON 디코딩 에러: \(error)") + } + case let .failure(err): + print("Network request failed: \(err)") + } + } + } + + @MainActor + public func fetchHomeList() { + authProvider.request(.fetchHomePostList(authorization: accessToken)) { result in + switch result { + case let .success(res): + do { + if let responseString = String(data: res.data, encoding: .utf8) { + print("서버 응답 데이터: \(responseString)") + } + + let responseModel = try JSONDecoder().decode([Post].self, from: res.data) + self.homePostList = responseModel + + print("게시물 불러오기 성공") + } catch { + print("JSON 디코딩 에러: \(error)") + } + case let .failure(err): + print("Network request failed: \(err)") + } + } + } + + @MainActor + public func fetchPlayGroundList() { + authProvider.request(.fetchPlaygroundPostList(authorization: accessToken)) { result in + switch result { + case let .success(res): + do { + if let responseString = String(data: res.data, encoding: .utf8) { + print("서버 응답 데이터: \(responseString)") + } + + let responseModel = try JSONDecoder().decode([Post].self, from: res.data) + self.playgroundPostList = responseModel + + print("게시물 불러오기 성공") + } catch { + print("JSON 디코딩 에러: \(error)") + } + case let .failure(err): + print("Network request failed: \(err)") + } + } + } + + @MainActor + public func fetchDomitoryList() { + authProvider.request(.fetchGymPostList(authorization: accessToken)) { result in + switch result { + case let .success(res): + do { + if let responseString = String(data: res.data, encoding: .utf8) { + print("서버 응답 데이터: \(responseString)") + } + + let responseModel = try JSONDecoder().decode([Post].self, from: res.data) + self.domitoryPostList = responseModel + + print("게시물 불러오기 성공") + } catch { + print("JSON 디코딩 에러: \(error)") + } + case let .failure(err): + print("Network request failed: \(err)") + } + } + } + + @MainActor + public func fetchWalkingTrailList() { + authProvider.request(.fetchWalkingTrailPostList(authorization: accessToken)) { result in + switch result { + case let .success(res): + do { + if let responseString = String(data: res.data, encoding: .utf8) { + print("서버 응답 데이터: \(responseString)") + } + + let responseModel = try JSONDecoder().decode([Post].self, from: res.data) + self.walkingTrailPostList = responseModel + + print("게시물 불러오기 성공") + } catch { + print("JSON 디코딩 에러: \(error)") + } + case let .failure(err): + print("Network request failed: \(err)") + } + } + } +} diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift b/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift index 6ad0a3d..90eb7a9 100644 --- a/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift +++ b/Projects/App/Sources/Feature/MainFeature/Sources/MainView.swift @@ -5,78 +5,106 @@ struct MainView: View { @StateObject var postViewModel: PostViewModel var body: some View { - NavigationStack { - ZStack { - GPleAsset.Color.back.swiftUIColor - .ignoresSafeArea() - - ScrollView(showsIndicators: false) { - VStack(spacing: 0) { - HStack(spacing: 0) { - GPleAsset.Assets.gpleBigLogo.swiftUIImage - .padding(.leading, 20) - - Spacer() - - NavigationLink(destination: MyPageView(viewModel: MyPageViewModel(), postViewModel: PostViewModel())) { - GPleAsset.Assets.profile.swiftUIImage - .padding(.trailing, 20) + NavigationView { + NavigationStack { + ZStack { + GPleAsset.Color.back.swiftUIColor + .ignoresSafeArea() + + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + HStack(spacing: 0) { + GPleAsset.Assets.gpleBigLogo.swiftUIImage + .padding(.leading, 20) + + Spacer() + + NavigationLink(destination: + MyPageView( + postViewModel: PostViewModel())) { + GPleAsset.Assets.profile.swiftUIImage + .padding(.trailing, 20) + } } - } - .padding(.top, 16) + .padding(.top, 16) - GPleAsset.Assets.backyard.swiftUIImage - .padding(.top, 61) - - GPleAsset.Assets.bongwan.swiftUIImage - .padding(.top, 6) - - HStack(spacing: 13) { - GPleAsset.Assets.geumbongGwan.swiftUIImage + NavigationLink(destination: LocationPostView(viewModel: LocationPostViewModel(), locationType: "WALKING_TRAIL")) { + GPleAsset.Assets.backyard.swiftUIImage + .padding(.top, 61) + } - GPleAsset.Assets.playground.swiftUIImage + NavigationLink(destination: LocationPostView(viewModel: LocationPostViewModel(), locationType: "HOME")) { + GPleAsset.Assets.bongwan.swiftUIImage + .padding(.top, 6) + } - GPleAsset.Assets.dongHaengGwan.swiftUIImage - } - .padding(.top, 8) + HStack(spacing: 13) { + NavigationLink(destination: LocationPostView(viewModel: LocationPostViewModel(), locationType: "GYM")) { + GPleAsset.Assets.geumbongGwan.swiftUIImage + } - HStack(spacing: 16) { - Spacer() + NavigationLink(destination: LocationPostView(viewModel: LocationPostViewModel(), locationType: "PLAYGROUND")) { + GPleAsset.Assets.playground.swiftUIImage + } - GPleAsset.Assets.zoomOut.swiftUIImage + NavigationLink(destination: LocationPostView(viewModel: LocationPostViewModel(), locationType: "DOMITORY")) { + GPleAsset.Assets.dongHaengGwan.swiftUIImage + } + } + .padding(.top, 8) - GPleAsset.Assets.zoomIn.swiftUIImage - } - .padding(.top, 19) - .padding(.trailing, 24) + HStack(spacing: 36) { + NavigationLink(destination: RankView(postViewModel: PostViewModel())) { + rankButton() + } - HStack(spacing: 36) { - rankButton() + NavigationLink(destination: PostCreateView(viewModel: PostViewModel())) { + imageUploadButton() + } - NavigationLink(destination: PostCreateView(viewModel: PostViewModel())) { - imageUploadButton() } - } - .padding(.top, 40) - - ForEach(viewModel.allPostList) { post in - postList( - name: post.author.name, - grade: post.author.grade, - title: post.title, - place: post.location, - tag: post.tagList.map { $0.name }, - date: post.createdTime, - imageURL: post.imageUrl - ) - } - .padding(.top, 60) + .padding(.top, 16) + + ForEach(viewModel.allPostList) { post in + NavigationLink(destination: DetailView( + postViewModel: PostViewModel(), + postId: post.id, + location: post.location, + title: post.title, + name: post.author.name, + grade: post.author.grade, + imageUrl: post.imageUrl, + tagList: post.tagList.map { ($0.name, $0.id) }, + emojiList: [ + post.emojiList.heartCount, + post.emojiList.congCount, + post.emojiList.thumbsCount, + post.emojiList.thinkCount, + post.emojiList.poopCount, + post.emojiList.chinaCount + ], + checkEmojiList: post.checkEmoji, + createTime: post.createdTime + )) { + postList( + name: post.author.name, + grade: post.author.grade, + title: post.title, + place: post.location, + tag: post.tagList.map { $0.name }, + date: post.createdTime, + imageURL: post.imageUrl + ) + } + } + .padding(.top, 60) - Spacer() + Spacer() + } + } + .onAppear { + viewModel.fetchAllPostList() } - } - .onAppear { - viewModel.fetchAllPostList() } } } diff --git a/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift b/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift index ccc8a47..80159d6 100644 --- a/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift +++ b/Projects/App/Sources/Feature/MainFeature/Sources/MainViewModel.swift @@ -7,6 +7,11 @@ public final class MainViewModel: ObservableObject { private var accessToken: String = "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzM0NjYyNTg4LCJleHAiOjE3NDQ2NjI1ODh9.FG4FVQ4oikC4HNy5h7gq0QyCIjVZtceIOKwAMnkULAt4y0lX5gGIF1s2Mdj9qr1H" @Published public var allPostList: [Post] = [] + @Published public var gymPostList: [Post] = [] + @Published public var homePostList: [Post] = [] + @Published public var playgroundPostList: [Post] = [] + @Published public var domitoryPostList: [Post] = [] + @Published public var walkingTrailPostList: [Post] = [] @MainActor public func fetchAllPostList() { diff --git a/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift b/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift index 5ea16da..49545fd 100644 --- a/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift +++ b/Projects/App/Sources/Feature/MyPageFeature/Sources/MyPageView.swift @@ -2,7 +2,6 @@ import SwiftUI import Domain struct MyPageView: View { - @StateObject var viewModel: MyPageViewModel @State private var topNavigationState = false @StateObject var postViewModel: PostViewModel @Environment(\.dismiss) private var dismiss @@ -31,7 +30,7 @@ struct MyPageView: View { HStack(spacing: 0) { VStack(alignment: .leading, spacing: 4) { - Text("\(viewModel.name)님,") + Text("\(postViewModel.myInfo?.name ?? "")님,") .foregroundStyle(.white) .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 20)) @@ -120,20 +119,9 @@ struct MyPageView: View { ScrollView { LazyVGrid(columns: columns, spacing: 2) { - ForEach(postViewModel.myPostList.indices, id: \.self) { index in - let myPost = postViewModel.myPostList[index] - - let myPostEmojiArray = [ - myPost.emojiList.heartCount, - myPost.emojiList.congCount, - myPost.emojiList.thumbsCount, - myPost.emojiList.thinkCount, - myPost.emojiList.poopCount, - myPost.emojiList.chinaCount - ] + ForEach(postViewModel.myPostList, id: \.id) { myPost in NavigationLink(destination: DetailView( - viewModel: DetailViewModel(), postViewModel: PostViewModel(), postId: myPost.id, location: myPost.location, @@ -142,7 +130,14 @@ struct MyPageView: View { grade: myPost.author.grade, imageUrl: myPost.imageUrl, tagList: myPost.tagList.map { ($0.name, $0.id) }, - emojiList: myPostEmojiArray, + emojiList: [ + myPost.emojiList.heartCount, + myPost.emojiList.congCount, + myPost.emojiList.thumbsCount, + myPost.emojiList.thinkCount, + myPost.emojiList.poopCount, + myPost.emojiList.chinaCount + ], checkEmojiList: myPost.checkEmoji, createTime: myPost.createdTime )) { @@ -173,20 +168,9 @@ struct MyPageView: View { ScrollView { LazyVGrid(columns: columns1, spacing: 2) { - ForEach(postViewModel.myReactionPostList.indices, id: \.self) { index in - let rtPost = postViewModel.myReactionPostList[index] - - let emojiArray = [ - rtPost.emojiList.heartCount, - rtPost.emojiList.congCount, - rtPost.emojiList.thumbsCount, - rtPost.emojiList.thinkCount, - rtPost.emojiList.poopCount, - rtPost.emojiList.chinaCount - ] + ForEach(postViewModel.myReactionPostList, id: \.id) { rtPost in NavigationLink(destination: DetailView( - viewModel: DetailViewModel(), postViewModel: PostViewModel(), postId: rtPost.id, location: rtPost.location, @@ -195,7 +179,14 @@ struct MyPageView: View { grade: rtPost.author.grade, imageUrl: rtPost.imageUrl, tagList: rtPost.tagList.map { ($0.name, $0.id) }, - emojiList: emojiArray, + emojiList: [ + rtPost.emojiList.heartCount, + rtPost.emojiList.congCount, + rtPost.emojiList.thumbsCount, + rtPost.emojiList.thinkCount, + rtPost.emojiList.poopCount, + rtPost.emojiList.chinaCount + ], checkEmojiList: rtPost.checkEmoji, createTime: rtPost.createdTime )) { @@ -242,6 +233,14 @@ struct MyPageView: View { print("반응 게시물 최신화 실패") } } + + postViewModel.myInfo { success in + if success { + print("내 정보 불러오기 성공") + } else { + print("내 정보 불러오기 실패") + } + } } .navigationBarBackButtonHidden(true) } diff --git a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift index 3090080..6fe571a 100644 --- a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift +++ b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostCreateView.swift @@ -19,7 +19,6 @@ struct PostCreateView: View { @State private var tagUserYear: [Int] = [0, 0, 0, 0, 0] @State private var tagUserId: [Int?] = Array(repeating: nil, count: 5) @State private var toast: FancyToast? = nil - @State private var boolState: Bool = false @State private var buttonState: Bool = true var body: some View { @@ -346,7 +345,6 @@ struct PostCreateView: View { buttonState: isFormValid && buttonState, buttonOkColor: GPleAsset.Color.main.swiftUIColor ){ - print("클릭") buttonState = false toast = FancyToast(type: .info, title: "업로드 중...", message: "해당 게시물의 업로드가 진행중입니다. 잠시만 기다려주세요.") @@ -447,15 +445,13 @@ struct PostCreateView: View { userProfileImage: student.profileImage, userName: student.name, userYear: student.grade, - userId: student.id, - userProfileImageList: $tagUserImages[index], - userNameList: $tagUserName[index], - userYearList: $tagUserYear[index], - tagUserId: $tagUserId[index] + userId: student.id ) + .padding(.bottom, 20) } } + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -470,81 +466,84 @@ struct PostCreateView: View { .navigationBarBackButtonHidden(true) } - private var isFormValid: Bool { - return !titleTextField.isEmpty && - images[0] != nil - } -} - -@ViewBuilder -func searchUserList( - userProfileImage: String, - userName: String, - userYear: Int, - userId: Int, - userProfileImageList: Binding<String>, - userNameList: Binding<String>, - userYearList: Binding<Int>, - tagUserId: Binding<Int?> -) -> some View { - Button { - userProfileImageList.wrappedValue = userProfileImage - userNameList.wrappedValue = userName - userYearList.wrappedValue = userYear - tagUserId.wrappedValue = userId - - print("추가: \(userId)") - print("추가: \(userName)") - print("추가: \(userYear)학년") - } label: { - HStack(spacing: 4) { - if let url = URL(string: userProfileImage) { - AsyncImage(url: url) { image in - image - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) - .clipShape(Circle()) - } placeholder: { + @ViewBuilder + func searchUserList( + userProfileImage: String, + userName: String, + userYear: Int, + userId: Int + ) -> some View { + Button { + if let emptyIndex = tagUserName.firstIndex(of: "") { + tagUserImages[emptyIndex] = userProfileImage + tagUserName[emptyIndex] = userName + tagUserYear[emptyIndex] = userYear + tagUserId[emptyIndex] = userId + + print("추가된 유저 ID: \(userId)") + print("추가된 유저 이름: \(userName)") + print("추가된 유저 학년: \(userYear)") + } else { + tagUserImages[4] = userProfileImage + tagUserName[4] = userName + tagUserYear[4] = userYear + tagUserId[4] = userId + } + } label: { + HStack(spacing: 4) { + if let url = URL(string: userProfileImage) { + AsyncImage(url: url) { image in + image + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + .clipShape(Circle()) + } placeholder: { + GPleAsset.Assets.profile.swiftUIImage + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + } + .padding(.leading, 24) + } else { GPleAsset.Assets.profile.swiftUIImage .resizable() .scaledToFit() .frame(width: 32, height: 32) + .padding(.leading, 24) } - .padding(.leading, 24) - } else { - GPleAsset.Assets.profile.swiftUIImage - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) - .padding(.leading, 24) - } - Text(userName) - .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 16)) - .foregroundStyle(.white) - .padding(.leading, 4) + Text(userName) + .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 16)) + .foregroundStyle(.white) + .padding(.leading, 4) - Text("· \(userYear)학년") - .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) - .foregroundStyle(GPleAsset.Color.gray800.swiftUIColor) + Text("· \(userYear)학년") + .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) + .foregroundStyle(GPleAsset.Color.gray800.swiftUIColor) - Spacer() + Spacer() - HStack(spacing: 8) { - GPleAsset.Assets.grayUserPlus.swiftUIImage + HStack(spacing: 8) { + GPleAsset.Assets.grayUserPlus.swiftUIImage - Text("추가하기") - .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) - .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) + Text("추가하기") + .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) + .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(GPleAsset.Color.gray1000.swiftUIColor) + ) + .padding(.trailing, 20) } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 8) - .foregroundStyle(GPleAsset.Color.gray1000.swiftUIColor) - ) - .padding(.trailing, 20) } } + + private var isFormValid: Bool { + return !titleTextField.isEmpty && + images[0] != nil + } } diff --git a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift index 5027e40..d1d9d53 100644 --- a/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift +++ b/Projects/App/Sources/Feature/PostCreateFeature/Sources/PostViewModel.swift @@ -8,6 +8,7 @@ public final class PostViewModel: ObservableObject { private let emojiProvider = MoyaProvider<EmojiAPI>( plugins: [NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))] ) + private let userProvider = MoyaProvider<UserAPI>() private var title: String = "" private var accessToken: String = "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzM0NjYyNTg4LCJleHAiOjE3NDQ2NjI1ODh9.FG4FVQ4oikC4HNy5h7gq0QyCIjVZtceIOKwAMnkULAt4y0lX5gGIF1s2Mdj9qr1H" private var userList: [Int] = [] @@ -20,8 +21,11 @@ public final class PostViewModel: ObservableObject { private var postId: Int = 0 private var emojiType: String = "" @Published public var allUserList: [UserListResponse] = [] + @Published public var popularityUserList: [PopularityRankingUserListResponse] = [] @Published var myPostList: [MyPostListResponse] = [] @Published var myReactionPostList: [MyReactionPostListResponse] = [] + @Published var popularityPostList: [PopularityResponse] = [] + @Published public var myInfo: MyInfoResponse? private var imageUploadResponse: ImageUploadResponse? @@ -104,7 +108,6 @@ public final class PostViewModel: ObservableObject { } } - public func myPostList(completion: @escaping (Bool) -> Void) { authProvider.request(.myPostList(authorization: accessToken)) { result in switch result { @@ -130,19 +133,52 @@ public final class PostViewModel: ObservableObject { } } + public func popularityPostList(completion: @escaping (Bool) -> Void) { + authProvider.request(.popularityPostList(authorization: accessToken)) { result in + switch result { + case let .success(response): + do { + let decodedResponse = try JSONDecoder().decode([PopularityResponse].self, from: response.data) + self.popularityPostList = decodedResponse + + for post in decodedResponse { + print("게시물 ID: \(post.id), 위치: \(post.location)") + print("이미지: \(post.imageUrl)") + } + + completion(true) + } catch { + print("JSON 디코딩 실패: \(error)") + completion(false) + } + case let .failure(err): + print("네트워크 요청 실패: \(err)") + completion(false) + } + } + } + public func allUserList(completion: @escaping (Bool) -> Void) { authProvider.request(.allUserList(authorization: accessToken)) { result in switch result { case let .success(response): do { - print("성공ㅣ유저 리스트 불러오기") + // 응답 데이터를 String으로 변환하여 출력 + if let responseString = String(data: response.data, encoding: .utf8) { + print("Response Data as String: \(responseString)") + } + + // JSON 디코딩 + print("성공: 유저 리스트 불러오기") self.allUserList = try JSONDecoder().decode([UserListResponse].self, from: response.data) completion(true) } catch { - print("Failed to decode JSON response") + // 디코딩 실패 시 에러 출력 + print("Failed to decode JSON response: \(error)") completion(false) } case let .failure(err): + // 네트워크 요청 실패 시 에러 출력 print("Network request failed: \(err)") completion(false) } @@ -150,6 +186,55 @@ public final class PostViewModel: ObservableObject { } + public func popularityUserList(completion: @escaping (Bool) -> Void) { + authProvider.request(.popularityUserList(authorization: accessToken)) { result in + switch result { + case let .success(response): + do { + print("성공: 유저 리스트 불러오기") + + self.popularityUserList = try JSONDecoder().decode([PopularityRankingUserListResponse].self, from: response.data) + + print("불러온 유저 리스트:") + for (index, user) in self.popularityUserList.enumerated() { + print("[\(index)] \(user)") + } + + completion(true) + } catch { + print("Failed to decode JSON response: \(error)") + completion(false) + } + case let .failure(err): + print("Network request failed: \(err)") + completion(false) + } + } + } + + public func myInfo(completion: @escaping (Bool) -> Void) { + userProvider.request(.userInfoInput(authorization: accessToken)) { result in + switch result { + case let .success(response): + do { + print("성공: 내 정보 불러오기") + + self.myInfo = try JSONDecoder().decode(MyInfoResponse.self, from: response.data) + + completion(true) + } catch { + print("Failed to decode JSON response: \(error)") + completion(false) + } + case let .failure(err): + print("Network request failed: \(err)") + completion(false) + } + } + } + + + public func uploadImages(completion: @escaping (Bool) -> Void) { authProvider.request(.uploadImage(files: imageDataArray, authorization: accessToken)) { result in switch result { diff --git a/Projects/App/Sources/Feature/RankFeature/Sources/RankView.swift b/Projects/App/Sources/Feature/RankFeature/Sources/RankView.swift new file mode 100644 index 0000000..bad36f2 --- /dev/null +++ b/Projects/App/Sources/Feature/RankFeature/Sources/RankView.swift @@ -0,0 +1,173 @@ +import SwiftUI + +struct RankView: View { + @Environment(\.dismiss) private var dismiss + @StateObject var postViewModel: PostViewModel + + var body: some View { + NavigationStack { + ZStack(alignment: .leading) { + GPleAsset.Color.back.swiftUIColor + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + ZStack { + HStack { + Button { + dismiss() + } label: { + GPleAsset.Assets.chevronRight.swiftUIImage + .padding(.leading, 20) + } + Spacer() + } + + Text("인기 순위") + .foregroundStyle(.white) + .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 18)) + .padding(.vertical, 16) + } + .padding(.bottom, 16) + + ScrollView(showsIndicators: false) { + HStack(spacing: 0) { + VStack(spacing: 4) { + Text("2위") + .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 16)) + .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) + .padding(.top, 16.5) + + Text(postViewModel.popularityUserList.indices.contains(1) ? postViewModel.popularityUserList[1].name : "") + .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) + .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) + + AsyncImage( + url: postViewModel.popularityUserList.indices.contains(1) ? + URL(string: postViewModel.popularityUserList[1].profileImage ?? "") : nil + ) { image in + image + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + .clipShape(Circle()) + } placeholder: { + GPleAsset.Assets.profile.swiftUIImage + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + } + .padding(.bottom, 8) + + GPleAsset.Assets.second.swiftUIImage + } + + VStack(spacing: 4) { + Text("1위") + .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 16)) + .foregroundStyle(GPleAsset.Color.rank1.swiftUIColor) + + Text(postViewModel.popularityUserList.first?.name ?? "") + .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) + .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) + + AsyncImage( + url: URL(string: postViewModel.popularityUserList.first?.profileImage ?? "")) { image in + image + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + .clipShape(Circle()) + } placeholder: { + GPleAsset.Assets.profile.swiftUIImage + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + } + .padding(.bottom, 8) + + GPleAsset.Assets.first.swiftUIImage + } + + VStack(spacing: 4) { + Text("3위") + .font(GPleFontFamily.Pretendard.semiBold.swiftUIFont(size: 16)) + .foregroundStyle(GPleAsset.Color.rank2.swiftUIColor) + .padding(.top, 28) + + Text(postViewModel.popularityUserList.indices.contains(2) ? postViewModel.popularityUserList[2].name : "") + .font(GPleFontFamily.Pretendard.regular.swiftUIFont(size: 14)) + .foregroundStyle(GPleAsset.Color.gray400.swiftUIColor) + + AsyncImage( + url: postViewModel.popularityUserList.indices.contains(2) ? + URL(string: postViewModel.popularityUserList[2].profileImage ?? "") : nil + ) { image in + image + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + .clipShape(Circle()) + } placeholder: { + GPleAsset.Assets.profile.swiftUIImage + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + } + .padding(.bottom, 8) + + GPleAsset.Assets.third.swiftUIImage + } + } + .padding(.horizontal, 15) + + ForEach(postViewModel.popularityPostList, id: \.id) { popularityPost in + + Rectangle() + .frame(height: 4) + .foregroundStyle(GPleAsset.Color.gray1050.swiftUIColor) + .padding(.top, 15) + + DetailView( + postViewModel: PostViewModel(), + postId: popularityPost.id, + title: popularityPost.title, + name: popularityPost.author.name, + grade: popularityPost.author.grade, + imageUrl: popularityPost.imageUrl, + tagList: popularityPost.tagList.map { ($0.name, $0.id) }, + emojiList: [ + popularityPost.emojiList.heartCount, + popularityPost.emojiList.congCount, + popularityPost.emojiList.thumbsCount, + popularityPost.emojiList.thinkCount, + popularityPost.emojiList.poopCount, + popularityPost.emojiList.chinaCount + ], + checkEmojiList: popularityPost.checkEmoji, + createTime: popularityPost.createdTime + ) + } + } + } + } + } + .onAppear { + postViewModel.popularityPostList { success in + if success { + print("인기순위 게시물 불러오기 성공") + } else { + print("인기순위 게시물 불러오기 실패") + } + } + + postViewModel.popularityUserList { success in + if success { + print("인기순위 리스트 불러오기 성공") + } else { + print("인기순위 리스트 불러오기 실패") + } + } + } + .navigationBarBackButtonHidden(true) + } +} diff --git a/Projects/App/Sources/Feature/SplashFeature/Sources/SplashView.swift b/Projects/App/Sources/Feature/SplashFeature/Sources/SplashView.swift new file mode 100644 index 0000000..44e518b --- /dev/null +++ b/Projects/App/Sources/Feature/SplashFeature/Sources/SplashView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct SplashView: View { + @State private var isActive = false + + var body: some View { + ZStack { + if isActive { + MainView(viewModel: MainViewModel(), postViewModel: PostViewModel()) + .transition(.opacity) + } else { + ZStack { + GPleAsset.Color.back.swiftUIColor + .ignoresSafeArea() + + VStack { + GPleAsset.Assets.gpleBigLogo.swiftUIImage + .resizable() + .frame(width: 160, height: 70) + .padding(.bottom, 40) + } + } + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 1), value: isActive) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation { + isActive = true + } + } + } + } +} diff --git a/Projects/Domain/Sources/API/Main/MainAPI.swift b/Projects/Domain/Sources/API/Main/MainAPI.swift index 9fcbd7e..efe2307 100644 --- a/Projects/Domain/Sources/API/Main/MainAPI.swift +++ b/Projects/Domain/Sources/API/Main/MainAPI.swift @@ -3,6 +3,11 @@ import Moya public enum MainAPI { case fetchAllPostList(authorization: String) + case fetchGymPostList(authorization: String) + case fetchPlaygroundPostList(authorization: String) + case fetchDommitoryPostList(authorization: String) + case fetchHomePostList(authorization: String) + case fetchWalkingTrailPostList(authorization: String) } extension MainAPI: TargetType { @@ -12,14 +17,14 @@ extension MainAPI: TargetType { public var path: String { switch self { - case .fetchAllPostList: + case .fetchAllPostList, .fetchGymPostList, .fetchDommitoryPostList, .fetchHomePostList, .fetchPlaygroundPostList, .fetchWalkingTrailPostList: return "/post" } } public var method: Moya.Method { switch self { - case .fetchAllPostList: + case .fetchAllPostList, .fetchGymPostList, .fetchDommitoryPostList, .fetchHomePostList, .fetchPlaygroundPostList, .fetchWalkingTrailPostList: return .get } } @@ -32,12 +37,22 @@ extension MainAPI: TargetType { switch self { case .fetchAllPostList: return .requestPlain + case .fetchGymPostList(authorization: let authorization): + return .requestParameters(parameters: ["location" : "GYM"], encoding: URLEncoding.queryString) + case .fetchPlaygroundPostList(authorization: let authorization): + return .requestParameters(parameters: ["location" : "PLAYGROUND"], encoding: URLEncoding.queryString) + case .fetchDommitoryPostList(authorization: let authorization): + return .requestParameters(parameters: ["location" : "DOMITORRY"], encoding: URLEncoding.queryString) + case .fetchHomePostList(authorization: let authorization): + return .requestParameters(parameters: ["location" : "HOME"], encoding: URLEncoding.queryString) + case .fetchWalkingTrailPostList(authorization: let authorization): + return .requestParameters(parameters: ["location" : "WALKING_TRAIL"], encoding: URLEncoding.queryString) } } public var headers: [String : String]? { switch self { - case .fetchAllPostList(let authorization): + case .fetchAllPostList(let authorization), .fetchGymPostList(let authorization), .fetchPlaygroundPostList(let authorization), .fetchDommitoryPostList(let authorization), .fetchHomePostList(let authorization), .fetchWalkingTrailPostList(let authorization): return ["Authorization": authorization] } } diff --git a/Projects/Domain/Sources/API/Post/PostAPI.swift b/Projects/Domain/Sources/API/Post/PostAPI.swift index 80d2426..5d67192 100644 --- a/Projects/Domain/Sources/API/Post/PostAPI.swift +++ b/Projects/Domain/Sources/API/Post/PostAPI.swift @@ -7,6 +7,8 @@ public enum PostAPI { case allUserList(authorization: String) case myPostList(authorization: String) case myReactionPostList(authorization: String) + case popularityPostList(authorization: String) + case popularityUserList(authorization: String) } extension PostAPI: TargetType { @@ -16,24 +18,23 @@ extension PostAPI: TargetType { public var path: String { switch self { - case .createPost: + case .createPost, .myPostList, .myReactionPostList, .popularityPostList: return "/post" case .uploadImage: return "/file/images" case .allUserList: return "/user" - case .myPostList: - return "/post/my" - case .myReactionPostList: - return "/post/react" + case .popularityUserList: + return "/user/popularity" } } + public var method: Moya.Method { switch self { case .createPost, .uploadImage: return .post - case .allUserList, .myPostList, .myReactionPostList: + case .allUserList, .myPostList, .myReactionPostList, .popularityPostList, .popularityUserList: return .get } } @@ -51,14 +52,20 @@ extension PostAPI: TargetType { MultipartFormData(provider: .data(fileData), name: "files", fileName: "image.jpg", mimeType: "image/jpeg") } return .uploadMultipart(formData) - case .allUserList, .myPostList, .myReactionPostList: + case .allUserList, .popularityUserList: return .requestPlain + case .myPostList: + return .requestParameters(parameters: ["type": "MY"], encoding: URLEncoding.queryString) + case .myReactionPostList: + return .requestParameters(parameters: ["type": "REACTED"], encoding: URLEncoding.queryString) + case .popularityPostList: + return .requestParameters(parameters: ["sort": "POPULAR"], encoding: URLEncoding.queryString) } } public var headers: [String : String]? { switch self { - case .createPost(_, let authorization), .uploadImage(_, let authorization), .allUserList(let authorization), .myPostList(let authorization), .myReactionPostList(let authorization): + case .createPost(_, let authorization), .uploadImage(_, let authorization), .allUserList(let authorization), .myPostList(let authorization), .myReactionPostList(let authorization), .popularityPostList(let authorization), .popularityUserList(let authorization): return ["Authorization": authorization] } } diff --git a/Projects/Domain/Sources/Response/Main/FetchAllPostListResponse.swift b/Projects/Domain/Sources/Response/Main/FetchAllPostListResponse.swift index 2ccd6f1..48cc616 100644 --- a/Projects/Domain/Sources/Response/Main/FetchAllPostListResponse.swift +++ b/Projects/Domain/Sources/Response/Main/FetchAllPostListResponse.swift @@ -6,6 +6,7 @@ public struct Post: Codable, Identifiable { public let location: String public let tagList: [Tag] public let emojiList: EmojiList + public let checkEmoji: [Bool] public let createdTime: String public struct Author: Codable, Identifiable { diff --git a/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift b/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift new file mode 100644 index 0000000..c7a2c27 --- /dev/null +++ b/Projects/Domain/Sources/Response/Post/MyInfoResponse.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct MyInfoResponse: Identifiable, Codable { + public let id: Int + public let grade: Int + public let name: String + public let profileImage: String +} diff --git a/Projects/Domain/Sources/Response/Post/PopularityRankingUserListResponse.swift b/Projects/Domain/Sources/Response/Post/PopularityRankingUserListResponse.swift new file mode 100644 index 0000000..7b756fa --- /dev/null +++ b/Projects/Domain/Sources/Response/Post/PopularityRankingUserListResponse.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct PopularityRankingUserListResponse: Codable { + public let id: Int + public let grade: Int + public let name: String + public let profileImage: String +} diff --git a/Projects/Domain/Sources/Response/Post/PopularityResponse.swift b/Projects/Domain/Sources/Response/Post/PopularityResponse.swift new file mode 100644 index 0000000..46c3676 --- /dev/null +++ b/Projects/Domain/Sources/Response/Post/PopularityResponse.swift @@ -0,0 +1,33 @@ +import Foundation + +public struct PopularityResponse: Codable { + public let id: Int + public let author: PopularityPostAuthorInfo + public let title: String + public let imageUrl: [String] + public let location: String + public let tagList: [PopularityPostListTaggedUser] + public let emojiList: PopularityPostListEmojiCounts + public let checkEmoji: [Bool] + public let createdTime: String +} + +public struct PopularityPostAuthorInfo: Codable { + public let id: Int + public let name: String + public let grade: Int +} + +public struct PopularityPostListTaggedUser: Codable { + public let name: String + public let id: Int +} + +public struct PopularityPostListEmojiCounts: Codable { + public let heartCount: Int + public let congCount: Int + public let thumbsCount: Int + public let thinkCount: Int + public let poopCount: Int + public let chinaCount: Int +} diff --git a/README.md b/README.md index 86ac0c1..250d595 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,21 @@ -# Template 개요 -## Template 사용법 -- 해당 레포지토리에서 `Use this template` 를 사용하여 Github에 Repo를 만들어 시작 -- 해당 레포지토리를 Fork 혹은 Download하여 시작 - -## 레이어 -Features - Services - Core - UserInterface - Shared -5개의 레이어를 가집니다. - -- Feature - - 사용자의 액션을 처리하거나 데이터를 보여주는, 사용자와 직접 맞닿는 레이어 - - ex) AuthFeature, ProfileFeature -- Domain - - 도메인 로직이 진행되는 레이어 - - ex) AuthDomain, ProfileDomain -- Core - - 앱의 비즈니스를 포함하지 않고 순수 기능성 모듈이 위치한 레이어 - - ex) NetworkingModule, DatabaseModule -- UserInterface - - 공용 View, 디자인 시스템, 리소스 등 UI 요소 모듈이 위치한 레이어 - - ex) DesignSystem, LocalizableManager -- Shared - - 로깅, extension 등 모든 레이어에서 공용으로 재사용될 모듈이 위치한 레이어 - - ex) UtilityModule, LoggingModule - -을 생각하여 레이어를 분리하였습니다. - -## Micro Feature -각 모듈은 Micro Feature 구조를 기반으로 설계됩니다. -확장 가능하고 커지는 프로젝트를 기능별로 수평 확장이 가능하도록 Micro Service에서 영감을 얻은 아키텍쳐입니다. - -<img src="https://user-images.githubusercontent.com/74440939/210211725-5ac7c9fe-bf25-4707-9775-4f46f1c0c522.png" width="200"> - -##### https://docs.tuist.io/building-at-scale/microfeatures/#product - -## 프로젝트 세팅 -프로젝트 루트에서 `make init` 를 실행하여, 프로젝트 이름과 organization 이름을 입력하여 기본 설정을 할 수 있습니다. - -프로젝트 루트에서 `make signing`를 실행하면 프로젝트 Team Signing을 할 수 있습니다. - -## 모듈 생성 -프로젝트 루트에서 `make module`를 실행하면 모듈 레이어, 이름, Micro Feature 종류를 선택하여 새 모듈을 생성합니다. - -## Makefile -프로젝트 루트에서 실행할 수 있는 명령어입니다. -- make init : `프로젝트 이름과 organization을 입력하여 프로젝트 기본 세팅` - - swift Scripts/InitEnvironment.swift - -- make signing : `프로젝트 Team Signing` - - swift Scripts/CodeSigning.swift - -- make generate : `외부 디펜던시 fetch 및 프로젝트 generate` - - tuist fetch - - tuist generate - -- make module : `모듈 생성` - - swift Scripts/GenerateModule.swift - -- make dependency : `디펜던시 추가` - - swift Scripts/NewDependency.swift - -- make ci_generate : `디펜던시 fetch 및 CI용 프로젝트 generate (SwiftLint X)` - - tuist fetch - - TUIST_ENV=CI tuist generate - -- make cd_generate : `디펜던시 fetch 및 CI용 프로젝트 generate (SwiftLint X)` - - tuist fetch - - TUIST_ENV=CD tuist generate - -- make clean : `전체 xcodeproj, xcworkspace 파일 삭제` - - rm -rf **/*.xcodeproj - - rm -rf *.xcworkspace - -- make reset : `tuist clean 후, 전체 xcodeproj, xcworkspace 파일 삭제` - - tuist clean - - rm -rf **/*.xcodeproj - - rm -rf *.xcworkspace - -## Scaffold -```sh -tuist Scaffold(Demo/Interface/Sources/Testing/Tests/UITests) - --layer (Features/Services/Core/Shared/UserInterface 레이어 이름) - --name (모듈 이름) -``` - -으로 Project 모듈의 Target 모듈을 직접 생성 가능합니다. + +ㅤ +# 🤔ㅣGPle? +"**GPle이란?** GSM Place의 줄임말로, GSM 아이디어페스티벌을 위해 7기 학생들이 한 팀이 되어 만든 프로젝트입니다. +이 앱은 GSM의 다양한 장소에서 촬영한 사진을 **손쉽게 공유**하고, 위치별로 모인 사진들을 통해 +**서로의 추억**을 되새기며 **즐거운 순간**들을 함께 나누는 경험을 제공합니다." + +ㅤ +### GPle 화면 +|온보딩|메인|자세히보기|랭킹|마이페이지 +|:---:|:---:|:---:|:---:|:---:| +|<img src="https://github.com/user-attachments/assets/f7aca390-8a24-4985-8ec9-6da2e7a4f71f" width="300px">|<img src="https://github.com/user-attachments/assets/ed773ab8-3493-44fc-a514-d187205bc07d" width="300px">|<img src="https://github.com/user-attachments/assets/474197f5-67a6-41a4-ac1a-9402fc8a43ea" width="300px">|<img src="https://github.com/user-attachments/assets/1bbc992b-3106-4b50-a1f3-6769ae82d2f8" width="300px">|<img src="https://github.com/user-attachments/assets/d440e43f-f377-4c81-b126-b751164823e5" width="300px"> + +ㅤ +# 🛠️ㅣTechStack +- SwiftUI +- Tuist +- Mvvm +- Github Action +- Moya +- GoogleSignIn