diff --git a/.gitignore b/.gitignore index e43b0f9..638ebc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.DS_Store +*.DS_Store + +*.xcuserstate + +*.xcodeproj \ No newline at end of file diff --git a/Fix-MBTI.xcodeproj/project.pbxproj b/Fix-MBTI.xcodeproj/project.pbxproj index b6da43f..bce6c2b 100644 --- a/Fix-MBTI.xcodeproj/project.pbxproj +++ b/Fix-MBTI.xcodeproj/project.pbxproj @@ -10,9 +10,22 @@ 1E08C1EF2D5201820010F54C /* Fix-MBTI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Fix-MBTI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 1E08C2202D52FF2D0010F54C /* Exceptions for "Fix-MBTI" folder in "Fix-MBTI" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 1E08C1EE2D5201820010F54C /* Fix-MBTI */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 1E08C1F12D5201820010F54C /* Fix-MBTI */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 1E08C2202D52FF2D0010F54C /* Exceptions for "Fix-MBTI" folder in "Fix-MBTI" target */, + ); path = "Fix-MBTI"; sourceTree = ""; }; @@ -249,12 +262,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Fix-MBTI/Fix-MBTI.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Fix-MBTI/Preview Content\""; DEVELOPMENT_TEAM = 59FP2PXRXK; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Fix-MBTI/Info.plist"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -278,12 +293,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Fix-MBTI/Fix-MBTI.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Fix-MBTI/Preview Content\""; DEVELOPMENT_TEAM = 59FP2PXRXK; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Fix-MBTI/Info.plist"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/Bookmarks/bookmarks.plist b/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/Bookmarks/bookmarks.plist new file mode 100644 index 0000000..13ff621 --- /dev/null +++ b/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/Bookmarks/bookmarks.plist @@ -0,0 +1,28 @@ + + + + + top-level-items + + + destination + + rebasable-url + + base + workspace + payload + + relative-path + Fix-MBTI/Info.plist + + + type + DVTDocumentLocation + + type + bookmark + + + + diff --git a/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/UserInterfaceState.xcuserstate b/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/UserInterfaceState.xcuserstate index e1f0cd3..a9d5895 100644 Binary files a/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/UserInterfaceState.xcuserstate and b/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/kimjunsoo.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Fix-MBTI/Fix-MBTI.entitlements b/Fix-MBTI/Fix-MBTI.entitlements new file mode 100644 index 0000000..903def2 --- /dev/null +++ b/Fix-MBTI/Fix-MBTI.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/Fix-MBTI/Fix_MBTIApp.swift b/Fix-MBTI/Fix_MBTIApp.swift index 392e281..10d096c 100644 --- a/Fix-MBTI/Fix_MBTIApp.swift +++ b/Fix-MBTI/Fix_MBTIApp.swift @@ -11,20 +11,26 @@ import SwiftData @main struct Fix_MBTIApp: App { @AppStorage("isFirstLaunch") private var isFirstLaunch: Bool = true // 첫 실행인지 여부 - + var sharedModelContainer: ModelContainer = { let schema = Schema([ Mission.self, + MBTIProfile.self, + ActiveMission.self, ]) let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) - + do { return try ModelContainer(for: schema, configurations: [modelConfiguration]) } catch { fatalError("Could not create ModelContainer: \(error)") } }() - + + init() { + NotificationManager.instance.requestPermission() // 앱 실행 시 알림 권한 요청 + } + var body: some Scene { WindowGroup { if isFirstLaunch { diff --git a/Fix-MBTI/Info.plist b/Fix-MBTI/Info.plist new file mode 100644 index 0000000..3eb488e --- /dev/null +++ b/Fix-MBTI/Info.plist @@ -0,0 +1,12 @@ + + + + + NSUserNotificationUsageDescription + 앱에서 알림을 보내기 위해 권한이 필요합니다. + UIBackgroundModes + + remote-notification + + + diff --git a/Fix-MBTI/Model/Mission.swift b/Fix-MBTI/Model/Mission.swift index 01ee47a..b254e9e 100644 --- a/Fix-MBTI/Model/Mission.swift +++ b/Fix-MBTI/Model/Mission.swift @@ -11,10 +11,10 @@ import SwiftData @Model final class Mission { var title: String = "" // 미션 제목 - var detailText: String = "" // 미션 설명 + var detailText: String = "" // 게시물 텍스트 var timestamp: Date = Date() // 미션 생성 날짜 var randomTime: Date? = nil // 랜덤 타임 - var imageName: String? = "" // 이미지 추가 + var imageName: String? = "" // 게시물 이미지 추가 var category: String = "" init(title: String, detailText: String, timestamp: Date = Date(), randomTime: Date? = nil, imageName: String? = nil, category: String) { @@ -27,45 +27,127 @@ final class Mission { } } -// 더미 데이터 -var missions: [Mission] = [ +@Model +class ActiveMission { + var title: String + var detailText: String + var category: String + var timestamp: Date + + init(mission: Mission) { + self.title = mission.title + self.detailText = mission.detailText + self.category = mission.category + self.timestamp = Date() + } +} + +// 미션용 데이터 +let missions: [Mission] = [ // 🔹 I(내향) → E(외향) 미션 Mission(title: "새로운 사람에게 먼저 인사하기", detailText: "3명에게 먼저 대화를 시도하세요.", category: "E"), Mission(title: "모임에서 의견 말하기", detailText: "모임이나 회의에서 최소 1번은 의견을 말해보세요.", category: "E"), Mission(title: "전화 대신 직접 만나기", detailText: "중요한 대화를 전화 대신 직접 만나서 해보세요.", category: "E"), + Mission(title: "사람 많은 곳에서 활동하기", detailText: "카페나 공원에서 1시간 이상 사람들과 함께 시간을 보내보세요.", category: "E"), + Mission(title: "새로운 그룹 활동 참여하기", detailText: "새로운 동호회나 그룹 활동에 참여해보세요.", category: "E"), // 🔹 E(외향) → I(내향) 미션 Mission(title: "혼자만의 시간 보내기", detailText: "카페나 공원에서 혼자 조용히 시간을 보내보세요.", category: "I"), Mission(title: "하루 동안 SNS 금지", detailText: "SNS를 하루 동안 사용하지 않고 자기 자신에게 집중하세요.", category: "I"), Mission(title: "하루 동안 3명 이상과 연락하지 않기", detailText: "의식적으로 혼자만의 시간을 늘려보세요.", category: "I"), + Mission(title: "명상 10분 하기", detailText: "하루 10분간 조용한 공간에서 명상을 해보세요.", category: "I"), + Mission(title: "혼자 영화 감상하기", detailText: "혼자 영화를 보며 내면의 시간을 가져보세요.", category: "I"), // 🔹 S(감각) → N(직관) 미션 Mission(title: "미래의 나에게 편지 쓰기", detailText: "5년 후의 나에게 편지를 써보세요.", category: "N"), Mission(title: "창의적인 스토리 만들어보기", detailText: "즉흥적으로 짧은 이야기를 만들어보세요.", category: "N"), Mission(title: "평소에 관심 없던 철학 책 읽기", detailText: "철학 또는 자기계발 서적을 10분 이상 읽어보세요.", category: "N"), - + Mission(title: "기발한 아이디어 3개 적기", detailText: "창의적인 아이디어 3개를 떠올려서 적어보세요.", category: "N"), + Mission(title: "상상 속 여행 계획 세우기", detailText: "가보고 싶은 여행지를 설정하고 가상으로 여행 계획을 세워보세요.", category: "N"), + // 🔹 N(직관) → S(감각) 미션 Mission(title: "하루 동안 주변의 소리 기록하기", detailText: "하루 동안 들린 소리를 메모해보세요.", category: "S"), Mission(title: "눈앞에 보이는 사물 세부 묘사하기", detailText: "지금 보이는 사물을 3가지 이상 자세히 설명해보세요.", category: "S"), Mission(title: "지금까지 경험한 것 중 가장 현실적인 조언 적기", detailText: "논리적으로 타인에게 줄 수 있는 조언을 적어보세요.", category: "S"), - + Mission(title: "자신이 좋아하는 장소의 디테일한 특징 적기", detailText: "좋아하는 장소를 구체적으로 묘사해보세요.", category: "S"), + Mission(title: "하루 동안 경험한 일 세부적으로 기록하기", detailText: "오늘 하루 동안 있었던 일을 가능한 한 자세히 기록해보세요.", category: "S"), + // 🔹 T(논리) → F(감성) 미션 Mission(title: "친구에게 감정 표현 문자 보내기", detailText: "감사의 표현이 담긴 메시지를 친구에게 보내보세요.", category: "F"), Mission(title: "오늘 하루 감정 일기 쓰기", detailText: "하루 동안 느낀 감정을 일기에 기록하세요.", category: "F"), Mission(title: "타인의 고민 듣고 공감해보기", detailText: "누군가의 고민을 듣고 공감을 표현해보세요.", category: "F"), - + Mission(title: "좋아하는 노래 듣고 감정 표현하기", detailText: "감성적인 노래를 들으며 느낀 감정을 적어보세요.", category: "F"), + Mission(title: "하루 동안 긍정적인 말 3번 이상 하기", detailText: "하루 동안 주변 사람들에게 긍정적인 말을 세 번 이상 해보세요.", category: "F"), + // 🔹 F(감성) → T(논리) 미션 Mission(title: "데이터 기반으로 결정 내리기", detailText: "오늘 한 가지 결정을 데이터와 논리를 사용해 내려보세요.", category: "T"), Mission(title: "감정이 아니라 논리로 주장해보기", detailText: "대화를 할 때 감정보다 논리를 중심으로 말해보세요.", category: "T"), Mission(title: "객관적인 기사 읽고 요약하기", detailText: "뉴스나 과학 기사를 읽고 3줄로 요약해보세요.", category: "T"), - + Mission(title: "통계 자료 분석해보기", detailText: "흥미로운 통계를 찾아 분석해보고 느낀 점을 정리하세요.", category: "T"), + Mission(title: "논리적 주장을 하는 토론 참여하기", detailText: "논리적으로 자신의 의견을 설명해야 하는 토론을 참여해보세요.", category: "T"), + // 🔹 J(계획) → P(즉흥) 미션 Mission(title: "즉흥적인 약속 잡기", detailText: "계획 없이 친구에게 연락해서 만나보세요.", category: "P"), Mission(title: "하루 동안 미리 계획 없이 생활해보기", detailText: "일정을 정하지 않고 하루를 보내보세요.", category: "P"), Mission(title: "음식 주문할 때 랜덤 선택하기", detailText: "메뉴를 고민하지 않고 즉흥적으로 골라보세요.", category: "P"), - + Mission(title: "무작위 활동 선택해서 도전하기", detailText: "즉흥적으로 새로운 활동을 선택해서 실행해보세요.", category: "P"), + Mission(title: "예정 없이 길을 걸어보기", detailText: "목적 없이 길을 걸으며 새로운 길을 발견해보세요.", category: "P"), + // 🔹 P(즉흥) → J(계획) 미션 Mission(title: "내일 하루 계획 세우기", detailText: "내일 할 일을 아침에 미리 계획해보세요.", category: "J"), Mission(title: "한 주의 목표 설정하기", detailText: "일주일 동안의 목표를 구체적으로 정리해보세요.", category: "J"), - Mission(title: "정해진 시간에 할 일 완료하기", detailText: "하나의 일을 정한 시간 안에 마무리해보세요.", category: "J") + Mission(title: "정해진 시간에 할 일 완료하기", detailText: "하나의 일을 정한 시간 안에 마무리해보세요.", category: "J"), + Mission(title: "월간 계획 세우기", detailText: "이번 달의 목표와 계획을 구체적으로 세워보세요.", category: "J"), + Mission(title: "시간 관리 앱 활용해보기", detailText: "시간 관리 앱을 사용해 하루 일정을 계획하고 기록해보세요.", category: "J") +] + +@Model +class PostMission { + var title: String + var detailText: String + var content: String + var timestamp: Date + var imageName: String? + var category: String + + init(mission: Mission, content: String) { // 생성자도 content 파라미터 추가 + self.title = mission.title + self.detailText = mission.detailText + self.content = content // 입력된 내용 저장 + self.timestamp = Date() + self.imageName = mission.imageName + self.category = mission.category + } +} + +var dummyPosts: [Mission] = [ + Mission(title: "새로운 사람에게 먼저 인사하기", + detailText: "처음 보는 사람에게 먼저 인사하는 게 어색했지만, 생각보다 기분이 좋았어요!", + timestamp: Date(timeIntervalSinceNow: -86400), // 하루 전 + imageName: "smile_photo", + category: "E"), + + Mission(title: "즉흥적인 약속 잡기", + detailText: "친구한테 갑자기 연락해서 만나자고 했는데, 정말 재밌었어요!", + timestamp: Date(timeIntervalSinceNow: -43200), // 반나절 전 + imageName: "friend_meetup", + category: "E"), + + Mission(title: "한적한 곳에서 조용히 명상하기", + detailText: "아무 생각 없이 10분 정도 명상했는데, 마음이 정리되는 느낌이었어요.", + timestamp: Date(timeIntervalSinceNow: -72000), // 20시간 전 + imageName: nil, + category: "I"), + + Mission(title: "즉흥적으로 여행 계획 세우기", + detailText: "급하게 여행 계획을 세우고 떠나봤는데, 예상보다 즐거운 경험이었어요!", + timestamp: Date(timeIntervalSinceNow: -172800), // 이틀 전 + imageName: "travel_plan", + category: "P"), + + Mission(title: "계획 없이 친구랑 만남 가지기", + detailText: "약속 없이 친구를 만나러 가니 새로운 경험이 되었어요!", + timestamp: Date(timeIntervalSinceNow: -36000), // 10시간 전 + imageName: "random_meetup", + category: "P"), ] diff --git a/Fix-MBTI/Utils/NotificationManager.swift b/Fix-MBTI/Utils/NotificationManager.swift new file mode 100644 index 0000000..90539ae --- /dev/null +++ b/Fix-MBTI/Utils/NotificationManager.swift @@ -0,0 +1,122 @@ +// +// NotificationManager.swift +// Fix-MBTI +// 랜덤 알림 관리 파일 +// Created by KimJunsoo on 2/5/25. +// + +import Foundation +import UserNotifications +import SwiftData + +class NotificationManager: NSObject, UNUserNotificationCenterDelegate { + + static let instance = NotificationManager() + + private var storedProfiles: [MBTIProfile] = [] + private var storedMissions: [Mission] = [] + private var storedModelContext: ModelContext? + + // ADDED: 초기화 시 delegate 설정 + override init() { + super.init() + UNUserNotificationCenter.current().delegate = self + } + + // 1. 알림 권한 요청 + func requestPermission() { + let center = UNUserNotificationCenter.current() +// center.delegate = self + center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + if granted { + print("알림 권한 허용됨 ✅") + } else { + print("알림 권한 거부됨 ❌") + } + } + } + + // 🔥 🔥 기존 알림 삭제 (새로운 설정을 반영하기 위함) + func removeAllNotifications() { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + print("🗑️ 기존 알림 모두 삭제 완료") + } + + // 🔹 랜덤한 시간 후 미션 알림 예약 + func scheduleMissionNotification(profiles: [MBTIProfile], missions: [Mission], modelContext: ModelContext) { + + self.storedProfiles = profiles + self.storedMissions = missions + self.storedModelContext = modelContext + + removeAllNotifications() // 기존 알림 삭제 + + let missionCount = UserDefaults.standard.integer(forKey: "missionCount") + let actualCount = max(1, missionCount) // 최소 1개, 최대 설정값까지 + + var accumulatedDelay: Double = 0 // 이전 알림의 delay를 누적 + + for _ in 1...actualCount { + let content = UNMutableNotificationContent() + content.title = "새로운 MBTI 미션이 도착했습니다!" + content.body = "지금 앱을 열어 미션을 확인하세요." + content.sound = .default + + let randomDelay = Double.random(in: 10...30) +// let randomDelay = Double.random(in: 1080...18000) + accumulatedDelay += randomDelay + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: accumulatedDelay, repeats: false) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request) + + print("📢 랜덤 미션 알림 예약 완료: \(randomDelay)초 후 도착 예정") + + } + + checkPendingNotifications() + } + + // 3. 앱이 실행 중일 때 알림을 받을 수 있도록 설정 + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([.banner, .sound, .badge]) + } + + // 예약된 알림 확인 + func checkPendingNotifications() { + UNUserNotificationCenter.current().getPendingNotificationRequests { requests in + print("📌 현재 예약된 알림 개수: \(requests.count)") + for request in requests { + print("📌 예약된 알림: \(request.identifier), 트리거: \(request.trigger.debugDescription)") + } + } + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + guard let profile = storedProfiles.first, + let modelContext = storedModelContext else { + completionHandler() + return + } + + let targetCategories = [profile.currentMBTI.last?.description, profile.targetMBTI.last?.description].compactMap { $0 } + let availableMissions = storedMissions.filter { targetCategories.contains(String($0.category)) } + + if let randomMission = availableMissions.randomElement() { + let newActiveMission = ActiveMission(mission: randomMission) + modelContext.insert(newActiveMission) + print("🎯 알림 수신으로 미션 추가됨: \(randomMission.title)") + } + + completionHandler() + } + +} diff --git a/Fix-MBTI/View/ContentView.swift b/Fix-MBTI/View/ContentView.swift index 502cd0e..c45c4fa 100644 --- a/Fix-MBTI/View/ContentView.swift +++ b/Fix-MBTI/View/ContentView.swift @@ -9,28 +9,48 @@ import SwiftUI import SwiftData struct ContentView: View { + @Query private var profiles: [MBTIProfile] + @Query private var missions: [Mission] @Environment(\.modelContext) private var modelContext - @Query private var items: [Item] - + @Query private var items: [Mission] + + @State private var selectedTab = 0 // 현재 선택된 탭을 추적하는 변수 + var body: some View { - TabView { + TabView(selection: $selectedTab) { MissionView() .tabItem { - Image(systemName: "house") +// Image(selectedTab == 0 ? "HomeOn" : "HomeOff") + Text("Home") } + .tag(0) + ListView() .tabItem { - Image(systemName: "archivebox") +// Image(selectedTab == 1 ? "ListOn" : "ListOff") + Text("List") } + .tag(1) + SettingView() .tabItem { - Image(systemName: "gear") +// Image(selectedTab == 2 ? "SettingOn" : "SettingOff") + Text("Setting") } + .tag(2) + } + .onAppear { + // 알림 예약 호출 + NotificationManager.instance.scheduleMissionNotification( + profiles: profiles, + missions: missions, + modelContext: modelContext + ) } } } #Preview { ContentView() - .modelContainer(for: Item.self, inMemory: true) + .modelContainer(for: Mission.self, inMemory: true) } diff --git a/Fix-MBTI/View/ListDetailView.swift b/Fix-MBTI/View/ListDetailView.swift index 60d8c43..c13019f 100644 --- a/Fix-MBTI/View/ListDetailView.swift +++ b/Fix-MBTI/View/ListDetailView.swift @@ -6,13 +6,50 @@ // import SwiftUI +import SwiftData struct ListDetailView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + var mission: Mission + var body: some View { - Text("게시물 디테일 뷰") + ScrollView { + VStack(alignment: .leading, spacing: 15) { + Image(mission.imageName ?? "") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: 300) + .clipShape(RoundedRectangle(cornerRadius: 15)) + + Text(mission.title) + .font(.title) + .fontWeight(.bold) + .padding(.horizontal) + + Text("\(mission.timestamp)") + .font(.subheadline) + .foregroundColor(.gray) + .padding(.horizontal) + + Divider() + .padding(.horizontal) + + Text(mission.detailText) + .font(.body) + .padding(.horizontal) + + Spacer() + } + .padding(.vertical) + } + .navigationTitle("게시물 상세") + .navigationBarTitleDisplayMode(.inline) } } #Preview { - ListDetailView() + ListDetailView(mission: Mission(title: "ㅇㅇㅇ", detailText: "ㅇㅇㅇ", imageName: "ListOn", category: "E") + ) } diff --git a/Fix-MBTI/View/ListView.swift b/Fix-MBTI/View/ListView.swift index 25d5bf4..fe113c2 100644 --- a/Fix-MBTI/View/ListView.swift +++ b/Fix-MBTI/View/ListView.swift @@ -6,10 +6,63 @@ // import SwiftUI +import SwiftData + +struct Post: Identifiable { + let id = UUID() + let title: String + let thumbnail: String + let description: String +} struct ListView: View { + @Environment(\.modelContext) private var modelContext + @Query private var missions: [Mission] + + let posts: [Post] = [ + Post(title: "감동적인 영화 한편 보는거 어때", thumbnail: "sample1", description: ""), + Post(title: "오늘은 친구 없이 혼자 놀아봐", thumbnail: "sample2", description: ""), + Post(title: "계획없이 여행을 떠나보자", thumbnail: "sample3", description: "") + ] + var body: some View { - Text("게시물뷰") + NavigationStack { + List(posts) { post in + HStack { + Image(post.thumbnail) + .resizable() + .scaledToFill() + .frame(width: 85, height: 85) + .background(Color.orange) + .clipShape(RoundedRectangle(cornerRadius: 5)) + + Spacer() + VStack(alignment: .leading, spacing: 5) { + Spacer() + + Text(post.title) + .font(.headline) + .foregroundColor(.primary) + Spacer() + Text("게시 날짜: 2024-02-05") + .font(.subheadline) + .foregroundColor(.gray) + Spacer() + + } + + Spacer() + } + .padding(.vertical, 5) + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("게시물") + .font(.headline) + } + } + } } } diff --git a/Fix-MBTI/View/MBTISelectionView.swift b/Fix-MBTI/View/MBTISelectionView.swift index 3df7b4a..0a2f24d 100644 --- a/Fix-MBTI/View/MBTISelectionView.swift +++ b/Fix-MBTI/View/MBTISelectionView.swift @@ -9,7 +9,10 @@ import SwiftUI import SwiftData struct MBTISelectionView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss @AppStorage("isFirstLaunch") private var isFirstLaunch: Bool = true + @Query private var profiles: [MBTIProfile] @State private var currentMBTI = ["E", "N", "T", "P"] @State private var targetMBTI = ["E", "N", "T", "P"] @@ -21,26 +24,33 @@ struct MBTISelectionView: View { ["P", "J"] // 인식형 vs 판단형 ] + var isCompleteButtonDisabled: Bool { + currentMBTI == targetMBTI + } + var body: some View { NavigationView { - VStack { - + VStack(spacing: 20) { + + MBTIPicker(selection: $currentMBTI, options: mbtiOptions) Image(systemName: "arrowshape.down.fill") .resizable() - .frame(width: 30, height: 30) + .frame(width: 28, height: 30) + .foregroundColor(Color("FA812F")) MBTIPicker(selection: $targetMBTI, options: mbtiOptions) Button("완료") { saveMBTI() isFirstLaunch = false + dismiss() } - .padding() - .buttonStyle(.borderedProminent) - .disabled(currentMBTI == targetMBTI) // 현재, 목표 mbti같을때 완료버튼 비활성화 + .foregroundStyle(isCompleteButtonDisabled ? .gray : Color("FA812F")) + .disabled(isCompleteButtonDisabled) + .opacity(isCompleteButtonDisabled ? 0.5 : 1.0) } .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -49,22 +59,35 @@ struct MBTISelectionView: View { .font(.headline) } } + .onAppear { + loadMBTI() + } } } private func saveMBTI() { - let current = currentMBTI.joined() // "ENTP" 형식으로 변환 - let target = targetMBTI.joined() + do { + let existingProfiles = try modelContext.fetch(FetchDescriptor()) + for profile in existingProfiles { + modelContext.delete(profile) + } + } catch { + print("❌ 기존 MBTI 데이터 삭제 실패: \(error)") + } - let profile = MBTIProfile(currentMBTI: current, targetMBTI: target) + let profile = MBTIProfile(currentMBTI: currentMBTI.joined(), + targetMBTI: targetMBTI.joined()) + modelContext.insert(profile) - let modelContext = try? ModelContainer(for: MBTIProfile.self).mainContext - modelContext?.insert(profile) + print("✅ MBTI 저장 완료: 현재 MBTI \(profile.currentMBTI), 목표 MBTI \(profile.targetMBTI)") + } + + private func loadMBTI() { + if let savedProfile = profiles.first { + currentMBTI = Array(savedProfile.currentMBTI).map { String($0) } + targetMBTI = Array(savedProfile.targetMBTI).map { String($0) } + } } -} - -#Preview { - MBTISelectionView() } struct MBTIPicker: View { @@ -73,19 +96,134 @@ struct MBTIPicker: View { var body: some View { HStack { - ForEach(0..<4, id: \.self) { index in + ForEach(0..<4, id: \ .self) { index in Picker("", selection: $selection[index]) { - ForEach(options[index], id: \.self) { option in + ForEach(options[index], id: \ .self) { option in Text(option) - .font(.title) + .font(.system(size: 32)) + .fontWeight(.medium) .frame(maxWidth: .infinity) + .foregroundStyle(selection[index] == option ? Color("FA812F") : .gray) } } .pickerStyle(.wheel) - .frame(width: 60, height: 150) + .frame(width: 60, height: 170) .clipped() } } .padding() } } + +#Preview { + MBTISelectionView() +} +/* + import SwiftUI + import SwiftData + + struct MBTISelectionView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + @AppStorage("isFirstLaunch") private var isFirstLaunch: Bool = true + @Query private var profiles: [MBTIProfile] + + // 전체 MBTI 타입 배열 + let mbtiTypes = [ + "ISTJ", "ISFJ", "INFJ", "INTJ", + "ISTP", "ISFP", "INFP", "INTP", + "ESTP", "ESFP", "ENFP", "ENTP", + "ESTJ", "ESFJ", "ENFJ", "ENTJ" + ] + + @State private var selectedCurrentMBTI: String = "" + @State private var selectedTargetMBTI: String = "" + + // 완료 버튼 활성화 조건을 계산하는 프로퍼티 + private var isCompleteButtonDisabled: Bool { + selectedCurrentMBTI.isEmpty || + selectedTargetMBTI.isEmpty || + selectedCurrentMBTI == selectedTargetMBTI + } + + var body: some View { + NavigationView { + VStack(spacing: 20) { + Text("현재 MBTI 선택") + .font(.headline) + + Picker("현재 MBTI", selection: $selectedCurrentMBTI) { + ForEach(mbtiTypes, id: \.self) { mbti in + Text(mbti).tag(mbti) + } + } + .pickerStyle(.wheel) + + Image(systemName: "arrowshape.down.fill") + .resizable() + .frame(width: 30, height: 30) + + Text("목표 MBTI 선택") + .font(.headline) + + Picker("목표 MBTI", selection: $selectedTargetMBTI) { + ForEach(mbtiTypes, id: \.self) { mbti in + Text(mbti).tag(mbti) + } + } + .pickerStyle(.wheel) + + Button("완료") { + saveMBTI() + isFirstLaunch = false + dismiss() + } + .buttonStyle(.borderedProminent) + .disabled(isCompleteButtonDisabled) + .padding() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("MBTI 설정") + .font(.headline) + } + } + .onAppear { + loadMBTI() + } + } + } + + private func saveMBTI() { + // 기존 데이터 삭제 + do { + let existingProfiles = try modelContext.fetch(FetchDescriptor()) + for profile in existingProfiles { + modelContext.delete(profile) + } + } catch { + print("❌ 기존 MBTI 데이터 삭제 실패: \(error)") + } + + // 새 프로필 저장 + let profile = MBTIProfile(currentMBTI: selectedCurrentMBTI, + targetMBTI: selectedTargetMBTI) + modelContext.insert(profile) + + print("✅ MBTI 저장 완료: 현재 MBTI \(selectedCurrentMBTI), 목표 MBTI \(selectedTargetMBTI)") + } + + private func loadMBTI() { + if let savedProfile = profiles.first { + selectedCurrentMBTI = savedProfile.currentMBTI + selectedTargetMBTI = savedProfile.targetMBTI + } + } + } + + + #Preview { + MBTISelectionView() + } + */ diff --git a/Fix-MBTI/View/MissionDetailView.swift b/Fix-MBTI/View/MissionDetailView.swift index cad467b..a57a3af 100644 --- a/Fix-MBTI/View/MissionDetailView.swift +++ b/Fix-MBTI/View/MissionDetailView.swift @@ -5,14 +5,123 @@ // Created by KimJunsoo on 2/4/25. // +import Foundation import SwiftUI +import SwiftData struct MissionDetailView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @State private var selectedImage: UIImage? = nil + @State private var isImagePickerPresented = false + @State private var inputText: String = "" + + private let mission: Mission // let으로 변경 + + init(mission: Mission) { // 명시적 생성자 추가 + self.mission = mission + } + var body: some View { - Text("미션뷰에서 미션 성공 후 후기작성(?) 페이지") + VStack { + Button(action: { isImagePickerPresented.toggle() }) { + if let image = selectedImage { + Image(uiImage: image) + .resizable() + .scaledToFit() + .frame(height: 250) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } else { + VStack { + Image(systemName: "photo.on.rectangle.angled") + .font(.system(size: 50)) + .foregroundColor(Color("FA812F")) + } + .frame(width: 350, height: 350) + .background(Color("F8F8F8")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + .padding() + .sheet(isPresented: $isImagePickerPresented) { + // ImagePicker(image: $selectedImage) + } + HStack { + Text(mission.title) + .font(.title2) + .fontWeight(.bold) + .lineLimit(2) + .padding(.leading) + + Spacer() + + Text(mission.category) + .font(.title) + .fontWeight(.bold) + .foregroundStyle(Color("FA812F")) + .padding(.trailing) + + } + + HStack { + Text(mission.detailText) + .font(.caption) + .lineLimit(1) + .padding(.leading) + Spacer() + } + + TextEditor(text: $inputText) + .font(.system(size: 18)) + .overlay(alignment: .topLeading) { + Text("문구 입력..") + .font(.system(size: 18)) + .foregroundStyle(inputText.isEmpty ? .gray : .clear) + .padding(.top, 8) + .padding(.horizontal, 5) + } + .frame(height: 200) + .background(Color(.systemGray6)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.horizontal) + + + Spacer() + + Text("완료") + .padding() + .frame(maxWidth: .infinity) + .background(inputText.isEmpty || selectedImage == nil ? Color.gray : Color("FA812F")) + .foregroundColor(.white) + .cornerRadius(10) + .padding() + .offset(y: -20) + .disabled(inputText.isEmpty) + .onTapGesture { + savePost() + } + } + .padding() + .navigationTitle("게시물 작성") + .navigationBarTitleDisplayMode(.inline) + } + + private func savePost() { + let postMission = PostMission(mission: mission, content: inputText) + modelContext.insert(postMission) + modelContext.delete(mission) + + do { + print("게시물 저장: \(inputText)") + try modelContext.save() + dismiss() + } catch { + print("저장 중 오류 발생: \(error)") + } } } #Preview { - MissionDetailView() + MissionDetailView(mission: Mission(title: "오늘하루 계획 짜봐", detailText: "sdsdsdsdsdsd", category: "E")) } diff --git a/Fix-MBTI/View/MissionView.swift b/Fix-MBTI/View/MissionView.swift index 6c1597f..e54ac64 100644 --- a/Fix-MBTI/View/MissionView.swift +++ b/Fix-MBTI/View/MissionView.swift @@ -6,13 +6,123 @@ // import SwiftUI +import SwiftData struct MissionView: View { + @Environment(\.modelContext) private var modelContext + // @Query private var missions: [Mission] + @Query private var profiles: [MBTIProfile] + @Query(sort: \ActiveMission.timestamp) private var activeMissions: [ActiveMission] + + @State private var showAlert = false + + // ADDED: NotificationDelegate 인스턴스 생성 + private let notificationDelegate = NotificationDelegate() + var body: some View { - Text("메인뷰") + NavigationStack { + List { + ForEach(activeMissions) { activeMission in + NavigationLink(destination: MissionDetailView(mission: Mission(title: activeMission.title, + detailText: activeMission.detailText, + category: activeMission.category))) { + HStack { + Text("\(activeMission.title), \(activeMission.category)체험") + } + } + } + .onDelete(perform: deleteMission) + } + .onAppear { + print("🔍 현재 MBTI: \(profiles.first?.currentMBTI ?? "default")") + print("🔍 목표 MBTI: \(profiles.first?.targetMBTI ?? "default")") + + // Delegate 설정 및 콜백 등록 + notificationDelegate.addMissionCallback = addMission + UNUserNotificationCenter.current().delegate = notificationDelegate + } + .navigationTitle("나의 미션") + .toolbar { + Button(action: addMission) { + Label("미션 추가", systemImage: "plus") + } + + Button(action: sendTestNotification) { + Label("알림 테스트", systemImage: "bell.fill") + } + } + } + } + + private func addMission() { + guard let profile = profiles.first else { return } + + let currentArray = Array(profile.currentMBTI) + let targetArray = Array(profile.targetMBTI) + var differentCategories: [String] = [] + + for i in 0..<4 { + if currentArray[i] != targetArray[i] { + differentCategories.append(String(targetArray[i])) + } + } + + print("🎯 변화해야 할 카테고리들: \(differentCategories)") + + let availableMissions = missions.filter { mission in + differentCategories.contains(mission.category) + } + + if let randomMission = availableMissions.randomElement() { + // 중복 체크 + if !activeMissions.contains(where: { $0.title == randomMission.title }) { + let newActiveMission = ActiveMission(mission: randomMission) + modelContext.insert(newActiveMission) + print("📝 새 미션 추가됨: \(randomMission.title) (카테고리: \(randomMission.category))") + } + } + } + + func deleteMission(offsets: IndexSet) { + for index in offsets { + modelContext.delete(activeMissions[index]) + } + } + + // 테스트용 알림 즉시 보내기 + private func sendTestNotification() { + let content = UNMutableNotificationContent() + content.title = "테스트 알림" + content.body = "이것은 즉시 발송된 테스트 알림입니다." + content.sound = .default + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // 5초 후 실행 + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + + UNUserNotificationCenter.current().add(request) + print("📢 테스트 알림 예약 완료 (5초 후 도착)") + } + +} + +class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { + var addMissionCallback: (() -> Void)? + + // 알림이 도착했을 때 호출되는 함수 + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + // 알림이 도착하면 바로 미션 추가 + addMissionCallback?() + + // 알림도 보여주기 + completionHandler([.banner, .sound, .badge]) } } #Preview { MissionView() } + diff --git a/Fix-MBTI/View/SettingView.swift b/Fix-MBTI/View/SettingView.swift index 2de0e74..aecce21 100644 --- a/Fix-MBTI/View/SettingView.swift +++ b/Fix-MBTI/View/SettingView.swift @@ -6,22 +6,81 @@ // import SwiftUI +import SwiftData struct SettingView: View { + @Environment(\.modelContext) private var modelContext + @Query private var missions: [Mission] + @Query private var profiles: [MBTIProfile] + @State private var isShowingMBTISelection = false - + @State private var isNotificationEnabled = true + + @AppStorage("missionCount") private var missionCount: Int = 1 // 기본값 1개 + + var body: some View { NavigationStack { List { + Section(header: Text("나의 MBTI")) { + Text(profiles.first?.currentMBTI ?? "미설정") + } + + Section(header: Text("체험 MBTI")) { + Text(profiles.first?.targetMBTI ?? "미설정") + } + Button("MBTI 변경") { isShowingMBTISelection = true } .foregroundColor(.primary) - + Button("MBTI 검사하러 가기") { openMBTITest() } - .foregroundColor(.blue) + .foregroundColor(.primary) + + HStack { + Button("알림 설정") { + isNotificationEnabled.toggle() + if isNotificationEnabled { + NotificationManager.instance.scheduleMissionNotification( + profiles: profiles, + missions: missions, + modelContext: modelContext + ) + } else { + NotificationManager.instance.removeAllNotifications() + } + } + .foregroundColor(.primary) + + Spacer() + Toggle("", isOn: $isNotificationEnabled) + .labelsHidden() + } + + Section(header: Text("미션 개수 설정")) { + Picker("미션 개수", selection: $missionCount) { + ForEach(1...5, id: \.self) { count in + Text("\(count)개").tag(count) + } + } + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: missionCount) { oldValue, newValue in + NotificationManager.instance.scheduleMissionNotification( + profiles: profiles, + missions: missions, + modelContext: modelContext + ) + print("🔄 미션 개수 변경됨: \(oldValue) 에서 \(newValue)") + + } + } + + } + .onAppear { + print("🔍 profiles (설정 화면): \(profiles.first?.currentMBTI ?? "default")") } .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -35,7 +94,7 @@ struct SettingView: View { } } } - + private func openMBTITest() { if let url = URL(string: "https://www.16personalities.com/ko") { UIApplication.shared.open(url)