Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 작품 좋아요 기능 구현 #76

Merged
merged 7 commits into from
Dec 2, 2024
Merged
6 changes: 4 additions & 2 deletions Projects/App/Support/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>2</string>
<string>3</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>KAKAO_NATIVE_APP_KEY</key>
<string>$(KAKAO_NATIVE_APP_KEY)</string>
<key>LSApplicationQueriesSchemes</key>
Expand Down Expand Up @@ -68,7 +70,7 @@
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>Launch Screen.storyboard</string>
<string>Launch Screen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
Expand Down
1 change: 1 addition & 0 deletions Projects/Common/Sources/Enums/UserDefaultKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public enum UserDefaultKeys: String, CaseIterable {
case refreshTokenExpiresAt
case isShowPopluarTooltip
case showRecentSearches
case favoriteShow
}
26 changes: 25 additions & 1 deletion Projects/Show/Sources/Client/ShowClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@ public struct ShowClient {
public var fetchShowList: (Int, ShowFeature.ShowType, ShowSortFeature.CategoryType) async throws -> FetchShowResponseDTO
var fetchShowSearchList: (String) async throws -> FetchShowResponseDTO
var fetchShowDetail: (String) async throws -> ShowDetailResponseContent
var fetchFavoriteShowList: () async throws -> FetchFavoriteShowListResponseDTO
var putFavoriteShow: (String) async throws -> Bool
var deleteFavoriteShow: (String) async throws -> Bool
var fetchIsFavoriteShow: (String) async throws -> FetchIsFavoriteShowResponseDTO
}

