diff --git a/.DS_Store b/.DS_Store index 04643d1..617eb70 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/MarketPlace.xcodeproj/project.pbxproj b/MarketPlace.xcodeproj/project.pbxproj index feb56b0..3657e97 100644 --- a/MarketPlace.xcodeproj/project.pbxproj +++ b/MarketPlace.xcodeproj/project.pbxproj @@ -40,7 +40,6 @@ 2941F7952DE5A5760063CEFE /* Top20DetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2941F7942DE5A5760063CEFE /* Top20DetailViewModel.swift */; }; 2941F7972DE5AAB50063CEFE /* NewEventViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2941F7962DE5AAB50063CEFE /* NewEventViewModel.swift */; }; 2941F79B2DE5DFDB0063CEFE /* MapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2941F79A2DE5DFDB0063CEFE /* MapViewModel.swift */; }; - 2941F7A12DE5F7FE0063CEFE /* MarketInfoCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2941F7A02DE5F7FE0063CEFE /* MarketInfoCellViewModel.swift */; }; 295DCBB02DED6DC0009B5D82 /* MyPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295DCBAF2DED6DC0009B5D82 /* MyPageViewModel.swift */; }; 295DCBB62DED78C0009B5D82 /* MyCouponViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295DCBB52DED78C0009B5D82 /* MyCouponViewModel.swift */; }; 295DCBB82DED7C92009B5D82 /* CouponStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295DCBB72DED7C92009B5D82 /* CouponStatus.swift */; }; @@ -73,6 +72,8 @@ 29BF18BF2DE45B3900836D7E /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BF18BE2DE45B3900836D7E /* Category.swift */; }; 29BF18C12DE4950700836D7E /* APIResDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BF18C02DE4950700836D7E /* APIResDto.swift */; }; 29BF193A2DE4AC9600836D7E /* MainHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BF19392DE4AC9600836D7E /* MainHeaderView.swift */; }; + 29CA0F212E54821C00EB4890 /* LoginRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CA0F202E54821C00EB4890 /* LoginRequiredView.swift */; }; + 29CA0F232E5484B100EB4890 /* MarketInfoCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CA0F222E5484B100EB4890 /* MarketInfoCellViewModel.swift */; }; 29D19D1C2E13CC2000DEEADD /* KakaoMarketDataResDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D19D1B2E13CC2000DEEADD /* KakaoMarketDataResDto.swift */; }; 29D19D202E13E63B00DEEADD /* RequestMarketMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D19D1F2E13E63B00DEEADD /* RequestMarketMapView.swift */; }; 29D19D2D2E14234900DEEADD /* RequestMarketMapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D19D2C2E14234900DEEADD /* RequestMarketMapViewModel.swift */; }; @@ -87,6 +88,7 @@ 29E749322DEEA91100E765E0 /* MyCouponCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E749312DEEA91100E765E0 /* MyCouponCellViewModel.swift */; }; 29E749342DEEABFA00E765E0 /* MyCouponResDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E749332DEEABFA00E765E0 /* MyCouponResDto.swift */; }; 29E749362DEECFBA00E765E0 /* FavoriteMarketModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E749352DEECFBA00E765E0 /* FavoriteMarketModel.swift */; }; + 29F588FB2E548EE00022EE89 /* LoginRequriedPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F588FA2E548EE00022EE89 /* LoginRequriedPopup.swift */; }; B207A25F2D54B18C0099A8DB /* MyCouponCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B207A25E2D54B18C0099A8DB /* MyCouponCell.swift */; }; B210EC242D3D686500B1551E /* MyHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B210EC232D3D686500B1551E /* MyHeaderView.swift */; }; B210EC282D3D797F00B1551E /* SearchHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B210EC272D3D797F00B1551E /* SearchHeader.swift */; }; @@ -212,7 +214,6 @@ 2941F7942DE5A5760063CEFE /* Top20DetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Top20DetailViewModel.swift; sourceTree = ""; }; 2941F7962DE5AAB50063CEFE /* NewEventViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewEventViewModel.swift; sourceTree = ""; }; 2941F79A2DE5DFDB0063CEFE /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = ""; }; - 2941F7A02DE5F7FE0063CEFE /* MarketInfoCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketInfoCellViewModel.swift; sourceTree = ""; }; 295DCBAF2DED6DC0009B5D82 /* MyPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageViewModel.swift; sourceTree = ""; }; 295DCBB52DED78C0009B5D82 /* MyCouponViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCouponViewModel.swift; sourceTree = ""; }; 295DCBB72DED7C92009B5D82 /* CouponStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponStatus.swift; sourceTree = ""; }; @@ -243,6 +244,8 @@ 29BF18BE2DE45B3900836D7E /* Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = ""; }; 29BF18C02DE4950700836D7E /* APIResDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResDto.swift; sourceTree = ""; }; 29BF19392DE4AC9600836D7E /* MainHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHeaderView.swift; sourceTree = ""; }; + 29CA0F202E54821C00EB4890 /* LoginRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRequiredView.swift; sourceTree = ""; }; + 29CA0F222E5484B100EB4890 /* MarketInfoCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketInfoCellViewModel.swift; sourceTree = ""; }; 29D19D1B2E13CC2000DEEADD /* KakaoMarketDataResDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KakaoMarketDataResDto.swift; sourceTree = ""; }; 29D19D1F2E13E63B00DEEADD /* RequestMarketMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestMarketMapView.swift; sourceTree = ""; }; 29D19D2C2E14234900DEEADD /* RequestMarketMapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestMarketMapViewModel.swift; sourceTree = ""; }; @@ -257,6 +260,7 @@ 29E749312DEEA91100E765E0 /* MyCouponCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCouponCellViewModel.swift; sourceTree = ""; }; 29E749332DEEABFA00E765E0 /* MyCouponResDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCouponResDto.swift; sourceTree = ""; }; 29E749352DEECFBA00E765E0 /* FavoriteMarketModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteMarketModel.swift; sourceTree = ""; }; + 29F588FA2E548EE00022EE89 /* LoginRequriedPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRequriedPopup.swift; sourceTree = ""; }; B207A25E2D54B18C0099A8DB /* MyCouponCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCouponCell.swift; sourceTree = ""; }; B210EC232D3D686500B1551E /* MyHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyHeaderView.swift; sourceTree = ""; }; B210EC272D3D797F00B1551E /* SearchHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeader.swift; sourceTree = ""; }; @@ -371,6 +375,7 @@ 290D93502DE57CCD008AC190 /* Main */ = { isa = PBXGroup; children = ( + 29CA0F222E5484B100EB4890 /* MarketInfoCellViewModel.swift */, B2122E3E2D6F7D4000BD37F8 /* MarketCategoryDetailViewModel.swift */, B28A22A72D64B0F900E86EF6 /* MarketDetailViewModel.swift */, 290D93512DE57CDE008AC190 /* CouponInfoCellViewModel.swift */, @@ -847,7 +852,6 @@ 290D93502DE57CCD008AC190 /* Main */, B2A42DCB2D843626005FAF15 /* Login */, B27E56CE2D703B0B000D2881 /* Cheer */, - 2941F7A02DE5F7FE0063CEFE /* MarketInfoCellViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -858,6 +862,8 @@ B2C3992C2CA1BF7F004800DD /* CategoryButton.swift */, B21F3D082D2D57BE00B83B34 /* Color.swift */, B28889A02D4BBCB80040EC3F /* Logger.swift */, + 29CA0F202E54821C00EB4890 /* LoginRequiredView.swift */, + 29F588FA2E548EE00022EE89 /* LoginRequriedPopup.swift */, ); path = UICommon; sourceTree = ""; @@ -1101,6 +1107,7 @@ B28349112D2E548A00EEDB02 /* MapView.swift in Sources */, 290D93542DE58418008AC190 /* CouponBasicModel.swift in Sources */, B2C398FD2CA145CB004800DD /* MarketPlaceApp.swift in Sources */, + 29CA0F232E5484B100EB4890 /* MarketInfoCellViewModel.swift in Sources */, 29B3EBC02E03F3260095A310 /* CustomAlertView.swift in Sources */, B28A22AD2D64F1C000E86EF6 /* StoreSearchButton.swift in Sources */, 2941F78D2DE5A28A0063CEFE /* CouponService.swift in Sources */, @@ -1148,7 +1155,6 @@ B28889AE2D4BDC4D0040EC3F /* MembersCouponModel.swift in Sources */, B293A15A2D6FE0B4003E8505 /* CheerView.swift in Sources */, 29E749342DEEABFA00E765E0 /* MyCouponResDto.swift in Sources */, - 2941F7A12DE5F7FE0063CEFE /* MarketInfoCellViewModel.swift in Sources */, 290A71B52E02C8AE009AEAF8 /* CouponPopupViewModel.swift in Sources */, 290D93522DE57CDE008AC190 /* CouponInfoCellViewModel.swift in Sources */, B293A1522D6FDC7B003E8505 /* CheerListView.swift in Sources */, @@ -1163,6 +1169,7 @@ 2941F79B2DE5DFDB0063CEFE /* MapViewModel.swift in Sources */, 290A71AF2E027909009AEAF8 /* ShimmeringAsyncImage.swift in Sources */, B27E56CD2D7037AF000D2881 /* CheerMarketModel.swift in Sources */, + 29F588FB2E548EE00022EE89 /* LoginRequriedPopup.swift in Sources */, B28A22C02D668B0600E86EF6 /* MyFavoriteShopListView.swift in Sources */, B24272632E2657C700E3B0ED /* SubmitReceiptViewModel.swift in Sources */, B26F4E892E54650C00C39C37 /* NotificationService.swift in Sources */, @@ -1177,6 +1184,7 @@ B215F2962DE6D77B00F53C51 /* AlertListView.swift in Sources */, B28349162D2E604800EEDB02 /* ShopModel.swift in Sources */, B21F3CF72D267AF900B83B34 /* NewEventView.swift in Sources */, + 29CA0F212E54821C00EB4890 /* LoginRequiredView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1344,13 +1352,15 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"MarketPlace/Preview Content\""; - DEVELOPMENT_TEAM = 7RR74CRWQW; + DEVELOPMENT_TEAM = F7GYZYWC6V; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MarketPlace/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "쿠러미"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "위치 권한 받아오기"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "위치 권한 받아오기"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "사용자 근처의 매장을 안내해드리기 위해 위치 정보가 필요합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "사용자 근처의 매장을 안내해드리기 위해 위치 정보가 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "매장 방문 인증을 위한 영수증을 제출하기 위해 사진 접근권한 라이브러리가 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "매장 방문 인증을 위한 영수증을 제출하기 위해 사진 접근권한 라이브러리가 필요합니다."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -1379,13 +1389,15 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"MarketPlace/Preview Content\""; - DEVELOPMENT_TEAM = 7RR74CRWQW; + DEVELOPMENT_TEAM = F7GYZYWC6V; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MarketPlace/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "쿠러미"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "위치 권한 받아오기"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "위치 권한 받아오기"; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "사용자 근처의 매장을 안내해드리기 위해 위치 정보가 필요합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "사용자 근처의 매장을 안내해드리기 위해 위치 정보가 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "매장 방문 인증을 위한 영수증을 제출하기 위해 사진 접근권한 라이브러리가 필요합니다."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "매장 방문 인증을 위한 영수증을 제출하기 위해 사진 접근권한 라이브러리가 필요합니다."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/MarketPlace/.DS_Store b/MarketPlace/.DS_Store index 5d6b541..374840c 100644 Binary files a/MarketPlace/.DS_Store and b/MarketPlace/.DS_Store differ diff --git a/MarketPlace/API/Endpoint/CouponEndpoint.swift b/MarketPlace/API/Endpoint/CouponEndpoint.swift index e1d1da7..1e58d87 100644 --- a/MarketPlace/API/Endpoint/CouponEndpoint.swift +++ b/MarketPlace/API/Endpoint/CouponEndpoint.swift @@ -9,6 +9,7 @@ import Foundation enum CouponEndpoint: Endpoint { case fetchValidCoupon(marketId: Int, couponId: Int?, size: Int?) + case fetchValidPaybackCoupon(marketId: Int, couponId: Int?, size: Int?) case fetchTopPoplarCoupon(pageSize: Int?) case fetchTopLatestCoupon(pageSize: Int?) case fetchTopClosingCoupon(pageSize: Int?) @@ -21,6 +22,7 @@ enum CouponEndpoint: Endpoint { var path: String { switch self { case .fetchValidCoupon: return "api/coupons" + case .fetchValidPaybackCoupon: return "api/payback-coupons" case .fetchTopPoplarCoupon: return "api/coupons/top/popular" case .fetchTopLatestCoupon: return "api/coupons/top/latest" case .fetchTopClosingCoupon: return "api/coupons/top/closing" @@ -90,7 +92,8 @@ enum CouponEndpoint: Endpoint { return items - case .fetchValidCoupon(let marketId, let couponId, let size): + case .fetchValidCoupon(let marketId, let couponId, let size), + .fetchValidPaybackCoupon(let marketId, let couponId, let size): var items: [URLQueryItem] = [] items.append(contentsOf: [ diff --git a/MarketPlace/API/Endpoint/MemberCouponEndpoint.swift b/MarketPlace/API/Endpoint/MemberCouponEndpoint.swift index 977d7ae..ba4cbd4 100644 --- a/MarketPlace/API/Endpoint/MemberCouponEndpoint.swift +++ b/MarketPlace/API/Endpoint/MemberCouponEndpoint.swift @@ -9,6 +9,7 @@ import Foundation enum MemberCouponEndpoint: Endpoint { case downloadCoupon(couponId: Int) + case downloadPaybackCoupon(couponId: Int) case fetchMemberCoupon(type: String, memberCouponId: Int?, size: Int?) case useMemberCoupon(memberCouponId: Int) case fetchMemeberPaybackCoupon(type: String, memberCouponId: Int?, size: Int?) @@ -18,6 +19,7 @@ enum MemberCouponEndpoint: Endpoint { var path: String { switch self { case .downloadCoupon(let couponId): return "api/members/coupons/\(couponId)" + case .downloadPaybackCoupon(let couponId): return "api/members/payback-coupons/\(couponId)" case .fetchMemberCoupon: return "api/members/coupons" case .useMemberCoupon: return "api/members/coupons" case .fetchMemeberPaybackCoupon: return "api/members/payback-coupons" @@ -26,7 +28,9 @@ enum MemberCouponEndpoint: Endpoint { var method: HTTPMethod { switch self { - case .downloadCoupon: .post + case .downloadCoupon, + .downloadPaybackCoupon: + .post case .fetchMemberCoupon, .fetchMemeberPaybackCoupon: .get diff --git a/MarketPlace/API/Service/NetworkService.swift b/MarketPlace/API/Service/NetworkService.swift index e09e0bc..60beb27 100644 --- a/MarketPlace/API/Service/NetworkService.swift +++ b/MarketPlace/API/Service/NetworkService.swift @@ -70,8 +70,6 @@ final class NetworkService: NetworkServiceProtocol { } print(request.url) - print(request.allHTTPHeaderFields) - print(request.httpBody) do { diff --git a/MarketPlace/ContentView.swift b/MarketPlace/ContentView.swift index c4afa64..77bf1ab 100644 --- a/MarketPlace/ContentView.swift +++ b/MarketPlace/ContentView.swift @@ -1,6 +1,7 @@ import SwiftUI struct ContentView: View { + @EnvironmentObject var loginVM: LoginViewModel init() { setupTabBarAppearance() @@ -31,29 +32,39 @@ struct ContentView: View { .multilineTextAlignment(.center) } } - - CheerView() - .tabItem { - VStack { - Image(systemName: "heart.fill") - .resizable() - .scaledToFit() - .frame(width: 18, height: 16) - Text("공감") - .pretendardFont(size: 12, weight: .medium) - .multilineTextAlignment(.center) - } + + Group { + if loginVM.isLoggedIn { + CheerView() + } else { + LoginRequiredView() } + }.tabItem { + VStack { + Image(systemName: "heart.fill") + .resizable() + .scaledToFit() + .frame(width: 18, height: 16) + Text("공감") + .pretendardFont(size: 12, weight: .medium) + .multilineTextAlignment(.center) + } + } - MyPageView() - .tabItem { - VStack { - Image(uiImage: resizeImage(named: "userIcon", width: 24, height: 24)) - Text("마이페이지") - .pretendardFont(size: 12, weight: .medium) - .multilineTextAlignment(.center) - } + Group { + if loginVM.isLoggedIn { + MyPageView() + } else { + LoginRequiredView() } + }.tabItem { + VStack { + Image(uiImage: resizeImage(named: "userIcon", width: 24, height: 24)) + Text("마이페이지") + .pretendardFont(size: 12, weight: .medium) + .multilineTextAlignment(.center) + } + } } .background(Color.white) .accentColor(Color(hex: "#303030")) diff --git a/MarketPlace/MarketPlaceApp.swift b/MarketPlace/MarketPlaceApp.swift index 42df81c..1e9a0e6 100644 --- a/MarketPlace/MarketPlaceApp.swift +++ b/MarketPlace/MarketPlaceApp.swift @@ -15,11 +15,7 @@ struct MarketPlaceApp: App { var body: some Scene { WindowGroup { - if loginVM.isLoggedIn { - ContentView() - } else { - LoginView() - } + ContentView() } .environmentObject(loginVM) } diff --git a/MarketPlace/Model/CouponModel.swift b/MarketPlace/Model/CouponModel.swift index 31eb09f..4430d33 100644 --- a/MarketPlace/Model/CouponModel.swift +++ b/MarketPlace/Model/CouponModel.swift @@ -36,9 +36,10 @@ struct CouponValidModel: Codable, Identifiable { let couponId: Int let couponName: String let couponDescription: String - let deadLine: String - var isAvailable: Bool + let deadLine: String? + var isAvailable: Bool? var isMemberIssued: Bool + var couponType: String var id: Int { couponId } } diff --git a/MarketPlace/Service/CouponService.swift b/MarketPlace/Service/CouponService.swift index f1e4e7f..fb345be 100644 --- a/MarketPlace/Service/CouponService.swift +++ b/MarketPlace/Service/CouponService.swift @@ -11,6 +11,9 @@ protocol CouponServiceProtocol { // MARK: - 유효 쿠폰 조회 리스트 func fetchValidCoupon(marketId: Int, couponId: Int?, size: Int?) async -> NetworkResult> + // MARK: - 유효 환급 쿠폰 조회 리스트 + func fetchValidPaybackCoupon(marketId: Int, couponId: Int?, size: Int?) async -> NetworkResult> + // MARK: - 인기 쿠폰 TOP 조회 func fetchCouponTopPopular(pageSize: Int?) async -> NetworkResult> @@ -85,7 +88,7 @@ final class CouponService: CouponServiceProtocol { ) } - // MARK: - 유효 쿠폰 조회 리스트 + // MARK: - 유효 쿠폰 조회 API func fetchValidCoupon( marketId: Int, couponId: Int?, @@ -99,6 +102,20 @@ final class CouponService: CouponServiceProtocol { ) } + // MARK: - 유효 환급쿠폰 조회 API + func fetchValidPaybackCoupon( + marketId: Int, + couponId: Int?, + size: Int? + ) async -> NetworkResult> { + return await networkService.request( + CouponEndpoint.fetchValidPaybackCoupon( + marketId: marketId, + couponId: couponId, + size: size) + ) + } + // MARK: - 쿠폰 영수증 제출 func putSubmitReceipt( memberCouponId: Int, diff --git a/MarketPlace/Service/MemberCouponService.swift b/MarketPlace/Service/MemberCouponService.swift index 03e3b30..8c75b2e 100644 --- a/MarketPlace/Service/MemberCouponService.swift +++ b/MarketPlace/Service/MemberCouponService.swift @@ -11,6 +11,9 @@ protocol MemberCouponServiceProtocol { // MARK: - 회원 쿠폰 발급 API func downloadCoupon(couponId: Int) async -> NetworkResult + // MARK: - 회원 환급쿠폰 발급 API + func downloadPaybackCoupon(couponId: Int) async -> NetworkResult + // MARK: - 회원 쿠폰 리스트 API func fetchMemberCoupon(type: String, memberCouponId: Int?, size: Int?) async -> NetworkResult> @@ -37,6 +40,13 @@ final class MemberCouponService: MemberCouponServiceProtocol { ) } + // MARK: - 회원 환급쿠폰 발급 API + func downloadPaybackCoupon(couponId: Int) async -> NetworkResult { + return await networkService.request( + MemberCouponEndpoint.downloadPaybackCoupon(couponId: couponId) + ) + } + // MARK: - 회원 쿠폰 리스트 API func fetchMemberCoupon(type: String, memberCouponId: Int?, size: Int?) async -> NetworkResult> { return await networkService.request( diff --git a/MarketPlace/Types/UserDefaultsKeys.swift b/MarketPlace/Types/UserDefaultsKeys.swift index ba423aa..56de8bc 100644 --- a/MarketPlace/Types/UserDefaultsKeys.swift +++ b/MarketPlace/Types/UserDefaultsKeys.swift @@ -9,5 +9,4 @@ import Foundation enum UserDefaultsKeys { static let saveId = "saveId" - static let savePassword = "savePassword" } diff --git a/MarketPlace/UICommon/LoginRequiredView.swift b/MarketPlace/UICommon/LoginRequiredView.swift new file mode 100644 index 0000000..e5fa890 --- /dev/null +++ b/MarketPlace/UICommon/LoginRequiredView.swift @@ -0,0 +1,37 @@ +// +// LoginRequiredView.swift +// MarketPlace +// +// Created by Bowon Han on 8/19/25. +// + +import SwiftUI + +struct LoginRequiredView: View { + @State private var showLogin = false + + var body: some View { + VStack { + Text("로그인하고 쿠폰 받아보기") + .pretendardFont(size: 20, weight: .semibold) + .foregroundStyle(Colors.gray_900) + + Button(action: { + showLogin = true + }, label: { + Text("로그인") + .pretendardFont(size: 14, weight: .bold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(.black) + .cornerRadius(8) + }) + } + .padding(.horizontal, 16) + .padding(.vertical, 32) + .fullScreenCover(isPresented: $showLogin) { + LoginView() + } + } +} diff --git a/MarketPlace/UICommon/LoginRequriedPopup.swift b/MarketPlace/UICommon/LoginRequriedPopup.swift new file mode 100644 index 0000000..18e0b4d --- /dev/null +++ b/MarketPlace/UICommon/LoginRequriedPopup.swift @@ -0,0 +1,62 @@ +// +// LoginRequriedPopup.swift +// MarketPlace +// +// Created by Bowon Han on 8/19/25. +// + +import SwiftUI + +struct LoginRequriedPopup: View { + @Binding var isPopupVisible: Bool + @Binding var showLogin: Bool + + var body: some View { + ZStack { + DashEffect() + .opacity(isPopupVisible ? 1 : 0) + .animation(.easeInOut, value: isPopupVisible) + + VStack { + HStack { + Spacer() + + Button(action: { + isPopupVisible = false + }, label: { + Image(systemName: "xmark") + .resizable() + .scaledToFit() + .frame(width: 20) + }).padding(.trailing, 10) + } + + Spacer() + + Text("로그인하고 쿠폰 받아보기") + .pretendardFont(size: 20, weight: .semibold) + .foregroundStyle(Colors.gray_900) + + Button(action: { + showLogin = true + }, label: { + Text("로그인") + .pretendardFont(size: 14, weight: .bold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(.black) + .cornerRadius(8) + }) + + Spacer() + } + .frame(width: 320, height: 270) + .padding(.horizontal, 16) + .padding(.vertical, 32) + .background(Color.white) + .cornerRadius(8) + .shadow(radius: 10) + } + } +} diff --git a/MarketPlace/View/.DS_Store b/MarketPlace/View/.DS_Store index 2f3eb9c..e6213fe 100644 Binary files a/MarketPlace/View/.DS_Store and b/MarketPlace/View/.DS_Store differ diff --git a/MarketPlace/View/Cheer/View/CheerView.swift b/MarketPlace/View/Cheer/View/CheerView.swift index 5ea582d..c1f8b21 100644 --- a/MarketPlace/View/Cheer/View/CheerView.swift +++ b/MarketPlace/View/Cheer/View/CheerView.swift @@ -5,11 +5,12 @@ struct CheerView: View { @Environment(\.presentationMode) var presentationMode @StateObject private var viewModel = CheerViewModel() @State var upcomingLastIndex: Int = 0 + @EnvironmentObject var loginVM: LoginViewModel @State private var hasData: Bool = true var body: some View { - NavigationStack { + NavigationView { ScrollView { CheerSearchView(searchText: $viewModel.searchText) @@ -32,7 +33,8 @@ struct CheerView: View { CheerSearchCardCell(market: $market) .padding(.vertical, 10) Divider() - }.onAppear { + } + .onAppear { guard index == viewModel.cheerMarket.count - 1, let lastId = viewModel.searchLastMarketId else { return } @@ -70,6 +72,6 @@ struct CheerView: View { viewModel.currentKeyword = newValue } } - } + }.environmentObject(loginVM) } } diff --git a/MarketPlace/View/Login/LoginView.swift b/MarketPlace/View/Login/LoginView.swift index 97bdc3b..995adc6 100644 --- a/MarketPlace/View/Login/LoginView.swift +++ b/MarketPlace/View/Login/LoginView.swift @@ -6,6 +6,8 @@ struct LoginView: View { @State private var password: String = "" @EnvironmentObject var viewModel: LoginViewModel + @Environment(\.dismiss) var dismiss + @AppStorage(UserDefaultsKeys.saveId) private var saveAccount: Bool = false @FocusState var isEditing: Bool @@ -13,142 +15,156 @@ struct LoginView: View { let schools = ["인천대학교"] var body: some View { - if viewModel.isLoggedIn { - ContentView() - } else { - VStack(alignment: .leading, spacing: 4) { - Image("logo") - .resizable() - .frame(width: 124, height: 40) - .padding(.top, 56) - .padding(.bottom, 20) - .padding(.leading, 20) + VStack(alignment: .leading, spacing: 4) { + HStack { + Spacer() + + Button(action: { + dismiss() + }, label: { + Image(systemName: "xmark") + .resizable() + .scaledToFit() + .frame(width: 20) + .foregroundStyle(.black) + }).padding(.trailing, 20) + } + + Image("logo") + .resizable() + .frame(width: 124, height: 40) + .padding(.top, 56) + .padding(.bottom, 20) + .padding(.leading, 20) + + VStack(alignment: .leading, spacing: 10) { + Text("매번 마라탕 한 그릇, 이천 원 더 내고 있어요.") + .pretendardFont(size: 12, weight: .semibold) + .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - VStack(alignment: .leading, spacing: 10) { - Text("매번 마라탕 한 그릇, 이천 원 더 내고 있어요.") - .pretendardFont(size: 12, weight: .semibold) + Text("이제, 다니는 대학 제휴 멤버십으로 \n쿠폰 꾸러미 받아볼까요?") + .pretendardFont(size: 16, weight: .medium) + .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) + } + .padding(.horizontal, 20) + .padding(.bottom, 14) + + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 12) { + Text("학교") + .pretendardFont(size: 14, weight: .regular) .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - Text("이제, 다니는 대학 제휴 멤버십으로 \n쿠폰 꾸러미 받아볼까요?") - .pretendardFont(size: 16, weight: .medium) - .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - } - .padding(.horizontal, 20) - .padding(.bottom, 14) - - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 12) { - Text("학교") - .pretendardFont(size: 14, weight: .regular) - .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - - Menu { - ForEach(schools, id: \.self) { school in - Button(action: { - selectedSchool = school - }) { - Text(school) - } + Menu { + ForEach(schools, id: \.self) { school in + Button(action: { + selectedSchool = school + }) { + Text(school) } - } label: { - HStack { - Text(selectedSchool.isEmpty ? "학교를 선택해주세요" : selectedSchool) - .pretendardFont(size: 13, weight: .regular) - .foregroundColor(selectedSchool.isEmpty ? .gray : .black) - .padding() - - Spacer() - - Image(systemName: "chevron.down") - .foregroundColor(.black) - .padding(.trailing) - } - .frame(height: 48) - .background(RoundedRectangle(cornerRadius: 2).stroke(Color.gray.opacity(0.5), lineWidth: 1)) } + } label: { + HStack { + Text(selectedSchool.isEmpty ? "학교를 선택해주세요" : selectedSchool) + .pretendardFont(size: 13, weight: .regular) + .foregroundColor(selectedSchool.isEmpty ? .gray : .black) + .padding() + + Spacer() + + Image(systemName: "chevron.down") + .foregroundColor(.black) + .padding(.trailing) + } + .frame(height: 48) + .background(RoundedRectangle(cornerRadius: 2).stroke(Color.gray.opacity(0.5), lineWidth: 1)) } - - // MARK: - 로그인 TextField - VStack(alignment: .leading, spacing: 8) { - Text("학번(ID)") - .pretendardFont(size: 14, weight: .regular) - .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - - TextField("학번을 입력해 주세요.", text: $studentID) - .pretendardFont(size: 13, weight: .regular) - .padding() - .frame(height: 48) - .background( - RoundedRectangle(cornerRadius: 2) - .stroke(studentID.isEmpty ? Color.gray.opacity(0.5) : .black, lineWidth: 1) - ) - .focused($isEditing) - } - - VStack(alignment: .leading, spacing: 8) { - Text("비밀번호") - .pretendardFont(size: 14, weight: .regular) - .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - - SecureField("비밀번호는 꼭꼭 지켜줄게요", text: $password) - .pretendardFont(size: 13, weight: .regular) - .padding() - .frame(height: 48) - .background( - RoundedRectangle(cornerRadius: 2) - .stroke(password.isEmpty ? Color.gray.opacity(0.5) : .black, lineWidth: 1)) - .focused($isEditing) - } - - Text("학교 포털 아이디 / 비밀번호를 적어주세요!") - .pretendardFont(size: 12, weight: .medium) + } + + // MARK: - 로그인 TextField + VStack(alignment: .leading, spacing: 8) { + Text("학번(ID)") + .pretendardFont(size: 14, weight: .regular) .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - if !isEditing, let errorMessage = viewModel.userErrorMessage { - Text(errorMessage) - .pretendardFont(size: 14, weight: .regular) - .foregroundColor(.red) - .padding(.top, 10) - } - - // MARK: - 로그인 버튼 - Button(action: { - Task { - isEditing = false - await viewModel.signIn(studentId: studentID, password: password, saveAccount: saveAccount) - } - }) { - Text("로그인") - .pretendardFont(size: 14, weight: .bold) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background(studentID.isEmpty || password.isEmpty ? Color.gray.opacity(0.4) : .black) - .cornerRadius(8) - }.disabled(studentID.isEmpty || password.isEmpty) + TextField("학번을 입력해 주세요.", text: $studentID) + .pretendardFont(size: 13, weight: .regular) + .padding() + .frame(height: 48) + .background( + RoundedRectangle(cornerRadius: 2) + .stroke(studentID.isEmpty ? Color.gray.opacity(0.5) : .black, lineWidth: 1) + ) + .focused($isEditing) + } + + VStack(alignment: .leading, spacing: 8) { + Text("비밀번호") + .pretendardFont(size: 14, weight: .regular) + .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) - HStack(spacing: 20) { - CheckboxView(title: "계정 정보 저장", isChecked: $saveAccount) - } + SecureField("비밀번호는 꼭꼭 지켜줄게요", text: $password) + .pretendardFont(size: 13, weight: .regular) + .padding() + .frame(height: 48) + .background( + RoundedRectangle(cornerRadius: 2) + .stroke(password.isEmpty ? Color.gray.opacity(0.5) : .black, lineWidth: 1)) + .focused($isEditing) } - .padding(.horizontal, 20) - Spacer() - } - .onAppear { - if saveAccount, let id = KeychainManager.load(KeyChainKeys.studentId), let password = KeychainManager.load(KeyChainKeys.password) { - self.studentID = id - self.password = password - + Text("학교 포털 아이디 / 비밀번호를 적어주세요!") + .pretendardFont(size: 12, weight: .medium) + .foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2)) + + if !isEditing, let errorMessage = viewModel.userErrorMessage { + Text(errorMessage) + .pretendardFont(size: 14, weight: .regular) + .foregroundColor(.red) + .padding(.top, 10) + } + + // MARK: - 로그인 버튼 + Button(action: { + Task { + isEditing = false + let result = await viewModel.signIn(studentId: studentID, password: password, saveAccount: saveAccount) + if result { dismiss() } + } + }) { + Text("로그인") + .pretendardFont(size: 14, weight: .bold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(studentID.isEmpty || password.isEmpty ? Color.gray.opacity(0.4) : .black) + .cornerRadius(8) + }.disabled(studentID.isEmpty || password.isEmpty) + + HStack(spacing: 20) { + CheckboxView(title: "계정 정보 저장", isChecked: $saveAccount) } } - .onChange(of: isEditing == true, { _, _ in - viewModel.userErrorMessage = nil - }) - .onTapGesture { - self.endTextEditing() + .padding(.horizontal, 20) + + Spacer() + } + .environmentObject(viewModel) + .onAppear { + if saveAccount, + let id = KeychainManager.load(KeyChainKeys.studentId), + let password = KeychainManager.load(KeyChainKeys.password) + { + self.studentID = id + self.password = password } } + .onChange(of: isEditing == true, { _, _ in + viewModel.userErrorMessage = nil + }) + .onTapGesture { + self.endTextEditing() + } } } diff --git a/MarketPlace/View/Main/Components/MainHeaderView.swift b/MarketPlace/View/Main/Components/MainHeaderView.swift index e84756f..8035841 100644 --- a/MarketPlace/View/Main/Components/MainHeaderView.swift +++ b/MarketPlace/View/Main/Components/MainHeaderView.swift @@ -11,6 +11,9 @@ struct MainHeaderView: View { @State private var searchText: String = "" @State private var isSearchViewActive: Bool = false @State private var isAlertViewActive: Bool = false + @State private var showLoginView: Bool = false + + @EnvironmentObject var loginVM: LoginViewModel var body: some View { HStack { @@ -35,8 +38,6 @@ struct MainHeaderView: View { .padding(.leading, 38) /// - NOTE: placeholder - /// - /// if searchText.isEmpty { Text("가고 싶은 매장을 찾아보세요") .pretendardFont(size: SearchViewConstants.FontSize.searchText, weight: .regular) @@ -51,23 +52,6 @@ struct MainHeaderView: View { .padding(.vertical, 8) .padding(.leading, 6) .padding(.leading, 35) - - -// Text("가고 싶은 매장을 찾아보세요") -// .pretendardFont(size: 14, weight: .regular) -// .foregroundColor(Color(hex: "#C6C6C6")) -// .padding(.leading, 6) -// .padding(.leading, 43) -// -// } - -// TextField("", text: $searchText) -// .pretendardFont(size: 14, weight: .regular) -// .foregroundColor(Color(hex: "#333333")) -// .padding(.vertical, 8) -// .padding(.leading, 6) -// .padding(.leading, 35) - } .frame(height: 35) .background(Color(hex: "#FAFAFA")) @@ -85,18 +69,23 @@ struct MainHeaderView: View { Spacer() Button(action: { - isAlertViewActive = true + if loginVM.isLoggedIn { isAlertViewActive = true } + else { showLoginView = true } }) { - Image(systemName: "bell") + Image(systemName: loginVM.isLoggedIn ? "bell" : "person") .font(.system(size: 20)) .foregroundColor(Color(hex: "#545454")) } - .navigationDestination(isPresented: $isAlertViewActive) { - AlertView() - } } .frame(height: 44) .padding(.horizontal, 20) .background(Color.white) + .environmentObject(loginVM) + .navigationDestination(isPresented: $isAlertViewActive) { + AlertView() + } + .fullScreenCover(isPresented: $showLoginView) { + LoginView() + } } } diff --git a/MarketPlace/View/Main/Components/MarketCouponListView.swift b/MarketPlace/View/Main/Components/MarketCouponListView.swift index 0724adb..3c06ede 100644 --- a/MarketPlace/View/Main/Components/MarketCouponListView.swift +++ b/MarketPlace/View/Main/Components/MarketCouponListView.swift @@ -45,27 +45,44 @@ struct MarketCouponListView: View { .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: 200, alignment: .leading) - - Text(coupon.deadLine.toKoreanDateFormat()) - .pretendardFont(size: 14, weight: .regular) - .foregroundColor(.white) + if let deadline = coupon.deadLine { + Text(deadline.toKoreanDateFormat()) + .pretendardFont(size: 14, weight: .regular) + .foregroundColor(.white) + } else { + Text("행사 종료시 까지") + .pretendardFont(size: 14, weight: .regular) + .foregroundColor(.white) + } } .padding(.leading, 25) .padding(.vertical, 5) - .opacity(coupon.isAvailable ? 1.0 : 0.5) } .padding(.horizontal, 16) .tag(index) .onTapGesture { - if coupon.isAvailable && !coupon.isMemberIssued { - selectedCouponId = coupon.id - isPopupVisible = true - } else if coupon.isMemberIssued { - toastMessage = "이미 발급 완료된 쿠폰입니다" - showToast = true - } else if !coupon.isAvailable { - toastMessage = "기한이 만료되었습니다" - showToast = true + // - CASE: 증정 쿠폰 + if let isAvailable = coupon.isAvailable { + if isAvailable && !coupon.isMemberIssued { + selectedCouponId = coupon.id + isPopupVisible = true + } else if coupon.isMemberIssued { + toastMessage = "이미 발급 완료된 쿠폰입니다." + showToast = true + } else if !isAvailable { + toastMessage = "기한이 만료되었습니다." + showToast = true + } + } + // - CASE: 환급 쿠폰 + else { + if !coupon.isMemberIssued { + selectedCouponId = coupon.id + isPopupVisible = true + } else { + toastMessage = "이미 발급 완료된 쿠폰입니다." + showToast = true + } } } } @@ -95,8 +112,6 @@ struct MarketCouponListView: View { } } - - struct ToastView: View { let message: String @Binding var isShowing: Bool diff --git a/MarketPlace/View/Main/Step2/components/File.swift b/MarketPlace/View/Main/Step2/components/File.swift deleted file mode 100644 index 90cd667..0000000 --- a/MarketPlace/View/Main/Step2/components/File.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SwiftUI -struct StoreCouponView: View { - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("메인 메뉴 30% 할인") - .font(.system(size: 16, weight: .medium)) - Text("2024년 10월 31일까지") - .font(.system(size: 12)) - .foregroundColor(.gray) - } - Spacer() - Button(action: {}) { - Image(systemName: "arrow.down.circle") - .resizable() - .frame(width: 24, height: 24) - .foregroundColor(.black) - } - } - .padding(16) - .background(Color.white) - .cornerRadius(8) - .shadow(color: Color.black.opacity(0.05), radius: 4, y: 2) - } -} diff --git a/MarketPlace/View/Main/View/CouponGetPopupView.swift b/MarketPlace/View/Main/View/CouponGetPopupView.swift index 276945a..5173bae 100644 --- a/MarketPlace/View/Main/View/CouponGetPopupView.swift +++ b/MarketPlace/View/Main/View/CouponGetPopupView.swift @@ -70,8 +70,18 @@ struct CouponGetPopupView: View { coupon.isMemberIssued = true isPopupVisible = false - Task { - await viewModel.downloadCoupons(couponId: coupon.couponId) + switch coupon.couponType { + case "PAYBACK": + Task { + await viewModel.downloadPaybackCoupons(couponId: coupon.couponId) + } + case "GIFT": + Task { + await viewModel.downloadCoupons(couponId: coupon.couponId) + } + + default: + break } } } diff --git a/MarketPlace/View/Main/View/MarketDetailView.swift b/MarketPlace/View/Main/View/MarketDetailView.swift index f2375fd..9191d2c 100644 --- a/MarketPlace/View/Main/View/MarketDetailView.swift +++ b/MarketPlace/View/Main/View/MarketDetailView.swift @@ -2,6 +2,7 @@ import SwiftUI struct MarketDetailView: View { @ObservedObject var viewModel: MarketDetailViewModel + @EnvironmentObject var loginViewModel: LoginViewModel @Environment(\.presentationMode) var presentationMode @State private var isBookmarked = false @@ -10,6 +11,9 @@ struct MarketDetailView: View { @State private var toastMessage: String = "" @State private var showToast: Bool = false + @State private var isLoginRequiredPopupVisible: Bool = false + @State private var showLoginView: Bool = false + private let marketId: Int init(viewModel: MarketDetailViewModel, marketId: Int) { @@ -67,7 +71,9 @@ struct MarketDetailView: View { } else if !viewModel.validCoupons.isEmpty { MarketCouponListView( coupons: $viewModel.validCoupons, - isPopupVisible: $isPopupVisible, + isPopupVisible: (loginViewModel.isLoggedIn + ? $isPopupVisible + : $isLoginRequiredPopupVisible), selectedCouponId: $selectedCouponId, showToast: $showToast, toastMessage: $toastMessage, @@ -126,24 +132,34 @@ struct MarketDetailView: View { } } + if !loginViewModel.isLoggedIn && isLoginRequiredPopupVisible { + LoginRequriedPopup( + isPopupVisible: $isLoginRequiredPopupVisible, + showLogin: $showLoginView + ).transition(.scale) + } + if isPopupVisible, let couponBinding = $viewModel.validCoupons.first( - where: { - $0.wrappedValue.id == selectedCouponId - } + where: { $0.wrappedValue.id == selectedCouponId } ) { CouponGetPopupView( isPopupVisible: $isPopupVisible, coupon: couponBinding - ) - .transition(.scale) + ).transition(.scale) } if showToast { - ToastView(message: toastMessage, isShowing: $showToast) - .transition(.move(edge: .bottom)) + ToastView( + message: toastMessage, + isShowing: $showToast + ).transition(.move(edge: .bottom)) } } + .environmentObject(loginViewModel) + .fullScreenCover(isPresented: $showLoginView) { + LoginView() + } } func openKakaoMap(latitude: Double, longitude: Double) { diff --git a/MarketPlace/View/MyPage/Step1/components/CouponValidGetViewModel.swift b/MarketPlace/View/MyPage/Step1/components/CouponValidGetViewModel.swift deleted file mode 100644 index e018f75..0000000 --- a/MarketPlace/View/MyPage/Step1/components/CouponValidGetViewModel.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// CouponValidGetViewModel.swift -// MarketPlace -// -// Created by 이예나 on 3/26/25. -// - diff --git a/MarketPlace/View/MyPage/View/MyPageView.swift b/MarketPlace/View/MyPage/View/MyPageView.swift index c5a017f..81159a2 100644 --- a/MarketPlace/View/MyPage/View/MyPageView.swift +++ b/MarketPlace/View/MyPage/View/MyPageView.swift @@ -38,6 +38,11 @@ struct MyPageView: View { .zIndex(1) } } - }.environmentObject(loginVM) + } + .environmentObject(loginVM) + .onDisappear { + isDropdownVisible = false + showLogoutAlert = false + } } } diff --git a/MarketPlace/View/ShopModel.swift b/MarketPlace/View/ShopModel.swift deleted file mode 100644 index c413a23..0000000 --- a/MarketPlace/View/ShopModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -//import SwiftUI -// -//struct ShowModel: View { -// var body: some View { -// VStack{ -// Image("mysample1") -// -// } -//} -// -//#Preview { -// ShowModel() -//} diff --git a/MarketPlace/ViewModel/.DS_Store b/MarketPlace/ViewModel/.DS_Store index 5008ddf..51bc0f5 100644 Binary files a/MarketPlace/ViewModel/.DS_Store and b/MarketPlace/ViewModel/.DS_Store differ diff --git a/MarketPlace/ViewModel/Cheer/CheerSearchMarketViewModel.swift b/MarketPlace/ViewModel/Cheer/CheerSearchMarketViewModel.swift deleted file mode 100644 index 6ed6d99..0000000 --- a/MarketPlace/ViewModel/Cheer/CheerSearchMarketViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// CheerSearchMarketViewModel.swift -// MarketPlace -// -// Created by 이예나 on 6/30/25. -// - -import Foundation - -final class CheerSearchMarketViewModel: ObservableObject { - private let cheerMarketService: CheerMarketServiceProtocol - - @Published var searchText: String = "" - @Published var market: [CheerMarketModel] = [] - - init(cheerMarketService: CheerMarketServiceProtocol = CheerMarketService()) { - self.cheerMarketService = cheerMarketService - } - - - @MainActor - -} diff --git a/MarketPlace/ViewModel/FavoritesViewModel.swift b/MarketPlace/ViewModel/FavoritesViewModel.swift deleted file mode 100644 index 31dfde2..0000000 --- a/MarketPlace/ViewModel/FavoritesViewModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -//import SwiftUI -// -//@MainActor -//class FavoritesViewModel: ObservableObject { -// @Published var errorMessage: String? -// -// func fetchMarkets(size: Int = 10) async { -// do { -// let response: APIResponse<> = try await NetworkManager.shared.fetch(APIEndpoint.favoritesGetmarkets) -// print("API Response:", response.response.marketResDtos) -// self.markets = response.response.marketResDtos -// } catch { -// handleError("데이터를 불러오는 중 오류가 발생했습니다.", error: error) -// } -// } -// -// private func handleError(_ message: String, error: Error? = nil) { -// self.errorMessage = message + (error?.localizedDescription ?? "") -// } -//} diff --git a/MarketPlace/ViewModel/Login/KeychainHelper.swift b/MarketPlace/ViewModel/Login/KeychainHelper.swift deleted file mode 100644 index 7c3efc7..0000000 --- a/MarketPlace/ViewModel/Login/KeychainHelper.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Security -import Foundation - -class KeychainHelper { - static let shared = KeychainHelper() - - func saveToken(_ token: String, forKey key: String) { - let data = Data(token.utf8) - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecValueData as String: data - ] - - // 기존 값이 있으면 업데이트, 없으면 새로 저장 - SecItemDelete(query as CFDictionary) - SecItemAdd(query as CFDictionary, nil) - } - - func getToken(forKey key: String) -> String? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecReturnData as String: true, - kSecMatchLimit as String: kSecMatchLimitOne - ] - - var dataTypeRef: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) - - guard status == errSecSuccess, let data = dataTypeRef as? Data else { - return nil - } - - return String(data: data, encoding: .utf8) - } - - func deleteToken(forKey key: String) { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key - ] - SecItemDelete(query as CFDictionary) - } -} diff --git a/MarketPlace/ViewModel/Login/LoginAction.swift b/MarketPlace/ViewModel/Login/LoginAction.swift deleted file mode 100644 index c50e7f8..0000000 --- a/MarketPlace/ViewModel/Login/LoginAction.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import Combine -import SwiftUI - -struct APIService { - static func login(school: String, studentID: String, password: String) -> AnyPublisher { - guard let url = URL(string: "https://marketplace.inuappcenter.kr/api/member") else { - return Fail(error: URLError(.badURL)).eraseToAnyPublisher() - } - - let body: [String: Any] = [ - "school": school, - "studentID": studentID, - "password": password - ] - - guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else { - return Fail(error: URLError(.cannotEncodeContentData)).eraseToAnyPublisher() - print("로그인 시도 - 학번: \(studentID)") - - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = jsonData - - return URLSession.shared.dataTaskPublisher(for: request) - .map { $0.data } - .decode(type: LoginResponse.self, decoder: JSONDecoder()) - .mapError { $0 as Error } - .eraseToAnyPublisher() - } -} diff --git a/MarketPlace/ViewModel/Login/LoginViewModel.swift b/MarketPlace/ViewModel/Login/LoginViewModel.swift index 7b22a76..02ea694 100644 --- a/MarketPlace/ViewModel/Login/LoginViewModel.swift +++ b/MarketPlace/ViewModel/Login/LoginViewModel.swift @@ -4,17 +4,26 @@ final class LoginViewModel: ObservableObject { @Published var isLoggedIn: Bool = false @Published var userErrorMessage: String? - var token: String? { - KeychainManager.getToken() - } - private var memberService: MemberServiceProtocol init(memberService: MemberServiceProtocol = MemberService()) { self.memberService = memberService + + if let id = KeychainManager.load(KeyChainKeys.studentId), + let password = KeychainManager.load(KeyChainKeys.password), + UserDefaults.standard.bool(forKey: UserDefaultsKeys.saveId) { + isLoggedIn = true + } else { + isLoggedIn = false + KeychainManager.delete(KeyChainKeys.token) + } } - func signIn(studentId: String, password: String, saveAccount: Bool) async { + var token: String? { + KeychainManager.getToken() + } + + func signIn(studentId: String, password: String, saveAccount: Bool) async -> Bool { let result = await memberService.signIn(studentId: studentId, password: password) switch result { @@ -31,17 +40,24 @@ final class LoginViewModel: ObservableObject { } else { KeychainManager.delete(KeyChainKeys.studentId) KeychainManager.delete(KeyChainKeys.password) - } + return true + case .failure(let statusCode, let message): print("[signIn] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") userErrorMessage = message + + return false } } func logout() { KeychainManager.delete(KeyChainKeys.token) + KeychainManager.delete(KeyChainKeys.studentId) + KeychainManager.delete(KeyChainKeys.password) + UserDefaults.standard.set(false, forKey: UserDefaultsKeys.saveId) + DispatchQueue.main.async { self.isLoggedIn = false } diff --git a/MarketPlace/ViewModel/Main/CouponPopupViewModel.swift b/MarketPlace/ViewModel/Main/CouponPopupViewModel.swift index 39e5019..a7807a7 100644 --- a/MarketPlace/ViewModel/Main/CouponPopupViewModel.swift +++ b/MarketPlace/ViewModel/Main/CouponPopupViewModel.swift @@ -29,4 +29,16 @@ final class CouponPopupViewModel: ObservableObject { print("[downloadCoupons] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") } } + + func downloadPaybackCoupons(couponId: Int) async { + let result = await memberCouponSerivce.downloadPaybackCoupon(couponId: couponId) + + switch result { + case .success(let data, _): + self.message = data.message + print(message) + case .failure(let statusCode, let message): + print("[downloadPaybackCoupons] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") + } + } } diff --git a/MarketPlace/ViewModel/Main/MarketDetailViewModel.swift b/MarketPlace/ViewModel/Main/MarketDetailViewModel.swift index 0768593..f60063c 100644 --- a/MarketPlace/ViewModel/Main/MarketDetailViewModel.swift +++ b/MarketPlace/ViewModel/Main/MarketDetailViewModel.swift @@ -1,6 +1,5 @@ import Foundation -@MainActor final class MarketDetailViewModel: ObservableObject { @Published var marketDetail: MarketDetailModel? @Published var errorMessage: String? @@ -44,17 +43,30 @@ final class MarketDetailViewModel: ObservableObject { size: Int? ) async { - let result = await couponService.fetchValidCoupon( + let validCoupon = await couponService.fetchValidCoupon( marketId: marketId, couponId: couponId, size: size ) - switch result { - case .success(let data, _): + let validPaybackCoupon = await couponService.fetchValidPaybackCoupon( + marketId: marketId, + couponId: couponId, + size: size + ) + + switch (validPaybackCoupon, validCoupon) { + case (.success(let data1, _), .success(let data2, _)): + self.validCoupons = data1.response.couponResDtos + self.validCoupons.append(contentsOf: data2.response.couponResDtos) + case (.success(let data, _), .failure(let statusCode, let message)): self.validCoupons = data.response.couponResDtos - case .failure(let statusCode, let message): - print("[fetchValidCoupons] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") + print("[fetchValidPaybackCoupon] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") + case (.failure(let statusCode, let message), .success(let data, _)): + self.validCoupons = data.response.couponResDtos + print("[fetchValidCoupon] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") + default: + print("[fetchValidCoupons] - 데이터가 없습니다.") } } } diff --git a/MarketPlace/ViewModel/Main/MarketInfoCellViewModel.swift b/MarketPlace/ViewModel/Main/MarketInfoCellViewModel.swift index 40d6a34..60ce78e 100644 --- a/MarketPlace/ViewModel/Main/MarketInfoCellViewModel.swift +++ b/MarketPlace/ViewModel/Main/MarketInfoCellViewModel.swift @@ -9,20 +9,30 @@ import Foundation final class MarketInfoCellViewModel: ObservableObject { private var marketService: MarketServiceProtocol - /// - NOTE: 처리 방법 생각해보기 - @Published var market: MarketDetailModel = MarketDetailModel(marketId: 0, name: "", description: "", operationHours: "", closedDays: "", phoneNumber: "", address: "", imageResList: []) + @Published var market: MarketDetailModel = MarketDetailModel( + marketId: 0, name: "", + description: "", + operationHours: "", + closedDays: "", + phoneNumber: "", + address: "", + imageResList: [] + ) private var marketId: Int + @Published var marketData: MarketModel init( marketService: MarketServiceProtocol = MarketService(), - marketId: Int + marketId: Int, + marketData: MarketModel ) { self.marketService = marketService self.marketId = marketId + self.marketData = marketData Task { - await fetchMarket(marketId: marketId) +// await fetchMarket(marketId: marketId) } } @@ -33,6 +43,7 @@ final class MarketInfoCellViewModel: ObservableObject { switch result { case .success(let data, _): self.market = data.response + print("매장 상세조회",data.response) case .failure(let statusCode, let message): print("[fetchMarket] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") } diff --git a/MarketPlace/ViewModel/MarketInfoCellViewModel.swift b/MarketPlace/ViewModel/MarketInfoCellViewModel.swift deleted file mode 100644 index 60ce78e..0000000 --- a/MarketPlace/ViewModel/MarketInfoCellViewModel.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// MarketInfoCellViewModel.swift -// MarketPlace -// -// Created by Bowon Han on 5/27/25. -// - -import Foundation - -final class MarketInfoCellViewModel: ObservableObject { - private var marketService: MarketServiceProtocol - @Published var market: MarketDetailModel = MarketDetailModel( - marketId: 0, name: "", - description: "", - operationHours: "", - closedDays: "", - phoneNumber: "", - address: "", - imageResList: [] - ) - - private var marketId: Int - @Published var marketData: MarketModel - - init( - marketService: MarketServiceProtocol = MarketService(), - marketId: Int, - marketData: MarketModel - ) { - self.marketService = marketService - self.marketId = marketId - self.marketData = marketData - - Task { -// await fetchMarket(marketId: marketId) - } - } - - // MARK: - 매장 상세 조회 - func fetchMarket(marketId: Int) async { - let result = await marketService.fetchMarketDetail(marketId: marketId) - - switch result { - case .success(let data, _): - self.market = data.response - print("매장 상세조회",data.response) - case .failure(let statusCode, let message): - print("[fetchMarket] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") - } - } - - // MARK: - 매장 찜하기 - func postFavoriteMarket(marketId: Int) async { - let result = await marketService.postFavoriteMarket(marketId: marketId) - - switch result { - case .success(let data, _): - print(data) - case .failure(let statusCode, let message): - print("[postFavoriteMarket] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") - } - } -} diff --git a/MarketPlace/ViewModel/MyTab.swift b/MarketPlace/ViewModel/MyTab.swift deleted file mode 100644 index c6e49df..0000000 --- a/MarketPlace/ViewModel/MyTab.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// MyTab.swift -// MarketPlace -// -// Created by 이예나 on 10/4/24. -// - -import Foundation diff --git a/MarketPlace/ViewModel/Request/MarketRequestViewModel.swift b/MarketPlace/ViewModel/Request/MarketRequestViewModel.swift index 34204b0..e9f38b5 100644 --- a/MarketPlace/ViewModel/Request/MarketRequestViewModel.swift +++ b/MarketPlace/ViewModel/Request/MarketRequestViewModel.swift @@ -7,23 +7,23 @@ import Foundation -final class RequestMarketViewModel: ObservableObject { - private let marketService: MarketServiceProtocol - - @Published var market: [KakaoMarketData] = [] - - init(marketService: MarketServiceProtocol=MarketService()) { - self.marketService = marketService - } - - func searchKakaoMarketKeyword(keyword: String) async { - let result = await marketService.searchKakaoMarketKeyword(keyword: keyword) - - switch result { - case .success(let data, _): - self.market = data.documents - case .failure(let statusCode, let message): - print("[searchKakaoMarketKeyword] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") - } - } -} +//final class RequestMarketViewModel: ObservableObject { +// private let marketService: MarketServiceProtocol +// +// @Published var market: [KakaoMarketData] = [] +// +// init(marketService: MarketServiceProtocol=MarketService()) { +// self.marketService = marketService +// } +// +// func searchKakaoMarketKeyword(keyword: String) async { +// let result = await marketService.searchKakaoMarketKeyword(keyword: keyword) +// +// switch result { +// case .success(let data, _): +// self.market = data.documents +// case .failure(let statusCode, let message): +// print("[searchKakaoMarketKeyword] - [\(statusCode)]: \(message ?? "알 수 없는 오류")") +// } +// } +//} diff --git a/MarketPlace/ViewModel/ViewModel.swift b/MarketPlace/ViewModel/ViewModel.swift deleted file mode 100644 index d5753ec..0000000 --- a/MarketPlace/ViewModel/ViewModel.swift +++ /dev/null @@ -1,49 +0,0 @@ -//import Foundation -// -//// String을 Identifiable로 확장하는 부분 -//extension String: Identifiable { -// public var id: Int { self.hashValue } -//} -// -//@MainActor -//class MainTop20ViewModel: ObservableObject { -// // 검색 결과 정보 -// @Published var top20Result: [TopResultItem] = [] -// -// // 로딩 중 여부 -// @Published var isLoading: Bool = false -// -// func request(marketId: Int, marketName: String, thumbnail: String, favorite: Bool) { -// isLoading = true -// -// APIManager().request( -// api: .init( -// url: "https://marketplace.inuappcenter.kr", -// method: .GET, -// parameter: [ -// "marketId": toString.marketId, -// "marketName": marketName, -// "thumbnail": thumbnail, -// "favorite": favorite -// ] -// )) { [weak self] result in -// guard let self = self else { return } -// -// self.isLoading = false -// -// switch result { -// case .success(let data): -// do { -// // 응답 데이터 처리 -// let decodedResponse = try JSONDecoder().decode(ResponseCommon.self, from: data) -// self.top20Result = decodedResponse.response.map { TopResultItem(responseModel: $0) } -// } catch { -// print("JSON decoding error: \(error.localizedDescription)") -// } -// -// case .failure(let error): -// print("API request failed: \(error.localizedDescription)") -// } -// } -// } -//}