diff --git a/.github/workflows/smeemDev.yml b/.github/workflows/smeemDev.yml index e1d4a674..ca303b36 100644 --- a/.github/workflows/smeemDev.yml +++ b/.github/workflows/smeemDev.yml @@ -6,6 +6,8 @@ name: Swift on: push: branches: [ "develop" ] + pull_request: + branches: [ "develop" ] jobs: build: diff --git a/Smeem-iOS/Smeem-iOS.xcodeproj/project.pbxproj b/Smeem-iOS/Smeem-iOS.xcodeproj/project.pbxproj index 54476ff4..cb6df42b 100644 --- a/Smeem-iOS/Smeem-iOS.xcodeproj/project.pbxproj +++ b/Smeem-iOS/Smeem-iOS.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 375B628B2C590D0D00DA8E30 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 375B628A2C590D0D00DA8E30 /* FirebaseMessaging */; }; 375B628D2C590D0D00DA8E30 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 375B628C2C590D0D00DA8E30 /* FirebaseRemoteConfig */; }; 3761116C2A278D0E0095EC5A /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761116B2A278D0E0095EC5A /* String+.swift */; }; + 376947892D0894A2006A46B1 /* Double+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376947882D0894A2006A46B1 /* Double+.swift */; }; 377B3BF02CF613710086E0BC /* HighlightModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377B3BEF2CF613710086E0BC /* HighlightModifier.swift */; }; 3785072F2BD1480C004CC922 /* SharedDiaryDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37499E3C2BD0C3A100BA6FAF /* SharedDiaryDataService.swift */; }; 378B20EE2BA0A01600604935 /* ForeignDiaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378B20ED2BA0A01600604935 /* ForeignDiaryViewModel.swift */; }; @@ -263,6 +264,7 @@ 374F828B2AC327A200C128B9 /* SmeemTextViewHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmeemTextViewHandler.swift; sourceTree = ""; }; 374FAF802A2CACCF00237A1A /* DiaryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryViewController.swift; sourceTree = ""; }; 3761116B2A278D0E0095EC5A /* String+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; + 376947882D0894A2006A46B1 /* Double+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+.swift"; sourceTree = ""; }; 377B3BEF2CF613710086E0BC /* HighlightModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightModifier.swift; sourceTree = ""; }; 378B20ED2BA0A01600604935 /* ForeignDiaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeignDiaryViewModel.swift; sourceTree = ""; }; 378B20EF2BA0A02400604935 /* StepOneKoreanDiaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepOneKoreanDiaryViewModel.swift; sourceTree = ""; }; @@ -446,23 +448,10 @@ A3D7ECB62A26566A009857D6 /* EditNicknameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditNicknameViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 370BD0B62CFB660C009560DB /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - CoachingComparisonView.swift, - CoachingCompletedView.swift, - CoachingExplanationView.swift, - CustomSegmentedControl.swift, - SwiftUINavigationView.swift, - ); - target = 4A8FFA4B29C9E1FD00FB76C0 /* Smeem-iOS */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - /* Begin PBXFileSystemSynchronizedRootGroup section */ 377B3BF12CF618B90086E0BC /* SwiftUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SwiftUI; sourceTree = ""; }; - 4AB3494D2CF229D10047C484 /* UIViewConponent */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (370BD0B62CFB660C009560DB /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = UIViewConponent; sourceTree = ""; }; + 4A17370E2D05C02E00B772C3 /* Toast */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Toast; sourceTree = ""; }; + 4A17370F2D05C03600B772C3 /* LoadingView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LoadingView; sourceTree = ""; }; 4AB3494E2CF22A070047C484 /* Global */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Global; sourceTree = ""; }; 4AB3494F2CF22A250047C484 /* Button */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Button; sourceTree = ""; }; 4AB349502CF22A2E0047C484 /* TextView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TextView; sourceTree = ""; }; @@ -473,7 +462,6 @@ 4AB349BA2CF23B770047C484 /* Coaching */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Coaching; sourceTree = ""; }; 4AB349BC2CF23D210047C484 /* Coaching */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Coaching; sourceTree = ""; }; 4AF050CE2CE9DE010055BC3F /* Coaching */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Coaching; sourceTree = ""; }; - 4AF050D72CEA01E20055BC3F /* DiaryComplete */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = DiaryComplete; sourceTree = ""; }; 4AF050D82CEA01EA0055BC3F /* Coaching */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Coaching; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -574,6 +562,7 @@ 373D29912CBFE16B00A559A3 /* SwiftUI */ = { isa = PBXGroup; children = ( + 377B3BEF2CF613710086E0BC /* HighlightModifier.swift */, 373D29922CBFE17B00A559A3 /* View+.swift */, ); path = SwiftUI; @@ -618,7 +607,6 @@ 37A574B929FE209F00312453 /* Resources */, 4AC4B82B2A2F9F5300E147AA /* Constants */, 37EB69E42A4B3B6B00075E4E /* Protocols */, - 377B3BEF2CF613710086E0BC /* HighlightModifier.swift */, ); path = Global; sourceTree = ""; @@ -627,7 +615,6 @@ isa = PBXGroup; children = ( 4AF050D82CEA01EA0055BC3F /* Coaching */, - 4AF050D72CEA01E20055BC3F /* DiaryComplete */, 4AC047232A8FCA9700EBDC0E /* AuthManagement */, 4AA6FEE72C3ABD7C00E588E9 /* ResignSummray */, 4AA5E4B52BF24CEB00F308C8 /* BadgeBottomSheet */, @@ -666,6 +653,7 @@ 4A4FEB002B721956001BBDF3 /* Combine+.swift */, 4AB7C9182B75F9B500845733 /* GesturePublisher+.swift */, 4AF050EA2CEA300F0055BC3F /* MoyaProvier+.swift */, + 376947882D0894A2006A46B1 /* Double+.swift */, ); path = Extensions; sourceTree = ""; @@ -674,7 +662,6 @@ isa = PBXGroup; children = ( 377B3BF12CF618B90086E0BC /* SwiftUI */, - 4AB3494D2CF229D10047C484 /* UIViewConponent */, 371107DB2ACAB4E3007A4AC2 /* Base */, 4A1EE9552A4DFB86007BFEF3 /* SmeemComponent */, ); @@ -1018,6 +1005,8 @@ 4A1EE9552A4DFB86007BFEF3 /* SmeemComponent */ = { isa = PBXGroup; children = ( + 4A17370F2D05C03600B772C3 /* LoadingView */, + 4A17370E2D05C02E00B772C3 /* Toast */, 4AB349502CF22A2E0047C484 /* TextView */, 4AB3494F2CF22A250047C484 /* Button */, 4AB3494E2CF22A070047C484 /* Global */, @@ -1752,7 +1741,8 @@ ); fileSystemSynchronizedGroups = ( 377B3BF12CF618B90086E0BC /* SwiftUI */, - 4AB3494D2CF229D10047C484 /* UIViewConponent */, + 4A17370E2D05C02E00B772C3 /* Toast */, + 4A17370F2D05C03600B772C3 /* LoadingView */, 4AB3494E2CF22A070047C484 /* Global */, 4AB3494F2CF22A250047C484 /* Button */, 4AB349502CF22A2E0047C484 /* TextView */, @@ -1762,7 +1752,6 @@ 4AB349562CF22BFF0047C484 /* EditUser */, 4AB349BA2CF23B770047C484 /* Coaching */, 4AF050CE2CE9DE010055BC3F /* Coaching */, - 4AF050D72CEA01E20055BC3F /* DiaryComplete */, 4AF050D82CEA01EA0055BC3F /* Coaching */, ); name = "Smeem-iOS"; @@ -1895,6 +1884,7 @@ 4AC705AE2BECF0D4003C5310 /* EditPlanViewModel.swift in Sources */, 4AF7C22F2BFB797600E8C6CD /* LockBadgeCollectionViewCell.swift in Sources */, 6F294A3E2A26177B00856CC8 /* TrainingGoalViewController.swift in Sources */, + 376947892D0894A2006A46B1 /* Double+.swift in Sources */, 4ABCBCCE2BDE95D0003138A8 /* MyPlanResponse.swift in Sources */, 4A9731D92BAAB70500DEC0C8 /* UpdateResponse.swift in Sources */, 4ABCBCEC2BE261B0003138A8 /* TrainingCollectionViewDatasource.swift in Sources */, @@ -2287,7 +2277,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.3; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = "Team.Smeem-dev"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2309,7 +2299,7 @@ CODE_SIGN_ENTITLEMENTS = "Smeem-iOS/Smeem-iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Distribution: YuJi Lee (K8LLWYQWXD)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 20241031; + CURRENT_PROJECT_VERSION = 2024121201; DEVELOPMENT_TEAM = K8LLWYQWXD; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Smeem-iOS/Info.plist"; @@ -2325,7 +2315,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.3; + MARKETING_VERSION = 3.0.0; PRODUCT_BUNDLE_IDENTIFIER = "Team.Smeem-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = smeem_release; diff --git a/Smeem-iOS/Smeem-iOS/Global/Constants/AmplitudeConstant.swift b/Smeem-iOS/Smeem-iOS/Global/Constants/AmplitudeConstant.swift index 2ba50fb8..0f513981 100644 --- a/Smeem-iOS/Smeem-iOS/Global/Constants/AmplitudeConstant.swift +++ b/Smeem-iOS/Smeem-iOS/Global/Constants/AmplitudeConstant.swift @@ -131,15 +131,24 @@ enum AmplitudeConstant { } enum diaryDetail { + case toggle_click(String) case mydiary_click - case mydiary_edit + case mydiary_view(Bool) + case mydiary_edit(Bool) + case mydiary_edit_complete_click(Bool) var event: BaseEvent { switch self { + case .toggle_click(let toggle): + return BaseEvent(eventType: "toggle_click", eventProperties: ["toggle": toggle]) case .mydiary_click: return BaseEvent(eventType: "mydiary_click", eventProperties: nil) - case .mydiary_edit: - return BaseEvent(eventType: "mydiary_edit", eventProperties: nil) + case .mydiary_view(let hasCoaching): + return BaseEvent(eventType: "mydiary_view", eventProperties: ["has_coaching": hasCoaching]) + case .mydiary_edit(let hasCoaching): + return BaseEvent(eventType: "mydiary_edit", eventProperties: ["has_coaching": hasCoaching]) + case .mydiary_edit_complete_click(let hasCoaching): + return BaseEvent(eventType: "mydiary_edit_complete_click", eventProperties: ["has_coaching": hasCoaching]) } } } @@ -175,4 +184,27 @@ enum AmplitudeConstant { } } } + + enum coaching { + case coaching_try_click(Bool) + case coaching_exit_click(Bool) + case coaching_load_view + case coaching_result_view + case coaching_feedback_view(Int) + + var event: BaseEvent { + switch self { + case .coaching_try_click(let isActive): + return BaseEvent(eventType: "coaching_try_click", eventProperties: ["active": isActive]) + case .coaching_exit_click(let isActive): + return BaseEvent(eventType: "coaching_exit_click", eventProperties: ["active": isActive]) + case .coaching_load_view: + return BaseEvent(eventType: "coaching_load_view", eventProperties: nil) + case .coaching_result_view: + return BaseEvent(eventType: "coaching_result_view", eventProperties: nil) + case .coaching_feedback_view(let index): + return BaseEvent(eventType: "badge_bottom_sheet_view", eventProperties: ["Index": index]) + } + } + } } diff --git a/Smeem-iOS/Smeem-iOS/Global/Extensions/Double+.swift b/Smeem-iOS/Smeem-iOS/Global/Extensions/Double+.swift new file mode 100644 index 00000000..e7e3e66d --- /dev/null +++ b/Smeem-iOS/Smeem-iOS/Global/Extensions/Double+.swift @@ -0,0 +1,20 @@ +// +// Double+.swift +// Smeem-iOS +// +// Created by Joon Baek on 12/1/24. +// + +import UIKit + +extension Double { + // 너비 기반 스케일링 (iPhone 13 mini) + func scaledByWidth() -> Double { + return (self / 375) * UIScreen.main.bounds.width + } + + // 높이 기반 스케일링 (iPhone 13 mini) + func scaledByHeight() -> Double { + return (self / 812) * UIScreen.main.bounds.height + } +} diff --git a/Smeem-iOS/Smeem-iOS/Global/HighlightModifier.swift b/Smeem-iOS/Smeem-iOS/Global/Extensions/SwiftUI/HighlightModifier.swift similarity index 100% rename from Smeem-iOS/Smeem-iOS/Global/HighlightModifier.swift rename to Smeem-iOS/Smeem-iOS/Global/Extensions/SwiftUI/HighlightModifier.swift diff --git a/Smeem-iOS/Smeem-iOS/Global/Extensions/SwiftUI/View+.swift b/Smeem-iOS/Smeem-iOS/Global/Extensions/SwiftUI/View+.swift index d4b33996..20560d41 100644 --- a/Smeem-iOS/Smeem-iOS/Global/Extensions/SwiftUI/View+.swift +++ b/Smeem-iOS/Smeem-iOS/Global/Extensions/SwiftUI/View+.swift @@ -13,13 +13,45 @@ extension View { } var screenHeight: Double { - return screenSize.height + return screenSize.height } var screenWidth: Double { return screenSize.width } + func showEditConfirmation( + title: String, + message: String, + firstActionTitle: String, + secondActionTitle: String, + firstActionHandler: (() -> Void)? = nil, + secondActionHandler: (() -> Void)? = nil + ) { + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: firstActionTitle, style: .cancel) { _ in + // 현재 presenting view controller를 찾아서 dismiss + if let presentedViewController = UIApplication.shared.windows.first?.rootViewController?.presentedViewController { + presentedViewController.dismiss(animated: true) { + firstActionHandler?() + } + } + }) + + alert.addAction(UIAlertAction(title: secondActionTitle, style: .default) { _ in + secondActionHandler?() + }) + + if let topViewController = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController { + topViewController.present(alert, animated: true) + } + } + func changeRootViewController(_ viewController: UIViewController) { guard let window = UIApplication.shared.windows.first else { return } UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: { @@ -27,4 +59,42 @@ extension View { window.rootViewController = rootVC }) } + + func pushToUIKitView(_ viewController: UIViewController, dismissFullScreenCover: Bool = true) { + // 현재 presenting된 view controller + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + + // 현재 최상위 view controller (fullScreenCover 아래의 view controller) + var topViewController = rootViewController + while let presentedViewController = topViewController.presentedViewController { + topViewController = presentedViewController + } + + if let navigationController = topViewController as? UINavigationController { + navigationController.pushViewController(viewController, animated: true) + + if dismissFullScreenCover, + let presentingView = topViewController.presentingViewController { + presentingView.dismiss(animated: false, completion: nil) + } + } + } + } + + func changeRootViewControllerAndPresent(_ viewControllerToPresent: UIViewController) { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + + let navigationController = UINavigationController(rootViewController: viewControllerToPresent) + window.rootViewController = navigationController + + UIView.transition(with: window, + duration: 0.5, + options: .transitionCrossDissolve, + animations: nil) + + window.makeKeyAndVisible() + } + } } diff --git a/Smeem-iOS/Smeem-iOS/Global/Extensions/UIViewController+.swift b/Smeem-iOS/Smeem-iOS/Global/Extensions/UIViewController+.swift index ff5331ab..7782617d 100644 --- a/Smeem-iOS/Smeem-iOS/Global/Extensions/UIViewController+.swift +++ b/Smeem-iOS/Smeem-iOS/Global/Extensions/UIViewController+.swift @@ -74,8 +74,13 @@ extension UIViewController { func changeRootViewControllerAndPresent(_ viewControllerToPresent: UIViewController) { if let window = UIApplication.shared.windows.first { - window.rootViewController = viewControllerToPresent - UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: nil) + let navigationController = UINavigationController(rootViewController: viewControllerToPresent) + window.rootViewController = navigationController + + UIView.transition(with: window, + duration: 0.5, + options: .transitionCrossDissolve, + animations: nil) } else { viewControllerToPresent.modalPresentationStyle = .overFullScreen self.present(viewControllerToPresent, animated: true, completion: nil) diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SmeemComponent/LoadingView/SmemeLoadingView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SmeemComponent/LoadingView/SmemeLoadingView.swift new file mode 100644 index 00000000..a59a2526 --- /dev/null +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SmeemComponent/LoadingView/SmemeLoadingView.swift @@ -0,0 +1,36 @@ +// +// SmemeLoadingView.swift +// Smeem-iOS +// +// Created by 황찬미 on 11/26/24. +// + +import SwiftUI + +struct SmemeLoadingView: View { + + var body: some View { + HStack { + Spacer() + VStack { + Spacer() + ProgressView() + Spacer() + } + Spacer() + } + .ignoresSafeArea() + .background(Color.white.opacity(0.1)) // 반투명 배경 + .allowsHitTesting(true) // 로딩 중 터치 차단 + } +} + +struct SmemeEmptyView: View { + + var body: some View { + ZStack { + Color.white.ignoresSafeArea() + } + .animation(.easeInOut(duration: 0.5), value: true) // 0.5초 뒤에 서서히 사라짐 + } +} diff --git a/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/SmemeToastView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SmeemComponent/Toast/SmemeToastView.swift similarity index 100% rename from Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/SmemeToastView.swift rename to Smeem-iOS/Smeem-iOS/Global/UIComponents/SmeemComponent/Toast/SmemeToastView.swift diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingComparisonView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingComparisonView.swift index 1de1dbdd..0d037969 100644 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingComparisonView.swift +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingComparisonView.swift @@ -9,26 +9,24 @@ import SwiftUI struct CoachingComparisonView: View { @Binding var coachingResponse: CoachingResponse - @State private var textHeight: CGFloat = 0 // Text의 높이를 저장할 변수 var body: some View { VStack(spacing: 20) { VStack(alignment: .leading, spacing: 8) { - HStack { - Rectangle() - .frame(width: 2, height: textHeight) - .foregroundStyle(Color(UIColor.black)) + HStack(spacing: 8) { + + GeometryReader { geomerty in + Rectangle() + .frame(height: geomerty.size.height) + .foregroundStyle(Color(UIColor.black)) + } + .frame(width: 2) Text("나의 일기") .font(Font.custom("Pretendard", size: 16).weight(.medium)) .foregroundColor(Color(UIColor.black)) - .background(GeometryReader { geometry in - Color.clear - .preference(key: TextHeightPreferenceKey.self, value: geometry.size.height) - }) - } - .onPreferenceChange(TextHeightPreferenceKey.self) { value in - textHeight = value + + Spacer() } Text(coachingResponse.originalSentence) @@ -39,14 +37,19 @@ struct CoachingComparisonView: View { .padding(.trailing, 18) VStack(alignment: .leading, spacing: 8) { - HStack { - Rectangle() - .frame(width: 2, height: textHeight) - .foregroundStyle(Color(UIColor.point)) + HStack(spacing: 8) { + GeometryReader { geomerty in + Rectangle() + .frame(height: geomerty.size.height) + .foregroundStyle(Color(UIColor.point)) + } + .frame(width: 2) Text("고친 문장") .font(Font.custom("Pretendard", size: 16).weight(.medium)) .foregroundColor(Color(UIColor.point)) + + Spacer() } Text(coachingResponse.correctedSentence) @@ -59,14 +62,3 @@ struct CoachingComparisonView: View { } } } - -struct TextHeightPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } -} - -//#Preview { -// CoachingComparisonView() -//} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingContentView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingContentView.swift index 872c1f0a..36a82153 100644 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingContentView.swift +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CoachingContentView.swift @@ -5,41 +5,41 @@ // Created by Joon Baek on 11/21/24. // -//import SwiftUI -// -//struct CoachingContentView: View { -// @Binding var currentIndex: Int -// @Binding var coachingResponse: CoachingsResponse -// -// var body: some View { -// VStack(spacing: screenHeight * (20 / screenHeight)) { -// Rectangle() -// .frame(height: screenHeight * (8 / screenHeight)) -// .foregroundStyle(Color(UIColor.gray100)) -// -// TabView(selection: $currentIndex) { -// ForEach(coachingResponse.corrections.indices, id: \.self) { item in -// ScrollView { -// VStack(spacing: screenWidth * (8 / screenWidth)) { -// CoachingComparisonView(coachingResponse: $coachingResponse.corrections[item]) -// -// CoachingExplanationView(coachingResponse: $coachingResponse.corrections[item]) -// } -// } -// } -// } -// .frame(width: screenWidth, height: screenHeight * (286 / screenHeight), alignment: .top) -// .tabViewStyle(.page(indexDisplayMode: .never)) -// .padding(.horizontal, screenWidth * (16 / screenWidth)) -// -// PageControl(currentPage: $currentIndex, -// coachingResponse: $coachingResponse) -// } -// } -//} -// +import SwiftUI + +struct CoachingContentView: View { + @Binding var currentIndex: Int + @Binding var detailDiaryResponse: DetailDiaryResponse + @Binding var corrections: [CoachingResponse] + + var body: some View { + VStack(spacing: 20.scaledByHeight()) { + Rectangle() + .frame(height: 8.scaledByHeight()) + .foregroundStyle(Color(UIColor.gray100)) + + TabView(selection: $currentIndex) { + ForEach(corrections.indices, id: \.self) { item in + ScrollView { + VStack(spacing: 8.scaledByHeight()) { + CoachingComparisonView(coachingResponse: $corrections[item]) + CoachingExplanationView(coachingResponse: $corrections[item]) + } + } + } + } + .frame(width: screenWidth, height: screenHeight * (286 / screenHeight), alignment: .top) + .tabViewStyle(.page(indexDisplayMode: .never)) + .padding(.horizontal, 16.scaledByWidth()) + + PageControl(currentPage: $currentIndex, coachingResponse: $corrections) + } + } +} + //#Preview { // @State var defaultIndex: Int = 0 -// @State var coachingResponse = CoachingsResponse.empty -// CoachingContentView(currentIndex: $defaultIndex, coachingResponse: $coachingResponse) +// @State var coachingsResponse = CoachingsResponse.empty +// @State var coachingResponse = CoachingsResponse.empty.corrections +// CoachingContentView(currentIndex: $defaultIndex, coachingsResponse: $coachingsResponse, coachingResponse: $coachingResponse) //} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CustomSegmentedControl.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CustomSegmentedControl.swift index 9576441a..52eabd3f 100644 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CustomSegmentedControl.swift +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/CustomSegmentedControl.swift @@ -16,39 +16,63 @@ struct CustomSegmentedControl: View { ForEach(options.indices, id: \.self) { index in SegmentButton( title: options[index], - isSelected: selectedIndex == index, + isSelected: isCoachingOn(index), + isFirstButton: index == 0, + isLastButton: index == options.count - 1, action: { selectedIndex = index } ) } } - .background(Color.gray.opacity(0.2)) - .cornerRadius(8) +// .background(Color.gray.opacity(0.2)) + .cornerRadius(6) + } + + private func isCoachingOn(_ index: Int) -> Bool { + return options[index] == "코칭 ON" && selectedIndex == index } } struct SegmentButton: View { let title: String let isSelected: Bool + let isFirstButton: Bool + let isLastButton: Bool let action: () -> Void var body: some View { Button(action: action) { Text(title) - .padding(.vertical, 8) - .padding(.horizontal, 11) + .padding(.vertical, 8.scaledByHeight()) + .padding(.horizontal, 10.scaledByWidth()) .frame(maxWidth: .infinity) .background(backgroundColor) .foregroundColor(foregroundColor) .font(Font(UIFont.c5)) +// .lineLimit(1) + .minimumScaleFactor(0.9) + .overlay( + Group { + if isSelected && isFirstButton { + CustomStrokeShape(includeLeadingCorners: false) + .stroke(Color(UIColor.gray500), lineWidth: 1) + } else if !isSelected { + CustomStrokeShape( + includeLeadingCorners: isFirstButton, + includeTrailingCorners: isLastButton + ) + .stroke(Color(UIColor.gray500), lineWidth: 1) + } + } + ) } } private var backgroundColor: Color { - isSelected ? (isCoachingOn ? Color(UIColor.point) : Color(UIColor.gray200)) : Color(UIColor.gray100) + isSelected ? Color(UIColor.point) : isFirstButton ? Color(UIColor.gray100) : Color(UIColor.white) } private var foregroundColor: Color { - isSelected ? .white : Color(UIColor.gray500) + isSelected ? Color(UIColor.smeemWhite) : Color(UIColor.gray500) } private var isCoachingOn: Bool { @@ -56,19 +80,83 @@ struct SegmentButton: View { } } -struct PreviewWrapper: View { - @State private var selectedIndex = 0 - let options = ["코칭 OFF", "코칭 ON"] +struct CustomStrokeShape: Shape { + var radius: CGFloat = 6 + var includeLeadingCorners: Bool = true + var includeTrailingCorners: Bool = true - var body: some View { - CustomSegmentedControl(selectedIndex: $selectedIndex, options: options) - .frame(height: 40) - .padding(117) + func path(in rect: CGRect) -> Path { + var path = Path() + + // Move to the starting point (TopLeading) + path.move(to: CGPoint(x: rect.minX + (includeLeadingCorners ? radius : 0), y: rect.minY)) + + // TopLeading Corner + if includeLeadingCorners { + path.addArc( + center: CGPoint(x: rect.minX + radius, y: rect.minY + radius), + radius: radius, + startAngle: .degrees(-90), + endAngle: .degrees(180), + clockwise: true + ) + } else { + path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) + } + + // BottomLeading Corner + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - (includeLeadingCorners ? radius : 0))) + if includeLeadingCorners { + path.addArc( + center: CGPoint(x: rect.minX + radius, y: rect.maxY - radius), + radius: radius, + startAngle: .degrees(180), + endAngle: .degrees(90), + clockwise: true + ) + } else { + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + } + + // BottomTrailing Corner + path.addLine(to: CGPoint(x: rect.maxX - (includeTrailingCorners ? radius : 0), y: rect.maxY)) + if includeTrailingCorners { + path.addArc( + center: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius), + radius: radius, + startAngle: .degrees(90), + endAngle: .degrees(0), + clockwise: true + ) + } else { + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + } + + // TopTrailing Corner + path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + (includeTrailingCorners ? radius : 0))) + if includeTrailingCorners { + path.addArc( + center: CGPoint(x: rect.maxX - radius, y: rect.minY + radius), + radius: radius, + startAngle: .degrees(0), + endAngle: .degrees(-90), + clockwise: true + ) + } else { + path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) + } + + path.closeSubpath() + return path } } @available(iOS 17.0, *) #Preview { - PreviewWrapper() + @State var selectedIndex = 1 + let options = ["코칭 OFF", "코칭 ON"] + + CustomSegmentedControl(selectedIndex: $selectedIndex, options: options) + .frame(height: 32.scaledByHeight()) + .padding(117.scaledByWidth()) } - diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/FloatingButtonsSwiftUIView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/FloatingButtonsSwiftUIView.swift deleted file mode 100644 index c2290fa4..00000000 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/FloatingButtonsSwiftUIView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// FloatingButtonsSwiftUIView.swift -// Smeem-iOS -// -// Created by Joon Baek on 11/26/24. -// - -import SwiftUI - -struct FloatingButtonsSwiftUIView: View { - @Environment(\.dismiss) var dismiss - @State private var showAlert = false - - var body: some View { - ZStack { - Color.black.opacity(0.3) - .ignoresSafeArea() - .onTapGesture { - dismiss() - } - - VStack { - Spacer() - - VStack(spacing: 0) { - Button(action: { - showAlert = true - }) { - Text("수정하기") - .font(.headline) - .frame(maxWidth: .infinity) - .padding() - .foregroundColor(.blue) - } - .alert("수정시 모든 코칭 내용이 사라집니다. 그래도 수정하시겠습니까?", - isPresented: $showAlert) { - Button("취소", role: .cancel) { dismiss() } - Button("확인", role: .destructive) { - print("수정 확인 버튼 액션") - } - } - - Divider() - .frame(height: 1) - .background(Color.gray.opacity(0.5)) - - Button(action: { - print("삭제하기 버튼 액션") - }) { - Text("삭제하기") - .font(.headline) - .frame(maxWidth: .infinity) - .padding() - .foregroundColor(.red) - } - } - .background(Color.white) - .cornerRadius(14) - - Button(action: { - dismiss() - }) { - Text("취소") - .font(.headline) - .frame(maxWidth: .infinity) - .padding() - .background(.white) - .foregroundColor(.blue) - .cornerRadius(14) - } - .padding(.top, 10) - - Spacer().frame(height: 20) - } - .padding(.horizontal, 20) - } - } -} - -#Preview { - FloatingButtonsSwiftUIView() -} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/RandomTopicViewSwiftUI.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/RandomTopicViewSwiftUI.swift new file mode 100644 index 00000000..82dc1adb --- /dev/null +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/RandomTopicViewSwiftUI.swift @@ -0,0 +1,45 @@ +// +// RandomTopicViewSwiftUI.swift +// Smeem-iOS +// +// Created by Joon Baek on 12/11/24. +// + +import SwiftUI + +struct RandomTopicViewSwiftUI: View { + + // MARK: - Properties + + var contentText: String? + + // MARK: - Body + + var body: some View { + GeometryReader { geometry in + HStack(alignment: .firstTextBaseline, spacing: 3) { + Text("Q.") + .font(Font(UIFont.b1)) + .foregroundColor(Color(UIColor.point)) + .padding(.leading, 18.scaledByWidth()) + + Text(contentText ?? "") + .font(Font(UIFont.b4)) + .foregroundColor(Color(UIColor.smeemBlack)) + .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 30.scaledByWidth()) + } + .padding(.top, 20.scaledByHeight()) + .padding(.bottom, 20.scaledByHeight()) + .background(Color(UIColor.gray100)) + } + .frame(height: contentText?.count ?? 0 > 20 ? 84.scaledByHeight() : 62.scaledByHeight()) + } +} + +#Preview { + var text: String = "랜덤주제 한줄일 경우 " + RandomTopicViewSwiftUI(contentText: text) +} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/ScrollableDiaryView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/ScrollableDiaryView.swift index e334c2d3..fa3ca016 100644 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/ScrollableDiaryView.swift +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/ScrollableDiaryView.swift @@ -29,9 +29,9 @@ struct ScrollableDiaryView: View { corrections: corrections, highlightIndex: selectedIndex != 0 ? currentIndex : -1 )) - .padding(.horizontal, screenHeight * (18 / screenHeight)) - .padding(.bottom, screenHeight * (16 / screenHeight)) - .foregroundColor(Color(UIColor.gray400)) + .padding(.horizontal, 18.scaledByHeight()) + .padding(.bottom, 16.scaledByHeight()) + .foregroundColor(selectedIndex == 0 ? Color(UIColor.smeemBlack) : Color(UIColor.gray400)) .background( // 콘텐츠 크기를 측정하기 위한 백그라운드 GeometryReader { geometry in Color.clear @@ -44,18 +44,18 @@ struct ScrollableDiaryView: View { // Footer (작성 날짜, 작성자) HStack { Spacer() - VStack(alignment: .trailing, spacing: screenWidth * (4 / screenWidth)) { + VStack(alignment: .trailing, spacing: 4.scaledByWidth()) { Text(dateText) Text(authorText) } .font(Font(UIFont.c3)) .foregroundColor(Color(UIColor.gray400)) } - .padding(.horizontal, screenWidth * (18 / screenWidth)) + .padding(.horizontal, 18.scaledByWidth()) } - .padding(.bottom, screenHeight * (16 / screenHeight)) + .padding(.bottom, 16.scaledByHeight()) } - .frame(maxHeight: contentHeight + screenHeight * (100 / screenHeight)) // 텍스트 높이에 따른 동적 변경 + .frame(maxHeight: contentHeight + 100.scaledByHeight()) // 텍스트 높이에 따른 동적 변경 } } } diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/SwiftUINavigationView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/SwiftUINavigationView.swift index 95d30984..387097cf 100644 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/SwiftUINavigationView.swift +++ b/Smeem-iOS/Smeem-iOS/Global/UIComponents/SwiftUI/SwiftUINavigationView.swift @@ -5,71 +5,78 @@ // Created by Joon Baek on 2024/10/18. // +import Combine import SwiftUI enum NavigationbarType { case coachingCompleted case diaryDetails + case unCoached +} + +final class NavigationViewModel: ObservableObject { + let leftButtonTapped = PassthroughSubject() + let rightButtonTapped = PassthroughSubject() } struct SwiftUINavigationView: View { - let navigationbarType: NavigationbarType - @State private var showFloatingView = false + + @ObservedObject var navigationViewModel: NavigationViewModel @Binding var selectedIndex: Int + let navigationbarType: NavigationbarType let options = ["코칭 OFF", "코칭 ON"] var body: some View { HStack { // 뒤로가기 버튼 - if navigationbarType == .diaryDetails { + if navigationbarType == .diaryDetails || navigationbarType == .unCoached { Button(action: { - // 뒤로가기 액션 + navigationViewModel.leftButtonTapped.send() }, label: { Image("icnBack") - .imageScale(.large) }) - .padding(.leading, 10 / screenWidth) + .padding(.leading, 12.scaledByWidth()) } else { - Spacer().frame(width: 30 / screenWidth) + Spacer().frame(width: 30.scaledByWidth()) } - // 중앙 콘텐츠 (CustomSegmentedControl) + // CustomSegmentedControl if navigationbarType == .diaryDetails { CustomSegmentedControl(selectedIndex: $selectedIndex, options: options) - .frame(height: 32 / screenHeight) - .padding(65) + .frame(height: 32.scaledByHeight()) + .padding(.leading, 64.scaledByWidth()) + .padding(.trailing, 58.scaledByWidth()) } Spacer() // 오른쪽 버튼 - if navigationbarType == .diaryDetails { + if navigationbarType == .diaryDetails || navigationbarType == .unCoached { Button(action: { - showFloatingView = true + navigationViewModel.rightButtonTapped.send() }, label: { Image("icnMore") }) - .padding(.trailing, 18 / screenWidth) - .fullScreenCover(isPresented: $showFloatingView) { - FloatingButtonsSwiftUIView() - } + .padding(.trailing, 18.scaledByWidth()) } else { Button(action: { - // 다른 버튼 액션 + navigationViewModel.rightButtonTapped.send() }, label: { Text("닫기") .tint(.black) }) - .padding(.trailing, 18 / screenWidth) + .padding(.trailing, 18.scaledByWidth()) } } - .frame(height: screenHeight * (66 / screenHeight)) + .frame(height: 54.scaledByHeight()) } } #Preview { @State var defaultIndex = 0 - SwiftUINavigationView(navigationbarType: .diaryDetails, selectedIndex: $defaultIndex) + NavigationView { + SwiftUINavigationView(navigationViewModel: NavigationViewModel(), selectedIndex: $defaultIndex, navigationbarType: .diaryDetails) + } } diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingComparisonView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingComparisonView.swift deleted file mode 100644 index d89d4b8e..00000000 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingComparisonView.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// CoachingComparisonView.swift -// Smeem-iOS -// -// Created by 황찬미 on 11/1/24. -// - -import SwiftUI - -struct CoachingComparisonView: View { - @Binding var coachingResponse: CoachingResponse - - var body: some View { - VStack(spacing: 20) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - - GeometryReader { geomerty in - Rectangle() - .frame(height: geomerty.size.height) - .foregroundStyle(Color(UIColor.black)) - } - .frame(width: 2) - - Text("나의 일기") - .font(Font.custom("Pretendard", size: 16).weight(.medium)) - .foregroundColor(Color(UIColor.black)) - - Spacer() - } - - Text(coachingResponse.original_sentence) - .font(Font.custom("Pretendard", size: 14)).fontWeight(.regular) - .frame(maxWidth: .infinity, alignment: .topLeading) - } - .padding(.leading, 18) - .padding(.trailing, 18) - - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - GeometryReader { geomerty in - Rectangle() - .frame(height: geomerty.size.height) - .foregroundStyle(Color(UIColor.point)) - } - .frame(width: 2) - - Text("고친 문장") - .font(Font.custom("Pretendard", size: 16).weight(.medium)) - .foregroundColor(Color(UIColor.point)) - - Spacer() - } - - Text(coachingResponse.corrected_sentence) - .font(Font.custom("Pretendard", size: 14)).fontWeight(.medium) - .foregroundColor(Color(UIColor.point)) - .frame(maxWidth: .infinity, alignment: .topLeading) - } - .padding(.leading, 18) - .padding(.trailing, 18) - } - } -} - -//#Preview { -// CoachingComparisonView() -//} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingCompletedView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingCompletedView.swift deleted file mode 100644 index 55199b66..00000000 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingCompletedView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// CoachingTextView.swift -// Smeem-iOS -// -// Created by Joon Baek on 2024/10/16. -// - -import SwiftUI - -struct CoachingTextView: View { - @Binding var coachingText: String - - var body: some View { - - VStack(spacing: 16) { - Text("일기를 잘 작성하셨어요! \n내용이 명확하고 흥미로웠습니다.\n이제 몇 가지 문법적 오류를 수정해 볼까요?") - .font(Font.custom("Pretendard", size: 16)) - // Colors 상수 등록 필요 - .foregroundColor(Color(UIColor.smeemBlack)) - .frame(maxWidth: .infinity, alignment: .leading) - - ScrollView { - Text(coachingText) - .font(Font.custom("Pretendard", size: 16)) - .foregroundColor(Color(UIColor.gray400)) - .lineSpacing(0.375) - } - - Spacer() - } - .padding(.horizontal, screenWidth * 0.048) - } -} -// -//@available (iOS 17, *) -//#Preview { -// CoachingTextView(coachingText: "") -//} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingExplanationView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingExplanationView.swift deleted file mode 100644 index 8686a472..00000000 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CoachingExplanationView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// CoachingExplanationView.swift -// Smeem-iOS -// -// Created by 황찬미 on 11/16/24. -// - -import SwiftUI - -struct CoachingExplanationView: View { - @Binding var coachingResponse: CoachingResponse - - var body: some View { - HStack() { - - ZStack(alignment: .leading) { - GeometryReader { geomerty in - Rectangle() - .fill(Color(UIColor.gray100)) - .frame(height: geomerty.size.height) - .cornerRadius(3) - } - - Text(coachingResponse.reason) - .font(Font.custom("Pretendard", size: 14).weight(.regular)) - .foregroundStyle(.black) - .padding(12) - } - .fixedSize(horizontal: false, vertical: true) - .padding(.leading, 18) - .padding(.trailing, 18) - } - } -} - -//#Preview { -// CoachingExplanationView() -//} diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CustomSegmentedControl.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CustomSegmentedControl.swift deleted file mode 100644 index 9576441a..00000000 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/CustomSegmentedControl.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// CustomSegmentedControl.swift -// Smeem-iOS -// -// Created by Joon Baek on 2024/10/22. -// - -import SwiftUI - -struct CustomSegmentedControl: View { - @Binding var selectedIndex: Int - let options: [String] - - var body: some View { - HStack(spacing: 0) { - ForEach(options.indices, id: \.self) { index in - SegmentButton( - title: options[index], - isSelected: selectedIndex == index, - action: { selectedIndex = index } - ) - } - } - .background(Color.gray.opacity(0.2)) - .cornerRadius(8) - } -} - -struct SegmentButton: View { - let title: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - Text(title) - .padding(.vertical, 8) - .padding(.horizontal, 11) - .frame(maxWidth: .infinity) - .background(backgroundColor) - .foregroundColor(foregroundColor) - .font(Font(UIFont.c5)) - } - } - - private var backgroundColor: Color { - isSelected ? (isCoachingOn ? Color(UIColor.point) : Color(UIColor.gray200)) : Color(UIColor.gray100) - } - - private var foregroundColor: Color { - isSelected ? .white : Color(UIColor.gray500) - } - - private var isCoachingOn: Bool { - title == "코칭 ON" - } -} - -struct PreviewWrapper: View { - @State private var selectedIndex = 0 - let options = ["코칭 OFF", "코칭 ON"] - - var body: some View { - CustomSegmentedControl(selectedIndex: $selectedIndex, options: options) - .frame(height: 40) - .padding(117) - } -} - -@available(iOS 17.0, *) -#Preview { - PreviewWrapper() -} - diff --git a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/SwiftUINavigationView.swift b/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/SwiftUINavigationView.swift deleted file mode 100644 index 0c80a744..00000000 --- a/Smeem-iOS/Smeem-iOS/Global/UIComponents/UIViewConponent/SwiftUINavigationView.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SwiftUINavigationView.swift -// Smeem-iOS -// -// Created by Joon Baek on 2024/10/18. -// - -import SwiftUI - -struct SwiftUINavigationView: View { - @State private var selectedIndex = 0 - let options = ["코칭 OFF", "코칭 ON"] - - var body: some View { - HStack() { - Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, - label: { - Image("icnBack") - .imageScale(.large) - }) - .padding(.leading, 10) - -// CustomSegmentedControl(selectedIndex: $selectedIndex, options: options) -// .frame(height: 32) -// .padding(65) - - Spacer() - - Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, - label: { - Text("닫기") - .tint(.black) - }) - .padding(.trailing, 18) - } - .frame(height: 66) - } -} - -#Preview { - SwiftUINavigationView() -} diff --git a/Smeem-iOS/Smeem-iOS/Network/API/DetailDiary/DetailDiaryEndPoint.swift b/Smeem-iOS/Smeem-iOS/Network/API/DetailDiary/DetailDiaryEndPoint.swift index e096fc93..d4753066 100644 --- a/Smeem-iOS/Smeem-iOS/Network/API/DetailDiary/DetailDiaryEndPoint.swift +++ b/Smeem-iOS/Smeem-iOS/Network/API/DetailDiary/DetailDiaryEndPoint.swift @@ -55,6 +55,7 @@ extension DetailDiaryEndPoint { "content": "일기 내용입니다", "createdAt": "2024년 5월 18일", "username": "찬미", + "isUpdated": true, "corrections": [ { "originalSentence": "original text", diff --git a/Smeem-iOS/Smeem-iOS/Network/NetworkDataModel/DetailDiary/DetailDiaryResponse.swift b/Smeem-iOS/Smeem-iOS/Network/NetworkDataModel/DetailDiary/DetailDiaryResponse.swift index 34473fc2..16bbe79e 100644 --- a/Smeem-iOS/Smeem-iOS/Network/NetworkDataModel/DetailDiary/DetailDiaryResponse.swift +++ b/Smeem-iOS/Smeem-iOS/Network/NetworkDataModel/DetailDiary/DetailDiaryResponse.swift @@ -11,17 +11,23 @@ struct DetailDiaryResponse: Codable, Equatable { var content: String let createdAt: String let username: String - let corrections: [CoachingResponse] + let isUpdated: Bool // JSON 키에 맞게 수정 + var corrections: [CoachingResponse] // JSON 구조에 맞게 수정 let correctionCount: Int let correctionMaxCount: Int -} -struct CorrentionsData: Codable { - let correntionId: Int - let before: String - let after: String + static func == (lhs: DetailDiaryResponse, rhs: DetailDiaryResponse) -> Bool { + return lhs.diaryId == rhs.diaryId + } } extension DetailDiaryResponse { - static let empty = DetailDiaryResponse(diaryId: 0, topic: "", content: "", createdAt: "", username: "", corrections: [CoachingResponse(originalSentence: "", correctedSentence: "", reason: "", isCorrected: true)], correctionCount: 3, correctionMaxCount: 3) + static let empty = DetailDiaryResponse(diaryId: 0, topic: "", content: "테스트임?", createdAt: "그래", username: "그래", isUpdated: true, corrections: [CoachingResponse(originalSentence: "I have went to the park yesterdayI have went to the park yesterdayI have went to the park yesterdayI have went to the park yesterday", + correctedSentence: "I went to the park yesterdayI went to the park yesterdayI went to the park yesterdayI went to the park yesterdayI went to the park yesterday", + reason: "현재완료 시제인 have went는 과거 시제인 went로 바꾸는 것이 맞습니다. yesterday와 함께 사용할 때는 단순 과거 시제를 사용해야 합니다.", + isCorrected: true), + CoachingResponse(originalSentence: "I have went to the park yesterdayI have went to the park yesterdayI have went럼뉴름ㄴ람ㄴㄹ 마넝롬나ㅣㅓㅇㄹ ㅗㅁ나ㅓㅇ롬나어롬나어롬나러ㅗㅁ나러 ㅗㄴ마러ㅗㅁ너ㅏ롬 ㄴ라ㅓ ㅗㄴㅁ라 왜 갑자 I have went to the park yesterdayI have went to the park yesterdayI have went to the park yesterdayI have went to the park yesterdayI have went to the park yesterday", + correctedSentence: "I went to the park yesterdayI went to the park yesterdayI went to the park yesterday", + reason: "이러 이러한 이유로 이건 맞습니다", + isCorrected: true)], correctionCount: 0, correctionMaxCount: 0) } diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Coaching/CoachingCompleteView.swift b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/CoachingCompleteView.swift deleted file mode 100644 index e22388f9..00000000 --- a/Smeem-iOS/Smeem-iOS/Presentation/Coaching/CoachingCompleteView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// CoachingCell.swift -// Smeem-iOS -// -// Created by 황찬미 on 11/16/24. -// - -import SwiftUI - -struct CoachingCompleteView: View { - - @Binding var diaryText: String - @Binding var coachingAppData: CoachingAppData - @State var currentIndex = 0 - - var body: some View { - - VStack(spacing: 15) { - VStack(spacing: 16) { - ScrollView { - HStack { - Text(diaryText) - .modifier(HighlightModifier( - diaryText: diaryText, - corrections: coachingAppData.corrections, - highlightIndex: currentIndex - )) - .font(Font.custom("Pretendard", size: 16)) - .foregroundColor(Color(UIColor.gray400)) - .lineSpacing(0.375) - - Spacer() - } - } - - Spacer() - } - .padding(.horizontal, screenWidth * 0.048) - - VStack(spacing: 20) { - Rectangle() - .frame(height: 8) - .foregroundStyle(Color(UIColor.gray100)) - TabView(selection: $currentIndex) { - ForEach(coachingAppData.corrections.indices, id: \.self) { item in - ScrollView { - VStack(spacing: 8) { - CoachingComparisonView(coachingResponse: $coachingAppData.corrections[item]) - - CoachingExplanationView(coachingResponse: $coachingAppData.corrections[item]) - } - } - } - } - .frame(width: screenWidth, height: screenHeight * (326/screenHeight), alignment: .top) - .tabViewStyle(.page(indexDisplayMode: .never)) - - PageControl(currentPage: $currentIndex, - coachingResponse: $coachingAppData.corrections) - } - } - } -} - -struct PageControl: View { - @Binding var currentPage: Int - @Binding var coachingResponse: [CoachingResponse] - - var body: some View { - HStack(spacing: 8) { - ForEach(coachingResponse.indices, id: \.self) { pagingIndex in - let isCurrentPage = currentPage == pagingIndex - - Capsule() - .fill(isCurrentPage ? .black : .gray) - .frame(width: 8.0, height: 8.0) - } - } - .animation(.linear, value: currentPage) - } -} - -#Preview { - @State var diaryText = "I watched Avatar with my boyfriend at Hongdae CGV. I should have skimmed the previous season 
what they were saying and the universe(??). What I was annoyed then was 두팔 didn’t know that as me. I think 두팔 who is my boyfriend should study before wathcing…. but Avatar2 is amazing movie I think. In my personal opinion, the jjin main character " - @State var coachingResponse = CoachingAppData(corrections: CoachingsResponse.sample.corrections, correctResultText: "테스트") - - CoachingCompleteView(diaryText: $diaryText, coachingAppData: $coachingResponse) -} diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Components/CoachingCompleteView.swift b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Components/CoachingCompleteView.swift new file mode 100644 index 00000000..55a2baab --- /dev/null +++ b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Components/CoachingCompleteView.swift @@ -0,0 +1,103 @@ +// +// CoachingCell.swift +// Smeem-iOS +// +// Created by 황찬미 on 11/16/24. +// + +import SwiftUI + +struct CoachingCompleteView: View { + + @Binding var coachingAppData: CoachingAppData + + var body: some View { + + VStack(spacing: 15) { + VStack(spacing: 16) { + ScrollView { + VStack(spacing: 16) { + HStack { + Text(coachingAppData.correctResultText) + .font(Font.custom("Pretendard", size: 16)).fontWeight(.regular) + .foregroundColor(Color(UIColor.black)) + .lineSpacing(0.375) + Spacer() + } + + HStack { + Text(coachingAppData.diaryText) + .modifier(HighlightModifier( + diaryText: coachingAppData.diaryText, + corrections: coachingAppData.corrections, + highlightIndex: coachingAppData.currentIndex + )) + .font(Font.custom("Pretendard", size: 16)) + .foregroundColor(coachingAppData.corrections.isEmpty + ? Color(UIColor.black) + : Color(UIColor.gray400)) + .lineSpacing(0.375) + + Spacer() + } + } + } + + Spacer() + } + .padding(.horizontal, screenWidth * 0.048) + + // 코칭 일기가 있을 때만 보여짐. + if !coachingAppData.corrections.isEmpty { + VStack(spacing: 20) { + Rectangle() + .frame(height: 8) + .foregroundStyle(Color(UIColor.gray100)) + TabView(selection: $coachingAppData.currentIndex) { + ForEach(coachingAppData.corrections.indices, id: \.self) { item in + ScrollView { + VStack(spacing: 8) { + CoachingComparisonView(coachingResponse: $coachingAppData.corrections[item]) + + CoachingExplanationView(coachingResponse: $coachingAppData.corrections[item]) + } + } + } + } + .frame(width: screenWidth, height: screenHeight * (326/screenHeight), alignment: .top) + .tabViewStyle(.page(indexDisplayMode: .never)) + + PageControl(currentPage: $coachingAppData.currentIndex, + coachingResponse: $coachingAppData.corrections) + } + } + } + } +} + +struct PageControl: View { + @Binding var currentPage: Int + @Binding var coachingResponse: [CoachingResponse] + + var body: some View { + HStack(spacing: 8) { + ForEach(coachingResponse.indices, id: \.self) { pagingIndex in + let isCurrentPage = currentPage == pagingIndex + + Capsule() + .fill(isCurrentPage ? .black : .gray) + .frame(width: 8.0, height: 8.0) + } + } + .animation(.linear, value: currentPage) + } +} + +//#Preview { +// @State var diaryText = "I watched Avatar with my boyfriend at Hongdae CGV. I should have skimmed the previous season 
what they were saying and the universe(??). What I was annoyed then was 두팔 didn’t know that as me. I think 두팔 who is my boyfriend should study before wathcing…. but Avatar2 is amazing movie I think. In my personal opinion, the jjin main character " +// @State var coachingResponse = CoachingAppData(diaryText: "", +// corrections: CoachingsResponse.sample.corrections, +// correctResultText: "테스트") +// +// CoachingCompleteView(coachingAppData: coachingResponse, currentIndex: 0) +//} diff --git a/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/DiaryDetailView.swift b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Components/DiaryDetailView.swift similarity index 100% rename from Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/DiaryDetailView.swift rename to Smeem-iOS/Smeem-iOS/Presentation/Coaching/Components/DiaryDetailView.swift diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Store/CoachingStore.swift b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Store/CoachingStore.swift new file mode 100644 index 00000000..bebbfee0 --- /dev/null +++ b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/Store/CoachingStore.swift @@ -0,0 +1,126 @@ +// +// CoachingInteractor.swift +// Smeem-iOS +// +// Created by 황찬미 on 11/18/24. +// + +import Foundation + +struct CoachingAppData { + var currentIndex: Int + var diaryText: String + var corrections: [CoachingResponse] + var correctResultText: String +} + +enum CoachingAmplitude { + case coachingButtonTapped(Bool) + case exitButtonTapped(Bool) + case coachingLoading + case coachingResult + case coachingSwipe(Int) +} + +final class CoachingStore: Store, ObservableObject { + + var service: CoachingServiceProtocol + @Published var state: State + + init(service: CoachingServiceProtocol, + diaryResponse: PostDiaryResponse) { + self.service = service + self.state = State(diaryResponse: diaryResponse) + } + + enum Action { + case detailDiaryAPI(diaryID: Int) + case coachingButton(diaryID: Int) + case amplitudeInput(type: CoachingAmplitude) + } + + struct State { + var detailDiaryResponse = DetailDiaryResponse.empty + var coachingAppData = CoachingAppData(currentIndex: 0, + diaryText: "", + corrections: CoachingsResponse.sample.corrections, + correctResultText: "첨삭 중이에요") + var toastErrorMessage: SmeemError? = nil + var toastMessage: SmeemToast? = .completed + + var diaryResponse: PostDiaryResponse + + var hiddenIndex: Int = 0 + var isEnabled: Bool = true + var isLoadingView: Bool = true + } + + func send(action: Action) { + switch action { + case .detailDiaryAPI(let ID): + Task { + do { + state.detailDiaryResponse = try await service.detailDiaryAPI(diaryID: ID) + state.isEnabled = state.detailDiaryResponse.correctionMaxCount-state.detailDiaryResponse.correctionCount == 0 ? false : true + state.isLoadingView = false + } catch let error { + let error = error as? SmeemError + state.toastErrorMessage = error + state.isLoadingView = false + } + } + case .coachingButton(let ID): + Task { + do { + state.hiddenIndex += 1 + let coachingResponse = try await service.coachingPostAPI(diaryID: ID) + state.coachingAppData = CoachingAppData(currentIndex: 0, + diaryText: combineCorrectionText(coachingResponse.corrections), + corrections: filiterCorrection(coachingResponse.corrections) ?? [], + correctResultText: correctTextResult(filiterCorrection(coachingResponse.corrections) ?? [])) + state.hiddenIndex += 1 + } catch let error { + let error = error as? SmeemError + state.toastErrorMessage = error + state.hiddenIndex = 0 + } + } + case .amplitudeInput(let type): + switch type { + case .coachingButtonTapped(let isActive): + AmplitudeManager.shared.track(event: AmplitudeConstant.coaching.coaching_try_click(isActive).event) + case .exitButtonTapped(let isActive): + AmplitudeManager.shared.track(event: AmplitudeConstant.coaching.coaching_exit_click(isActive).event) + case .coachingLoading: + AmplitudeManager.shared.track(event: AmplitudeConstant.coaching.coaching_load_view.event) + case .coachingResult: + AmplitudeManager.shared.track(event: AmplitudeConstant.coaching.coaching_result_view.event) + case .coachingSwipe(let index): + AmplitudeManager.shared.track(event: AmplitudeConstant.coaching.coaching_feedback_view(index+1).event) + } + } + } + + func combineCorrectionText(_ response: [CoachingResponse]) -> String { + return response.map{ $0.originalSentence }.joined(separator: " ") + } + + func filiterCorrection(_ response: [CoachingResponse]) -> [CoachingResponse]? { + return response.filter { $0.isCorrected }.prefix(10).map{$0} + } + + func correctTextResult(_ response: [CoachingResponse]) -> String { + switch response.count { + case 0: + return "완벽한 일기예요!👍\n문장이 자연스럽고 오류가 없어요" + case 1: + return "잘 작성했어요!🙌\n작은 부분만 다듬으면 완벽해요" + case 2...: + return "대단해요!🥳🎉\n몇 가지 피드백을 준비해 봤어요." + default: + return "완벽한 일기예요!👍\n문장이 자연스럽고 오류가 없어요" + } + } +} + + diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Coaching/View/CoachingView.swift b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/View/CoachingView.swift new file mode 100644 index 00000000..a1f27ac1 --- /dev/null +++ b/Smeem-iOS/Smeem-iOS/Presentation/Coaching/View/CoachingView.swift @@ -0,0 +1,123 @@ +// +// DiaryCompleteView.swift +// Smeem-iOS +// +// Created by 황찬미 on 11/17/24. +// + +import SwiftUI +import LottieUI + +struct CoachingView: View { + + @StateObject var store: CoachingStore + + var body: some View { + // MARK: 네비뷰 + VStack { + if store.state.hiddenIndex != 1 { + HStack() { + Spacer() + Button(action: { + store.send(action: .amplitudeInput(type: .exitButtonTapped(store.state.isEnabled))) + let homeVC = HomeViewController() + homeVC.handlePostDiaryAPI(with: store.state.diaryResponse) + changeRootViewController(homeVC) + }, + label: { + Text("닫기") + .tint(.black) + }) + .padding(.trailing, 18) + } + .frame(height: 66) + } + + // MARK: 일기 작성 완료 화면 + if store.state.hiddenIndex == 0 { + Button(action: { + if store.state.isEnabled { + store.send(action: .coachingButton(diaryID: store.state.diaryResponse.diaryID)) + store.send(action: .amplitudeInput(type: .coachingButtonTapped(store.state.isEnabled))) + } + }) { + HStack { + Image("icnCrownMono") + .frame(width: 24, height: 24) + Text("하루 한 번 무료 AI 코칭") + .font(Font.custom("Pretendard", size: 16).weight(.bold)) + .lineSpacing(0.19) + .foregroundColor(.white) + } + } + .disabled(!store.state.isEnabled) + .frame(width: screenWidth-32, height: 48, alignment: .center) + .background { + if store.state.isEnabled { + LinearGradient( + stops: [ + Gradient.Stop(color: Color(red: 1, green: 0, blue: 0.02).opacity(0.2), location: 0.00), + Gradient.Stop(color: .white.opacity(0.2), location: 0.28), + Gradient.Stop(color: .white.opacity(0.2), location: 0.83), + Gradient.Stop(color: Color(red: 1, green: 0, blue: 0.02).opacity(0.2), location: 1.00), + ], + startPoint: UnitPoint(x: 0.15, y: -1.24), + endPoint: UnitPoint(x: 0.72, y: 3.4) + ) + } + } + .background(store.state.isEnabled ? Color(UIColor.point) : Color(UIColor.gray400)) + .cornerRadius(5) + + DiaryDetailView(diaryInformation: $store.state.detailDiaryResponse) + + Spacer() + + // MARK: 로티 화면 + } else if store.state.hiddenIndex == 1 { + VStack { + LottieView("smeemLoading") + .loopMode(.loop) + .frame(width: screenWidth, height: 164, alignment: .center) + .onAppear { + store.send(action: .amplitudeInput(type: .coachingLoading)) + } + + Text("AI 코치가 내 일기를 분석하고 있어요\n잠시만 기다려주세요") + .font(Font.custom("Pretendard", size: 16)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + } + + // MARK: 첨삭 화면 + } else { + CoachingCompleteView(coachingAppData: $store.state.coachingAppData) + .onAppear { + store.send(action: .amplitudeInput(type: .coachingResult)) + store.send(action: .amplitudeInput(type: .coachingSwipe(1))) + } + .onChange(of: store.state.coachingAppData.currentIndex) { index in + store.send(action: .amplitudeInput(type: .coachingSwipe(index))) + } + } + + // MARK: 첫 진입시 토스트뷰 실행 + SmemeToastView(type: $store.state.toastMessage) + SmeemErrorToastView(type: $store.state.toastErrorMessage) + } + .overlay(alignment: .center) { + // MARK: 최상단바에 로딩뷰 + if store.state.isLoadingView { + SmemeEmptyView() + SmemeLoadingView() + } + } + .onAppear() { + store.send(action: .detailDiaryAPI(diaryID: store.state.diaryResponse.diaryID)) + } + } +} + +#Preview { + CoachingView(store: CoachingStore(service: CoachingService(), diaryResponse: PostDiaryResponse.empty)) +} diff --git a/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryCoachedView.swift b/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryCoachedView.swift index 94b6eb5e..8d11396f 100644 --- a/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryCoachedView.swift +++ b/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryCoachedView.swift @@ -5,46 +5,217 @@ // Created by Joon Baek on 11/21/24. // +import Combine import SwiftUI +import UIKit struct DetailDiaryCoachedView: View { - @Binding var diaryText: String - @Binding var coachingResponse: CoachingsResponse + // MARK: - Properties + + @StateObject private var navigationViewModel = NavigationViewModel() + @State private var cancelBag = Set() + + @State private var response: DetailDiaryResponse? + @State private var isLoading = false + @State private var onError = false + @State private var error: SmeemError? + + @Binding var diaryID: Int? + @State var diaryContent = "" + @State var randomTopic = "" @State var currentIndex = 0 + @State private var filteredCorrections: [CoachingResponse] = [] + @State private var isShowingFloatingButtons = false @State private var selectedIndex = 0 + @State private var navigationbarType: NavigationbarType = .diaryDetails + + @State private var toastErrorMessage: SmeemError? = nil + var toastMessage: SmeemToast? = .completed + var hasCorrections: Bool { + return !(response?.corrections.isEmpty ?? true) + } + @Environment(\.dismiss) private var dismiss + + private let detailDiaryService = DetailDiaryService.shared + + // MARK: - Body var body: some View { - VStack(spacing: screenWidth * (16 / screenWidth)) { - SwiftUINavigationView(navigationbarType: .diaryDetails, - selectedIndex: $selectedIndex) - - ScrollableDiaryView( - diaryText: diaryText, - corrections: coachingResponse.corrections, - currentIndex: currentIndex, - selectedIndex: selectedIndex, - dateText: "2023년 3월 27일 4:18PM", - authorText: "유진이" + VStack(spacing: 0) { + SwiftUINavigationView(navigationViewModel: navigationViewModel, + selectedIndex: $selectedIndex, + navigationbarType: navigationbarType ) + .onReceive(navigationViewModel.leftButtonTapped) { + dismiss() + } + .onReceive(navigationViewModel.rightButtonTapped) { + isShowingFloatingButtons = true + } + .confirmationDialog("", isPresented: $isShowingFloatingButtons) { + Button("수정하기", role: .none) { + if navigationbarType == .diaryDetails { + showEditConfirmation( + title: "수정 확인", + message: "수정시 모든 코칭 내용이 사라집니다. 그래도 수정하시겠습니까?", + firstActionTitle: "취소", + secondActionTitle: "확인", + firstActionHandler: { }, + secondActionHandler: { + navigateToEditDiary() + } + ) + } else { + navigateToEditDiary() + } + } + .onTapGesture { + AmplitudeManager.shared.track(event: AmplitudeConstant.diaryDetail.mydiary_edit(hasCorrections).event) + } + + Button("삭제하기", role: .destructive) { + deleteDiaryWithAPI(diaryID: diaryID ?? 0) + } + + Button("취소", role: .cancel) { + isShowingFloatingButtons = false + } + } + + if let topic = response?.topic { + if topic != "" { + RandomTopicViewSwiftUI(contentText: response?.topic) + } + } + + if let response = response { + ScrollableDiaryView( + diaryText: response.content, + corrections: filteredCorrections, + currentIndex: currentIndex, + selectedIndex: selectedIndex, + dateText: response.createdAt, + authorText: response.username + ) + } else { + if isLoading { + SmemeEmptyView() + SmemeLoadingView() + } + } // "코칭 ON"일 때만 표시 if selectedIndex == 1 { - Spacer() -// CoachingContentView(currentIndex: $currentIndex, -// coachingResponse: $coachingResponse) - } else { - Spacer(minLength: screenHeight * (342 / screenHeight)) + Spacer() + CoachingContentView( + currentIndex: $currentIndex, + detailDiaryResponse: Binding( + get: { self.response ?? .empty }, + set: { _ in } + ), + corrections: $filteredCorrections + ) + } else { + Spacer() + } + } + .onAppear { + AmplitudeManager.shared.track(event: AmplitudeConstant.diaryDetail.mydiary_view(hasCorrections).event) + Task { + await fetchCoachingData(diaryID: diaryID ?? 0) + } + } + .onChange(of: selectedIndex) { newValue in + AmplitudeManager.shared.track(event: AmplitudeConstant.diaryDetail.toggle_click(convertSelectedIndexToString(newValue)).event + ) + } + .overlay { + if isLoading { + SmemeEmptyView() + SmemeLoadingView() + } + + if onError { + ZStack { + Color.clear + VStack { + Spacer() + SmeemErrorToastView(type: $toastErrorMessage) + .padding(.bottom, 20.scaledByHeight()) + } + } + .onDisappear { + onError = false + } } } } } -@available(iOS 17, *) -#Preview { - @State var diaryText = "I watched Avatar with my boyfriend at Hongdae CGV. I should have skimmed the previous season - Avatar1.. I really couldn’t get what they weere saying and the universe(??). What I was annoyed then was 두팔 didn’t know that as me. I think 두팔 who is my boyfriend should study before wathcing…. but Avatar2 is amazing movie I think. In my personal opinion, the jjin main character of Avatar2 is not Sully, but his son." +// MARK: - Extension + +extension DetailDiaryCoachedView { + func filterCorrection(_ response: DetailDiaryResponse) -> [CoachingResponse] { + return response.corrections.filter { $0.isCorrected }.prefix(10).map{$0} + } + + private func navigateToEditDiary() { + let editVC = EditDiaryViewController() + editVC.diaryID = self.diaryID ?? 0 + editVC.randomContent = self.randomTopic + editVC.diaryTextView.text = self.diaryContent + editVC.hasCoached = self.hasCorrections + editVC.randomSubjectView.setData(contentText: self.randomTopic) + self.pushToUIKitView(editVC) + } - @State var coachingResponse = CoachingsResponse.sample + private func convertSelectedIndexToString(_ index: Int) -> String { + return index == 0 ? "코칭 OFF" : "코칭 ON" + } + + @MainActor + private func fetchCoachingData(diaryID: Int) async { + isLoading = true + do { + let response = try await detailDiaryService.getDetailDiary(diaryID: diaryID) + self.response = response + self.diaryContent = response.content + self.randomTopic = response.topic + + self.filteredCorrections = filterCorrection(response) + + if filteredCorrections.isEmpty { + navigationbarType = .unCoached + } + + isLoading = false + } catch { + isLoading = false + self.error = error as? SmeemError + self.onError = true + } + } - DetailDiaryCoachedView(diaryText: $diaryText, coachingResponse: $coachingResponse) + func deleteDiaryWithAPI(diaryID: Int) { + SmeemLoadingView.showLoading() + + detailDiaryService.deleteDiary(diaryID: diaryID) { result in + + switch result { + case .success(_): + let homeVC = HomeViewController() + self.changeRootViewControllerAndPresent(homeVC) + case .failure(let error): + toastErrorMessage = error + break + } + SmeemLoadingView.hideLoading() + } + } +} + +@available(iOS 17, *) +#Preview { + DetailDiaryCoachedView(diaryID: .constant(0)) } diff --git a/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryViewController.swift b/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryViewController.swift index 2d067606..80bad1d1 100644 --- a/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryViewController.swift +++ b/Smeem-iOS/Smeem-iOS/Presentation/DetailDiary/DetailDiaryViewController.swift @@ -82,7 +82,6 @@ extension DetailDiaryViewController { @objc func showActionSheet() { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let modifyAction = UIAlertAction (title: "수정", style: .default, handler: { (action) in - AmplitudeManager.shared.track(event: AmplitudeConstant.diaryDetail.mydiary_edit.event) let editVC = EditDiaryViewController() editVC.diaryID = self.diaryId editVC.randomContent = self.isRandomTopic diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Diaries/View/Controllers/EditDiaryViewController.swift b/Smeem-iOS/Smeem-iOS/Presentation/Diaries/View/Controllers/EditDiaryViewController.swift index 99d9633b..896841ee 100644 --- a/Smeem-iOS/Smeem-iOS/Presentation/Diaries/View/Controllers/EditDiaryViewController.swift +++ b/Smeem-iOS/Smeem-iOS/Presentation/Diaries/View/Controllers/EditDiaryViewController.swift @@ -15,6 +15,7 @@ final class EditDiaryViewController: BaseViewController { var diaryID = Int() var randomContent = String() + var hasCoached = Bool() // MARK: - UI Property @@ -48,7 +49,7 @@ final class EditDiaryViewController: BaseViewController { lazy var diaryTextView: UITextView = { let textView = UITextView() - textView.text = "dafd???" + textView.text = "목데이터" textView.configureDiaryTextView(topInset: 20) textView.configureAttributedText() textView.delegate = self @@ -169,7 +170,8 @@ extension EditDiaryViewController { func patchDiaryAPI() { PostDiaryAPI.shared.patchDiary(param: PatchDiaryRequest(content: diaryTextView.text), diaryID: diaryID) { response in DispatchQueue.main.async { - self.navigationController?.popViewController(animated: true) + AmplitudeConstant.diaryDetail.mydiary_edit_complete_click(self.hasCoached).event + self.changeRootViewControllerAndPresent(HomeViewController()) } } } @@ -193,3 +195,8 @@ extension EditDiaryViewController: UITextViewDelegate { return viewController.diaryTextView.text.getArrayAfterRegex(regex: "[a-zA-z]").count > 0 } } + +//@available(iOS 17, *) +//#Preview { +// EditDiaryViewController() +//} diff --git a/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/CoachingStore.swift b/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/CoachingStore.swift deleted file mode 100644 index 959b925e..00000000 --- a/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/CoachingStore.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// CoachingInteractor.swift -// Smeem-iOS -// -// Created by 황찬미 on 11/18/24. -// - -import Foundation - -struct CoachingAppData { - var corrections: [CoachingResponse] - var correctResultText: String -} - -final class CoachingStore: Store, ObservableObject { - - var service: CoachingServiceProtocol - @Published var state: State - - init(service: CoachingServiceProtocol, - diaryResponse: PostDiaryResponse) { - self.service = service - self.state = State(diaryResponse: diaryResponse) - } - - enum Action { - case detailDiaryAPI(diaryID: Int) - case coachingButton(diaryID: Int) - } - - struct State { - var detailDiaryResponse = DetailDiaryResponse.empty - var coachingAppData = CoachingAppData(corrections: CoachingsResponse.sample.corrections, - correctResultText: "첨삭 중이에요") - var toastErrorMessage: SmeemError? = nil - var toastMessage: SmeemToast? = .completed - - var diaryResponse: PostDiaryResponse - - var hiddenIndex: Int = 0 - var isEnabled: Bool = true - var isLoadingView: Bool = true - } - - func send(action: Action) { - switch action { - case .detailDiaryAPI(let ID): - Task { - do { - state.detailDiaryResponse = try await service.detailDiaryAPI(diaryID: ID) - state.isEnabled = state.detailDiaryResponse.correctionMaxCount-state.detailDiaryResponse.correctionCount == 0 ? false : true - } catch let error { - let error = error as? SmeemError - state.toastErrorMessage = error - } - } - case .coachingButton(let ID): - Task { - do { - state.hiddenIndex += 1 - let coachingResponse = try await service.coachingPostAPI(diaryID: ID) - state.coachingAppData = CoachingAppData(corrections: coachingResponse.corrections, - correctResultText: correctTextResult(coachingResponse.corrections.count)) - state.hiddenIndex += 1 - } catch let error { - let error = error as? SmeemError - state.toastErrorMessage = error - state.hiddenIndex = 0 - } - } - } - } - - func correctTextResult(_ count: Int) -> String { - switch count { - case 0: - return "완벽한 일기예요! 문장이 자연스럽고 오류가 없어요" - case 1: - return "잘 작성했어요! 작은 부분만 다듬으면 완벽해요" - case 2...: - return "대단해요! 몇가지 피드백을 준비해봤어요." - default: - return "대단해요! 몇가지 피드백을 준비해봤어요." - } - } -} - - diff --git a/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/CoachingView.swift b/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/CoachingView.swift deleted file mode 100644 index 2e1f211e..00000000 --- a/Smeem-iOS/Smeem-iOS/Presentation/DiaryComplete/CoachingView.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// DiaryCompleteView.swift -// Smeem-iOS -// -// Created by 황찬미 on 11/17/24. -// - -import SwiftUI -import LottieUI - -struct CoachingView: View { - - @StateObject var store: CoachingStore - - var body: some View { - - if store.state.hiddenIndex != 1 { - HStack() { - Spacer() - Button(action: { - let homeVC = HomeViewController() - homeVC.handlePostDiaryAPI(with: store.state.diaryResponse) - changeRootViewController(homeVC) - }, - label: { - Text("닫기") - .tint(.black) - }) - .padding(.trailing, 18) - } - .frame(height: 66) - } - - // MARK: 일기 작성 완료 화면 - if store.state.hiddenIndex == 0 { - Button(action: { - if store.state.isEnabled { - store.send(action: .coachingButton(diaryID: store.state.diaryResponse.diaryID)) - } - }) { - HStack { - Image("icnCrownMono") - .frame(width: 24, height: 24) - Text("하루 한 번 무료 AI 코칭") - .font(Font.custom("Pretendard", size: 16).weight(.bold)) - .lineSpacing(0.19) - .foregroundColor(.white) - } - } - .disabled(!store.state.isEnabled) - .frame(width: screenWidth-32, height: 48, alignment: .center) - .background { - if store.state.isEnabled { - LinearGradient( - stops: [ - Gradient.Stop(color: Color(red: 1, green: 0, blue: 0.02).opacity(0.2), location: 0.00), - Gradient.Stop(color: .white.opacity(0.2), location: 0.28), - Gradient.Stop(color: .white.opacity(0.2), location: 0.83), - Gradient.Stop(color: Color(red: 1, green: 0, blue: 0.02).opacity(0.2), location: 1.00), - ], - startPoint: UnitPoint(x: 0.15, y: -1.24), - endPoint: UnitPoint(x: 0.72, y: 3.4) - ) - } - } - .background(store.state.isEnabled ? Color(UIColor.point) : Color(UIColor.gray400)) - .cornerRadius(5) - - DiaryDetailView(diaryInformation: $store.state.detailDiaryResponse) - .onAppear { - store.send(action: .detailDiaryAPI(diaryID: store.state.diaryResponse.diaryID)) - } - - Spacer() - - // MARK: 로티 화면 - } else if store.state.hiddenIndex == 1 { - VStack { - LottieView("smeemLoading") - .loopMode(.loop) - .frame(width: screenWidth, height: 164, alignment: .center) - - Text("AI 코치가 내 일기를 분석하고 있어요\n잠시만 기다려주세요") - .font(Font.custom("Pretendard", size: 16)) - .multilineTextAlignment(.center) - .foregroundColor(.black) - } - - // MARK: 첨삭 화면 - } else { - CoachingCompleteView(diaryText: $store.state.detailDiaryResponse.content, - coachingAppData: $store.state.coachingAppData) - } - - // MARK: 첫 진입시 토스트뷰 실행 - SmemeToastView(type: $store.state.toastMessage) - SmeemErrorToastView(type: $store.state.toastErrorMessage) - } -} - -#Preview { - CoachingView(store: CoachingStore(service: CoachingService(), diaryResponse: PostDiaryResponse.empty)) -} diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Home/HomeViewController.swift b/Smeem-iOS/Smeem-iOS/Presentation/Home/HomeViewController.swift index 70cc60e1..e0eb102b 100644 --- a/Smeem-iOS/Smeem-iOS/Presentation/Home/HomeViewController.swift +++ b/Smeem-iOS/Smeem-iOS/Presentation/Home/HomeViewController.swift @@ -5,6 +5,7 @@ // Created by 임주민 on 2023/05/05. // +import SwiftUI import UIKit import Combine @@ -252,10 +253,20 @@ final class HomeViewController: BaseViewController { } } - @objc func fullViewButtonDidTap(_ gesture: UITapGestureRecognizer) { - let detailDiaryVC = DetailDiaryViewController() - detailDiaryVC.diaryId = homeDiaryDict[currentDate.toString("yyyy-MM-dd")]?.diaryId ?? 0 - self.navigationController?.pushViewController(detailDiaryVC, animated: true) + @objc func fullViewButtonDidTap(_ gesture: UITapGestureRecognizer) { + let diaryID = homeDiaryDict[currentDate.toString("yyyy-MM-dd")]?.diaryId ?? 0 + + @State var defaultIndex = 0 + + let detailDiaryVC = DetailDiaryCoachedView(diaryID: .constant(diaryID)) + let hostingController = UIHostingController(rootView: detailDiaryVC) + + addChild(hostingController) + hostingController.view.frame = view.bounds + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + self.navigationController?.pushViewController(hostingController, animated: true) } @objc func floatingViewDidTap(_ gesture: UITapGestureRecognizer) { diff --git a/Smeem-iOS/Smeem-iOS/Presentation/Splash/Splah/ViewModel/SplashViewModel.swift b/Smeem-iOS/Smeem-iOS/Presentation/Splash/Splah/ViewModel/SplashViewModel.swift index 1949b5f1..f1cc663e 100644 --- a/Smeem-iOS/Smeem-iOS/Presentation/Splash/Splah/ViewModel/SplashViewModel.swift +++ b/Smeem-iOS/Smeem-iOS/Presentation/Splash/Splah/ViewModel/SplashViewModel.swift @@ -10,7 +10,7 @@ import Combine final class SplashViewModel: ViewModel { - private let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String struct Input { let checkUpdatePopup: PassthroughSubject @@ -52,7 +52,6 @@ final class SplashViewModel: ViewModel { switch result { case .success(let response): if self.checkVersion(client: self.appVersion, - now: response.iosVersion.version, force: response.iosVersion.forceVersion) { promise(.success(UpdateTextModel(title: response.title, @@ -133,7 +132,24 @@ final class SplashViewModel: ViewModel { } extension SplashViewModel { - func checkVersion(client: String, now: String, force: String) -> Bool { + func checkVersion(client: String, force: String) -> Bool { + let clientVersion = client.split(separator: ".").map{$0} + let forceVersion = force.split(separator: ".").map{$0} + + // x 버전이 더 크면 강제 업데이트 + if forceVersion[0] > clientVersion[0] { + return true + // x 버전이 같고, y 버전이 더 크면 강제 업데이트 + } else if forceVersion[0] == clientVersion[0] && forceVersion[1] > clientVersion[1] { + return true + // x, y 버전이 같고, z버전이 더 크면 강제 업데이트 + } else if (forceVersion[0] == clientVersion[0] && forceVersion[1] > clientVersion[1]) && forceVersion[2] > clientVersion[2] { + return true + } + return false + } + + func checkVersion2(client: String, now: String, force: String) -> Bool { let clientVersion = client.split(separator: ".").map{$0} let nowVersion = now.split(separator: ".").map{$0} let forceVersion = force.split(separator: ".").map{$0} @@ -147,5 +163,6 @@ extension SplashViewModel { return false } } + } diff --git a/Smeem-iOS/Smeem-iOSTests/Coaching/Service/CoachingServiceTest.swift b/Smeem-iOS/Smeem-iOSTests/Coaching/Service/CoachingServiceTest.swift index cf154ba6..8be821d0 100644 --- a/Smeem-iOS/Smeem-iOSTests/Coaching/Service/CoachingServiceTest.swift +++ b/Smeem-iOS/Smeem-iOSTests/Coaching/Service/CoachingServiceTest.swift @@ -51,6 +51,7 @@ extension CoachingServiceTest { content: "일기 내용입니다", createdAt: "2024년 5월 18일", username: "찬미", + isUpdated: true, corrections: [CoachingResponse(originalSentence: "original text", correctedSentence: "corrected text", reason: "수정된 문구입니다.", diff --git a/Smeem-iOS/Smeem-iOSTests/Coaching/Store/CoachingStoreTest.swift b/Smeem-iOS/Smeem-iOSTests/Coaching/Store/CoachingStoreTest.swift new file mode 100644 index 00000000..cc408a58 --- /dev/null +++ b/Smeem-iOS/Smeem-iOSTests/Coaching/Store/CoachingStoreTest.swift @@ -0,0 +1,89 @@ +// +// CoachingStore.swift +// Smeem-iOSTests +// +// Created by 황찬미 on 12/10/24. +// + +import XCTest + +@testable import Smeem_iOS + +final class CoachingStoreTest: XCTestCase { + + var sut: CoachingStore! + var service: CoachingServiceProtocol! + + override func setUpWithError() throws { + service = CoachingService() + sut = CoachingStore(service: service, + diaryResponse: PostDiaryResponse.empty) + } + + override func tearDownWithError() throws { + sut = nil + } + + func test_첨삭데이터15개일때_10개로잘필터하는지() { + // Given + var filterResult = [CoachingResponse]() + for _ in 1...15 { + filterResult.append(CoachingResponse(originalSentence: "", + correctedSentence: "", + reason: "", + isCorrected: true)) + } + + var expectedResult = [CoachingResponse]() + for _ in 1...10 { + expectedResult.append(CoachingResponse(originalSentence: "", + correctedSentence: "", + reason: "", + isCorrected: true)) + } + + // When + let outputResult = sut.filiterCorrection(filterResult) + + // Then + XCTAssertEqual(outputResult, expectedResult) + } + + func test_첨삭데이터0개일때_0개로잘필터하는지() { + // Given + var filterResult = [CoachingResponse]() + for _ in 1...15 { + filterResult.append(CoachingResponse(originalSentence: "", + correctedSentence: "", + reason: "", + isCorrected: false)) + } + + let expectedResult = [CoachingResponse]() + + // When + let outputResult = sut.filiterCorrection(filterResult) + + // Then + XCTAssertEqual(outputResult, expectedResult) + } + + func test_원문일기_잘조합해주는지() { + // Given + var filterResult = [CoachingResponse]() + for _ in 1...3 { + filterResult.append(CoachingResponse(originalSentence: "테스트", + correctedSentence: "", + reason: "", + isCorrected: false)) + } + + let expectedResult = "테스트 테스트 테스트" + + // When + let outputResult = sut.combineCorrectionText(filterResult) + + // Then + XCTAssertEqual(outputResult, expectedResult) + } +} diff --git a/Smeem-iOS/Smeem-iOSTests/Onboarding/ViewModel/SplashViewModelTest.swift b/Smeem-iOS/Smeem-iOSTests/Onboarding/ViewModel/SplashViewModelTest.swift index 0e784a7d..24f72215 100644 --- a/Smeem-iOS/Smeem-iOSTests/Onboarding/ViewModel/SplashViewModelTest.swift +++ b/Smeem-iOS/Smeem-iOSTests/Onboarding/ViewModel/SplashViewModelTest.swift @@ -14,15 +14,17 @@ final class SplashViewModelTest: XCTestCase { private var viewModel: SplashViewModel! private var mockService: SplashServiceMock! + private var appVersion: String! override func setUpWithError() throws { self.mockService = SplashServiceMock() self.viewModel = SplashViewModel(provider: mockService) + self.appVersion = viewModel.appVersion } func test_강제업데이트안한유저_정확한데이터_return하는지() { // Given - let result = self.viewModel.checkVersion(client: "2.0.0", now: "2.0.1", force: "3.0.0") + let result = self.viewModel.checkVersion(client: "2.0.3", force: "3.0.0") // When let expectedResult = true @@ -33,7 +35,7 @@ final class SplashViewModelTest: XCTestCase { func test_강제업데이트로하고온유저_정확한데이터_return하는지() { // Given - let result = self.viewModel.checkVersion(client: "2.0.1", now: "2.0.1", force: "3.0.0") + let result = self.viewModel.checkVersion(client: self.appVersion, force: "3.0.0") // When let expectedResult = false @@ -42,12 +44,12 @@ final class SplashViewModelTest: XCTestCase { XCTAssertEqual(result, expectedResult) } - func test_강제업데이트하지않아도되는유저_정확한데이터_return하는지() { + func test_이전업데이트로직유저_강제업데이트팝업잘뜨는지() { // Given - let result = self.viewModel.checkVersion(client: "2.0.1", now: "2.0.1", force: "2.0.0") + let result = self.viewModel.checkVersion2(client: "2.0.3", now: "2.0.4", force: "3.0.0") // When - let expectedResult = false + let expectedResult = true // Then XCTAssertEqual(result, expectedResult)