diff --git a/hybin/MegaBox/.gitignore b/hybin/MegaBox/.gitignore new file mode 100644 index 0000000..347d81e --- /dev/null +++ b/hybin/MegaBox/.gitignore @@ -0,0 +1,18 @@ +# Xcode 설정 등 기본적으로 무시할 것들 +.DS_Store +.AppleDouble +UserInterfaceState.xcuserstate +xcuserdata/ +*.xcscmblueprint +*.xccheckout +*.xcuserdatad + +DerivedData/ +Build/ +.build/ + +Packages/ +.swiftpm/ + +# Secret Key, Config +*.xcconfig diff --git a/hybin/MegaBox/Application/MainTabView.swift b/hybin/MegaBox/Application/MainTabView.swift index e110649..876e5f2 100644 --- a/hybin/MegaBox/Application/MainTabView.swift +++ b/hybin/MegaBox/Application/MainTabView.swift @@ -9,17 +9,25 @@ import Foundation import SwiftUI struct MainTabView : View { + + @Environment(UserSessionManager.self) var userSessionManager + var body : some View { TabView{ Tab("Home", systemImage: "house.fill"){ HomeView() } + Tab("Lable", systemImage: "popcorn"){ + OrderItemView() + } Tab("Profile", systemImage: "person.fill"){ ProfileView() } - } - + .fullScreenCover(isPresented: .constant(!userSessionManager.isLoggedIn)){ + LoginView() + .environment(userSessionManager) + } } } diff --git a/hybin/MegaBox/Application/MegaBoxApp.swift b/hybin/MegaBox/Application/MegaBoxApp.swift index d5cb1eb..c87e1bb 100644 --- a/hybin/MegaBox/Application/MegaBoxApp.swift +++ b/hybin/MegaBox/Application/MegaBoxApp.swift @@ -4,11 +4,22 @@ import SwiftUI struct MegaBoxApp: App { @State var userSession = UserSessionManager() + @State var kakaoAuthService = KakaoAuthService() var body: some Scene { WindowGroup { - LoginView() + SplashView() .environment(userSession) + .environment(kakaoAuthService) + .onOpenURL { url in + Task { + let success = await kakaoAuthService.handleRedirect(url: url) + + if success { + await userSession.login(id: "kakaologin" , password: "kakaopassword") + } + } + } } } } diff --git a/hybin/MegaBox/Data/DTOs/MovieResponseDTO.swift b/hybin/MegaBox/Data/DTOs/MovieResponseDTO.swift new file mode 100644 index 0000000..000ccf1 --- /dev/null +++ b/hybin/MegaBox/Data/DTOs/MovieResponseDTO.swift @@ -0,0 +1,23 @@ +// +// MovieResponseDTO.swift +// MegaBox +// +// Created by 전효빈 on 11/16/25. +// + +import Foundation + +struct MovieResponseDTO: Decodable, Sendable { + let results: [MovieResultDTO] + let page: Int +} + +struct MovieResultDTO: Decodable, Sendable { + let id: Int + let title:String + let original_title:String? + let overview:String? + let poster_path:String? + let backdrop_path:String? + let release_date:String? +} diff --git a/hybin/MegaBox/Data/Mappers/ScheduleMapper.swift b/hybin/MegaBox/Data/Mappers/ScheduleMapper.swift index f30fedb..98e6c74 100644 --- a/hybin/MegaBox/Data/Mappers/ScheduleMapper.swift +++ b/hybin/MegaBox/Data/Mappers/ScheduleMapper.swift @@ -41,35 +41,31 @@ import Foundation struct ScheduleMapper { - static func toDomain - (from dto:ScheduleResponseDTO, for movieID: String, on date: String) - -> [TheaterSchedule] { - - guard let movieDTO = dto.data.movies.first(where: { $0.id == movieID }) else {return []} - - guard let scheduleDTO = movieDTO.schedules.first(where: { $0.date == date }) else {return []} - - let theaterSchedules = scheduleDTO.areas.map{areaDTO in + static func mapToDomain(areas: [AreaDTO]) -> [TheaterSchedule] { - let rooms: [ScreeningTime] = areaDTO.items.flatMap { itemDTO in + // 1. DTO -> Domain Model + return areas.map { areaDTO in - return itemDTO.showtimes.map{ showtimeDTO in - - return ScreeningTime( - time: showtimeDTO.start, - endTime: "~" + showtimeDTO.end, - remainingSeats: showtimeDTO.available, - totalSeats: showtimeDTO.total, - is2D: itemDTO.format.uppercased() == "2D", - specialTheaterName: itemDTO.auditorium - ) + // 2. [AreaDTO] 안의 [ItemDTO]를 -> [ScreeningTime]으로 변환 + // (ItemDTO 1개가 방(auditorium)이고, 그 안에 상영시간(showtimes)이 여러 개 있음) + let screeningTimes = areaDTO.items.flatMap { itemDTO in + // 3. 'flatMap'을 사용해 ShowTimeDTOs -> ScreeningTime 배열로 "펼침" + itemDTO.showtimes.map { showtimeDTO in + ScreeningTime( + time: showtimeDTO.start, + endTime: showtimeDTO.end, + remainingSeats: showtimeDTO.available, + totalSeats: showtimeDTO.total, + is2D: (itemDTO.format == "2D"), // (format으로 2D 여부 추정) + specialTheaterName: itemDTO.auditorium // (auditorium을 방 이름으로 사용) + ) + } } + + return TheaterSchedule( + theaterName: areaDTO.area, + rooms: screeningTimes + ) } - - return TheaterSchedule( - theaterName: areaDTO.area, rooms: rooms - ) } - return theaterSchedules } -} diff --git a/hybin/MegaBox/Domain/Models/MenuItemModel.swift b/hybin/MegaBox/Domain/Models/MenuItemModel.swift new file mode 100644 index 0000000..f9c9e2d --- /dev/null +++ b/hybin/MegaBox/Domain/Models/MenuItemModel.swift @@ -0,0 +1,20 @@ +// +// MenuItModel.swift +// MegaBox +// +// Created by 전효빈 on 11/23/25. +// + +import Foundation +import SwiftUI + +struct MenuItemModel:Identifiable { + let id = UUID() + var menuImageName: String + var menuItem: String + var menuTitle: String + var menuPrice: Int + var itemIsBest : Bool + var itemIsRecommend : Bool + var itemIsSoldOut : Bool +} diff --git a/hybin/MegaBox/Domain/Models/MovieModel.swift b/hybin/MegaBox/Domain/Models/MovieModel.swift index 3a87323..4ac4ab2 100644 --- a/hybin/MegaBox/Domain/Models/MovieModel.swift +++ b/hybin/MegaBox/Domain/Models/MovieModel.swift @@ -22,6 +22,21 @@ struct MovieModel: Identifiable { } +struct MovieCardModel : Identifiable { + var id: Int + + let movieTitle: String + let moviePoster: String + let releaseDate: String + let ageLimit: String + let bookRanking: Double + let totalAudience: String + + let backdropPath: String + let originalTitle: String + let overview: String +} + struct ScreeningTime: Identifiable { let id = UUID() let time: String // "11:30" diff --git a/hybin/MegaBox/Domain/Models/UserModel.swift b/hybin/MegaBox/Domain/Models/UserModel.swift index 16babc5..d16a947 100644 --- a/hybin/MegaBox/Domain/Models/UserModel.swift +++ b/hybin/MegaBox/Domain/Models/UserModel.swift @@ -8,10 +8,10 @@ import Foundation -struct UserModel { - var userId: String +struct User { + var id: String var password: String - var userName: String + var name: String var membership: MembershipLevel //enum 처리 var membershipPoints : Int diff --git a/hybin/MegaBox/Domain/Services/KakaoAuthService.swift b/hybin/MegaBox/Domain/Services/KakaoAuthService.swift new file mode 100644 index 0000000..3c2cfc7 --- /dev/null +++ b/hybin/MegaBox/Domain/Services/KakaoAuthService.swift @@ -0,0 +1,80 @@ +// +// KakaoAuthService.swift +// MegaBox +// +// Created by 전효빈 on 11/10/25. +// + +import Foundation +import Alamofire +import SwiftUI + +@Observable +final class KakaoAuthService { + + func startKakaoLogin() { + let authURLString = "https://kauth.kakao.com/oauth/authorize?client_id=\(KakaoConfig.restAPIKey)&redirect_uri=\(KakaoConfig.redirectURI)&response_type=code" + + guard let authURL = URL(string: authURLString) else{ + print("카카오 인증 URL 생성 실패") + return + } + + if UIApplication.shared.canOpenURL(authURL) { + UIApplication.shared.open(authURL) + } + } + + func handleRedirect(url: URL) async -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), + let queryItems = components.queryItems, + let code = queryItems.first(where: { $0.name == "code" })?.value else { + print("리다이렉트 URL에서 인증코드를 찾을 수 없습니다") + return false + } + print("인증 코드 획득: \(code)") + return await fetchToken(code: code) + } + + private func fetchToken(code: String) async -> Bool { + let url = "https://kauth.kakao.com/oauth/token" + let parameters: [String: String] = [ + "grant_type":"authorization_code", + "client_id":KakaoConfig.restAPIKey, + "redirect_uri":KakaoConfig.redirectURI, + "code":code + ] + + do { + let response = try await AF.request(url,method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder.default) + .validate() + .serializingDecodable(KakaoTokenResponse.self) + .value + + print("토큰 획득 성공 : \(response.accessToken)") + + try? KeychainService.save(Data(response.accessToken.utf8),account: "kakaoAccessToken") + try? KeychainService.save(Data(response.refreshToken.utf8),account: "kakaoRefreshToken") + + return true + } catch { + print("토큰 요청 실패(Alamofire): \(error)") + return false + } + } +} + nonisolated struct KakaoTokenResponse: Decodable,Sendable { + let accessToken: String + let tokenType: String + let refreshToken: String + let expiresIn: Int + let refreshTokenExpiresIn: Int + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case tokenType = "token_type" + case refreshToken = "refresh_token" + case expiresIn = "expires_in" + case refreshTokenExpiresIn = "refresh_token_expires_in" + } + } diff --git a/hybin/MegaBox/Domain/Services/KakaoConfig.swift b/hybin/MegaBox/Domain/Services/KakaoConfig.swift new file mode 100644 index 0000000..c7d475c --- /dev/null +++ b/hybin/MegaBox/Domain/Services/KakaoConfig.swift @@ -0,0 +1,20 @@ +// +// KakaoConfig.swift +// MegaBox +// +// Created by 전효빈 on 11/10/25. +// + +import Foundation + +enum KakaoConfig { + static var restAPIKey = info("REST_APP_KEY") + static let nativeAppKey = info("NATIVE_APP_KEY") + static var redirectURI: String { "kakao\(nativeAppKey)://oauth" } +} + +private extension KakaoConfig { + static func info(_ key: String) -> String { + Bundle.main.object(forInfoDictionaryKey: key) as? String ?? "" + } +} diff --git a/hybin/MegaBox/Domain/Services/KeychainService.swift b/hybin/MegaBox/Domain/Services/KeychainService.swift new file mode 100644 index 0000000..034142b --- /dev/null +++ b/hybin/MegaBox/Domain/Services/KeychainService.swift @@ -0,0 +1,64 @@ +// +// KeychainService.swift +// MegaBox +// +// Created by 전효빈 on 11/10/25. +// + +import Foundation +import Security + +enum KeychainService { + + private static let service = "com.megabox.app" + + static func save(_ data: Data, account: String) throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, + kSecValueData as String: data + ] + + SecItemDelete(query as CFDictionary) + + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { throw KeychainError(status: status)} + } + + static func read(account: String) throws -> Data? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var item : CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess else { throw KeychainError(status: status) } + + return item as? Data + } + + static func delete(account: String) throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account + ] + + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess else { throw KeychainError(status: status)} + } + + struct KeychainError: LocalizedError { + let status : OSStatus + var errorDescription: String? { + SecCopyErrorMessageString(status, nil) as String? ?? "Keychain Error\(status)" + } + } +} diff --git a/hybin/MegaBox/Domain/Services/MovieService.swift b/hybin/MegaBox/Domain/Services/MovieService.swift index 2b181bc..9486f9f 100644 --- a/hybin/MegaBox/Domain/Services/MovieService.swift +++ b/hybin/MegaBox/Domain/Services/MovieService.swift @@ -51,25 +51,25 @@ class MovieService { } //---MARK: MovieReserveView용 - func fetchSchedules(for movieID: String, on date: Date) async throws - -> [TheaterSchedule] { - - let responseDTO = try await decodeLocalJSON() - - guard responseDTO.status == "success" else { - throw ApiError.serverError(responseDTO.message) - } - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - let dateString = dateFormatter.string(from: date) - - let schedules = ScheduleMapper.toDomain( - from: responseDTO, for: movieID, on: dateString - ) - return schedules - } - +// func fetchSchedules(for movieID: String, on date: Date) async throws +// -> [TheaterSchedule] { +// +// let responseDTO = try await decodeLocalJSON() +// +// guard responseDTO.status == "success" else { +// throw ApiError.serverError(responseDTO.message) +// } +// +// let dateFormatter = DateFormatter() +// dateFormatter.dateFormat = "yyyy-MM-dd" +// let dateString = dateFormatter.string(from: date) +// +// let schedules = ScheduleMapper.toDomain( +// from: responseDTO, for: movieID, on: dateString +// ) +// return schedules +// } +// private func decodeLocalJSON() async throws -> ScheduleResponseDTO { //url찾기 diff --git a/hybin/MegaBox/Domain/Services/UserSessionManager.swift b/hybin/MegaBox/Domain/Services/UserSessionManager.swift index 8beac81..40facbd 100644 --- a/hybin/MegaBox/Domain/Services/UserSessionManager.swift +++ b/hybin/MegaBox/Domain/Services/UserSessionManager.swift @@ -9,40 +9,86 @@ import Foundation import SwiftUI @Observable +@MainActor class UserSessionManager { - var inputUserID: String = "" - var inputUserPassword: String = "" - var inputUserName: String = "Default" - var inputUserMembership: UserModel.MembershipLevel = UserModel.MembershipLevel.welcome - var inputUserPoints: Int = 0 + var currentUser: User? + var isLoggedIn: Bool = false - var currentUser: UserModel? { - guard isLoggedIn else { - return nil - } - - return UserModel( - userId: inputUserID, - password: inputUserPassword, - userName: inputUserName, - membership: inputUserMembership, - membershipPoints: inputUserPoints - ) - } + private let userIDKey = "userID" + private let userPasswordKey = "userPassword" + private let userNameKey = "userName" + private let mockUsers: [User] = [ // 더미데이터 + User(id: "test", password: "1234", name: "테스트유저", membership: .vip, membershipPoints: 5000), + User(id: "mega", password: "box", name: "메가박스", membership: .gold, membershipPoints: 1000), + User(id: "user", password: "user", name: "홍길동", membership: .welcome, membershipPoints: 0), + + User(id: "kakaologin", password: "kakaopassword", name: "카카오유저", membership: .welcome, membershipPoints: 0) + ] - var isLoggedIn:Bool = false + func checkAutoLogin() async -> Bool { + print("키체인에서 자동 로그인 정보 확인 중...") + do { + guard let idData = try KeychainService.read(account: userIDKey), + let passwordData = try KeychainService.read(account: userPasswordKey), + let id = String(data: idData, encoding: .utf8), + let password = String(data: passwordData, encoding: .utf8) + else { + print("저장된 키체인 정보 없음. 자동 로그인 실패") + self.isLoggedIn = false + return false + } + print("저장된 ID(\(id))로 자동 로그인 시도....") + return await login(id: id, password: password) + } catch let error { + print("키체인 읽기 실패" + error.localizedDescription) + self.isLoggedIn = false + return false + } + } - func login(id: String, password:String) -> Bool { - inputUserID = id - inputUserPassword = password - isLoggedIn = true + @discardableResult func login(id: String, password: String) async -> Bool { + guard let user = mockUsers.first(where: { $0.id == id && $0.password == password}) else{ + print("로그인 실패: ID 또는 PW 불일치") + self.isLoggedIn = false + return false + } + print("로그인 성공: \(user.name)님") + self.currentUser = user + self.isLoggedIn = true + + do{ + try KeychainService.save(Data(user.id.utf8), account: userIDKey) + try KeychainService.save(Data(user.password.utf8), account: userPasswordKey) + try KeychainService.save(Data(user.name.utf8), account: userNameKey) + print("키체인에 사용자 정보 저장 완료") + } catch let error{ + print("키체인 저장 실패: \(error)") + } return true } - func updateUserName (editName:String) { - inputUserName = editName + func logout() { + print("로그아웃 중..") + self.currentUser = nil + self.isLoggedIn = false + + try? KeychainService.delete(account: userIDKey) + try? KeychainService.delete(account: userPasswordKey) + try? KeychainService.delete(account: userNameKey) + print("키체인에서 사용자 정보 삭제 완료") + } + + func updateUserName(newName: String) { + self.currentUser?.name = newName + + do{ + try KeychainService.save(Data(newName.utf8), account: userNameKey) + print("키체인에 새 이름 저장 완료") + }catch let error{ + print("키체인 이름 저장 실패\(error)") + } } } diff --git a/hybin/MegaBox/Feature/Home/HomeView.swift b/hybin/MegaBox/Feature/Home/HomeView.swift index 7336e58..a852b8e 100644 --- a/hybin/MegaBox/Feature/Home/HomeView.swift +++ b/hybin/MegaBox/Feature/Home/HomeView.swift @@ -7,23 +7,15 @@ import Foundation import SwiftUI +import Kingfisher struct HomeView: View{ @State private var viewModel = HomeViewModel() var body: some View { - if let errorMessage = viewModel.errorMessage { - VStack { - Text("데이터 로딩 실패") - .font(.headline) - Text(errorMessage) // 👈 여기에 에러 내용이 뜹니다. - .font(.caption) - .foregroundStyle(.red) - } - .padding() - } - else{NavigationStack{ + + NavigationStack{ ScrollView(.vertical) { megaboxLogoView .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) @@ -43,9 +35,9 @@ struct HomeView: View{ movieArticleView } .task { - await viewModel.loadMovies() + await viewModel.fetchNowPlayingMovies() } - }} + } } private var megaboxLogoView : some View { @@ -79,64 +71,61 @@ struct HomeView: View{ VStack{ HStack(spacing: 24) { Button { -// viewModel.selectTab(.movieChart) + // viewModel.selectTab(.movieChart) print("hi") - } label: { - Text("무비차트") - // 색상을 흰색으로 지정 - .foregroundStyle(Color.white) - .font(.pretend(type: .medium, size: 14)) - .padding(.horizontal, 16) - .padding(.vertical, 9) - .background(Color.black) - .clipShape(Capsule()) - } + } label: { + Text("무비차트") + // 색상을 흰색으로 지정 + .foregroundStyle(Color.white) + .font(.pretend(type: .medium, size: 14)) + .padding(.horizontal, 16) + .padding(.vertical, 9) + .background(Color.black) + .clipShape(Capsule()) + } Button { -// viewModel.selectTab(.movieChart) + // viewModel.selectTab(.movieChart) print("hi") - } label: { - Text("상영예정") - // 색상을 흰색으로 지정 - .foregroundStyle(Color.white) - .font(.pretend(type: .medium, size: 14)) - .padding(.horizontal, 16) - .padding(.vertical, 9) - .background(Color.gray) - .clipShape(Capsule()) - } + } label: { + Text("상영예정") + // 색상을 흰색으로 지정 + .foregroundStyle(Color.white) + .font(.pretend(type: .medium, size: 14)) + .padding(.horizontal, 16) + .padding(.vertical, 9) + .background(Color.gray) + .clipShape(Capsule()) + } }.frame(width:192) } } -// private func moviePosterView(movieVM: HomeViewModel) -> some View { -// ScrollView(.horizontal) { -// LazyHStack(spacing:24){ -// ForEach(movieVM.movieModel) { movie in -// -// movieCardView(movie: movie) -// } -// } -// } -// } - private var moviePoster : some View{ ScrollView(.horizontal){ LazyHStack(spacing:24){ - ForEach(viewModel.movieModel) { movie in + ForEach(viewModel.movieCharts, id: \.id) { movie in movieCardView(movie: movie) } } } } - private func movieCardView(movie: MovieModel) -> some View{ + private func movieCardView(movie: MovieCardModel) -> some View{ VStack(alignment:.leading,spacing:4){ - NavigationLink(destination: MovieDetailView(movie: movie)) { - movie.posterImage + NavigationLink(destination: MovieDetailView(movie:movie)) { + KFImage(URL(string: movie.moviePoster)) + .placeholder { // 로딩 중 + ProgressView() + .frame(width: 170, height: 240) // (크기 고정) + .background(Color.gray.opacity(0.1)) + } .resizable() + .scaledToFill() + .frame(width: 170, height: 240) // (크기 고정) + .clipShape(RoundedRectangle(cornerRadius: 10)) } -// ---MARK: 여기 고치기 + // ---MARK: 여기 고치기 NavigationLink(destination: MovieReserveView(selectedMovie: movie)) { Text("바로 예매") @@ -153,10 +142,10 @@ struct HomeView: View{ ) } - Text(movie.title) + Text(movie.movieTitle) .font(.pretend(type:.bold, size: 22)) .frame(alignment:.leading) - Text("누적 관객수 \(movie.audience)만") + Text("누적 관객수 \(movie.totalAudience)만") .font(.pretend(type:.medium, size: 18)) // Text("\(movie.bookranking)") } @@ -179,8 +168,8 @@ struct HomeView: View{ .foregroundStyle(Color.black) }) } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .padding(0) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .padding(0) Image(.exampleMovieFeed) .resizable() @@ -202,7 +191,7 @@ struct HomeView: View{ .resizable() .scaledToFit() .clipped() - ) + ) .clipShape(RoundedRectangle(cornerRadius: 10)) VStack(alignment:.leading,spacing: 25){ Text("9월, 메가박스의 영화들(1) - 명작들의 재개봉’") @@ -225,14 +214,14 @@ struct HomeView: View{ .resizable() .scaledToFit() .clipped() - ) + ) .clipShape(RoundedRectangle(cornerRadius: 10)) VStack(alignment:.leading,spacing:25){ Text("메가박스 오리지널 티켓 Re.37 <얼굴>") .font(.pretend(type: .semiBold, size: 16)) .foregroundStyle(Color.black) - + Text("영화 속 양극적인 감정의 대비") .font(.pretend(type: .semiBold, size: 11)) diff --git a/hybin/MegaBox/Feature/Home/HomeViewModel.swift b/hybin/MegaBox/Feature/Home/HomeViewModel.swift index 06ce0c6..87bb359 100644 --- a/hybin/MegaBox/Feature/Home/HomeViewModel.swift +++ b/hybin/MegaBox/Feature/Home/HomeViewModel.swift @@ -12,36 +12,46 @@ import Combine @Observable class HomeViewModel { - var movieModel: [MovieModel] = [] + var movieCharts: [MovieCardModel] = [] - @ObservationIgnored // UI 로직이 아니니 무시하게 끔.. Observable은 class 내부를 다 감시하니까.. - private let movieService = MovieService() + var isLoading: Bool = false - var errorMessage: String? = nil + private let movieService = MovieAPIService.shared - @MainActor //메인 쓰레드에 뜨게끔,, @Observable에서 .receive(on: DispatchQueue.main)과 같은 역할 - func loadMovies() async { - do{ - let movies = try await movieService.fetchMovies() + @MainActor + func fetchNowPlayingMovies() async { + self.isLoading = true + + + do { + let dto = try await movieService.provider.asyncRequest( + .nowPlaying(language: "ko-KR", page: 1, region: "KR"), responseType: MovieResponseDTO.self) - self.movieModel = movies - self.errorMessage = nil - } catch let error as ApiError { - switch error { - case .jsonFileNotFound: - self.errorMessage = "파일을 찾을 수 없습니다." - case .decodingError: - self.errorMessage = "데이터 형식이 잘못되었습니다." - case .serverError: - self.errorMessage = "서버 오류: \(error.localizedDescription)" - case .unknown: - self.errorMessage = "알 수 없는 오류가 발생했습니다." + let imageBaseURL = "https://image.tmdb.org/t/p/w500" + let backdropBaseURL = "https://image.tmdb.org/t/p/w780" + + self.movieCharts = dto.results.map { result in + MovieCardModel( + id: result.id, + movieTitle : result.title, + moviePoster: "\(imageBaseURL)\(result.poster_path ?? "")", + releaseDate: result.release_date ?? "N/A", + ageLimit: "15", + bookRanking: 0.0, + totalAudience: "10만", + + //디테일뷰 용 + backdropPath: "\(backdropBaseURL)\(result.backdrop_path ?? "")", + originalTitle: result.original_title ?? "N/A", + overview: result.overview ?? "N/A" + ) } - self.movieModel = [] //실패시 빈배열로 초기화 - } catch { - self .errorMessage = "알 수 없는 오류가 발생했습니다." - self .movieModel = [] + }catch { + print("TMDB API 호출 실패") + } + self.isLoading = false } + } diff --git a/hybin/MegaBox/Feature/Login/LoginView.swift b/hybin/MegaBox/Feature/Login/LoginView.swift index a3770c1..af4c221 100644 --- a/hybin/MegaBox/Feature/Login/LoginView.swift +++ b/hybin/MegaBox/Feature/Login/LoginView.swift @@ -10,56 +10,53 @@ import SwiftUI import Observation struct LoginView: View { -// @State var viewModel: LoginViewModel = .init() -// -// @AppStorage("ID") private var userID: String = "?" -// @AppStorage("PWD") private var userPWD: String = "!" + + @State private var viewModel = LoginViewModel() @Environment(UserSessionManager.self) var usm : UserSessionManager - @AppStorage("ID") private var userIDInput: String = "" - @AppStorage("PWD") private var userPWDInput: String = "" - @State private var showMain: Bool = false + @Environment(KakaoAuthService.self) var kakaoAuthService var body: some View { + VStack{ + NavigationBarTitle + Spacer() VStack{ - NavigationBarTitle Spacer() - VStack{ - Spacer() - Group{ - loginTextView - .padding(.vertical,50) - loginButtonView - .padding(.vertical,30) - socialLogin - - Spacer().frame(height:39) - - } + Group{ + loginTextView + .padding(.vertical,50) + loginButtonView + .padding(.vertical,30) + socialLogin + + Spacer().frame(height:39) + } - - Spacer().frame(height:39) - UMCImage } - .padding(.horizontal) + + Spacer().frame(height:39) + UMCImage + } + .padding(.horizontal) } private var loginTextView: some View { VStack(alignment : .center){ - TextField("아이디", text: $userIDInput) - .frame(maxWidth: .infinity,alignment:.leading) - .font(.pretend(type: .medium, size: 16)) - .foregroundStyle(Color.loginTextBackgroundColor) - Divider() - - SecureField("비밀번호", text:$userPWDInput) - .frame(maxWidth:.infinity,alignment: .leading) - .font(.pretend(type: .medium, size: 16)) - .foregroundStyle(Color.loginTextBackgroundColor) - - Divider() + TextField("아이디", text: $viewModel.userIDInput) + .textInputAutocapitalization(.never) + .frame(maxWidth: .infinity,alignment:.leading) + .font(.pretend(type: .medium, size: 16)) + .foregroundStyle(Color.loginTextBackgroundColor) + Divider() + + SecureField("비밀번호", text:$viewModel.userPWDInput) + .frame(maxWidth:.infinity,alignment: .leading) + .font(.pretend(type: .medium, size: 16)) + .foregroundStyle(Color.loginTextBackgroundColor) + + Divider() }.padding(0) } @@ -77,18 +74,14 @@ struct LoginView: View { VStack{ Button(action: { print("login") - //nil 이 들어오는 경우 방지 - let success = usm.login(id: userIDInput, password: userPWDInput) - if success { - //currentUser가 nil일 경우 방지 (옵셔널이니까) - if let current = usm.currentUser { - print("Current User: \(current)") - print(usm.isLoggedIn) - showMain = true - } else {print("No user")} - } else { - print("No user logged in") + + Task{ + let success = await usm.login( //success를 통해 UI쪽 관리 가능 + id: viewModel.userIDInput, + password: viewModel.userPWDInput + ) } + },label:{ Text("로그인") .font(.pretend(type: .bold, size: 18)) @@ -99,9 +92,6 @@ struct LoginView: View { .background(Color.loginBackgroundColor) .clipShape(RoundedRectangle(cornerRadius: 10)) .frame(maxWidth: .infinity) - .fullScreenCover(isPresented: $showMain){ - MainTabView() - } Text("회원가입") .font(.pretend(type: .medium, size: 12)) @@ -114,7 +104,11 @@ struct LoginView: View { Image(.naverLogin) Spacer() - Image(.kakaoLogin) + Button { + kakaoAuthService.startKakaoLogin() + } label: { + Image(.kakaoLogin) + } Spacer() Image(.appleLogin) diff --git a/hybin/MegaBox/Feature/Login/LoginViewModel.swift b/hybin/MegaBox/Feature/Login/LoginViewModel.swift index ff10705..d20b95e 100644 --- a/hybin/MegaBox/Feature/Login/LoginViewModel.swift +++ b/hybin/MegaBox/Feature/Login/LoginViewModel.swift @@ -12,7 +12,7 @@ import SwiftUI class LoginViewModel { var userIDInput: String = "" - var passwordInput: String = "" + var userPWDInput: String = "" var userName: String = "전효빈" var membership: String = "WELLCOME" var membershipPoints: Int = 500 diff --git a/hybin/MegaBox/Feature/Movie/MovieDetailView.swift b/hybin/MegaBox/Feature/Movie/MovieDetailView.swift index d1c3c9e..dcc95be 100644 --- a/hybin/MegaBox/Feature/Movie/MovieDetailView.swift +++ b/hybin/MegaBox/Feature/Movie/MovieDetailView.swift @@ -7,16 +7,15 @@ import Foundation import SwiftUI - - +import Kingfisher struct MovieDetailView : View { - let movie : MovieModel + let movie : MovieCardModel @Environment(\.dismiss) private var dismiss var body : some View{ - VStack{ + VStack(spacing: 0){ HStack { Button(action: { dismiss() @@ -26,79 +25,95 @@ struct MovieDetailView : View { .foregroundStyle(Color.black) } Spacer() - Text(movie.title) + Text(movie.movieTitle) .font(.pretend(type: .bold, size: 20)) .lineLimit(1) Spacer() - Spacer().frame(width: 44) // 오른쪽 빈 공간 + Spacer().frame(width: 44) } .padding(.horizontal) .padding(.top, 10) - VStack{ - movieImageDetailView - movieTitleDetailView - movieDescriptionView - } - VStack(alignment:.leading){ - HStack(alignment: .center){ - //탭버튼 위치 - Text("상세정보") - .frame(maxWidth: .infinity) - Text("관람후기") - .frame(maxWidth: .infinity) + + ScrollView { + VStack{ + movieImageDetailView + movieTitleDetailView + .padding(.top, 10) + movieDescriptionView + .padding(.top, 20) } - Divider() - HStack(alignment:.top){ - movie.posterImage - .resizable() - .frame(width:100, height:120) - .scaledToFit() - VStack(spacing: 10){ - Text("12세 이상 관람가") - Text("2025.10.2 개봉") + + VStack(alignment:.leading){ + HStack(alignment: .center){ + Text("상세정보") + .frame(maxWidth: .infinity) + Text("관람후기") + .frame(maxWidth: .infinity) } + Divider() + + + HStack(alignment:.top){ + + KFImage(URL(string: movie.moviePoster)) + .placeholder { ProgressView() } + .resizable() + .frame(width:100, height:120) + .scaledToFit() + + VStack(alignment: .leading, spacing: 10){ + Text(movie.ageLimit) // + Text("\(movie.releaseDate) 개봉") + } + .padding(.leading, 10) + } + .padding(.horizontal, 20) } - .padding(.horizontal, 20) - }.padding(.top ,20) + .padding(.top ,20) + } Spacer() - } .navigationBarBackButtonHidden(true) - +// .ignoresSafeArea(edges: .top) } + private var movieImageDetailView: some View{ - Rectangle() - .foregroundStyle(Color.clear) - .frame(maxWidth: .infinity,minHeight: 248, maxHeight: 248) - .background( - Image(.exampleMovieDetailView) - .resizable() - .scaledToFit() - .clipped() - ) + KFImage(URL(string: movie.backdropPath)) + .placeholder { + ZStack { + Color.gray.opacity(0.1) + ProgressView() + } + .frame(maxWidth: .infinity, minHeight: 248, maxHeight: 248) + } + .resizable() + .scaledToFill() // + .frame(maxWidth: .infinity, minHeight: 248, maxHeight: 248) + .clipped() } + private var movieTitleDetailView: some View{ Group { - Text(movie.title) - .font(.pretend(type: .bold ,size: 24)) - Text("영어" + movie.title)//영어 + Text(movie.movieTitle) + .font(.pretend(type: .bold ,size: 24)) + Text(movie.originalTitle) .font(.pretend(type: .semiBold, size: 14)) - .foregroundStyle(Color.loginTextBackground) + .foregroundStyle(Color.loginTextBackgroundColor) } } + private var movieDescriptionView: some View{ - Text("최고가 되지 못한 전설 VS 최고가 되고 싶은 루키\n\n한때 주목받는 유망주였지만 끔찍한 사고로 F1에서 우승하지 못하고\n한순간에 추락한 드라이버 ‘손; 헤이스'(브래드 피트).\n그의 오랜 동료인 ‘루벤 세르반테스'(하비에르 바르뎀)에게\n레이싱 복귀를 제안받으며 최하위 팀인 APGXP에 합류한다.") + Text(movie.overview) .font(.pretend(type: .semiBold, size: 18)) - .foregroundStyle(Color.loginTextBackground) + .foregroundStyle(Color.loginTextBackgroundColor) .padding(.horizontal, 10) } } - //#Preview { // MovieDetailView() diff --git a/hybin/MegaBox/Feature/Movie/MovieReserveView.swift b/hybin/MegaBox/Feature/Movie/MovieReserveView.swift index a65cb4a..6f9f989 100644 --- a/hybin/MegaBox/Feature/Movie/MovieReserveView.swift +++ b/hybin/MegaBox/Feature/Movie/MovieReserveView.swift @@ -8,12 +8,16 @@ import Foundation import SwiftUI import Combine +import Kingfisher struct MovieReserveView: View { - @State private var vm: MovieReserveViewModel + @State var vm: MovieReserveViewModel - init(selectedMovie: MovieModel) { + let selectedMovie: MovieCardModel + + init(selectedMovie: MovieCardModel) { + self.selectedMovie = selectedMovie _vm = State(initialValue: MovieReserveViewModel(selectedMovie: selectedMovie)) } @@ -38,6 +42,7 @@ struct MovieReserveView: View { Spacer() } } + .ignoresSafeArea(edges: .top) .sheet(isPresented: $isShowingSearchSheet, content: { MovieSearchSheetView( vm: vm, @@ -47,24 +52,24 @@ struct MovieReserveView: View { ) }) .navigationBarBackButtonHidden(true) - + .task { await vm.loadAllMovies() } .onChange(of: vm.selectedMovie?.id) { Task{ - await vm.loadSchedules() + vm.loadSchedules() } } .onChange(of: vm.selectedTheater) { Task { - await vm.loadSchedules() + vm.loadSchedules() } } .onChange(of: vm.calendarVM.selectedDate) { Task { - await vm.loadSchedules() + vm.loadSchedules() } } } @@ -74,34 +79,35 @@ struct MovieReserveView: View { //MARK: - 하위뷰 private var header: some View { - ZStack { - - ZStack { - Text("영화별 예매") - .font(.pretend(type: .bold, size: 22)) + + + ZStack { + Text("영화별 예매") + .font(.pretend(type: .bold, size: 22)) + .foregroundStyle(Color.white) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top,16) + + HStack { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.title2) .foregroundStyle(Color.white) - .frame(maxWidth: .infinity, alignment: .center) - - HStack { - Button { - dismiss() - } label: { - Image(systemName: "chevron.left") - .font(.title2) - .foregroundStyle(Color.white) - } - .padding(.leading, 16) - - Spacer() - } } - .padding(.top, 50) - .padding(.bottom, 10) + .padding(.leading, 16) + Spacer() } - .frame(maxWidth: .infinity) - .background(Color.loginBackgroundColor, ignoresSafeAreaEdges: .top) } + .padding(.top, 50) + .padding(.bottom, 10) + + + .frame(maxWidth: .infinity) + .background(Color.loginBackgroundColor) + } private var movieList : some View { VStack(alignment: .leading,spacing: 20){ @@ -114,7 +120,7 @@ struct MovieReserveView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) - Text("15") + Text(selectedMovie.ageLimit) .font(.pretend(type: .bold, size: 18)) .multilineTextAlignment(.center) .foregroundStyle(Color.white) @@ -122,7 +128,7 @@ struct MovieReserveView: View { }.padding(.trailing, 34) if let selected = vm.selectedMovie{ - Text(selected.title) + Text(selected.movieTitle) .font(.pretend(type: .bold, size: 18)) } else { Text("영화를 선택해주세요") @@ -159,24 +165,21 @@ struct MovieReserveView: View { } } - private func movieCardView(movie: MovieModel) -> some View{ + private func movieCardView(movie: MovieCardModel) -> some View{ Button(action:{ vm.selectedMovie = movie }) { - Rectangle() - .foregroundStyle(Color.clear) + KFImage(URL(string: movie.moviePoster)) + .placeholder { ProgressView() } + .resizable() + .scaledToFill() // (scaledToFit보다 Fill이 나음) .frame(width: 62, height: 89) - .background{ - movie.posterImage - .resizable() - .scaledToFit() - .clipped() - } .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .inset(by: 0.5) .stroke( + // ⭐️ [수정!] ID 비교 vm.selectedMovie?.id == movie.id ? Color.loginBackgroundColor : Color.clear ,lineWidth: 2 ) ) @@ -363,5 +366,4 @@ struct MovieReserveView: View { } -#Preview { -} + diff --git a/hybin/MegaBox/Feature/Movie/MovieReserveViewModel.swift b/hybin/MegaBox/Feature/Movie/MovieReserveViewModel.swift index fb50b20..3e55bc5 100644 --- a/hybin/MegaBox/Feature/Movie/MovieReserveViewModel.swift +++ b/hybin/MegaBox/Feature/Movie/MovieReserveViewModel.swift @@ -7,15 +7,14 @@ import SwiftUI class MovieReserveViewModel { @ObservationIgnored - private let movieService = MovieService() - + private let movieService = MovieAPIService.shared var calendarVM: CalendarViewModel = .init() //MARK: - 시트뷰 @ObservationIgnored private let searchTextSubject = PassthroughSubject() - + var searchText: String = "" { didSet { searchTextSubject.send(searchText) @@ -40,18 +39,19 @@ class MovieReserveViewModel { //MARK: - 리저브 뷰 - var movies: [MovieModel] = [] + var movies: [MovieCardModel] = [] let theaters: [String] = ["강남", "홍대", "신촌"] var selectedTheater: String? = nil - var selectedMovie: MovieModel? + var selectedMovie: MovieCardModel? var canReserve: Bool = false var schedules: [TheaterSchedule] = [] var errorMessage: String? = nil - init(selectedMovie: MovieModel) { + init(selectedMovie: MovieCardModel) { self.selectedMovie = selectedMovie setupDebounce() + loadSchedules() } //MARK: - 비동기 로드 함수 @@ -59,7 +59,29 @@ class MovieReserveViewModel { @MainActor func loadAllMovies() async { do { - self.movies = try await movieService.fetchMovies() + let dto = try await movieService.provider.asyncRequest( + .nowPlaying(language: "ko-KR", page: 1, region: "KR"), + responseType: MovieResponseDTO.self + ) + + let imageBaseURL = "https://image.tmdb.org/t/p/w500" + let backdropBaseURL = "https://image.tmdb.org/t/p/w780" + + // API 결과를 'movies' 변수에 매핑 + self.movies = dto.results.map { result in + MovieCardModel( + id: result.id, + movieTitle : result.title, + moviePoster: "\(imageBaseURL)\(result.poster_path ?? "")", + releaseDate: result.release_date ?? "N/A", + ageLimit: "15", // (하드코딩) + bookRanking: 0.0, // (하드코딩) + totalAudience: "10만", // (하드코딩) + backdropPath: "\(backdropBaseURL)\(result.backdrop_path ?? "")", + originalTitle: result.original_title ?? "N/A", + overview: result.overview ?? "개요 정보가 없습니다." + ) + } self.errorMessage = nil } catch { self.errorMessage = "전체 영화 목록 로드 실패: \(error.localizedDescription)" @@ -68,32 +90,108 @@ class MovieReserveViewModel { } @MainActor - func loadSchedules() async { - guard let movie = selectedMovie, let _ = selectedTheater else { + func loadSchedules() { + guard let movie = selectedMovie, let theaterName = selectedTheater else { self.schedules = [] self.canReserve = false return } - do { - let fetchedSchedules = try await movieService.fetchSchedules( - for: movie.id, - on: calendarVM.selectedDate - ) - - self.schedules = fetchedSchedules - self.canReserve = !fetchedSchedules.isEmpty - self.errorMessage = nil - - } catch let error as ApiError { - self.errorMessage = "시간표 로드 실패: \(error.localizedDescription)" + let selectedDate = calendarVM.selectedDate + print("날짜: \(selectedDate), 영화: \(movie.movieTitle), 극장: \(theaterName) 시간표 로드 중...") + + // 1. JSON 파일 경로 찾기 + guard let url = Bundle.main.url(forResource: "MovieSchedule", withExtension: "json") else { + self.errorMessage = "MovieSchedule.json 파일을 찾을 수 없습니다." + return + } + + // 2. JSON 데이터 읽기 + guard let data = try? Data(contentsOf: url) else { + self.errorMessage = "MovieSchedule.json 파일을 읽을 수 없습니다." + return + } + + // 3. JSON 디코딩 + guard let responseDTO = try? JSONDecoder().decode(ScheduleResponseDTO.self, from: data) else { + self.errorMessage = "MovieSchedule.json 디코딩 실패. DTO 구조 확인!" + return + } + + // 4. "새로운" DTO 구조에 맞춰 필터링 + + // 4-1. 날짜 포맷 맞추기 (JSON의 "yyyy-MM-dd" 형식) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: selectedDate) + + // 4-2. "영화 ID"로 필터링 + // (주의! 님의 새 DTO는 영화 ID가 String, MovieCardModel은 Int입니다. 타입을 맞춰줍니다) + guard let movieScheduleDTO = responseDTO.data.movies.first(where: { + $0.id == String(movie.id) + }) else { + print("이 영화(\(movie.id))의 시간표가 DTO에 없습니다.") self.schedules = [] self.canReserve = false - } catch { - self.errorMessage = "알 수 없는 오류: \(error.localizedDescription)" + return + } + + // 4-3. "날짜"로 필터링 + guard let scheduleDTO = movieScheduleDTO.schedules.first(where: { $0.date == dateString }) else { + print("이 날짜(\(dateString))의 시간표가 DTO에 없습니다.") + self.schedules = [] + self.canReserve = false + return + } + + // 4-4. "극장(Area)"으로 필터링 + guard let areaDTO = scheduleDTO.areas.first(where: { $0.area == theaterName }) else { + print("이 극장(\(theaterName))의 시간표가 DTO에 없습니다.") self.schedules = [] self.canReserve = false + return } + + // 5. 필터링된 "Area 1개"를 -> "TheaterSchedule 1개"로 매핑 + let finalSchedules = ScheduleMapper.mapToDomain(areas: [areaDTO]) + + // 6. 최종 시간표를 View에 할당 + self.schedules = finalSchedules + self.canReserve = !finalSchedules.isEmpty + self.errorMessage = nil + + print("\(finalSchedules.first?.rooms.count ?? 0)개의 시간표 로드 성공!") } + + + + // @MainActor + // func loadSchedules() async { + // guard let movie = selectedMovie, let _ = selectedTheater else { + // self.schedules = [] + // self.canReserve = false + // return + // } + // + // do { + // let fetchedSchedules = try await movieService.fetchSchedules( + // for: movie.id, + // on: calendarVM.selectedDate + // ) + // + // self.schedules = fetchedSchedules + // self.canReserve = !fetchedSchedules.isEmpty + // self.errorMessage = nil + // + // } catch let error as ApiError { + // self.errorMessage = "시간표 로드 실패: \(error.localizedDescription)" + // self.schedules = [] + // self.canReserve = false + // } catch { + // self.errorMessage = "알 수 없는 오류: \(error.localizedDescription)" + // self.schedules = [] + // self.canReserve = false + // } + // } } diff --git a/hybin/MegaBox/Feature/Movie/MovieSearchSheetView.swift b/hybin/MegaBox/Feature/Movie/MovieSearchSheetView.swift index e3c59bf..30997a8 100644 --- a/hybin/MegaBox/Feature/Movie/MovieSearchSheetView.swift +++ b/hybin/MegaBox/Feature/Movie/MovieSearchSheetView.swift @@ -1,10 +1,11 @@ import Foundation +import Kingfisher import SwiftUI struct MovieSearchSheetView: View { @Bindable var vm: MovieReserveViewModel - var onMovieSelected: (MovieModel) -> Void + var onMovieSelected: (MovieCardModel) -> Void @Environment(\.dismiss) var dismiss @@ -14,11 +15,11 @@ struct MovieSearchSheetView: View { GridItem(.flexible(), spacing: 15) ] - var filteredMovies: [MovieModel] { + var filteredMovies: [MovieCardModel] { if vm.debouncedText.isEmpty { return vm.movies // 'allMovies'가 아닌 vm.movies } else { - return vm.movies.filter { $0.title.localizedCaseInsensitiveContains(vm.debouncedText) } + return vm.movies.filter { $0.movieTitle.localizedCaseInsensitiveContains(vm.debouncedText) } } } @@ -40,14 +41,14 @@ struct MovieSearchSheetView: View { Spacer() } } - .navigationTitle("영화 선택") // 5. ⭐️ 상단 제목 + .navigationTitle("영화 선택") // 5.상단 제목 .navigationBarTitleDisplayMode(.inline) .toolbar { // 닫기 버튼 ToolbarItem(placement: .cancellationAction) { Button("닫기") { dismiss() } } } - .safeAreaInset(edge: .bottom) { // 6. ⭐️ 하단 검색창 + .safeAreaInset(edge: .bottom) { // 6.하단 검색창 searchBar } } @@ -66,7 +67,7 @@ struct MovieSearchSheetView: View { Image(systemName: "magnifyingglass") .foregroundStyle(.gray) - // 7. ⭐️ VM의 searchText와 정상적으로 바인딩 + // 7.VM의 searchText와 정상적으로 바인딩 TextField("Search", text: $vm.searchText) .textFieldStyle(PlainTextFieldStyle()) .frame(maxWidth: .infinity) @@ -92,8 +93,8 @@ struct MovieSearchSheetView: View { // MARK: - 영화 포스터 셀 컴포넌트 (개별 셀) private struct MoviePosterCell: View { - let movie: MovieModel - var action: (MovieModel) -> Void + let movie: MovieCardModel + var action: (MovieCardModel) -> Void var body: some View { Button { @@ -101,7 +102,7 @@ private struct MoviePosterCell: View { } label: { VStack(spacing: 8) { // 포스터 이미지 - movie.posterImage + KFImage(URL(string: movie.moviePoster)) .resizable() .scaledToFill() .frame(width: 95, height: 135) @@ -112,9 +113,8 @@ private struct MoviePosterCell: View { } // 영화 제목 - Text(movie.title) + Text(movie.movieTitle) .font(.pretend(type: .semiBold, size: 14)) // 폰트가 있다면 - // .font(.system(size: 14, weight: .semibold)) // 폰트가 없다면 .multilineTextAlignment(.center) .foregroundStyle(.black) .lineLimit(2) // 제목이 길 경우 두 줄로 제한 diff --git a/hybin/MegaBox/Feature/Order/OrderItemDetailView.swift b/hybin/MegaBox/Feature/Order/OrderItemDetailView.swift new file mode 100644 index 0000000..007ded9 --- /dev/null +++ b/hybin/MegaBox/Feature/Order/OrderItemDetailView.swift @@ -0,0 +1,35 @@ +// +// OrderItemDetailView.swift +// MegaBox +// +// Created by 전효빈 on 11/23/25. +// + +import Foundation +import SwiftUI + +struct OrderItemDetailView : View{ + @State private var viewModel: OrderItemViewModel = .init() + var body : some View { + ZStack{ + + ScrollView { + VStack(spacing: 0) { + TheaterChangeBarView(selectedTheaterName: "강남", action: {print("극장 변경")}) + .newTheaterBar(newForegroundColor: .loginBackground, newBackgroundColor: .clear) + } + .padding(.vertical, 10) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 15), count: 2),spacing:10) { + ForEach(viewModel.menuItem) { item in + MenuItemView(item:item) + .bestBadge(isBest: item.itemIsBest) + .recommendBadge(isRecommend: item.itemIsRecommend) + + } + } + + } + } + } +} diff --git a/hybin/MegaBox/Feature/Order/OrderItemView.swift b/hybin/MegaBox/Feature/Order/OrderItemView.swift new file mode 100644 index 0000000..bad9b8c --- /dev/null +++ b/hybin/MegaBox/Feature/Order/OrderItemView.swift @@ -0,0 +1,153 @@ +// +// OrderView.swift +// MegaBox +// +// Created by 전효빈 on 11/23/25. +// + +import Foundation +import SwiftUI + + +struct OrderItemView: View { + @State private var viewModel = OrderItemViewModel() + + var body: some View { + NavigationStack{ + ZStack{ + + ScrollView{ + VStack(spacing: 0) { + Image(.megaBoxLogo) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 20) + .padding(.vertical, 10) + TheaterChangeBarView( + selectedTheaterName: "강남", action:{ print("hi")} + ) + .newTheaterBar(newForegroundColor: .white, newBackgroundColor: .loginBackground) + .padding(.vertical, 10) + .frame(maxWidth:.infinity) + OrderItemButtonSection + .padding(.vertical, 15) + + RecommendItemSection + BestItemSection + } + } + } + } + } + + private var OrderItemButtonSection: some View { + VStack(spacing: 20){ + + HStack(alignment:.center , spacing: 15){ + NavigationLink { + OrderItemDetailView() + } label: { + MenuItemOrderButtonView( + title:"바로 주문", + description:"이제 줄서지 말고 \n모바일로 바로 픽업!", + symbol:"popcorn" + ) + } + VStack{ + MenuItemOrderButtonView(title: "스토어 교환권", description: nil, symbol: "ticket") + MenuItemOrderButtonView(title: "선물하기", description: nil, symbol: "gift") + } + } + HStack{ + VStack(spacing: 10){ + Text("어디서든 팝콘 만나기") + .font(.pretend(type: .bold, size: 20)) + Text("팝콘 콜라 스낵 모든 메뉴 배달 가능!") + .font(.pretend(type: .regular, size: 12)) + .foregroundStyle(Color.loginTextBackground) + } + Spacer() + Image(systemName: "motorcycle") + } + .padding(.horizontal,15) + .padding(.vertical,25) + .overlay( + RoundedRectangle(cornerRadius: 10) + .inset(by: 0.5) + .stroke(Color(red: 0.79, green: 0.77, blue: 0.77), lineWidth: 1) + ) + } + .padding(.horizontal,20) + } + private var RecommendItemSection: some View { + VStack(alignment: .leading, spacing: 10){ + itemSectionTitle(title: "추천 메뉴", description: "영화 볼 때 뭐먹지 고민될 땐 추천 메뉴!") + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(viewModel.menuItem.filter { $0.itemIsRecommend }) { item in + MenuItemView(item: item) + } + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 15) + } + + private var BestItemSection : some View { + VStack(alignment: .leading, spacing: 10){ + itemSectionTitle(title: "베스트 메뉴", description: "영화 볼 때 뭐먹지 고민될 땐 베스트 메뉴!") + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(viewModel.menuItem.filter { $0.itemIsBest }) { item in + MenuItemView(item: item) + } + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 15) + } + + + private func itemSectionTitle(title: String, description: String) -> some View{ + VStack(alignment: .leading ,spacing: 10){ + Text(title) + .font(.pretend(type: .semiBold, size: 20)) + Text(description) + .font(.pretend(type: .regular, size: 12)) + } + } + private func MenuItemOrderButtonView(title: String, description: String? ,symbol: String) -> some View { + + VStack(alignment: .leading) { + Text(title) + .font(.pretend(type: .bold, size: 20)) + .foregroundStyle(Color.black) + if let description { + Text(description) + .font(.pretend(type: .regular, size: 12)) + .foregroundStyle(Color.loginTextBackground) + } + Spacer() + HStack{ + Spacer() + Image(systemName:symbol) + } + } + .foregroundStyle(Color.black) + .padding(.horizontal,12) + .padding(.vertical,15) + .overlay( + RoundedRectangle(cornerRadius: 10) + .inset(by: 0.5) + .stroke(Color(red: 0.79, green: 0.77, blue: 0.77), lineWidth: 1) + ) + } +} + +#Preview { + OrderItemView() +} + diff --git a/hybin/MegaBox/Feature/Order/OrderItemViewModel.swift b/hybin/MegaBox/Feature/Order/OrderItemViewModel.swift new file mode 100644 index 0000000..88d39f2 --- /dev/null +++ b/hybin/MegaBox/Feature/Order/OrderItemViewModel.swift @@ -0,0 +1,27 @@ +// +// OrderItemViewModel.swift +// MegaBox +// +// Created by 전효빈 on 11/23/25. +// + +import Foundation +import SwiftUI + +@Observable +class OrderItemViewModel { + var menuItem : [MenuItemModel] = [ + MenuItemModel(menuImageName: "love_combo", menuItem: "Combo", menuTitle: "러브 콤보", menuPrice: 10900, itemIsBest: false, itemIsRecommend: true, itemIsSoldOut: false), + MenuItemModel(menuImageName: "double_combo", menuItem: "Combo", menuTitle: "더블 콤보", menuPrice: 24900, itemIsBest: false, itemIsRecommend: true, itemIsSoldOut: false), + + //추천 + MenuItemModel(menuImageName: "single_combo", menuItem: "Combo", menuTitle: "싱글 콤보", menuPrice: 10900, itemIsBest: true, itemIsRecommend: false, itemIsSoldOut: false), + + //품절 + MenuItemModel(menuImageName: "disney_poster", menuItem: "Goods", menuTitle: "디즈니 픽사 포스터", menuPrice: 15900, itemIsBest: false, itemIsRecommend: false, itemIsSoldOut: true), + + //기타 + MenuItemModel(menuImageName: "insideout_figure", menuItem: "Goods", menuTitle: "인사이드아웃2 감정", menuPrice: 29900, itemIsBest: false, itemIsRecommend: false, itemIsSoldOut: false) + + ] +} diff --git a/hybin/MegaBox/Feature/Profile/ImagePicker.swift b/hybin/MegaBox/Feature/Profile/ImagePicker.swift new file mode 100644 index 0000000..0a105a7 --- /dev/null +++ b/hybin/MegaBox/Feature/Profile/ImagePicker.swift @@ -0,0 +1,49 @@ +// +// ImagePicker.swift +// MegaBox +// +// Created by 전효빈 on 11/30/25. +// + +import SwiftUI +import UIKit + +struct ImagePicker: UIViewControllerRepresentable { + + @Binding var selectedImage: UIImage? + @Environment(\.presentationMode) var presentationMode + + func makeUIViewController(context: Context) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.delegate = context.coordinator + picker.sourceType = .photoLibrary + + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage { + parent.selectedImage = image + } + + parent.presentationMode.wrappedValue.dismiss() + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + parent.presentationMode.wrappedValue.dismiss() + } + } +} diff --git a/hybin/MegaBox/Feature/Profile/ProfileDetailView.swift b/hybin/MegaBox/Feature/Profile/ProfileDetailView.swift index 452ab9a..bda60ea 100644 --- a/hybin/MegaBox/Feature/Profile/ProfileDetailView.swift +++ b/hybin/MegaBox/Feature/Profile/ProfileDetailView.swift @@ -15,36 +15,6 @@ struct ProfileDetailView: View { @State var isNameEditing: Bool = false @State var tempName : String = "" -// private var customHeader: some View { -// HStack { -// Button { -// dismiss() -// } label: { -// Image(systemName: "chevron.left") -// .resizable() -// .scaledToFit() -// .frame(width: 12, height: 18) -// .foregroundStyle(Color.black) -// } -// .padding(.trailing, 20) -// -// Spacer() -// -// Text("회원정보 관리") -// .font(.pretend(type: .medium, size: 18)) -// .multilineTextAlignment(.center) -// .foregroundStyle(Color.black) -// -// Spacer() -// -// Rectangle() -// .fill(Color.clear) -// .frame(width: 12 + 20, height: 18) -// } -// .padding(.horizontal, 20) -// .padding(.top, 10) -// } - //SwiftUI 내장 네비게이션 UI 사용 var body: some View { VStack(spacing: 0) { if let user = usm.currentUser{ @@ -54,7 +24,7 @@ struct ProfileDetailView: View { VStack(spacing: 0){ HStack{ - Text(user.userId) + Text(user.id) .frame(maxWidth: .infinity, alignment:.leading) .font(.pretend(type: .medium, size: 16)) .foregroundStyle(Color.black) @@ -69,7 +39,7 @@ struct ProfileDetailView: View { .textFieldStyle(.roundedBorder) .font(.system(size: 16, weight: .medium)) } else { - Text(user.userName) + Text(user.name) .frame(maxWidth: .infinity, alignment:.leading) .font(.system(size: 16, weight: .medium)) .foregroundStyle(Color.black) @@ -79,10 +49,10 @@ struct ProfileDetailView: View { Button(action: { if isNameEditing == true { - usm.updateUserName(editName: tempName) + usm.updateUserName(newName: tempName) isNameEditing = false } else { - tempName = user.userName + tempName = user.name isNameEditing = true } }, label: { diff --git a/hybin/MegaBox/Feature/Profile/ProfileView.swift b/hybin/MegaBox/Feature/Profile/ProfileView.swift index 0df2bc1..d6b027e 100644 --- a/hybin/MegaBox/Feature/Profile/ProfileView.swift +++ b/hybin/MegaBox/Feature/Profile/ProfileView.swift @@ -13,18 +13,20 @@ struct ProfileView: View { // 환경 객체 주입 @Environment(UserSessionManager.self) var usm : UserSessionManager + @State private var profileImage: UIImage? = nil + @State private var isImagePickerPresented: Bool = false var body: some View { NavigationStack{ - + ScrollView { VStack(alignment: .leading,spacing:33){ VStack(alignment:.leading){ - + if let user = usm.currentUser{ userInformation(user: user) - membershipPoint(user: user) + // logoutButton } else { // 로그아웃 상태일 때 대체 UI Text("로그인 상태가 아닙니다.").font(.title).padding(.leading, 10) @@ -37,62 +39,112 @@ struct ProfileView: View { customerStatus bottomImage - + } - + .padding(.top, 20) .padding(.horizontal, 15) } } } - - // 사용자 정보 (이름, 등급, 회원정보 버튼 포함) - private func userInformation(user : UserModel) -> some View{ - HStack{ - - Text(user.userName + "님") - .font(.pretend(type: .bold, size: 24)) - - Text(user.membership.rawValue) - .font(.pretend(type:.medium, size:14)) - .foregroundStyle(Color.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(red: 0.28, green: 0.8, blue: 0.82)) - .clipShape(RoundedRectangle(cornerRadius: 6)) - - Spacer() // 오른쪽 버튼 밀어내기 - - // 회원정보 관리 NavigationLink - NavigationLink(destination: ProfileDetailView()){ - Text("회원정보") - .font(.pretend(type:.semiBold, size:14)) - .foregroundStyle(Color.white) - .padding(4) - .frame(width: 72, alignment: .center) - .background(Color(red: 0.28, green: 0.28, blue: 0.28)) - .clipShape(RoundedRectangle(cornerRadius: 16)) + //로그아웃버튼 + private var logoutButton: some View { + ZStack{ + Button("로그아웃"){ + usm.logout() } - }.padding(0) + .buttonStyle(PlainButtonStyle()) + .padding(.top, 10) + } } - // 멤버십 포인트 정보 - private func membershipPoint(user:UserModel) -> some View { + // 사용자 정보 (이름, 등급, 회원정보 버튼 포함) + private func userInformation(user : User) -> some View{ HStack{ - Text("멤버십 포인트") - .font(.pretend(type:.semiBold, size:14)) - .padding(.horizontal,10) - .padding(.top,15) - Text(String(user.membershipPoints) + "P") - .font(.pretend(type:.medium, size:14)) - .padding(.top,15) - Spacer() + VStack { + if let image = profileImage { + Image(uiImage: image) + .resizable() + .scaledToFill() + .frame(width: 55, height: 55) + .clipShape(Circle()) + } else { + Image("gg_profile") + .resizable() + .scaledToFill() + .frame(width: 55, height: 55) + } + } + .onLongPressGesture(minimumDuration: 1.0) { + print("press.") + isImagePickerPresented = true + } + .sheet(isPresented: $isImagePickerPresented) { + ImagePicker(selectedImage: $profileImage) + } + + VStack(spacing: 0){ + HStack{ + + Text(user.name + "님") + .font(.pretend(type: .bold, size: 24)) + + Text(user.membership.rawValue) + .font(.pretend(type:.medium, size:14)) + .foregroundStyle(Color.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(red: 0.28, green: 0.8, blue: 0.82)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + Spacer() // 오른쪽 버튼 밀어내기 + + // 회원정보 관리 NavigationLink + NavigationLink(destination: ProfileDetailView()){ + Text("회원정보") + .font(.pretend(type:.semiBold, size:14)) + .foregroundStyle(Color.white) + .padding(4) + .frame(width: 72, alignment: .center) + .background(Color(red: 0.28, green: 0.28, blue: 0.28)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } + .padding(0) + + HStack{ + Text("멤버십 포인트") + .font(.pretend(type:.semiBold, size:14)) + .padding(.horizontal,10) + .padding(.top,15) + + Text(String(user.membershipPoints) + "P") + .font(.pretend(type:.medium, size:14)) + .padding(.top,15) + Spacer() + } + } } } - + // // 멤버십 포인트 정보 + // private func membershipPoint(user:User) -> some View { + // HStack{ + // Text("멤버십 포인트") + // .font(.pretend(type:.semiBold, size:14)) + // .padding(.horizontal,10) + // .padding(.top,15) + // + // Text(String(user.membershipPoints) + "P") + // .font(.pretend(type:.medium, size:14)) + // .padding(.top,15) + // Spacer() + // } + // } + + private var clubMembership: some View { HStack(alignment: .center, spacing: 3) { @@ -193,8 +245,8 @@ struct ProfileView: View { .font(.pretend(type:.medium,size:14)) } }.frame(width:66, height:67) - .padding(.horizontal, 10) // 중앙 정렬을 위해 padding 추가 - + .padding(.horizontal, 10) // 중앙 정렬을 위해 padding 추가 + Spacer() // 극장별예매 @@ -209,8 +261,8 @@ struct ProfileView: View { .font(.pretend(type:.medium,size:14)) } }.frame(width:66, height:67) - .padding(.horizontal, 10) - + .padding(.horizontal, 10) + Spacer() // 특별관예매 @@ -225,7 +277,7 @@ struct ProfileView: View { .font(.pretend(type:.medium,size:14)) } }.frame(width:66, height:67) - .padding(.horizontal, 10) + .padding(.horizontal, 10) Spacer() @@ -241,7 +293,7 @@ struct ProfileView: View { .font(.pretend(type:.medium,size:14)) } }.frame(width:66, height:67) - .padding(.horizontal, 10) + .padding(.horizontal, 10) } .padding(.horizontal, 5) } diff --git a/hybin/MegaBox/MegaBox.xcodeproj/project.pbxproj b/hybin/MegaBox/MegaBox.xcodeproj/project.pbxproj index e00d397..87638bc 100644 --- a/hybin/MegaBox/MegaBox.xcodeproj/project.pbxproj +++ b/hybin/MegaBox/MegaBox.xcodeproj/project.pbxproj @@ -6,6 +6,21 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 4C0C2FFE2EC2300A0079566D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 4C0C2FFD2EC2300A0079566D /* Alamofire */; }; + 4C0C30082EC9AFBC0079566D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C30072EC9AFBC0079566D /* .gitignore */; }; + 4C0C30092EC9AFBC0079566D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C30072EC9AFBC0079566D /* .gitignore */; }; + 4C0C300A2EC9AFBC0079566D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C30072EC9AFBC0079566D /* .gitignore */; }; + 4C0C30C02EC9F3CF0079566D /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 4C0C30BF2EC9F3CF0079566D /* CombineMoya */; }; + 4C0C30C22EC9F3CF0079566D /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 4C0C30C12EC9F3CF0079566D /* Moya */; }; + 4C0C30C92EC9FCF10079566D /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C0C30C82EC9FCF10079566D /* Kingfisher */; }; + 4CACFB152EC2298500643999 /* KakaoSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 4CACFB142EC2298500643999 /* KakaoSDK */; }; + 4CACFB172EC2298500643999 /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4CACFB162EC2298500643999 /* KakaoSDKAuth */; }; + 4CACFB192EC2298500643999 /* KakaoSDKCert in Frameworks */ = {isa = PBXBuildFile; productRef = 4CACFB182EC2298500643999 /* KakaoSDKCert */; }; + 4CACFB1B2EC2298500643999 /* KakaoSDKCertCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4CACFB1A2EC2298500643999 /* KakaoSDKCertCore */; }; + 4CACFB1D2EC2298500643999 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 4CACFB1C2EC2298500643999 /* KakaoSDKCommon */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 4CF19B022E8806F00078679E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -23,25 +38,88 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 4CACFB062EC2286C00643999 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 4CACFB0C2EC228ED00643999 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 4CACFB122EC228F500643999 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 4C0C30072EC9AFBC0079566D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 4CF19AF42E8806EE0078679E /* MegaBox.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MegaBox.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4CF19B012E8806F00078679E /* MegaBoxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MegaBoxTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4CF19B0B2E8806F00078679E /* MegaBoxUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MegaBoxUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 4C0C30DF2ED310C20079566D /* Exceptions for "Moya" folder in "MegaBox" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + MovieAPIService.swift, + MoyaExtension.swift, + ); + target = 4CF19AF32E8806EE0078679E /* MegaBox */; + }; + 4C0C30E02ED310C20079566D /* Exceptions for "Moya" folder in "MegaBoxTests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + MovieAPIService.swift, + MoyaExtension.swift, + ); + target = 4CF19B002E8806F00078679E /* MegaBoxTests */; + }; + 4C0C30E12ED310C20079566D /* Exceptions for "Moya" folder in "MegaBoxUITests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + MovieAPIService.swift, + MoyaExtension.swift, + ); + target = 4CF19B0A2E8806F00078679E /* MegaBoxUITests */; + }; 4CA2D65C2EA91CA2009D27BE /* Exceptions for "Feature" folder in "MegaBox" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Home/HomeView.swift, Home/HomeViewModel.swift, Login/LoginView.swift, + Login/LoginViewModel.swift, Movie/CalendarModel.swift, Movie/CalendarViewModel.swift, Movie/MovieDetailView.swift, Movie/MovieReserveView.swift, Movie/MovieReserveViewModel.swift, Movie/MovieSearchSheetView.swift, + Order/OrderItemDetailView.swift, + Order/OrderItemView.swift, + Order/OrderItemViewModel.swift, + Profile/ImagePicker.swift, Profile/ProfileDetailView.swift, Profile/ProfileView.swift, ); @@ -53,12 +131,17 @@ Home/HomeView.swift, Home/HomeViewModel.swift, Login/LoginView.swift, + Login/LoginViewModel.swift, Movie/CalendarModel.swift, Movie/CalendarViewModel.swift, Movie/MovieDetailView.swift, Movie/MovieReserveView.swift, Movie/MovieReserveViewModel.swift, Movie/MovieSearchSheetView.swift, + Order/OrderItemDetailView.swift, + Order/OrderItemView.swift, + Order/OrderItemViewModel.swift, + Profile/ImagePicker.swift, Profile/ProfileDetailView.swift, Profile/ProfileView.swift, ); @@ -70,12 +153,17 @@ Home/HomeView.swift, Home/HomeViewModel.swift, Login/LoginView.swift, + Login/LoginViewModel.swift, Movie/CalendarModel.swift, Movie/CalendarViewModel.swift, Movie/MovieDetailView.swift, Movie/MovieReserveView.swift, Movie/MovieReserveViewModel.swift, Movie/MovieSearchSheetView.swift, + Order/OrderItemDetailView.swift, + Order/OrderItemView.swift, + Order/OrderItemViewModel.swift, + Profile/ImagePicker.swift, Profile/ProfileDetailView.swift, Profile/ProfileView.swift, ); @@ -108,8 +196,12 @@ 4CA2D66A2EA91DE5009D27BE /* Exceptions for "Domain" folder in "MegaBox" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Models/MenuItemModel.swift, Models/MovieModel.swift, Models/UserModel.swift, + Services/KakaoAuthService.swift, + Services/KakaoConfig.swift, + Services/KeychainService.swift, Services/MovieService.swift, Services/UserSessionManager.swift, ); @@ -118,8 +210,12 @@ 4CA2D66B2EA91DE5009D27BE /* Exceptions for "Domain" folder in "MegaBoxTests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Models/MenuItemModel.swift, Models/MovieModel.swift, Models/UserModel.swift, + Services/KakaoAuthService.swift, + Services/KakaoConfig.swift, + Services/KeychainService.swift, Services/MovieService.swift, Services/UserSessionManager.swift, ); @@ -128,8 +224,12 @@ 4CA2D66C2EA91DE5009D27BE /* Exceptions for "Domain" folder in "MegaBoxUITests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Models/MenuItemModel.swift, Models/MovieModel.swift, Models/UserModel.swift, + Services/KakaoAuthService.swift, + Services/KakaoConfig.swift, + Services/KeychainService.swift, Services/MovieService.swift, Services/UserSessionManager.swift, ); @@ -138,6 +238,7 @@ 4CA2D6802EAF17C3009D27BE /* Exceptions for "Data" folder in "MegaBox" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + DTOs/MovieResponseDTO.swift, DTOs/MovieScheduleDTOs.swift, Mappers/MovieMapper.swift, Mappers/ScheduleMapper.swift, @@ -147,6 +248,7 @@ 4CA2D6812EAF17C3009D27BE /* Exceptions for "Data" folder in "MegaBoxTests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + DTOs/MovieResponseDTO.swift, DTOs/MovieScheduleDTOs.swift, Mappers/MovieMapper.swift, Mappers/ScheduleMapper.swift, @@ -156,6 +258,7 @@ 4CA2D6822EAF17C3009D27BE /* Exceptions for "Data" folder in "MegaBoxUITests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + DTOs/MovieResponseDTO.swift, DTOs/MovieScheduleDTOs.swift, Mappers/MovieMapper.swift, Mappers/ScheduleMapper.swift, @@ -214,6 +317,8 @@ 4CF19C232E880B8C0078679E /* Exceptions for "View" folder in "MegaBox" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Component/Modifiers.swift, + Component/ReusableComponents.swift, SplashView.swift, ); target = 4CF19AF32E8806EE0078679E /* MegaBox */; @@ -221,6 +326,8 @@ 4CF19C242E880B8C0078679E /* Exceptions for "View" folder in "MegaBoxTests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Component/Modifiers.swift, + Component/ReusableComponents.swift, SplashView.swift, ); target = 4CF19B002E8806F00078679E /* MegaBoxTests */; @@ -228,6 +335,8 @@ 4CF19C252E880B8C0078679E /* Exceptions for "View" folder in "MegaBoxUITests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Component/Modifiers.swift, + Component/ReusableComponents.swift, SplashView.swift, ); target = 4CF19B0A2E8806F00078679E /* MegaBoxUITests */; @@ -235,6 +344,16 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 4C0C30DE2ED310B20079566D /* Moya */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4C0C30DF2ED310C20079566D /* Exceptions for "Moya" folder in "MegaBox" target */, + 4C0C30E02ED310C20079566D /* Exceptions for "Moya" folder in "MegaBoxTests" target */, + 4C0C30E12ED310C20079566D /* Exceptions for "Moya" folder in "MegaBoxUITests" target */, + ); + path = Moya; + sourceTree = ""; + }; 4CA2D61D2EA91C85009D27BE /* Feature */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -314,6 +433,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4CACFB1D2EC2298500643999 /* KakaoSDKCommon in Frameworks */, + 4CACFB1B2EC2298500643999 /* KakaoSDKCertCore in Frameworks */, + 4C0C30C22EC9F3CF0079566D /* Moya in Frameworks */, + 4C0C30C92EC9FCF10079566D /* Kingfisher in Frameworks */, + 4CACFB152EC2298500643999 /* KakaoSDK in Frameworks */, + 4C0C30C02EC9F3CF0079566D /* CombineMoya in Frameworks */, + 4C0C2FFE2EC2300A0079566D /* Alamofire in Frameworks */, + 4CACFB172EC2298500643999 /* KakaoSDKAuth in Frameworks */, + 4CACFB192EC2298500643999 /* KakaoSDKCert in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -334,17 +462,27 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4CACFB002EC2286700643999 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 4CF19AEB2E8806EE0078679E = { isa = PBXGroup; children = ( + 4C0C30072EC9AFBC0079566D /* .gitignore */, 4CA2D6622EA91CFE009D27BE /* Application */, 4CA2D61D2EA91C85009D27BE /* Feature */, 4CA2D6672EA91DB2009D27BE /* Domain */, 4CA2D66D2EA91E04009D27BE /* Data */, + 4C0C30DE2ED310B20079566D /* Moya */, 4CF19BDC2E880B8C0078679E /* Resources */, - 4CF19BE12E880B8C0078679E /* Tests */, 4CF19BE42E880B8C0078679E /* View */, + 4CF19BE12E880B8C0078679E /* Tests */, 4CF19AF62E8806EE0078679E /* MegaBox */, + 4CACFB002EC2286700643999 /* Frameworks */, 4CF19AF52E8806EE0078679E /* Products */, ); sourceTree = ""; @@ -369,6 +507,7 @@ 4CF19AF02E8806EE0078679E /* Sources */, 4CF19AF12E8806EE0078679E /* Frameworks */, 4CF19AF22E8806EE0078679E /* Resources */, + 4CACFB062EC2286C00643999 /* Embed Frameworks */, ); buildRules = ( ); @@ -380,6 +519,15 @@ ); name = MegaBox; packageProductDependencies = ( + 4CACFB142EC2298500643999 /* KakaoSDK */, + 4CACFB162EC2298500643999 /* KakaoSDKAuth */, + 4CACFB182EC2298500643999 /* KakaoSDKCert */, + 4CACFB1A2EC2298500643999 /* KakaoSDKCertCore */, + 4CACFB1C2EC2298500643999 /* KakaoSDKCommon */, + 4C0C2FFD2EC2300A0079566D /* Alamofire */, + 4C0C30BF2EC9F3CF0079566D /* CombineMoya */, + 4C0C30C12EC9F3CF0079566D /* Moya */, + 4C0C30C82EC9FCF10079566D /* Kingfisher */, ); productName = MegaBox; productReference = 4CF19AF42E8806EE0078679E /* MegaBox.app */; @@ -392,6 +540,7 @@ 4CF19AFD2E8806F00078679E /* Sources */, 4CF19AFE2E8806F00078679E /* Frameworks */, 4CF19AFF2E8806F00078679E /* Resources */, + 4CACFB0C2EC228ED00643999 /* Embed Frameworks */, ); buildRules = ( ); @@ -412,6 +561,7 @@ 4CF19B072E8806F00078679E /* Sources */, 4CF19B082E8806F00078679E /* Frameworks */, 4CF19B092E8806F00078679E /* Resources */, + 4CACFB122EC228F500643999 /* Embed Frameworks */, ); buildRules = ( ); @@ -457,6 +607,12 @@ ); mainGroup = 4CF19AEB2E8806EE0078679E; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */, + 4C0C2FFC2EC2300A0079566D /* XCRemoteSwiftPackageReference "Alamofire" */, + 4C0C30BE2EC9F3CF0079566D /* XCRemoteSwiftPackageReference "Moya" */, + 4C0C30C72EC9FCF10079566D /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 4CF19AF52E8806EE0078679E /* Products */; projectDirPath = ""; @@ -474,6 +630,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C0C30082EC9AFBC0079566D /* .gitignore in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -481,6 +638,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C0C30092EC9AFBC0079566D /* .gitignore in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -488,6 +646,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C0C300A2EC9AFBC0079566D /* .gitignore in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -533,6 +692,8 @@ /* Begin XCBuildConfiguration section */ 4CF19B132E8806F00078679E /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 4CA2D6672EA91DB2009D27BE /* Domain */; + baseConfigurationReferenceRelativePath = Services/Secret.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -589,6 +750,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -596,6 +758,8 @@ }; 4CF19B142E8806F00078679E /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 4CA2D6672EA91DB2009D27BE /* Domain */; + baseConfigurationReferenceRelativePath = Services/Secret.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -645,6 +809,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; @@ -660,6 +825,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MegaBox/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -692,6 +858,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MegaBox/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -834,6 +1001,89 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4C0C2FFC2EC2300A0079566D /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.10.2; + }; + }; + 4C0C30BE2EC9F3CF0079566D /* XCRemoteSwiftPackageReference "Moya" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Moya/Moya"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 15.0.3; + }; + }; + 4C0C30C72EC9FCF10079566D /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.6.1; + }; + }; + 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kakao/kakao-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.25.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4C0C2FFD2EC2300A0079566D /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 4C0C2FFC2EC2300A0079566D /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; + 4C0C30BF2EC9F3CF0079566D /* CombineMoya */ = { + isa = XCSwiftPackageProductDependency; + package = 4C0C30BE2EC9F3CF0079566D /* XCRemoteSwiftPackageReference "Moya" */; + productName = CombineMoya; + }; + 4C0C30C12EC9F3CF0079566D /* Moya */ = { + isa = XCSwiftPackageProductDependency; + package = 4C0C30BE2EC9F3CF0079566D /* XCRemoteSwiftPackageReference "Moya" */; + productName = Moya; + }; + 4C0C30C82EC9FCF10079566D /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 4C0C30C72EC9FCF10079566D /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 4CACFB142EC2298500643999 /* KakaoSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDK; + }; + 4CACFB162EC2298500643999 /* KakaoSDKAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKAuth; + }; + 4CACFB182EC2298500643999 /* KakaoSDKCert */ = { + isa = XCSwiftPackageProductDependency; + package = 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKCert; + }; + 4CACFB1A2EC2298500643999 /* KakaoSDKCertCore */ = { + isa = XCSwiftPackageProductDependency; + package = 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKCertCore; + }; + 4CACFB1C2EC2298500643999 /* KakaoSDKCommon */ = { + isa = XCSwiftPackageProductDependency; + package = 4CACFB132EC2298500643999 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; + productName = KakaoSDKCommon; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 4CF19AEC2E8806EE0078679E /* Project object */; } diff --git a/hybin/MegaBox/Moya/MovieAPIService.swift b/hybin/MegaBox/Moya/MovieAPIService.swift new file mode 100644 index 0000000..f0a87b8 --- /dev/null +++ b/hybin/MegaBox/Moya/MovieAPIService.swift @@ -0,0 +1,74 @@ +// +// MovieAPIService.swift +// MegaBox +// +// Created by 전효빈 on 11/16/25. +// + +import Foundation +import Moya +import Alamofire + +enum TMBDConfig { + static var apiAccessToken : String { + guard let token = Bundle.main.infoDictionary?["TMDB_API_ACCESS_TOKEN"] as? String else { + fatalError("Info.plist에 TMBD_API_ACCESS_TOKEN이 설정되지 않았습니다") + } + return token + } +} + +enum MovieAPI { + case nowPlaying(language: String, page: Int, region: String) + +} + +final class MovieAPIService { + static let shared = MovieAPIService() + let provider : MoyaProvider + + private init() { + self.provider = MoyaProvider() + } +} + +extension MovieAPI: TargetType { + + var baseURL: URL { + return URL(string: "https://api.themoviedb.org")! + } + + var path: String { + switch self{ + case .nowPlaying: + return "/3/movie/now_playing" + } + } + + var method: Moya.Method { + switch self{ + case .nowPlaying: + return .get + } + } + + var task: Task{ + switch self { + case .nowPlaying(let language, let page, let region): + let params: [String: Any] = [ + "language" : language, + "page" : page, + "region" : region + ] + + return .requestParameters(parameters: params, encoding: URLEncoding.default) + } + } + + var headers: [String : String]? { + return [ + "Authorization": "Bearer \(TMBDConfig.apiAccessToken)", + "accpet" : "apllication/json" + ] + } +} diff --git a/hybin/MegaBox/Moya/MoyaExtension.swift b/hybin/MegaBox/Moya/MoyaExtension.swift new file mode 100644 index 0000000..10c2a04 --- /dev/null +++ b/hybin/MegaBox/Moya/MoyaExtension.swift @@ -0,0 +1,31 @@ +// +// MoyaExtension.swift +// MegaBox +// +// Created by 전효빈 on 11/16/25. +// + +import Foundation +import Moya + +extension MoyaProvider { + func asyncRequest(_ target: Target, responseType: T.Type) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + self.request(target) { result in + switch result { + case .success(let response): + do { + let decodedResponse = try response.map(T.self) + continuation.resume(returning: decodedResponse) + } catch { + // 디코딩 실패 + continuation.resume(throwing: error) + } + case .failure(let error): + // 네트워크 실패 + continuation.resume(throwing: error) + } + } + } + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/disney_poster.imageset/Contents.json b/hybin/MegaBox/Resources/Assets.xcassets/disney_poster.imageset/Contents.json new file mode 100644 index 0000000..023ff6b --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/disney_poster.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image 4.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/disney_poster.imageset/image 4.pdf b/hybin/MegaBox/Resources/Assets.xcassets/disney_poster.imageset/image 4.pdf new file mode 100644 index 0000000..d9a49f7 Binary files /dev/null and b/hybin/MegaBox/Resources/Assets.xcassets/disney_poster.imageset/image 4.pdf differ diff --git a/hybin/MegaBox/Resources/Assets.xcassets/double_combo.imageset/Contents.json b/hybin/MegaBox/Resources/Assets.xcassets/double_combo.imageset/Contents.json new file mode 100644 index 0000000..a9c689c --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/double_combo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image 1 (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/double_combo.imageset/image 1 (1).pdf b/hybin/MegaBox/Resources/Assets.xcassets/double_combo.imageset/image 1 (1).pdf new file mode 100644 index 0000000..bbedec8 Binary files /dev/null and b/hybin/MegaBox/Resources/Assets.xcassets/double_combo.imageset/image 1 (1).pdf differ diff --git a/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/Contents.json b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/Contents.json new file mode 100644 index 0000000..ce92377 --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gg_profile.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gg_profile 1.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gg_profile 2.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile 1.svg b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile 1.svg new file mode 100644 index 0000000..f09e443 --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile 1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile 2.svg b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile 2.svg new file mode 100644 index 0000000..f09e443 --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile 2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile.svg b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile.svg new file mode 100644 index 0000000..f09e443 --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/gg_profile.imageset/gg_profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/hybin/MegaBox/Resources/Assets.xcassets/insideout_figure.imageset/Contents.json b/hybin/MegaBox/Resources/Assets.xcassets/insideout_figure.imageset/Contents.json new file mode 100644 index 0000000..be343c5 --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/insideout_figure.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image 5.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/insideout_figure.imageset/image 5.pdf b/hybin/MegaBox/Resources/Assets.xcassets/insideout_figure.imageset/image 5.pdf new file mode 100644 index 0000000..49bc6d2 Binary files /dev/null and b/hybin/MegaBox/Resources/Assets.xcassets/insideout_figure.imageset/image 5.pdf differ diff --git a/hybin/MegaBox/Resources/Assets.xcassets/love_combo.imageset/Contents.json b/hybin/MegaBox/Resources/Assets.xcassets/love_combo.imageset/Contents.json new file mode 100644 index 0000000..e30fd0c --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/love_combo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image 2 (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/love_combo.imageset/image 2 (2).pdf b/hybin/MegaBox/Resources/Assets.xcassets/love_combo.imageset/image 2 (2).pdf new file mode 100644 index 0000000..b8073eb Binary files /dev/null and b/hybin/MegaBox/Resources/Assets.xcassets/love_combo.imageset/image 2 (2).pdf differ diff --git a/hybin/MegaBox/Resources/Assets.xcassets/single_combo.imageset/Contents.json b/hybin/MegaBox/Resources/Assets.xcassets/single_combo.imageset/Contents.json new file mode 100644 index 0000000..cfa3598 --- /dev/null +++ b/hybin/MegaBox/Resources/Assets.xcassets/single_combo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image 8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hybin/MegaBox/Resources/Assets.xcassets/single_combo.imageset/image 8.pdf b/hybin/MegaBox/Resources/Assets.xcassets/single_combo.imageset/image 8.pdf new file mode 100644 index 0000000..9caf0a9 Binary files /dev/null and b/hybin/MegaBox/Resources/Assets.xcassets/single_combo.imageset/image 8.pdf differ diff --git a/hybin/MegaBox/View/Component/Modifiers.swift b/hybin/MegaBox/View/Component/Modifiers.swift new file mode 100644 index 0000000..9435677 --- /dev/null +++ b/hybin/MegaBox/View/Component/Modifiers.swift @@ -0,0 +1,97 @@ +// +// Modifiers.swift +// MegaBox +// +// Created by 전효빈 on 11/23/25. +// + +import Foundation +import SwiftUI + +// --MARK: 극장 변경 바 스타일 수정자 +struct TheaterBarStyleModifier:ViewModifier{ + let newforegroundColor:Color + let newBackgroundColor: Color + + func body (content: Content) -> some View{ + content + .foregroundStyle(newforegroundColor) + .background(newBackgroundColor) + } +} + +// - MARK: 뱃지(추천)/ 오버레이 수정자 + +struct BestBadgeModifier: ViewModifier { + let isBest : Bool + + func body(content: Content) -> some View { + content + .overlay(alignment: .topLeading) { + if isBest{ + Text("Best") + .font(.pretend(type: .semiBold, size: 12)) + .foregroundStyle(Color.white) + .padding(.horizontal , 8) + .padding(.vertical , 4) + .background(Color.red) + .clipShape(Capsule()) + } + } + } +} + +struct RecommendBadgeModifier: ViewModifier { + let isRecommend : Bool + + func body(content: Content) -> some View { + content + .overlay(alignment: .topLeading) { + if isRecommend { + Text("Recommend") + .font(.pretend(type: .semiBold, size: 12)) + .foregroundStyle(Color.white) + .padding(.horizontal , 8) + .padding(.vertical , 4) + .background(Color.blue) + .clipShape(Capsule()) + } + } + } +} + +struct SoldOutOverlayModifier: ViewModifier { + let isSoldOut : Bool + + func body(content: Content) -> some View{ + content + .overlay { + if isSoldOut { + Color.black.opacity(0.6) + Text("품절") + .font(.pretend(type: .semiBold, size: 12)) + .foregroundStyle(Color.white) + } + } + } +} + + +extension View { + + func newTheaterBar(newForegroundColor: Color, newBackgroundColor: Color) -> some View { + self.modifier(TheaterBarStyleModifier(newforegroundColor: newForegroundColor, newBackgroundColor: newBackgroundColor)) + } + + func bestBadge(isBest: Bool) -> some View { + self.modifier(BestBadgeModifier(isBest: isBest)) + } + + func recommendBadge(isRecommend: Bool) -> some View { + self.modifier(RecommendBadgeModifier(isRecommend: isRecommend)) + } + + func soldOutOverlay(isSoldOut: Bool) -> some View { + self.modifier(SoldOutOverlayModifier(isSoldOut: isSoldOut)) + } +} diff --git a/hybin/MegaBox/View/Component/ReusableComponents.swift b/hybin/MegaBox/View/Component/ReusableComponents.swift new file mode 100644 index 0000000..e20770e --- /dev/null +++ b/hybin/MegaBox/View/Component/ReusableComponents.swift @@ -0,0 +1,80 @@ +// +// ReusableComponents.swift +// MegaBox +// +// Created by 전효빈 on 11/23/25. +// + +import Foundation +import SwiftUI + +// MARK: 극장변경 컴퍼넌트 +struct TheaterChangeBarView: View { + let selectedTheaterName :String + let action : () -> Void + + var body : some View { + HStack{ + HStack{ + Image(systemName: "pin") + .frame(width:27, height:27) + Text(selectedTheaterName) + .font(.pretend(type: .semiBold, size: 15)) + } + .padding(0) + .frame(width: 86) + + Spacer() + + HStack{ + Button(action: action) { + Text("극장변경") + .font(.pretend(type: .semiBold, size: 13)) } + } + .padding(0) + .frame(width: 65, height: 36, alignment: .center) + .cornerRadius(5) + .overlay( + RoundedRectangle(cornerRadius: 5) + .inset(by: 0.5) + .stroke(Color(red: 0.95, green: 0.95, blue: 0.95), lineWidth: 1) + ) + } + .padding(.vertical,10) + .padding(.horizontal,20) + } +} + +//MARK: 메뉴아이템 컴포넌트 +struct MenuItemView: View { + let item : MenuItemModel + + var body: some View { + VStack(alignment:.leading,spacing:12) { + Rectangle() + .foregroundStyle(Color.clear) + .frame(width:152, height: 152) + .background{ + Image(item.menuImageName) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width:152) + .clipped() + .soldOutOverlay(isSoldOut: item.itemIsSoldOut) + } + VStack{ + Text(item.menuTitle) + .font(.pretend(type: .regular, size: 14)) + .foregroundStyle(Color.black) + Text("\(item.menuPrice)원") + .font(.pretend(type: .semiBold, size: 14)) + .foregroundStyle(Color.black) + } + } + .padding(.horizontal, 3) + .padding(.vertical, 4) + .frame(width: 152 , alignment: .center) + } +} + + diff --git a/hybin/MegaBox/View/SplashView.swift b/hybin/MegaBox/View/SplashView.swift index d1b4b97..9f6092a 100644 --- a/hybin/MegaBox/View/SplashView.swift +++ b/hybin/MegaBox/View/SplashView.swift @@ -10,10 +10,27 @@ import SwiftUI struct SplashView : View { + + @State private var isActive: Bool = false + + @Environment(UserSessionManager.self) var userSessionManager + var body: some View { ZStack(alignment: .center){ Image(.meboxLogo) - }.foregroundStyle(Color.white) + } + .foregroundStyle(Color.white) + .onAppear { + Task { + _ = await userSessionManager.checkAutoLogin() + + self.isActive = true + } + } + .fullScreenCover(isPresented: $isActive) { + MainTabView() + .environment(userSessionManager) + } } }