Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ disabled_rules: # 실행에서 제외할 룰 식별자들
- function_parameter_count # 함수 파라미터 개수 제한 규칙 제외 (5개 초과 허용)
- identifier_name # 식별자 이름 길이 제한 규칙 제외
- large_tuple # 튜플 멤버 개수 제한 규칙 제외 (2개 초과 허용)
- cyclomatic_complexity # 복잡도 10 이하로 유지 제한 규칙 제외

opt_in_rules: # 선택적으로 추가하는 규칙
- empty_count # .count == 0보다 isEmpty 사용 권장
Expand Down
4 changes: 4 additions & 0 deletions Popcorn-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
5606D56B2D87EF6D008C3E6F /* MainTitleHeaderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5606D56A2D87EF5B008C3E6F /* MainTitleHeaderViewDelegate.swift */; };
5606D56D2D87FC31008C3E6F /* CategoryMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5606D56C2D87FC27008C3E6F /* CategoryMapper.swift */; };
5606D56F2D880EC2008C3E6F /* MainSceneMockDataConstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5606D56E2D880EB7008C3E6F /* MainSceneMockDataConstant.swift */; };
560970222DA62975008021A9 /* ClosingSoonPopupResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560970212DA62969008021A9 /* ClosingSoonPopupResponseDTO.swift */; };
562447072D33975B000E94A0 /* FullScreenImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562447062D33975B000E94A0 /* FullScreenImageViewController.swift */; };
562447162D33B65D000E94A0 /* ReviewCollectionViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562447152D33B65A000E94A0 /* ReviewCollectionViewCellDelegate.swift */; };
563482FE2D2240B8001646B5 /* PopupTitleCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563482FD2D2240B8001646B5 /* PopupTitleCollectionViewCell.swift */; };
Expand Down Expand Up @@ -201,6 +202,7 @@
5606D56A2D87EF5B008C3E6F /* MainTitleHeaderViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTitleHeaderViewDelegate.swift; sourceTree = "<group>"; };
5606D56C2D87FC27008C3E6F /* CategoryMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryMapper.swift; sourceTree = "<group>"; };
5606D56E2D880EB7008C3E6F /* MainSceneMockDataConstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneMockDataConstant.swift; sourceTree = "<group>"; };
560970212DA62969008021A9 /* ClosingSoonPopupResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosingSoonPopupResponseDTO.swift; sourceTree = "<group>"; };
562447062D33975B000E94A0 /* FullScreenImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenImageViewController.swift; sourceTree = "<group>"; };
562447152D33B65A000E94A0 /* ReviewCollectionViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewCollectionViewCellDelegate.swift; sourceTree = "<group>"; };
563482FD2D2240B8001646B5 /* PopupTitleCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupTitleCollectionViewCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -588,6 +590,7 @@
563A1E2C2D53912E008DA4F8 /* PopupMainListResponseDTO.swift */,
563A1E382D54AA96008DA4F8 /* PopupPreviewResponseDTO.swift */,
56ED58D22D815B8A00046377 /* PopupOverviewResponseDTO.swift */,
560970212DA62969008021A9 /* ClosingSoonPopupResponseDTO.swift */,
);
path = MainScene;
sourceTree = "<group>";
Expand Down Expand Up @@ -1718,6 +1721,7 @@
BCF6ED492D3426A60038CCAE /* TokenRepository.swift in Sources */,
BC90A0B82CF4CC9A003488F0 /* SignUpInterestButton.swift in Sources */,
567931322D86E12100BF828D /* PopupOverviewViewController.swift in Sources */,
560970222DA62975008021A9 /* ClosingSoonPopupResponseDTO.swift in Sources */,
BC5F3C0D2D52447900248DB0 /* SignUpFirstViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
4 changes: 4 additions & 0 deletions Popcorn-iOS/Source/Data/DTO/Common/InterestCategoryDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
struct InterestCategoryDTO: Codable, Hashable {
let category: String

init(category: String) {
self.category = category
}

init(from entity: InterestCategory) {
switch entity {
case .fashion:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// ClosingSoonPopupResponseDTO.swift
// Popcorn-iOS
//
// Created by 제민우 on 4/9/25.
//

struct ClosingSoonPopupResponseDTO: Decodable {
let popups: [PopupPreviewResponseDTO]
let totalPages: Int
let currentPages: Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@
import Foundation

struct PopupMainListResponseDTO: Decodable {
let userPickPopups: [PopupPreviewResponseDTO]
let userInterestPopups: [InterestCategoryDTO: [PopupPreviewResponseDTO]]
let todayRecommendPopups: [PopupPreviewResponseDTO]
let userPickPopups: [PopupPreviewResponseDTO]?
let userInterestPopups: [String: [PopupPreviewResponseDTO]]?
let closingSoonPopups: [PopupPreviewResponseDTO]
let totalPage: Int
let currentPage: Int

enum CodingKeys: String, CodingKey {
case todayRecommendPopups = "todayRecommend"
case userPickPopups = "topLikedPopups"
case userInterestPopups = "categoryPopups"
case userInterestPopups = "interestedPopups"
case closingSoonPopups = "allPopups"
case totalPage = "totalPages"
case currentPage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ struct PopupPreviewResponseDTO: Decodable {
let startDate: String
let endDate: String
let address: String
let interestCategory: String

enum CodingKeys: String, CodingKey {
case popupId
case title
case imageUrl = "popupImage"
case startDate = "startedAt"
case endDate = "endedAt"
case address = "location"
case interestCategory = "interest"
}
}

extension PopupPreviewResponseDTO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class PopupListRepository: PopupListRepositoryProtocol {
self.tokenRepository = tokenRepository
}

func fetchPopupMainList(completion: @escaping (Result<PopupMainList, Error>) -> Void) {
func fetchPopupMainList(completion: @escaping (Result<(data: PopupMainList, hasNextPage: Bool), Error>) -> Void) {
guard let token = tokenRepository.fetchAccessToken() else {
// TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링
completion(.failure(NSError(
Expand All @@ -29,85 +29,57 @@ final class PopupListRepository: PopupListRepositoryProtocol {
return
}

let dispatchGroup = DispatchGroup()
var popupMainListResponse: PopupMainListResponseDTO?
var todayRecommendPopupResponse: [PopupPreviewResponseDTO]?
var capturedErrors = [NetworkError]()

let popupMainListEndpoint = Endpoint<PopupMainListResponseDTO>(
httpMethod: .get,
path: APIConstant.mainScenePath,
queryItems: [URLQueryItem(name: "page", value: "1")],
headers: ["Authorization": "Bearer \(token)"]
)

let todayRecommendPopupEndpoint = Endpoint<[PopupPreviewResponseDTO]>(
httpMethod: .get,
path: APIConstant.mainScenePath
)

dispatchGroup.enter()
networkManager.request(endpoint: popupMainListEndpoint) { [weak self] result in
guard let self else {
dispatchGroup.leave()
return
}

self.popupListSyncQueue.async {
defer { dispatchGroup.leave() }

if !capturedErrors.isEmpty { return }

switch result {
case .success(let response):
popupMainListResponse = response
case .failure(let error):
capturedErrors.append(error)
}
guard let self else { return }
switch result {
case .success(let response):
let hasNextPage = response.currentPage < response.totalPage
let popupMainList = self.convertToPopupMainList(response)
completion(.success((popupMainList, hasNextPage)))
case .failure(let error):
print(error.description)
}
}
}

dispatchGroup.enter()
networkManager.request(endpoint: todayRecommendPopupEndpoint) { [weak self] result in
guard let self else {
dispatchGroup.leave()
return
}

self.popupListSyncQueue.async {
defer { dispatchGroup.leave() }

if !capturedErrors.isEmpty { return }

switch result {
case .success(let response):
todayRecommendPopupResponse = response
case .failure(let error):
capturedErrors.append(error)
}
}
func fetchClosingSoonPopup(
page: Int,
completion: @escaping (Result<(data: [PopupPreview], hasNextPage: Bool), Error>) -> Void
) {
guard let token = tokenRepository.fetchAccessToken() else {
// TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링
completion(.failure(NSError(
domain: "PopupListRepository",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"]
)))
return
}

dispatchGroup.notify(queue: .main) { [weak self] in
if !capturedErrors.isEmpty {
let combinedError = NSError(
domain: "PopupListRepsitory",
code: -2,
userInfo: [
NSLocalizedDescriptionKey: "메인화면 데이터 요청 실패",
"error": capturedErrors
]
)

completion(.failure(combinedError))
return
}

guard let self,
let popupMainListResponse,
let todayRecommendPopupResponse else { return }
let closingSoonPopupEndpoint = Endpoint<ClosingSoonPopupResponseDTO>(
httpMethod: .get,
path: APIConstant.mainScenePath,
queryItems: [URLQueryItem(name: "page", value: String(page))],
headers: ["Authorization": "Bearer \(token)"]
)

let popupMainList = self.convertToPopupMainList(popupMainListResponse, todayRecommendPopupResponse)
completion(.success(popupMainList))
networkManager.request(endpoint: closingSoonPopupEndpoint) { result in
switch result {
case .success(let response):
let popups = response.popups.map { $0.toEntity() }
let hasNextPage = response.currentPages < response.totalPages
completion(.success((popups, hasNextPage)))
case .failure(let error):
print(error)
completion(.failure(error))
}
}
}

Expand Down Expand Up @@ -170,19 +142,18 @@ final class PopupListRepository: PopupListRepositoryProtocol {

extension PopupListRepository {
private func convertToPopupMainList(
_ mainListResponseDTO: PopupMainListResponseDTO,
_ todayRecommendResponseDTO: [PopupPreviewResponseDTO]
_ mainListResponseDTO: PopupMainListResponseDTO
) -> PopupMainList {
let recommendedPopups = todayRecommendResponseDTO.map { $0.toEntity() }
let userPickPopups = mainListResponseDTO.userPickPopups.map { $0.toEntity() }

let userInterestPopups: [UserInterestPopup] = mainListResponseDTO.userInterestPopups.compactMap { key, value in
guard let interestCategory = key.toEntity() else { return nil }
return UserInterestPopup(
interestCategory: interestCategory,
popups: value.map { $0.toEntity() }
)
}
let recommendedPopups = mainListResponseDTO.todayRecommendPopups.map { $0.toEntity() }
let userPickPopups = mainListResponseDTO.userPickPopups?.map { $0.toEntity() } ?? []
let userInterestPopups: [UserInterestPopup] = mainListResponseDTO.userInterestPopups?
.compactMap { key, value in
if let interestCategory = InterestCategoryDTO(category: key).toEntity() {
return UserInterestPopup(interestCategory: interestCategory, popups: value.map { $0.toEntity() })
} else {
return nil
}
} ?? []

let closingSoonPopups = mainListResponseDTO.closingSoonPopups.map { $0.toEntity() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
//

protocol PopupListRepositoryProtocol {
func fetchPopupMainList(completion: @escaping (Result<PopupMainList, Error>) -> Void)
func fetchPopupMainList(completion: @escaping (Result<(data: PopupMainList, hasNextPage: Bool), Error>) -> Void)

func fetchClosingSoonPopup(
page: Int,
completion: @escaping (Result<(data: [PopupPreview], hasNextPage: Bool), Error>) -> Void
)

func fetchPopupOverview(
category: PopupSectionCategory,
page: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ final class PopupFetchListUseCase: PopupFetchListUseCaseProtocol {
self.repository = repository
}

func fetchPopupMainList(completion: @escaping (Result<PopupMainList, any Error>) -> Void) {
func fetchPopupMainList(
completion: @escaping (Result<(data: PopupMainList, hasNextPage: Bool), any Error>) -> Void
) {
repository.fetchPopupMainList(completion: completion)
}

func fetchClosingSoongPopups(
page: Int,
completion: @escaping (Result<(data: [PopupPreview], hasNextPage: Bool), any Error>) -> Void
) {
repository.fetchClosingSoonPopup(page: page, completion: completion)
}

func fetchPopupOverview(
category: PopupSectionCategory,
page: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
//

protocol PopupFetchListUseCaseProtocol {
func fetchPopupMainList(completion: @escaping (Result<PopupMainList, Error>) -> Void)
func fetchPopupMainList(completion: @escaping (Result<(data: PopupMainList, hasNextPage: Bool), Error>) -> Void)

func fetchClosingSoongPopups(
page: Int,
completion: @escaping (Result<(data: [PopupPreview], hasNextPage: Bool), Error>) -> Void
)

func fetchPopupOverview(
category: PopupSectionCategory,
page: Int,
completion: @escaping (Result<[PopupOverview], Error>) -> Void
)
)
}
Loading