diff --git a/Climeet-iOS/Climeet-iOS.xcodeproj/project.pbxproj b/Climeet-iOS/Climeet-iOS.xcodeproj/project.pbxproj index e914b17..72dc86c 100644 --- a/Climeet-iOS/Climeet-iOS.xcodeproj/project.pbxproj +++ b/Climeet-iOS/Climeet-iOS.xcodeproj/project.pbxproj @@ -36,20 +36,11 @@ /* Begin PBXFileReference section */ 0453294E2C5B8E3400BBE289 /* Climeet-iOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Climeet-iOSUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 0489EEF92C3BE58E00BFC55B /* Climeet-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Climeet-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 04ED90052CD3A519009B59A0 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; B94EC2222BD13CBB00DC3FDB /* Climeet-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Climeet-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; B94EC2412BD1473E00DC3FDB /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 041DA0422CF419B600CFDC31 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Debug.xcconfig, - Shared.xcconfig, - ); - target = B94EC2212BD13CBB00DC3FDB /* Climeet-iOS */; - }; 604B97A62CC394F2005EDD29 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -60,7 +51,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 604B958E2CC394EF005EDD29 /* XCConfig */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (041DA0422CF419B600CFDC31 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = XCConfig; sourceTree = ""; }; + 604B958E2CC394EF005EDD29 /* XCConfig */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = XCConfig; sourceTree = ""; }; 604B96BF2CC394F2005EDD29 /* Climeet-iOS */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (604B97A62CC394F2005EDD29 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "Climeet-iOS"; sourceTree = ""; }; 604B97A82CC394F5005EDD29 /* Climeet-iOSTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Climeet-iOSTests"; sourceTree = ""; }; 604B97AC2CC394F7005EDD29 /* Climeet-iOSUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "Climeet-iOSUITests"; sourceTree = ""; }; @@ -208,7 +199,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1530; - LastUpgradeCheck = 1600; + LastUpgradeCheck = 1610; TargetAttributes = { 0453294D2C5B8E3400BBE289 = { CreatedOnToolsVersion = 15.3; @@ -336,8 +327,6 @@ /* Begin XCBuildConfiguration section */ 045329572C5B8E3400BBE289 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReferenceAnchor = 604B958E2CC394EF005EDD29 /* XCConfig */; - baseConfigurationReferenceRelativePath = Debug.xcconfig; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -382,8 +371,6 @@ }; 0489EF002C3BE58E00BFC55B /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReferenceAnchor = 604B958E2CC394EF005EDD29 /* XCConfig */; - baseConfigurationReferenceRelativePath = Debug.xcconfig; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -430,6 +417,8 @@ }; B94EC22E2BD13CBD00DC3FDB /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 604B958E2CC394EF005EDD29 /* XCConfig */; + baseConfigurationReferenceRelativePath = Release.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -493,6 +482,8 @@ }; B94EC22F2BD13CBD00DC3FDB /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 604B958E2CC394EF005EDD29 /* XCConfig */; + baseConfigurationReferenceRelativePath = Release.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -550,7 +541,7 @@ B94EC2312BD13CBD00DC3FDB /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = 604B958E2CC394EF005EDD29 /* XCConfig */; - baseConfigurationReferenceRelativePath = Debug.xcconfig; + baseConfigurationReferenceRelativePath = Release.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -558,7 +549,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Climeet-iOS/Preview Content\""; - DEVELOPMENT_TEAM = 7MJ69FU8BU; + DEVELOPMENT_TEAM = MV89SHQKF4; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -586,7 +577,8 @@ }; B94EC2322BD13CBD00DC3FDB /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 04ED90052CD3A519009B59A0 /* Release.xcconfig */; + baseConfigurationReferenceAnchor = 604B958E2CC394EF005EDD29 /* XCConfig */; + baseConfigurationReferenceRelativePath = Release.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -594,7 +586,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Climeet-iOS/Preview Content\""; - DEVELOPMENT_TEAM = 7MJ69FU8BU; + DEVELOPMENT_TEAM = MV89SHQKF4; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; diff --git a/Climeet-iOS/Climeet-iOS.xcodeproj/xcshareddata/xcschemes/Climeet-iOS.xcscheme b/Climeet-iOS/Climeet-iOS.xcodeproj/xcshareddata/xcschemes/Climeet-iOS.xcscheme index b40aad4..b647777 100644 --- a/Climeet-iOS/Climeet-iOS.xcodeproj/xcshareddata/xcschemes/Climeet-iOS.xcscheme +++ b/Climeet-iOS/Climeet-iOS.xcodeproj/xcshareddata/xcschemes/Climeet-iOS.xcscheme @@ -1,6 +1,6 @@ { @@ -33,5 +35,8 @@ struct HomeReducer { Scope(state: \.bestClimber, action: \.bestClimber) { HomeBestClimberReducer() } + Scope(state: \.popularShorts, action: \.popularShorts) { + WeeklyPopularShortsReducer() + } } } diff --git a/Climeet-iOS/Climeet-iOS/Presentation/Home/HomeView.swift b/Climeet-iOS/Climeet-iOS/Presentation/Home/HomeView.swift index 5b522be..fb96b57 100644 --- a/Climeet-iOS/Climeet-iOS/Presentation/Home/HomeView.swift +++ b/Climeet-iOS/Climeet-iOS/Presentation/Home/HomeView.swift @@ -26,7 +26,7 @@ struct HomeView: View { .padding(.bottom, 48) HomeBestClimberView(store: store.scope(state: \.bestClimber, action: \.bestClimber)) .padding(.bottom, 48) - WeeklyPopularShortsView() + WeeklyPopularShortsView(store: store.scope(state: \.popularShorts, action: \.popularShorts)) .padding(.bottom, 48) WeeklyPopularGymView() .padding(.bottom, 48) diff --git a/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/Model/GymDifficulty.swift b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/Model/GymDifficulty.swift new file mode 100644 index 0000000..7def054 --- /dev/null +++ b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/Model/GymDifficulty.swift @@ -0,0 +1,22 @@ +// +// GymDifficulty.swift +// Climeet-iOS +// +// Created by 권승용 on 11/29/24. +// + +import Foundation + +struct GymDifficulty: Equatable { + let level: Int + let color: String + + init(from dto: DifficultyMappingDTO.GymDifficulty.ResponseElement) throws { + guard let level = dto.difficulty, + let color = dto.gymDifficultyColor else { + throw AppError.dataParsingError("dto property nil") + } + self.level = level + self.color = color + } +} diff --git a/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/Model/PopularShorts.swift b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/Model/PopularShorts.swift new file mode 100644 index 0000000..fd98e50 --- /dev/null +++ b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/Model/PopularShorts.swift @@ -0,0 +1,25 @@ +// +// PopularShorts.swift +// Climeet-iOS +// +// Created by 권승용 on 11/29/24. +// + +import Foundation + +struct PopularShorts: Equatable, Identifiable { + let id = UUID() + let gymName: String + let thumbnailImageURL: String + let difficulty: GymDifficulty + + init(from dto: ShortsDTO.Shorts.Response, difficulty: GymDifficulty) throws { + guard let gymName = dto.gymName, + let thumbnailImageURL = dto.thumbnailImageURL else { + throw AppError.dataParsingError("dto property nil") + } + self.gymName = gymName + self.thumbnailImageURL = thumbnailImageURL + self.difficulty = difficulty + } +} diff --git a/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsReducer.swift b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsReducer.swift new file mode 100644 index 0000000..39ec7e7 --- /dev/null +++ b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsReducer.swift @@ -0,0 +1,73 @@ +// +// WeeklyPopularShortsReducer.swift +// Climeet-iOS +// +// Created by 권승용 on 11/26/24. +// + +import Foundation +import ComposableArchitecture + +@Reducer +struct WeeklyPopularShortsReducer { + @ObservableState + struct State: Equatable { + var shortsItems: IdentifiedArrayOf = [] + } + + enum Action { + case onFirstAppear + case popularShortsResponse([PopularShorts]) + } + + @Dependency(\.shortsClient) var shortsClient + @Dependency(\.difficultyMappingClient) var difficultyMappingClient + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onFirstAppear: + return .run { send in + let request = ShortsDTO.List.Request(page: 0, size: 10) + let response = try await shortsClient.popularShorts(request) + + let result = try await withThrowingTaskGroup(of: PopularShorts?.self) { group in + for responseItem in response.result { + group.addTask { + guard let id = responseItem.shortsDetailInfo?.gymID else { + throw AppError.dataParsingError("gymID not found") + } + + let difficulty = try await difficultyMappingClient.gymDifficulty(id) + + guard !difficulty.isEmpty else { return nil } + + return try PopularShorts( + from: responseItem, + difficulty: try GymDifficulty(from: difficulty[0]) + ) + } + } + + var popularShorts: [PopularShorts] = [] + + for try await item in group { + if let item = item { + popularShorts.append(item) + } + } + + return popularShorts + } + + await send(.popularShortsResponse(result)) + } + + case let .popularShortsResponse(response): + state.shortsItems = IdentifiedArray(uniqueElements: response) + print(state.shortsItems) + return .none + } + } + } +} diff --git a/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsView.swift b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsView.swift index c5e26c5..5c1226b 100644 --- a/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsView.swift +++ b/Climeet-iOS/Climeet-iOS/Presentation/Home/WeeklyPopularShorts/WeeklyPopularShortsView.swift @@ -6,8 +6,13 @@ // import SwiftUI +import ComposableArchitecture +import Kingfisher +import DesignSystem struct WeeklyPopularShortsView: View { + @Bindable var store: StoreOf + var body: some View { VStack(spacing: 0) { header @@ -15,6 +20,9 @@ struct WeeklyPopularShortsView: View { .padding(.bottom, 20) shorts } + .onFirstAppear { + store.send(.onFirstAppear) + } } private var header: some View { @@ -29,11 +37,34 @@ struct WeeklyPopularShortsView: View { private var shorts: some View { ScrollView(.horizontal) { - HStack(spacing: 7.75) { - ForEach(0..<8, id: \.self) { _ in - RoundedRectangle(cornerRadius: 5) - .foregroundStyle(.gray) - .frame(width: 96, height: 160) + HStack(spacing: 8) { + if store.shortsItems.isEmpty { + ForEach(0..<8, id: \.self) { _ in + RoundedRectangle(cornerRadius: 5) + .foregroundStyle(.gray) + .frame(width: 96, height: 160) + } + } else { + ForEach(store.shortsItems) { item in + ShortsThumbnailView(imageURL: item.thumbnailImageURL) + .overlay { + VStack { + HStack { + RouteInfo(gymTitle: item.gymName, holdColor: .red) + Spacer() + } + .padding(.top, 4) + .padding(.leading, 4) + Spacer() + HStack { + Spacer() + DifficultyMark(difficulty: item.difficulty) + } + .padding(.trailing, 5) + .padding(.bottom, 10) + } + } + } } } } @@ -41,7 +72,67 @@ struct WeeklyPopularShortsView: View { } } +fileprivate struct ShortsThumbnailView: View { + let imageURL: String + + var body: some View { + KFImage(URL(string: imageURL)) + .placeholder({ + Color.gray + }) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 96, height: 160) + .clipped() + .cornerRadius(5) + } +} + +fileprivate struct DifficultyMark: View { + let difficulty: GymDifficulty + + var body: some View { + Text("V\(difficulty.level)") + .foregroundStyle(Color(hex: difficulty.color) ?? .gray) + .font(.climeetFontParagraph5()) + .frame(width: 30, height: 30) + .overlay { + Circle() + .stroke( + Color(hex: difficulty.color) ?? .gray, + style: StrokeStyle(lineWidth: 1.5) + ) + .frame(width: 30, height: 30) + } + } +} + +fileprivate struct RouteInfo: View { + let gymTitle: String + let holdColor: Color + + var body: some View { + HStack(alignment: .center, spacing: 2) { + Text(gymTitle) + .font(.climeetFontCustom(size: 8, weight: .regular)) + .foregroundStyle(.white) + Circle() + .foregroundStyle(holdColor) + .frame(width: 8, height: 8) + } + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background { + Capsule() + .foregroundStyle(Color(hex: "#000000")?.opacity(0.5) ?? Color.red) + } + } +} + #Preview { - WeeklyPopularShortsView() + let store = Store(initialState: WeeklyPopularShortsReducer.State()) { + WeeklyPopularShortsReducer() + } + WeeklyPopularShortsView(store: store) .background(Color.climeetBackground) } diff --git a/Climeet-iOS/Climeet-iOS/Presentation/Shorts/ShortsDeckView/ShortsDeckView.swift b/Climeet-iOS/Climeet-iOS/Presentation/Shorts/ShortsDeckView/ShortsDeckView.swift index 7c5c6f1..4e1c6fd 100644 --- a/Climeet-iOS/Climeet-iOS/Presentation/Shorts/ShortsDeckView/ShortsDeckView.swift +++ b/Climeet-iOS/Climeet-iOS/Presentation/Shorts/ShortsDeckView/ShortsDeckView.swift @@ -95,14 +95,13 @@ struct ShortsDeckView: View { } } + @ViewBuilder private func GymInfoView(gymName: String?, gymDifficultyColor: String?) -> some View { - if let gymName = gymName, let difficultyColor = gymDifficultyColor { let gymDifficultyColor = convertHexadecimal(difficultyColor) - - return HStack(spacing: 4) { + HStack(spacing: 4) { Text(gymName) .font(.system(size: 12, weight: .light)) .foregroundColor(.white) @@ -119,17 +118,18 @@ struct ShortsDeckView: View { .padding(.horizontal, 7) .padding(.vertical, 10) } else { - return Rectangle() + Rectangle() .foregroundStyle(.clear) } } - + + @ViewBuilder private func DifficultyView(colorHexadecimal: String?, difficulty: String?) -> some View { if let colorHexadecimal = colorHexadecimal, let difficulty = difficulty { let hexadecimal = convertHexadecimal(colorHexadecimal) - return ZStack { + ZStack { Circle() .stroke(hexadecimal, lineWidth: 1.5) .frame(width: 30, height: 30) @@ -139,7 +139,7 @@ struct ShortsDeckView: View { .lineLimit(1) } } else { - return Rectangle() + Rectangle() .foregroundStyle(.clear) } } diff --git a/DesignSystem/Sources/DesignSystem/Util/Font+.swift b/DesignSystem/Sources/DesignSystem/Util/Font+.swift index 7838fcd..de9aba7 100644 --- a/DesignSystem/Sources/DesignSystem/Util/Font+.swift +++ b/DesignSystem/Sources/DesignSystem/Util/Font+.swift @@ -61,5 +61,20 @@ extension Font { public static func climeetFontCaptionText3() -> Self { Self.custom("Pretendard-Regular", size: 12) } + + public static func climeetFontCustom(size: CGFloat, weight: Font.Weight) -> Self { + switch weight { + case .bold: + Self.custom("Pretendard-Bold", size: size) + case .semibold: + Self.custom("Pretendard-SemiBold", size: size) + case .medium: + Self.custom("Pretendard-Medium", size: size) + case .regular: + Self.custom("Pretendard-Regular", size: size) + default: + Font.system(size: size, weight: weight) + } + } } #endif