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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions hybin/MegaBox/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Xcode ์„ค์ • ๋“ฑ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฌด์‹œํ•  ๊ฒƒ๋“ค
.DS_Store
.AppleDouble
UserInterfaceState.xcuserstate
xcuserdata/
*.xcscmblueprint
*.xccheckout
*.xcuserdatad

DerivedData/
Build/
.build/

Packages/
.swiftpm/

# Secret Key, Config
*.xcconfig
12 changes: 10 additions & 2 deletions hybin/MegaBox/Application/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down
13 changes: 12 additions & 1 deletion hybin/MegaBox/Application/MegaBoxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
}
}
}
23 changes: 23 additions & 0 deletions hybin/MegaBox/Data/DTOs/MovieResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -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?
}
48 changes: 22 additions & 26 deletions hybin/MegaBox/Data/Mappers/ScheduleMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
20 changes: 20 additions & 0 deletions hybin/MegaBox/Domain/Models/MenuItemModel.swift
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions hybin/MegaBox/Domain/Models/MovieModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions hybin/MegaBox/Domain/Models/UserModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
80 changes: 80 additions & 0 deletions hybin/MegaBox/Domain/Services/KakaoAuthService.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
20 changes: 20 additions & 0 deletions hybin/MegaBox/Domain/Services/KakaoConfig.swift
Original file line number Diff line number Diff line change
@@ -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 ?? ""
}
}
64 changes: 64 additions & 0 deletions hybin/MegaBox/Domain/Services/KeychainService.swift
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
Loading