diff --git a/Fix-MBTI.xcodeproj/project.pbxproj b/Fix-MBTI.xcodeproj/project.pbxproj index 9066e2c..f0e2207 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 */ + FD47524A2D5452E400549F90 /* 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 = ( + FD47524A2D5452E400549F90 /* Exceptions for "Fix-MBTI" folder in "Fix-MBTI" target */, + ); path = "Fix-MBTI"; sourceTree = ""; }; diff --git a/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/zsuant.xcuserdatad/UserInterfaceState.xcuserstate b/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/zsuant.xcuserdatad/UserInterfaceState.xcuserstate index 467dbf3..956c20f 100644 Binary files a/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/zsuant.xcuserdatad/UserInterfaceState.xcuserstate and b/Fix-MBTI.xcodeproj/project.xcworkspace/xcuserdata/zsuant.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Contents.json b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..e278d81 100644 --- a/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "Page 1@1x.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "Page 1@1x 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -23,6 +25,7 @@ "value" : "tinted" } ], + "filename" : "Page 1@1x 2.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x 1.png b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x 1.png new file mode 100644 index 0000000..4766414 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x 1.png differ diff --git a/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x 2.png b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x 2.png new file mode 100644 index 0000000..4766414 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x 2.png differ diff --git a/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x.png b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x.png new file mode 100644 index 0000000..4766414 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/AppIcon.appiconset/Page 1@1x.png differ diff --git a/Fix-MBTI/Assets.xcassets/HomeOff.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/HomeOff.imageset/Contents.json index 7f2c0a7..34ba340 100644 --- a/Fix-MBTI/Assets.xcassets/HomeOff.imageset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/HomeOff.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "home.png", + "filename" : "icons8-home-48.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Fix-MBTI/Assets.xcassets/HomeOff.imageset/home.png b/Fix-MBTI/Assets.xcassets/HomeOff.imageset/home.png deleted file mode 100644 index 8a4704a..0000000 Binary files a/Fix-MBTI/Assets.xcassets/HomeOff.imageset/home.png and /dev/null differ diff --git a/Fix-MBTI/Assets.xcassets/HomeOff.imageset/icons8-home-48.png b/Fix-MBTI/Assets.xcassets/HomeOff.imageset/icons8-home-48.png new file mode 100644 index 0000000..d0ccd9b Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/HomeOff.imageset/icons8-home-48.png differ diff --git a/Fix-MBTI/Assets.xcassets/HomeOn.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/HomeOn.imageset/Contents.json index 6c94e2a..ffe2bf6 100644 --- a/Fix-MBTI/Assets.xcassets/HomeOn.imageset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/HomeOn.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "home-screen.png", + "filename" : "icons8-hone.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Fix-MBTI/Assets.xcassets/HomeOn.imageset/home-screen.png b/Fix-MBTI/Assets.xcassets/HomeOn.imageset/home-screen.png deleted file mode 100644 index 85ce33e..0000000 Binary files a/Fix-MBTI/Assets.xcassets/HomeOn.imageset/home-screen.png and /dev/null differ diff --git a/Fix-MBTI/Assets.xcassets/HomeOn.imageset/icons8-hone.png b/Fix-MBTI/Assets.xcassets/HomeOn.imageset/icons8-hone.png new file mode 100644 index 0000000..9e8d82b Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/HomeOn.imageset/icons8-hone.png differ diff --git a/Fix-MBTI/Assets.xcassets/ImagePickerBackgroundColor.colorset/Contents.json b/Fix-MBTI/Assets.xcassets/ImagePickerBackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..bdd8284 --- /dev/null +++ b/Fix-MBTI/Assets.xcassets/ImagePickerBackgroundColor.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xF0", + "green" : "0xF0", + "red" : "0xF0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xF0", + "green" : "0xF0", + "red" : "0xF0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x1A", + "green" : "0x1A", + "red" : "0x1A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Fix-MBTI/Assets.xcassets/ListOff.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/ListOff.imageset/Contents.json index 7046d40..ca69337 100644 --- a/Fix-MBTI/Assets.xcassets/ListOff.imageset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/ListOff.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "container (1).png", + "filename" : "icons8-48.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Fix-MBTI/Assets.xcassets/ListOff.imageset/container (1).png b/Fix-MBTI/Assets.xcassets/ListOff.imageset/container (1).png deleted file mode 100644 index e45834c..0000000 Binary files a/Fix-MBTI/Assets.xcassets/ListOff.imageset/container (1).png and /dev/null differ diff --git a/Fix-MBTI/Assets.xcassets/ListOff.imageset/icons8-48.png b/Fix-MBTI/Assets.xcassets/ListOff.imageset/icons8-48.png new file mode 100644 index 0000000..e3f1589 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/ListOff.imageset/icons8-48.png differ diff --git a/Fix-MBTI/Assets.xcassets/ListOn.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/ListOn.imageset/Contents.json index 7bdd234..93c3d47 100644 --- a/Fix-MBTI/Assets.xcassets/ListOn.imageset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/ListOn.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "cube.png", + "filename" : "icons8-portraits-48.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Fix-MBTI/Assets.xcassets/ListOn.imageset/cube.png b/Fix-MBTI/Assets.xcassets/ListOn.imageset/cube.png deleted file mode 100644 index df5e61a..0000000 Binary files a/Fix-MBTI/Assets.xcassets/ListOn.imageset/cube.png and /dev/null differ diff --git a/Fix-MBTI/Assets.xcassets/ListOn.imageset/icons8-portraits-48.png b/Fix-MBTI/Assets.xcassets/ListOn.imageset/icons8-portraits-48.png new file mode 100644 index 0000000..4e65f24 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/ListOn.imageset/icons8-portraits-48.png differ diff --git a/Fix-MBTI/Assets.xcassets/NotificationIcon.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/NotificationIcon.imageset/Contents.json new file mode 100644 index 0000000..d2fc166 --- /dev/null +++ b/Fix-MBTI/Assets.xcassets/NotificationIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Page 1@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Fix-MBTI/Assets.xcassets/NotificationIcon.imageset/Page 1@1x.png b/Fix-MBTI/Assets.xcassets/NotificationIcon.imageset/Page 1@1x.png new file mode 100644 index 0000000..4766414 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/NotificationIcon.imageset/Page 1@1x.png differ diff --git a/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Contents.json index 17f6fd9..b2439f7 100644 --- a/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Image.png", + "filename" : "icons-48.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Image.png b/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Image.png deleted file mode 100644 index 1c1e3f7..0000000 Binary files a/Fix-MBTI/Assets.xcassets/SettingOff.imageset/Image.png and /dev/null differ diff --git a/Fix-MBTI/Assets.xcassets/SettingOff.imageset/icons-48.png b/Fix-MBTI/Assets.xcassets/SettingOff.imageset/icons-48.png new file mode 100644 index 0000000..72565b3 Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/SettingOff.imageset/icons-48.png differ diff --git a/Fix-MBTI/Assets.xcassets/SettingOn.imageset/Contents.json b/Fix-MBTI/Assets.xcassets/SettingOn.imageset/Contents.json index 02d0050..2d047b2 100644 --- a/Fix-MBTI/Assets.xcassets/SettingOn.imageset/Contents.json +++ b/Fix-MBTI/Assets.xcassets/SettingOn.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "settings-gear-icon.png", + "filename" : "icons8-settings-48.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Fix-MBTI/Assets.xcassets/SettingOn.imageset/icons8-settings-48.png b/Fix-MBTI/Assets.xcassets/SettingOn.imageset/icons8-settings-48.png new file mode 100644 index 0000000..2d810cc Binary files /dev/null and b/Fix-MBTI/Assets.xcassets/SettingOn.imageset/icons8-settings-48.png differ diff --git a/Fix-MBTI/Assets.xcassets/SettingOn.imageset/settings-gear-icon.png b/Fix-MBTI/Assets.xcassets/SettingOn.imageset/settings-gear-icon.png deleted file mode 100644 index 1e51dcd..0000000 Binary files a/Fix-MBTI/Assets.xcassets/SettingOn.imageset/settings-gear-icon.png and /dev/null differ diff --git a/Fix-MBTI/Assets.xcassets/SettingTextColor.colorset/Contents.json b/Fix-MBTI/Assets.xcassets/SettingTextColor.colorset/Contents.json new file mode 100644 index 0000000..a199847 --- /dev/null +++ b/Fix-MBTI/Assets.xcassets/SettingTextColor.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x44", + "green" : "0x44", + "red" : "0x44" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xBB", + "green" : "0xBB", + "red" : "0xBB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Fix-MBTI/Assets.xcassets/ThemeColor.colorset/Contents.json b/Fix-MBTI/Assets.xcassets/ThemeColor.colorset/Contents.json new file mode 100644 index 0000000..49fcc81 --- /dev/null +++ b/Fix-MBTI/Assets.xcassets/ThemeColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x2F", + "green" : "0x81", + "red" : "0xFA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Fix-MBTI/Fix_MBTIApp.swift b/Fix-MBTI/Fix_MBTIApp.swift index 392e281..b0a129c 100644 --- a/Fix-MBTI/Fix_MBTIApp.swift +++ b/Fix-MBTI/Fix_MBTIApp.swift @@ -11,20 +11,27 @@ 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, + PostMission.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 index 7ae724a..9c8c025 100644 --- a/Fix-MBTI/Info.plist +++ b/Fix-MBTI/Info.plist @@ -2,6 +2,12 @@ + NSPhotoLibraryUsageDescription + Photo Library Permission Request + NSCameraUsageDescription + Camera Permission Request + UNNotificationIcon + NotificationIcon NSUserNotificationUsageDescription NSUserNotificationUsageDescription 앱에서 알림을 보내기 위해 권한이 필요합니다. diff --git a/Fix-MBTI/Model/Mission.swift b/Fix-MBTI/Model/Mission.swift index d3fdb1d..134c57c 100644 --- a/Fix-MBTI/Model/Mission.swift +++ b/Fix-MBTI/Model/Mission.swift @@ -27,45 +27,188 @@ 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") +//] + + +// 미션용 데이터 (제안형) +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: "새로운 사람에게 먼저 인사를 건네고 대화를 시도하세요.", category: "E"), + Mission(title: "너의 의견을 당당하게 주장해봐", detailText: "모임이나 회의에서 의견을 당당하게 말해보세요.", category: "E"), + Mission(title: "전화 대신 직접 만나는건 어때?", detailText: "중요한 대화를 전화나 문자 대신 직접 만나서 해보세요.", category: "E"), + Mission(title: "핫플에서 술 한잔 어때?", detailText: "번화가에 나가서 많은 사람들 사이에서 활동해보세요", 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: "밖에서 혼자만의 시간을 가져봐", detailText: "카페나 공원에서 혼자 조용히 시간을 보내보세요.", category: "I"), + Mission(title: "오늘 하루 SNS 금지 X", detailText: "SNS를 하루 동안 사용하지 않고 자기 자신에게 집중해보세요.", category: "I"), + Mission(title: "이불 밖은 위험해!", detailText: "이불 안에서 오늘 하루를 보내보세요.", category: "I"), + Mission(title: "10분 동안 명상 해볼래?", detailText: "10분간 조용한 공간에서 명상을 해보세요.", category: "I"), + Mission(title: "오늘은 혼영 어때?", detailText: "혼자 영화를 감상하고 여운을 느껴보세요.", 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: "미래의 나에게 편지를 써보자", detailText: "n년 후의 나에게 편지를 써보세요.", category: "N"), + Mission(title: "영화 주인공이 되어봐", detailText: "영화를 감상하고 주인공에 자신을 대입해보세요.", category: "N"), + Mission(title: "철학 책 한번 읽어볼래?", detailText: "철학 또는 공상과학 서적을 한 권 읽어보세요.", category: "N"), + Mission(title: "기발한 아이디어 3개 적어보자", detailText: "창의적인 아이디어 3개를 떠올려서 적어보세요.", category: "N"), + Mission(title: "상상 속에서 여행 가보자", detailText: "가보고 싶은 여행지를 설정하고 가상으로 여행을 떠나보세요.", category: "N"), + Mission(title: "상상하면서 잠들어봐", detailText: "행복한 상상을하며 잠에 들어보세요.", 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"), + Mission(title: "친구에게 현실적인 조언해봐", detailText: "고민이 있는 친구에서 현실적으로 조언해보세요.", category: "S"), + Mission(title: "10분동안 아무 생각도 하지마", detailText: "10분동안 아무 생각하지 않고 멍 때려보세요.", 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: "일기 써볼래?", detailText: "하루 동안 느낀 감정을 일기에 기록하세요.", category: "F"), + Mission(title: "친구의 고민에 공감해봐", detailText: "누군가의 고민을 듣고 공감을 표현해보세요.", category: "F"), + Mission(title: "노래 감상하고 느낀 감정을 적어봐", detailText: "감성적인 노래를 들으며 느낀 감정을 적어보세요.", category: "F"), + Mission(title: "하루 동안 긍정적인 말 3번 이상 하기", detailText: "하루 동안 주변 사람들에게 긍정적인 말을 세 번 이상 해보세요.", category: "F"), + Mission(title: "감사함을 표현해보자", detailText: "제일 가까운 사람에게 감사함을 표현해보세요.", category: "F"), + Mission(title: "가족들에게 사랑한다고 해봐", 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"), + Mission(title: "기사 읽고 객관적으로 해석해봐", detailText: "뉴스나 기사를 읽고 객관적인 관점에서 해석해보세요.", 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"), + 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"), + 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, imageName: String? = nil) { + self.title = mission.title + self.detailText = mission.detailText + self.content = content // 입력된 내용 저장 + self.timestamp = Date() + self.imageName = imageName + self.category = mission.category + } +} + +// 더미 데이터 +let dummyPosts: [PostMission] = [ + PostMission( + mission: Mission( + title: "새로운 사람에게 먼저 인사하기", + detailText: "3명에게 먼저 대화를 시도하세요.", + category: "E" + ), + content: "새로 보는 사람에게 먼저 인사하는 게 어색했지만, 생각보다 기분이 좋았어요!", + imageName: "person.2.fill" + ), + PostMission( + mission: Mission( + title: "즉흥적인 여행 잡기", + detailText: "계획없이 무작정 여행을 떠나보세요.", + category: "P" + ), + content: "긴장되다 갔지만 언제하면 만나요! 믿으려면, 정말 재밌었어요!", + imageName: "car.fill" + ) ] diff --git a/Fix-MBTI/Utils/Color.swift b/Fix-MBTI/Utils/Color.swift new file mode 100644 index 0000000..272fff6 --- /dev/null +++ b/Fix-MBTI/Utils/Color.swift @@ -0,0 +1,25 @@ +// +// Color.swift +// Fix-MBTI +// +// Created by 이수겸 on 2/6/25. +// + +// ColorExtentsion.swift + +import SwiftUI + +extension Color { + init(hex: String) { + let scanner = Scanner(string: hex) + _ = scanner.scanString("#") + + var rgb: UInt64 = 0 + scanner.scanHexInt64(&rgb) + + let r = Double((rgb >> 16) & 0xFF) / 255.0 + let g = Double((rgb >> 8) & 0xFF) / 255.0 + let b = Double((rgb >> 0) & 0xFF) / 255.0 + self.init(red: r, green: g, blue: b) + } +} diff --git a/Fix-MBTI/Utils/ImagePicker.swift b/Fix-MBTI/Utils/ImagePicker.swift new file mode 100644 index 0000000..dcaa3ce --- /dev/null +++ b/Fix-MBTI/Utils/ImagePicker.swift @@ -0,0 +1,38 @@ +import SwiftUI +import UIKit + +struct ImagePicker: UIViewControllerRepresentable { + @Binding var image: UIImage? + var sourceType: UIImagePickerController.SourceType = .camera // 기본값: 앨범 + + func makeUIViewController(context: Context) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.delegate = context.coordinator + picker.sourceType = sourceType + picker.allowsEditing = true // 편집 기능 활성화 + 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 editedImage = info[.editedImage] as? UIImage { + parent.image = editedImage + } else if let originalImage = info[.originalImage] as? UIImage { + parent.image = originalImage + } + picker.dismiss(animated: true) + } + } +} diff --git a/Fix-MBTI/Utils/NotificationManager.swift b/Fix-MBTI/Utils/NotificationManager.swift index 8ef8bf1..f868841 100644 --- a/Fix-MBTI/Utils/NotificationManager.swift +++ b/Fix-MBTI/Utils/NotificationManager.swift @@ -7,13 +7,26 @@ 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() { + func requestPermission() { let center = UNUserNotificationCenter.current() +// center.delegate = self center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { print("알림 권한 허용됨 ✅") @@ -21,24 +34,47 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { print("알림 권한 거부됨 ❌") } } - // 앱 실행 중에도 알림을 받을 수 있도록 설정 - center.delegate = self } - // 2. 랜덤한 시간 뒤 미션 알림 예약 - func scheduleMissionNotification() { - let content = UNMutableNotificationContent() - content.title = "새로운 MBTI 미션이 도착했습니다!" - content.body = "지금 앱을 열어 미션을 확인하세요." - content.sound = .default + // 🔥 🔥 기존 알림 삭제 (새로운 설정을 반영하기 위함) + func removeAllNotifications() { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + print("🗑️ 기존 알림 모두 삭제 완료") + } + + // 🔹 랜덤한 시간 후 미션 알림 예약 + func scheduleMissionNotification(profiles: [MBTIProfile], missions: [Mission], modelContext: ModelContext) { + + self.storedProfiles = profiles + self.storedMissions = missions + self.storedModelContext = modelContext - let randomDelay = Double.random(in: 1800...10800) // 30분 ~ 3시간 후 - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: randomDelay, repeats: false) + removeAllNotifications() // 기존 알림 삭제 - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request) + let missionCount = UserDefaults.standard.integer(forKey: "missionCount") + let actualCount = max(1, missionCount) // 최소 1개, 최대 설정값까지 - print("📢 랜덤 미션 알림 예약 완료: \(randomDelay)초 후 도착 예정") + 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. 앱이 실행 중일 때 알림을 받을 수 있도록 설정 @@ -49,4 +85,38 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { ) { 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 851145d..0f3965d 100644 --- a/Fix-MBTI/View/ContentView.swift +++ b/Fix-MBTI/View/ContentView.swift @@ -16,6 +16,7 @@ struct ContentView: View { var body: some View { + TabView(selection: $selectedTab) { MissionView() .tabItem { diff --git a/Fix-MBTI/View/ListCellView.swift b/Fix-MBTI/View/ListCellView.swift new file mode 100644 index 0000000..592adb1 --- /dev/null +++ b/Fix-MBTI/View/ListCellView.swift @@ -0,0 +1,75 @@ +// +// ListCellView.swift +// Fix-MBTI +// +// Created by 이수겸 on 2/14/25. +// + + +import SwiftUI +import SwiftData + +struct ListCellView: View { + var post: PostMission + + // 이미지 로드 함수 추가 + private func loadImage(fileName: String) -> UIImage? { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + if let imagePath = documentsDirectory?.appendingPathComponent(fileName) { + return UIImage(contentsOfFile: imagePath.path) + } + return nil + } + + var body: some View { + HStack(spacing: 10) { + // 이미지 표시 로직 변경 + if let imageName = post.imageName, + let uiImage = loadImage(fileName: imageName) { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: 90, height: 90) + .background(Color.orange) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } else { + Image(systemName: "photo.fill") + .resizable() + .scaledToFill() + .frame(width: 90, height: 90) + .background(Color.orange) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } + + VStack(alignment: .leading) { + + + Text(post.title) + .font(.headline) + .foregroundColor(.primary) +// Text(post.detailText) +// .font(.caption2) +// .foregroundStyle(.gray) + Spacer() + HStack(alignment: .bottom) { + Text("\(post.category)") + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(Color("ThemeColor")) + Spacer() +// Text("\(post.timestamp.formatted(date: .numeric, time: .omitted))") +// .font(.caption2) +// .foregroundColor(.gray) + } + } + Spacer() + } + .frame(height: 90) + .padding(.vertical, 5) + } + +} + +#Preview { + ListCellView(post: PostMission(mission: Mission(title: "하루 동안 긍정적인 말 3번 이상 하기", detailText: "하루동안 어머고 어어얼니런하세요 ㅇ그리고 아아아", category: "E"), content: "dddddddddd")) +} diff --git a/Fix-MBTI/View/ListDetailView.swift b/Fix-MBTI/View/ListDetailView.swift index c13019f..c70c39a 100644 --- a/Fix-MBTI/View/ListDetailView.swift +++ b/Fix-MBTI/View/ListDetailView.swift @@ -11,45 +11,87 @@ import SwiftData struct ListDetailView: View { @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss - - var mission: Mission + + let post: PostMission var body: some View { ScrollView { VStack(alignment: .leading, spacing: 15) { - Image(mission.imageName ?? "") - .resizable() - .scaledToFit() - .frame(maxWidth: .infinity, maxHeight: 300) - .clipShape(RoundedRectangle(cornerRadius: 15)) + // 이미지 로드 로직 추가 + HStack { + Spacer() + if let imageName = post.imageName, + let uiImage = loadImage(fileName: imageName) { + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + .frame(height: 335) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } else { + Image(systemName: "photo.fill") + .resizable() + .scaledToFit() + .frame(width: 335, height: 335) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + Spacer() + } + HStack(alignment: .bottom) { + Text(post.title) + .font(.title3) + .fontWeight(.bold) + .padding(.horizontal) + Spacer() + Text(post.category) + .font(.title3) + .fontWeight(.bold) + .foregroundStyle(Color(hex: "FA812F")) + .padding(.trailing) } - Text(mission.title) - .font(.title) - .fontWeight(.bold) + HStack(alignment: .bottom) { + Text(post.detailText) + .font(.footnote) + .foregroundColor(.gray) + .lineLimit(1) + .padding(.horizontal) + Spacer() + Text(post.timestamp.formatted(date: .numeric, time: .omitted)) + .font(.caption2) + .foregroundColor(.gray) + .padding(.horizontal) + } + Divider() .padding(.horizontal) - Text("\(mission.timestamp)") - .font(.subheadline) - .foregroundColor(.gray) - .padding(.horizontal) - Divider() - .padding(.horizontal) - Text(mission.detailText) + Text(post.content) .font(.body) .padding(.horizontal) - Spacer() + } .padding(.vertical) } .navigationTitle("게시물 상세") .navigationBarTitleDisplayMode(.inline) } + + private func deletePost() { + // 게시물 삭제 로직 추가 + modelContext.delete(post) + } + + // 이미지 로드 함수 + private func loadImage(fileName: String) -> UIImage? { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + if let imagePath = documentsDirectory?.appendingPathComponent(fileName) { + return UIImage(contentsOfFile: imagePath.path) + } + return nil + } } #Preview { - ListDetailView(mission: Mission(title: "ㅇㅇㅇ", detailText: "ㅇㅇㅇ", imageName: "ListOn", category: "E") - ) + ListDetailView(post: PostMission(mission: Mission(title: "sfldjfksdfjsdfsdf", detailText: "sfdsdfsdfsdfsdf", timestamp: Date(), imageName: "", category: "E"), content: "sdfsfsdfsdfs")) } diff --git a/Fix-MBTI/View/ListView.swift b/Fix-MBTI/View/ListView.swift index b24db70..be10c38 100644 --- a/Fix-MBTI/View/ListView.swift +++ b/Fix-MBTI/View/ListView.swift @@ -8,67 +8,98 @@ 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] + @Query private var posts: [PostMission] + @State var stackPath = NavigationPath() + @State var categories = ["전체보기", "E", "I", "N", "S", "T", "F", "P", "J"] + @State var category = "전체보기" - let posts: [Post] = [ - Post(title: "감동적인 영화 한편 보는거 어때", thumbnail: "sample1", description: ""), - Post(title: "오늘은 친구 없이 혼자 놀아봐", thumbnail: "sample2", description: ""), - Post(title: "계획없이 여행을 떠나보자", thumbnail: "sample3", description: "") - ] + private var filteredPosts: [PostMission] { + category == "전체보기" ? posts : posts.filter { $0.category == category } + } + + private var categoryPicker: some View { + HStack { + Spacer() + Picker("Category", selection: $category) { + ForEach(categories, id: \.self) { + Text($0) + } + } + .frame(width: 160, height: 30, alignment: .trailing) + .cornerRadius(10) + } + } var body: some View { - 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() + NavigationStack(path: $stackPath) { + VStack { + categoryPicker + .offset(y: 4) + if filteredPosts.isEmpty { + ContentUnavailableView("게시물 없음", systemImage: "doc.text") + } else { + List { + ForEach(filteredPosts) { post in + NavigationLink(destination: ListDetailView(post: post)) { + ListCellView(post: post) + .padding(.init(top: -6, leading: -6, bottom: -6, trailing: 0)) + } + } + .onDelete { index in + deletePost(at: index) + } } + .listRowSpacing(10) - Spacer() + } } - .padding(.vertical, 5) - } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { Text("게시물") .font(.headline) } + + } + + } + .accentColor(Color("ThemeColor")) + } + + // 게시물 삭제 함수 + private func deletePost(at indexSet: IndexSet) { + for index in indexSet { + let postToDelete = posts[index] + + // 이미지 삭제 + if let imageName = postToDelete.imageName { + deleteImage(named: imageName) } + + modelContext.delete(postToDelete) + } + + do { + try modelContext.save() + } catch { + print("게시물 삭제 실패: \(error)") + } + } + + // 이미지 파일 삭제 함수 + private func deleteImage(named: String) { + if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let fileURL = documentsDirectory.appendingPathComponent(named) + try? FileManager.default.removeItem(at: fileURL) } } } -struct ListView_Previews: PreviewProvider { - static var previews: some View { - ListView() - } + + +#Preview { + ListView() } diff --git a/Fix-MBTI/View/MBTISelectionView.swift b/Fix-MBTI/View/MBTISelectionView.swift index dbc3fc7..708daec 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,36 @@ struct MBTISelectionView: View { ["P", "J"] // 인식형 vs 판단형 ] + private 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: 28, height: 30) + .foregroundColor(Color("ThemeColor")) + + MBTIPicker(selection: $targetMBTI, options: mbtiOptions) Button("완료") { saveMBTI() isFirstLaunch = false + dismiss() } .padding() - .foregroundStyle(currentMBTI == targetMBTI ? .gray : .orange) - .disabled(currentMBTI == targetMBTI) - .opacity(currentMBTI == targetMBTI ? 0.5 : 1.0) + .fontWeight(.semibold) + .foregroundStyle(isCompleteButtonDisabled ? .gray : Color("ThemeColor")) + .disabled(isCompleteButtonDisabled) + .opacity(isCompleteButtonDisabled ? 0.5 : 1.0) } .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -49,42 +62,56 @@ 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) } + } } } - struct MBTIPicker: View { @Binding var selection: [String] let options: [[String]] 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(.system(size: 32)) .fontWeight(.medium) .frame(maxWidth: .infinity) - .foregroundStyle(selection[index] == option ? .orange : .gray) - + .foregroundStyle(selection[index] == option ? Color("ThemeColor") : .gray) } } .pickerStyle(.wheel) .frame(width: 60, height: 170) .clipped() - } } .padding() @@ -94,3 +121,112 @@ struct MBTIPicker: View { #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/MissionCellView.swift b/Fix-MBTI/View/MissionCellView.swift new file mode 100644 index 0000000..95e079d --- /dev/null +++ b/Fix-MBTI/View/MissionCellView.swift @@ -0,0 +1,44 @@ +// +// MissionCellView.swift +// Fix-MBTI +// +// Created by 이수겸 on 2/18/25. +// + +import SwiftUI +import SwiftData + +struct MissionCellView: View { + var mission: ActiveMission + var body: some View { + HStack { + VStack(alignment: .leading) { + Text("\(mission.title)") + .font(.headline) + // .foregroundStyle(Color(hex: "222222")) + + Spacer() + HStack(alignment: .bottom) { + HStack { + Text(mission.category) + .font(.footnote) + .fontWeight(.bold) + .foregroundStyle(Color("ThemeColor")) // category만 오렌지색으로 + Text("미션") + .font(.footnote) + .fontWeight(.bold) + // .foregroundStyle(Color(hex: "222222")) // "체험"은 기존 색상 유지 + } + Spacer() + +// Text(mission.timestamp, format: Date.FormatStyle().month(.defaultDigits).day(.defaultDigits)) +// .font(.caption2) +// .foregroundColor(.gray) + // .foregroundStyle(Color(hex: "222222")) + + } + } + Spacer() + } + } +} diff --git a/Fix-MBTI/View/MissionDetailView.swift b/Fix-MBTI/View/MissionDetailView.swift index 533f452..1d857b0 100644 --- a/Fix-MBTI/View/MissionDetailView.swift +++ b/Fix-MBTI/View/MissionDetailView.swift @@ -5,18 +5,176 @@ // Created by KimJunsoo on 2/4/25. // +import Foundation import SwiftUI import SwiftData struct MissionDetailView: View { - var mission: Mission + @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 = "" + @State private var useCamera: Bool = false + @State private var showingAlert: Bool = false + @FocusState private var isFocused: Bool + + let alert = UIAlertController(title: "Title", message: "message", preferredStyle: .actionSheet) + + + private let mission: Mission // let으로 변경 + private var isCompleteButtonDisabled: Bool { + return inputText.isEmpty || selectedImage == nil + } + + init(mission: Mission) { // 명시적 생성자 + self.mission = mission + } var body: some View { - Text("미션뷰에서 미션 성공 후 후기작성(?) 페이지") - Text(mission.title) + VStack { + Button(action: { showingAlert.toggle() }) { + if let image = selectedImage { + Image(uiImage: image) + .resizable() + .scaledToFit() + + .frame(height: 335) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } else { + VStack { + Image(systemName: "photo.on.rectangle.angled") + .font(.system(size: 50)) + .foregroundColor(Color("ThemeColor")) + } + .frame(width: 335, height: 335) + .background(Color("ImagePickerBackgroundColor")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + .padding() + .offset(y: 15) + .confirmationDialog("이미지를 선택하세요", isPresented: $showingAlert, titleVisibility: .visible) { + Button("카메라 촬영") { + useCamera = true + DispatchQueue.main.async { + isImagePickerPresented = true + } + } + Button("앨범에서 선택") { + useCamera = false + DispatchQueue.main.async { + isImagePickerPresented = true + } + } + Button("취소", role: .cancel) {} // 취소 버튼 추가 + } + + .sheet(isPresented: $isImagePickerPresented) { + ImagePicker(image: $selectedImage, sourceType: useCamera ? .camera : .photoLibrary) + + } + HStack { + Text(mission.title) + .font(.title2) + .fontWeight(.bold) + .lineLimit(2) + .padding(.leading) + + Spacer() + + Text(mission.category) + .font(.system(size: 20)) + .fontWeight(.bold) + .foregroundStyle(Color("ThemeColor")) + .padding(.trailing) + + } + + HStack { + Text(mission.detailText) + .foregroundStyle(Color.gray) + .font(.caption) + .lineLimit(1) + .padding(.leading) + .offset(y: 1) + Spacer() + } + + TextEditor(text: $inputText) + .font(.system(size: 17)) + .overlay(alignment: .topLeading) { + Text("문구 입력..") + .font(.system(size: 17)) + .foregroundStyle(inputText.isEmpty ? .gray : .clear) + .padding(.top, 8) + .padding(.horizontal, 5) + } + .focused($isFocused) + .frame(height: 200) + .background(Color(.systemGray6)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.horizontal) + + + Spacer() + + Button("완료") { + savePost() + } + .padding() + .frame(maxWidth: .infinity) + .background(isCompleteButtonDisabled ? Color.gray : Color("ThemeColor")) + .disabled(isCompleteButtonDisabled) + .foregroundColor(.white) + .cornerRadius(10) + .padding() + .offset(y: -20) + + } + .onTapGesture { + isFocused = false + } + .padding() + .navigationTitle("게시물 작성") + .navigationBarTitleDisplayMode(.inline) + } + + private func savePost() { + var fileName: String? = nil + + // 이미지가 선택되었을 경우에만 이미지 저장 + if let imageData = selectedImage?.jpegData(compressionQuality: 0.8) { + fileName = UUID().uuidString + ".jpg" + if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let fileURL = documentsDirectory.appendingPathComponent(fileName!) + try? imageData.write(to: fileURL) + } + } + + // PostMission 생성 및 저장 + let postMission = PostMission( + mission: mission, + content: inputText, + imageName: fileName + ) + modelContext.insert(postMission) + + // 원본 ActiveMission 삭제 + let activeMissions = try? modelContext.fetch(FetchDescriptor()) + activeMissions?.forEach { activeMission in + if activeMission.title == mission.title { + modelContext.delete(activeMission) + } + } + try? modelContext.save() + + dismiss() } } -//#Preview { -// MissionDetailView(mission: Mission) -//} + +#Preview { + MissionDetailView(mission: Mission(title: "오늘하루 계획 짜봐", detailText: "sdsdsdsdsdsd", category: "E")) +} diff --git a/Fix-MBTI/View/MissionView.swift b/Fix-MBTI/View/MissionView.swift index c723319..1138781 100644 --- a/Fix-MBTI/View/MissionView.swift +++ b/Fix-MBTI/View/MissionView.swift @@ -10,22 +10,74 @@ import SwiftData struct MissionView: View { @Environment(\.modelContext) private var modelContext - @Query private var missions: [Mission] + // @Query private var missions: [Mission] + @Query private var profiles: [MBTIProfile] + @Query(sort: \ActiveMission.timestamp) private var activeMissions: [ActiveMission] @State private var showAlert = false + @State var categories = ["전체보기", "E", "I", "N", "S", "T", "F", "P", "J"] + @State var category = "전체보기" + private var filteredMissions: [ActiveMission] { + category == "전체보기" ? activeMissions : activeMissions.filter { $0.category == category } + } + private var categoryPicker: some View { + HStack { + Spacer() + Picker("Category", selection: $category) { + ForEach(categories, id: \.self) { + Text($0) + } + } + .frame(width: 160, height: 30, alignment: .trailing) + .cornerRadius(10) + } + } + + // NotificationDelegate 인스턴스 생성 + private let notificationDelegate = NotificationDelegate() var body: some View { NavigationStack { - List { - ForEach(missions) { mission in - NavigationLink(destination: MissionDetailView(mission: mission)) { - HStack { - Text(mission.title) + VStack { + categoryPicker + .offset(y: 4) + if filteredMissions.isEmpty { + ContentUnavailableView("미션 없음", systemImage: "paperplane") + } else { + List { + ForEach(filteredMissions) { activeMission in + NavigationLink(destination: + MissionDetailView(mission: Mission( + title: activeMission.title, + detailText: activeMission.detailText, + category: activeMission.category))) { + MissionCellView(mission: activeMission) + .padding(.bottom) + .padding(.top) + .cornerRadius(15) + } } + .onDelete(perform: deleteMission) + } + .onAppear { + print("🔍 현재 MBTI: \(profiles.first?.currentMBTI ?? "default")") + print("🔍 목표 MBTI: \(profiles.first?.targetMBTI ?? "default")") + + // Delegate 설정 및 콜백 등록 + notificationDelegate.addMissionCallback = addMission + UNUserNotificationCenter.current().delegate = notificationDelegate + } + .listRowSpacing(20) + + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("미션") + .font(.headline) } - .onDelete(perform: deleteMission) } - .navigationTitle("나의 미션") .toolbar { Button(action: addMission) { Label("미션 추가", systemImage: "plus") @@ -36,9 +88,45 @@ struct MissionView: View { } } } + .accentColor(Color("ThemeColor")) + } + + 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 = "테스트 알림" @@ -52,19 +140,26 @@ struct MissionView: View { print("📢 테스트 알림 예약 완료 (5초 후 도착)") } - func addMission() { - let newMission = Mission(title: "즉흥적인 약속 잡기", detailText: "계획 없이 친구에게 연락해서 만나기", category: "P") - modelContext.insert(newMission) - } +} + +class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { + var addMissionCallback: (() -> Void)? - func deleteMission(offsets: IndexSet) { - for index in offsets { - modelContext.delete(missions[index]) - } + // 알림이 도착했을 때 호출되는 함수 + 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 71848f9..cea5540 100644 --- a/Fix-MBTI/View/SettingView.swift +++ b/Fix-MBTI/View/SettingView.swift @@ -6,49 +6,117 @@ // import SwiftUI +import SwiftData + +import SwiftUI +import SwiftData struct SettingView: View { - @State private var isShowingMBTISelection = false - @State private var isNotificationEnabled = true + @Environment(\.modelContext) private var modelContext + @Query private var missions: [Mission] + @Query private var profiles: [MBTIProfile] + @State private var isShowingMBTISelection = false + @State private var isNotificationEnabled = false + @AppStorage("missionCount") private var missionCount: Int = 1 // 기본값 1개 var body: some View { NavigationStack { List { - Button("MBTI 변경") { - isShowingMBTISelection = true + + Section(header: Text("내 MBTI").font(.caption).foregroundColor(Color("SettingTextColor"))) { + HStack { + Text(profiles.first?.currentMBTI ?? "미설정") + .font(.headline) + Spacer() + } } - .foregroundColor(.primary) - Button("MBTI 검사하러 가기") { - openMBTITest() + Section(header: Text("체험 MBTI").font(.caption).foregroundColor(Color("SettingTextColor"))) { + HStack { + Text(profiles.first?.targetMBTI ?? "미설정") + .font(.headline) + + Spacer() + } } - .foregroundColor(.primary) - - HStack { - Button("알림 설정") { - isNotificationEnabled.toggle() + Section { + Button(action: { isShowingMBTISelection = true }) { + HStack { + Image(systemName: "pencil") + .foregroundColor(Color("ThemeColor")) + Text("MBTI 변경") + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(.gray) + } + } + + Button(action: { openMBTITest() }) { + HStack { + Image(systemName: "globe") + .foregroundColor(Color("ThemeColor")) + Text("MBTI 검사하러 가기") + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(.gray) + } } - .foregroundColor(.primary) - - Spacer() - Toggle("", isOn: $isNotificationEnabled) - .labelsHidden() } - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text("환경설정") - .font(.headline) + // 알림 설정 섹션 + Section { + Toggle(isOn: $isNotificationEnabled) { + HStack { + Image(systemName: "bell.fill") + .foregroundColor(Color("ThemeColor")) + Text("알림 설정") + } + } + .onChange(of: isNotificationEnabled) { _, newValue in + if newValue { + NotificationManager.instance.scheduleMissionNotification( + profiles: profiles, + missions: missions, + modelContext: modelContext + ) + } else { + NotificationManager.instance.removeAllNotifications() + } + } + } + + // 🔹 미션 개수 설정 + Section(header: Text("미션 개수 설정").font(.caption).foregroundColor(Color("SettingTextColor"))) { + Picker("미션 개수", selection: $missionCount) { + ForEach(1...5, id: \.self) { count in + Text("\(count)개").tag(count) + } + } + .padding(5) + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: missionCount) { oldValue, newValue in + NotificationManager.instance.scheduleMissionNotification( + profiles: profiles, + missions: missions, + modelContext: modelContext + ) + print("🔄 미션 개수 변경됨: \(oldValue) → \(newValue)") + } } } + .padding(.top, 10) + .navigationTitle("환경 설정") + .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $isShowingMBTISelection) { MBTISelectionView() } } + + .listStyle(.grouped) } private func openMBTITest() {