extension ShowClient: DependencyKey {
public static var liveValue = {
Self(
fetchShowList: fetchShowList,
fetchShowSearchList: fetchShowSearchList(keyword:),
fetchShowDetail: fetchShowDetail(id:)
fetchShowDetail: fetchShowDetail(id:),
fetchFavoriteShowList: fetchFavoriteShowList,
putFavoriteShow: putFavoriteShow(id:),
deleteFavoriteShow: deleteFavoriteShow(id:),
fetchIsFavoriteShow: fetchIsFavoriteShow(id:)
)
}()

Expand All @@ -35,6 +43,22 @@ extension ShowClient: DependencyKey {
public static func fetchShowDetail(id: String) async throws -> ShowDetailResponseContent {
return try await MoyaProvider<ShowAPI>().request(.fetchShowDetail(id: id))
}

public static func fetchFavoriteShowList() async throws -> FetchFavoriteShowListResponseDTO {
return try await MoyaProvider<ShowAPI>().request(.fetchFavoriteShow)
}

public static func putFavoriteShow(id: String) async throws -> Bool {
return try await MoyaProvider<ShowAPI>().request(.putFavoriteShow(id: id))
}

public static func deleteFavoriteShow(id: String) async throws -> Bool {
return try await MoyaProvider<ShowAPI>().request(.deleteFavoriteShow(id: id))
}

static func fetchIsFavoriteShow(id: String) async throws -> FetchIsFavoriteShowResponseDTO {
return try await MoyaProvider<ShowAPI>().request(.fetchIsFavoriteShow(id: id))
}
}

extension DependencyValues {
Expand Down
58 changes: 58 additions & 0 deletions Projects/Show/Sources/Feature/ShowDetailFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public struct ShowDetailFeature {
var currentSelectedCategory: ShowDetailCategoryType = .detail
var facilityInfo: FetchFacilityResponseDTO?
var detailImageHeight: CGFloat = 300
var isLikeShow: Bool = false
var review: ReviewFeature.State?
}

Expand All @@ -41,7 +42,15 @@ public struct ShowDetailFeature {
case fetchFacilityDetail(id: String)
case facilityDetailResponse(FetchFacilityResponseDTO)
case didTappedMoreDetailImage
case fetchIsFavoriteShow
case review(ReviewFeature.Action)
case isFavoriteShowResponse(Bool)
case failedToFetchIsLike(Error)
case didTappedFaovorite
case selectedFavorite
case deselectedFavorite
case isSucessSelectedFavorite(Bool)
case isSucessDeselectedFavorite(Bool)
}

@Dependency (\.showClient) var showClient
Expand All @@ -54,6 +63,7 @@ public struct ShowDetailFeature {
return .run { [id = state.showId] send in
do {
try await send(.showDetailResponse(showClient.fetchShowDetail(id)))
try await send(.fetchIsFavoriteShow)
} catch {
print(error.localizedDescription)
}
Expand Down Expand Up @@ -91,8 +101,56 @@ public struct ShowDetailFeature {
case .didTappedMoreDetailImage:
state.detailImageHeight = state.detailImageHeight == 300 ? .infinity: 300
return .none
case .fetchIsFavoriteShow:
return .run { [id = state.showId] send in
do {
try await send(.isFavoriteShowResponse(showClient.fetchIsFavoriteShow(id).content.first?.favorite ?? false))
} catch {
await send(.failedToFetchIsLike(error))
}
}
case .isFavoriteShowResponse(let isFavorite):
state.isLikeShow = isFavorite
return .none
case .failedToFetchIsLike(let error):
print(error.localizedDescription)
return .none
case .review:
return .none
case .didTappedFaovorite:
return .run { [isFavorite = state.isLikeShow ] send in
if isFavorite {
await send(.deselectedFavorite)
} else {
await send(.selectedFavorite)
}
}
case .selectedFavorite:
return .run { [showId = state.showId] send in
do {
try await send(.isSucessSelectedFavorite(showClient.putFavoriteShow(showId)))
} catch {
await send(.isSucessSelectedFavorite(false))
}
}
case .deselectedFavorite:
return .run { [showId = state.showId] send in
do {
try await send(.isSucessDeselectedFavorite(showClient.deleteFavoriteShow(showId)))
} catch {
await send(.isSucessDeselectedFavorite(false))
}
}
case .isSucessSelectedFavorite(let isSuccess):
guard isSuccess else { return .none }
return .run { send in
await send(.fetchDetailResponse)
}
case .isSucessDeselectedFavorite(let isSuccess):
guard isSuccess else { return .none }
return .run { send in
await send(.fetchDetailResponse)
}
}
}
.ifLet(\.review, action: \.review) {
Expand Down
52 changes: 52 additions & 0 deletions Projects/Show/Sources/Feature/ShowFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public struct ShowFeature {
var page: Int = 0
@Presents var bottomSheet: ShowSortFeature.State?
var isShowTooltip = !UserDefaults.standard.bool(forKey: UserDefaultKeys.isShowPopluarTooltip.rawValue)
var favoriteShowList: Set<String> = []
var path = StackState<Path.State>()
}

Expand All @@ -56,6 +57,14 @@ public struct ShowFeature {
case path(StackAction<Path.State, Path.Action>)
case didTappedSearch
case didTappedShow(showId: String)
case showFavoriteListResponse(FetchFavoriteShowListResponseDTO)
case fetchFavoriteShowList
case failedToFavoriteList(Error)
case didTappedFavorite(id: String)
case selectedFavorite(id: String)
case deselectedFavorite(id: String)
case isSucessSelectedFavorite(Bool)
case isSucessDeselectedFavorite(Bool)
}

@Dependency (\.showClient) var showClient
Expand Down Expand Up @@ -98,6 +107,20 @@ public struct ShowFeature {
case .showListResponse(let response):
state.showList.append(contentsOf: response)
return .none
case .fetchFavoriteShowList:
return .run { send in
do {
try await send(.showFavoriteListResponse(showClient.fetchFavoriteShowList()))
} catch {
await send(.failedToFavoriteList(error))
}
}
case .showFavoriteListResponse(let response):
state.favoriteShowList = Set(response.content.map { $0.id })
return .none
case .failedToFavoriteList(let error):
print(error.localizedDescription)
return .none
case .didScrollToLastItem:
return .run { [page = state.page] send in
await send(.fetchShowList(page: page + 1))
Expand All @@ -109,6 +132,35 @@ public struct ShowFeature {
case .didTappedSearch:
state.path.append(.showSearch())
return .none
case .didTappedFavorite(let id):
let isCancle = state.favoriteShowList.contains(id)
return .run { send in
isCancle ? await send(.deselectedFavorite(id: id)) : await send(.selectedFavorite(id: id))
}
case .selectedFavorite(let id):
return .run { send in
do {
try await send(.isSucessSelectedFavorite(showClient.putFavoriteShow(id)))
} catch {
await send(.isSucessSelectedFavorite(false))
}
}
case .deselectedFavorite(let id):
return .run { send in
do {
try await send(.isSucessDeselectedFavorite(showClient.deleteFavoriteShow(id)))
} catch {
await send(.isSucessDeselectedFavorite(false))
}
}
case .isSucessSelectedFavorite:
return .run { send in
await send(.fetchFavoriteShowList)
}
case .isSucessDeselectedFavorite:
return .run { send in
await send(.fetchFavoriteShowList)
}
case .path(.element(id: _, action: .showSearch(.didTappedCancelButton))):
state.path.removeAll()
return .none
Expand Down
32 changes: 32 additions & 0 deletions Projects/Show/Sources/Model/FetchFavoriteShowListResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// FetchFavoriteShowListResponseDTO.swift
// Show
//
// Created by 김민석 on 8/10/24.
//

import Foundation

import Common

public struct FetchFavoriteShowListResponseDTO: Decodable {
public let content: [FetchFavoriteShowListContent]
}

public struct FetchFavoriteShowListContent: Hashable, Equatable, Decodable {
public static func == (lhs: FetchFavoriteShowListContent, rhs: FetchFavoriteShowListContent) -> Bool {
lhs.id == rhs.id
}

public let id: String
public let name: String
public let startDate: String
public let endDate: String
public let facilityName: String
public let poster: String
public let genre: Genre
public let showTimes: [ShowTime]
public let runtime: String
public let reviewCount: Int?
public let reviewGradeSum: Int?
}
17 changes: 17 additions & 0 deletions Projects/Show/Sources/Model/FetchIsFavoriteShowResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// FetchIsFavoriteShowResponseDTO.swift
// Show
//
// Created by 김민석 on 11/23/24.
//

import Foundation

struct FetchIsFavoriteShowResponseDTO: Decodable {
let content: [FetchIsFavoriteShowContent]
}

struct FetchIsFavoriteShowContent: Decodable {
let showId: String
let favorite: Bool
}
40 changes: 35 additions & 5 deletions Projects/Show/Sources/Service/ShowService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,35 @@ enum ShowAPI {
case fetchShowList(page: Int, genre: ShowFeature.ShowType, sort: ShowSortFeature.CategoryType)
case fetchShowSearchList(keyword: String)
case fetchShowDetail(id: String)
case fetchFavoriteShow
case putFavoriteShow(id: String)
case deleteFavoriteShow(id: String)
case fetchIsFavoriteShow(id: String)
}

extension ShowAPI: TargetType {
var baseURL: URL { URL(string: "\(Secret.BASE_URL)")! }
var baseURL: URL { URL(string: "\(Secret.BASE_URL)")!.absoluteURL }
var path: String {
switch self {
case .fetchShowList: return "/shows"
case .fetchShowSearchList: return "/search/shows"
case .fetchShowDetail(let id): return "/shows/\(id)"
case .fetchFavoriteShow:
let memberId = UserDefaults.standard.integer(forKey: UserDefaultKeys.userId.rawValue)
return "/members/\(memberId)/favorite"
case .putFavoriteShow(let id): return "/shows/\(id)/favorite"
case .deleteFavoriteShow(let id): return "/shows/\(id)/favorite"
case .fetchIsFavoriteShow:
return "/member/favorite"
}
}
var method: Moya.Method {
switch self {
case .putFavoriteShow: return .put
case .deleteFavoriteShow: return .delete
default: return .get
}
}
var method: Moya.Method { .get }

var task: Moya.Task {
var param: [String: Any] = [:]
Expand All @@ -39,10 +56,23 @@ extension ShowAPI: TargetType {
case .fetchShowSearchList(let keyword):
param.updateValue(keyword, forKey: "keyword")
return .requestParameters(parameters: param, encoding: URLEncoding.default)
case .fetchShowDetail:
return .requestPlain
case .fetchShowDetail: return .requestPlain
case .fetchFavoriteShow: return .requestPlain
case .putFavoriteShow: return .requestPlain
case .deleteFavoriteShow: return .requestPlain
case .fetchIsFavoriteShow(let id):
param.updateValue(id, forKey: "showIds")
return .requestParameters(parameters: param, encoding: URLEncoding.default)
}
}

var headers: [String : String]? { nil }
var headers: [String : String]? {
switch self {
case .fetchFavoriteShow: return Utils.authHeader
case .putFavoriteShow: return Utils.authHeader
case .deleteFavoriteShow: return Utils.authHeader
case .fetchIsFavoriteShow: return Utils.authHeader
default: return nil
}
}
}
5 changes: 4 additions & 1 deletion Projects/Show/Sources/Views/ShowDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public struct ShowDetailView: View {
VStack(spacing: 0) {
HStack {
Spacer()
Image(asset: CommonAsset.showDetailFavoriteHeartUnfill)
Image(asset: store.isLikeShow ? CommonAsset.showDetailFavoriteHeartFill : CommonAsset.showDetailFavoriteHeartUnfill)
.onTapGestureRectangle {
store.send(.didTappedFaovorite)
}
}
.padding([.top, .trailing], 18)

Expand Down
6 changes: 5 additions & 1 deletion Projects/Show/Sources/Views/ShowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ public struct ShowView: View {

HStack {
Spacer()
Image(asset: CommonAsset.showFavoriteUnfill)
Image(asset: store.favoriteShowList.contains(show.id) ? CommonAsset.showFavoriteFill : CommonAsset.showFavoriteUnfill)
.frame(width: 28, height: 28)
.onTapGestureRectangle {
store.send(.didTappedFavorite(id: show.id))
}
}
.padding([.bottom, .trailing], 10)
}
Expand All @@ -92,6 +95,7 @@ public struct ShowView: View {
}
.onAppear {
store.send(.fetchShowList(page: 0))
store.send(.fetchFavoriteShowList)
}
.sheet(item: $store.scope(state: \.bottomSheet, action: \.bottomSheet)) { store in
ShowSortBottomSheet(store: store)
Expand Down