diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 0ff6c64..8a6aadf 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -31,11 +31,6 @@ public class ChatViewController: BaseViewController { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(true, animated: false) - } public override func viewDidLoad() { super.viewDidLoad() diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index 0beaa11..1b05ba4 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -27,8 +27,8 @@ 736D1C5EB53FB204B030C397 /* DiaryAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179BF0E6EE3B59F510B8AF40 /* DiaryAddView.swift */; }; 7D319882A302F75CCE46A48C /* HomeQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A77FC9A0E2D7F62CA16E90 /* HomeQuizView.swift */; }; 7DC59B80630854028C7C80F4 /* TuistBundle+CommonUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94EF204F3CD6F86C6167C300 /* TuistBundle+CommonUI.swift */; }; - 7DD1D21C569B040F1AB7067B /* DiaryResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EEBE047F5F8EC16F0985204 /* DiaryResultView.swift */; }; 8E0E6BDEA9574116ECBD55E4 /* ChatAnalysisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963B9323B265D5683B336EF2 /* ChatAnalysisView.swift */; }; + 95256BFF2E82F7F900AC3FE1 /* SpellingCategoryColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95256BFE2E82F7F800AC3FE1 /* SpellingCategoryColor.swift */; }; 9532C7A92E786B3C00B4BADE /* DiaryLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9532C7A82E786B3800B4BADE /* DiaryLoadingView.swift */; }; 956C4D852E76F05000E32F93 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D842E76F04A00E32F93 /* CalendarView.swift */; }; 956C4D892E76F0F500E32F93 /* DividerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D882E76F0F400E32F93 /* DividerView.swift */; }; @@ -93,10 +93,10 @@ 75EF1CA00DA4B6E64F0CB1F0 /* CommonUIAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonUIAssets.swift; sourceTree = ""; }; 7CE59FBFD1AAA1B5E7B45FD5 /* HomeProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProgressView.swift; sourceTree = ""; }; 7DE79D12719B54383633DF73 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; - 7EEBE047F5F8EC16F0985204 /* DiaryResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryResultView.swift; sourceTree = ""; }; 83D64639CB805B71A9CA6C17 /* ChatAnalysisLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisLoadingView.swift; sourceTree = ""; }; 9266801EFBA6178C5B70C641 /* MyPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageView.swift; sourceTree = ""; }; 94EF204F3CD6F86C6167C300 /* TuistBundle+CommonUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TuistBundle+CommonUI.swift"; sourceTree = ""; }; + 95256BFE2E82F7F800AC3FE1 /* SpellingCategoryColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpellingCategoryColor.swift; sourceTree = ""; }; 9532C7A82E786B3800B4BADE /* DiaryLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryLoadingView.swift; sourceTree = ""; }; 956C4D842E76F04A00E32F93 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; 956C4D882E76F0F400E32F93 /* DividerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DividerView.swift; sourceTree = ""; }; @@ -174,6 +174,7 @@ 2DBBCE824C7AA1459F0CEA0F /* Enum */ = { isa = PBXGroup; children = ( + 95256BFE2E82F7F800AC3FE1 /* SpellingCategoryColor.swift */, 75EF1CA00DA4B6E64F0CB1F0 /* CommonUIAssets.swift */, ); path = Enum; @@ -260,7 +261,6 @@ FDCDDE79ED900C96E8D0EC4A /* DiaryEditView.swift */, 9532C7A82E786B3800B4BADE /* DiaryLoadingView.swift */, 95EDE8D42E7E60C50091ED75 /* DiaryDetailView.swift */, - 7EEBE047F5F8EC16F0985204 /* DiaryResultView.swift */, ACC4F396C35AD4C9F54E4BF6 /* DiaryTodayView.swift */, ); path = Diary; @@ -424,6 +424,7 @@ 21B9DB651F4D3F7DFB5CE055 /* LMTextField.swift in Sources */, E0865F2849CCB89CC83D3B77 /* Coordinator.swift in Sources */, E055AA66777B1D4CC8C884E4 /* CommonUIAssets.swift in Sources */, + 95256BFF2E82F7F900AC3FE1 /* SpellingCategoryColor.swift in Sources */, 956C4D852E76F05000E32F93 /* CalendarView.swift in Sources */, B9F3C0F9AA675E70FB8ED969 /* UIStackView+Extension.swift in Sources */, F151F3B566BCA8E16853B241 /* ChatAnalysisLoadingView.swift in Sources */, @@ -435,7 +436,6 @@ 736D1C5EB53FB204B030C397 /* DiaryAddView.swift in Sources */, 9532C7A92E786B3C00B4BADE /* DiaryLoadingView.swift in Sources */, F8B8A383C8670A714B41F2EE /* DiaryEditView.swift in Sources */, - 7DD1D21C569B040F1AB7067B /* DiaryResultView.swift in Sources */, 95EDE8D52E7E60CA0091ED75 /* DiaryDetailView.swift in Sources */, 568C33E681D9B58B71273564 /* DiaryTodayView.swift in Sources */, 318A2464888400F05D38B7E5 /* DiaryView.swift in Sources */, diff --git a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue.colorset/Contents.json b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue01.colorset/Contents.json similarity index 76% rename from Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue.colorset/Contents.json rename to Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue01.colorset/Contents.json index 1651786..ad76e7f 100644 --- a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue.colorset/Contents.json +++ b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue01.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xEA", - "red" : "0xD9" + "blue" : "0xEA", + "green" : "0x77", + "red" : "0x1A" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xEA", - "red" : "0xD9" + "blue" : "0xEA", + "green" : "0x77", + "red" : "0x1A" } }, "idiom" : "universal" diff --git a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue02.colorset/Contents.json b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue02.colorset/Contents.json index ad76e7f..1651786 100644 --- a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue02.colorset/Contents.json +++ b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMBlue02.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xEA", - "green" : "0x77", - "red" : "0x1A" + "blue" : "0xFF", + "green" : "0xEA", + "red" : "0xD9" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xEA", - "green" : "0x77", - "red" : "0x1A" + "blue" : "0xFF", + "green" : "0xEA", + "red" : "0xD9" } }, "idiom" : "universal" diff --git a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen01.colorset/Contents.json b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen01.colorset/Contents.json new file mode 100644 index 0000000..32fbe01 --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen01.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3D", + "green" : "0xAA", + "red" : "0x1F" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3D", + "green" : "0xAA", + "red" : "0x1F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen.colorset/Contents.json b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen02.colorset/Contents.json similarity index 100% rename from Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen.colorset/Contents.json rename to Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMGreen02.colorset/Contents.json diff --git a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMOrange00.colorset/Contents.json b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMOrange00.colorset/Contents.json new file mode 100644 index 0000000..75f48d9 --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMOrange00.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x11", + "green" : "0x67", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x11", + "green" : "0x67", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMPink.colorset/Contents.json b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMPink.colorset/Contents.json new file mode 100644 index 0000000..37b6757 --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Colors.xcassets/LMPointColor/LMPink.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB3", + "green" : "0x5B", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB3", + "green" : "0x5B", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/Contents.json b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/Contents.json new file mode 100644 index 0000000..48ee59a --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "next2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "next2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/next2@2x.png b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/next2@2x.png new file mode 100644 index 0000000..c6f1760 Binary files /dev/null and b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/next2@2x.png differ diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/next2@3x.png b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/next2@3x.png new file mode 100644 index 0000000..de9f909 Binary files /dev/null and b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/next2.imageset/next2@3x.png differ diff --git a/Projects/CommonUI/Sources/Component/LMAlert.swift b/Projects/CommonUI/Sources/Component/LMAlert.swift index 44a1e8c..4fb1a40 100644 --- a/Projects/CommonUI/Sources/Component/LMAlert.swift +++ b/Projects/CommonUI/Sources/Component/LMAlert.swift @@ -98,25 +98,46 @@ public class LMAlert: UIView { containerView.snp.makeConstraints { $0.center.equalToSuperview() $0.width.equalTo(280) - $0.height.equalTo(140) + $0.height.greaterThanOrEqualTo(140) } titleLabel.snp.makeConstraints { $0.top.equalToSuperview().inset(24) $0.horizontalEdges.equalToSuperview().inset(20) + $0.bottom.lessThanOrEqualTo(buttonStackView.snp.top).offset(-16) } buttonStackView.snp.makeConstraints { $0.bottom.equalToSuperview().inset(20) $0.horizontalEdges.equalToSuperview().inset(20) $0.height.equalTo(44) + $0.top.greaterThanOrEqualTo(titleLabel.snp.bottom).offset(16) } } private func configure(title: String, cancelTitle: String, confirmTitle: String) { - titleLabel.text = title + // 줄간격을 적용한 attributed string 생성 + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 4 + paragraphStyle.alignment = .center + + let attributedString = NSAttributedString( + string: title, + attributes: [ + .font: UIFont.systemFont(ofSize: 16, weight: .medium), + .foregroundColor: UIColor.black, + .paragraphStyle: paragraphStyle + ] + ) + + titleLabel.attributedText = attributedString cancelButton.setTitle(cancelTitle, for: .normal) confirmButton.setTitle(confirmTitle, for: .normal) + + // cancelTitle이 비어있으면 취소 버튼 숨기기 + if cancelTitle.isEmpty { + cancelButton.isHidden = true + } } // MARK: - Actions diff --git a/Projects/CommonUI/Sources/Component/LMInputField.swift b/Projects/CommonUI/Sources/Component/LMInputField.swift index fa26590..dcdcefd 100644 --- a/Projects/CommonUI/Sources/Component/LMInputField.swift +++ b/Projects/CommonUI/Sources/Component/LMInputField.swift @@ -24,18 +24,21 @@ public class LMInputField: UIStackView { private var waringText: String? private var inputType: InputType? private var buttonTitle: String? + private var isSecureTextEntry: Bool = false public init(inputType: InputType? = nil, inputText: String?, inputPlaceholder: String?, warningText: String?, - buttonTitle: String? = "" + buttonTitle: String? = "", + isSecureTextEntry: Bool = false ) { self.inputType = inputType self.inputText = inputText self.inputPlaceholder = inputPlaceholder self.waringText = warningText self.buttonTitle = buttonTitle + self.isSecureTextEntry = isSecureTextEntry super.init(frame: .zero) initUI() initAttribute() @@ -60,6 +63,7 @@ public class LMInputField: UIStackView { inputTextField = inputTextField.then { $0.placeholder = inputPlaceholder + $0.isSecureTextEntry = isSecureTextEntry } warningLabel = warningLabel.then { diff --git a/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift b/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift index f4ff5af..cdeae2c 100644 --- a/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift +++ b/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift @@ -56,9 +56,11 @@ public enum CommonUIAssets { public static let IconBubble = image(named: "bubble") public static let IconPrevious = image(named: "previous") public static let IconNext = image(named: "next") + public static let IconNext2 = image(named: "next2") public static let IconAdd = image(named: "add") /// color + public static let LMOrange0 = color(named: "LMOrange00") public static let LMOrange1 = color(named: "LMOrange01") public static let LMOrange3 = color(named: "LMOrange03") public static let LMOrange4 = color(named: "LMOrange04") @@ -69,11 +71,13 @@ public enum CommonUIAssets { public static let LMGray4 = color(named: "LMGray04") public static let LMGray5 = color(named: "LMGray05") public static let LMGray6 = color(named: "LMGray06") - public static let LMBlue = color(named: "LMBlue") + public static let LMBlue = color(named: "LMBlue01") public static let LMBlue2 = color(named: "LMBlue02") - public static let LMGreen = color(named: "LMGreen") + public static let LMGreen = color(named: "LMGreen01") + public static let LMGreen2 = color(named: "LMGreen02") public static let LMRed = color(named: "LMRed") public static let LMRed2 = color(named: "LMRed02") + public static let LMPink = color(named: "LMPink") } private func image(named name: String) -> UIImage? { diff --git a/Projects/CommonUI/Sources/Enum/SpellingCategoryColor.swift b/Projects/CommonUI/Sources/Enum/SpellingCategoryColor.swift new file mode 100644 index 0000000..879c40a --- /dev/null +++ b/Projects/CommonUI/Sources/Enum/SpellingCategoryColor.swift @@ -0,0 +1,49 @@ +// +// SpellingCategoryColor.swift +// CommonUI +// +// Created by 박지윤 on 9/24/25. +// + +import UIKit + +public enum SpellingCategoryColor { + case spelling // 맞춤법 - 주황색 + case spacing // 띄어쓰기 - 파란색 + case standard // 표준어 위반 - 녹색 + case other // 기타 - 분홍색 + + public var color: UIColor? { + switch self { + case .spelling: + return CommonUIAssets.LMOrange0 + case .spacing: + return CommonUIAssets.LMBlue + case .standard: + return CommonUIAssets.LMGreen + case .other: + return CommonUIAssets.LMPink + } + } + + public var lightColor: UIColor? { + return color?.withAlphaComponent(0.3) + } + + public var borderColor: UIColor? { + return color?.withAlphaComponent(0.5) + } + + public static func color(for category: String) -> SpellingCategoryColor { + switch category { + case "맞춤법": + return .spelling + case "띄어쓰기": + return .spacing + case "표준어 위반": + return .standard + default: + return .other + } + } +} diff --git a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift index 9409bbd..0b62436 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift @@ -82,7 +82,7 @@ open class ChatAnalysisView: UIView { nonScrollView.snp.makeConstraints { $0.width.equalToSuperview() - $0.bottom.equalTo(self.safeAreaLayoutGuide).offset(-20) + $0.bottom.equalTo(self.safeAreaLayoutGuide).offset(-10) $0.height.equalTo(75) } @@ -147,7 +147,7 @@ open class ChatAnalysisView: UIView { let bubbleView = UIView().then { $0.layer.cornerRadius = 16 - $0.backgroundColor = author == "HUMAN" ? CommonUIAssets.LMBlue2 : UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) + $0.backgroundColor = author == "HUMAN" ? CommonUIAssets.LMBlue : UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) } let messageLabel = UILabel().then { diff --git a/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift b/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift index feebeae..4b898dc 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift @@ -93,7 +93,7 @@ open class ChatDetailView: UIView { // 말풍선 배경 let bubbleView = UIView().then { - $0.backgroundColor = chat.author == 0 ? UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) : CommonUIAssets.LMBlue2 + $0.backgroundColor = chat.author == 0 ? UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) : CommonUIAssets.LMBlue $0.layer.cornerRadius = 16 } diff --git a/Projects/CommonUI/Sources/View/Chat/ChatView.swift b/Projects/CommonUI/Sources/View/Chat/ChatView.swift index abdceec..aafc748 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatView.swift @@ -105,7 +105,7 @@ open class ChatView: UIView { public let sendButton = UIButton().then { $0.setImage(CommonUIAssets.IconSend, for: .normal) - $0.backgroundColor = CommonUIAssets.LMBlue2 + $0.backgroundColor = CommonUIAssets.LMBlue $0.layer.cornerRadius = 22 $0.isEnabled = false } @@ -273,7 +273,7 @@ open class ChatView: UIView { let bubbleView = UIView().then { $0.layer.cornerRadius = 16 - $0.backgroundColor = message.author == "HUMAN" ? CommonUIAssets.LMBlue2 : UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) + $0.backgroundColor = message.author == "HUMAN" ? CommonUIAssets.LMBlue : UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) } let messageLabel = UILabel().then { diff --git a/Projects/CommonUI/Sources/View/Diary/DiaryDetailView.swift b/Projects/CommonUI/Sources/View/Diary/DiaryDetailView.swift index f27a100..209aa8a 100644 --- a/Projects/CommonUI/Sources/View/Diary/DiaryDetailView.swift +++ b/Projects/CommonUI/Sources/View/Diary/DiaryDetailView.swift @@ -10,9 +10,12 @@ import SnapKit import Then import Domain import RxSwift +import RxRelay open class DiaryDetailView: UIView { - + + private let isResult: Bool + // MARK: UI Components private let scrollView = UIScrollView().then { $0.showsVerticalScrollIndicator = true @@ -25,11 +28,11 @@ open class DiaryDetailView: UIView { $0.textColor = .black $0.font = UIFont.systemFont(ofSize: 20, weight: .semibold) } - + private let dateUnderlineView = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange1 } - + // 원형 점수 표시기 private let scoreContainerView = UIView() private let scoreCircleView = UIView().then { @@ -38,111 +41,132 @@ open class DiaryDetailView: UIView { $0.layer.borderColor = CommonUIAssets.LMOrange1?.cgColor $0.layer.cornerRadius = 60 } - + private let scoreProgressView = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange3 $0.layer.cornerRadius = 60 } - + private let scoreLabel = UILabel().then { $0.font = UIFont.systemFont(ofSize: 24, weight: .bold) $0.textColor = CommonUIAssets.LMBlack $0.textAlignment = .center } - + // 내가 쓴 일기 섹션 private let originalSectionTitleLabel = UILabel().then { $0.text = "내가 쓴 일기" $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) $0.textColor = CommonUIAssets.LMBlack } - + private let originalContentContainer = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.1) $0.layer.cornerRadius = 12 } - + private let originalContentLabel = UILabel().then { $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) $0.textColor = CommonUIAssets.LMBlack $0.numberOfLines = 0 } - + // 화살표 private let arrowImageView = UIImageView().then { $0.image = UIImage(systemName: "arrow.down") $0.tintColor = CommonUIAssets.LMGray3 $0.contentMode = .scaleAspectFit } - + // 맞춤법 교정 결과 섹션 private let revisedSectionTitleLabel = UILabel().then { $0.text = "맞춤법 교정 결과" $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) $0.textColor = CommonUIAssets.LMBlack } - + private let revisedContentContainer = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.1) $0.layer.cornerRadius = 12 } - + private let revisedContentLabel = UILabel().then { $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) $0.textColor = CommonUIAssets.LMBlack $0.numberOfLines = 0 } - + // 맞춤법 오류 섹션 private let errorSectionTitleLabel = UILabel().then { $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) $0.textColor = CommonUIAssets.LMBlack } - + private let errorStackView = UIStackView().then { $0.axis = .vertical $0.spacing = 12 $0.distribution = .fill } - + // AI 피드백 섹션 private let feedbackSectionTitleLabel = UILabel().then { $0.text = "AI 피드백" $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) $0.textColor = CommonUIAssets.LMBlack } - + private let feedbackContainer = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.1) $0.layer.cornerRadius = 12 } - + private let feedbackLabel = UILabel().then { $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) $0.textColor = CommonUIAssets.LMBlack $0.numberOfLines = 0 } - + + private let nonScrollView = UIView().then { + $0.backgroundColor = CommonUIAssets.LMWhite + } + + private var confirmButton = LMButton(textColor: CommonUIAssets.LMBlack, + bgColor: CommonUIAssets.LMOrange1) + // MARK: Properties private var diaryData: DiaryVO? let disposeBag = DisposeBag() - + // MARK: Public Properties - public var onSaveButtonTapped: (() -> Void)? - + public var onSaveButtonTapped = PublishRelay() + public override init(frame: CGRect) { + self.isResult = false super.init(frame: frame) setupUI() bindEvents() } - + + public init(isResult: Bool) { + self.isResult = isResult + super.init(frame: .zero) + setupUI() + bindEvents() + } + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: Setup private func setupUI() { backgroundColor = CommonUIAssets.LMWhite + confirmButton.setTitle("일기 저장하기", for: .normal) + + if isResult { + addSubview(nonScrollView) + nonScrollView.addSubview(confirmButton) + } addSubview(scrollView) scrollView.addSubview(contentView) @@ -151,122 +175,142 @@ open class DiaryDetailView: UIView { scoreContainerView.addSubview(scoreCircleView) scoreContainerView.addSubview(scoreProgressView) scoreContainerView.addSubview(scoreLabel) - + // 원문 컨테이너 설정 originalContentContainer.addSubview(originalContentLabel) - + // 수정문 컨테이너 설정 revisedContentContainer.addSubview(revisedContentLabel) - + // 피드백 컨테이너 설정 feedbackContainer.addSubview(feedbackLabel) - + [dateLabel, dateUnderlineView, scoreContainerView, originalSectionTitleLabel, originalContentContainer, arrowImageView, revisedSectionTitleLabel, revisedContentContainer, errorSectionTitleLabel, errorStackView, feedbackSectionTitleLabel, feedbackContainer] .forEach { contentView.addSubview($0) } - + setupConstraints() } - + private func bindEvents() { + confirmButton.rx.tap + .bind(to: onSaveButtonTapped) + .disposed(by: disposeBag) } - + private func setupConstraints() { - scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() + if (isResult) { + scrollView.snp.makeConstraints { + $0.top.width.equalToSuperview() + $0.bottom.equalToSuperview().inset(100) + } + + nonScrollView.snp.makeConstraints { + $0.top.equalTo(scrollView.snp.bottom) + $0.width.bottom.equalToSuperview() + } + + confirmButton.snp.makeConstraints { + $0.width.equalToSuperview().inset(20) + $0.center.equalToSuperview() + } + } else { + scrollView.snp.makeConstraints { + $0.edges.equalToSuperview() + } } - + contentView.snp.makeConstraints { $0.edges.equalToSuperview() $0.width.equalToSuperview() } - + dateLabel.snp.makeConstraints { $0.top.equalToSuperview().inset(20) $0.leading.equalToSuperview().inset(20) } - + dateUnderlineView.snp.makeConstraints { $0.top.equalTo(dateLabel.snp.bottom) $0.centerX.width.equalTo(dateLabel) $0.height.equalTo(3) } - + scoreContainerView.snp.makeConstraints { $0.top.equalTo(dateUnderlineView.snp.bottom).offset(30) $0.centerX.equalToSuperview() $0.width.height.equalTo(120) } - + scoreCircleView.snp.makeConstraints { $0.edges.equalToSuperview() } - + scoreProgressView.snp.makeConstraints { $0.edges.equalToSuperview() } - + scoreLabel.snp.makeConstraints { $0.center.equalToSuperview() } - + // 내가 쓴 일기 섹션 originalSectionTitleLabel.snp.makeConstraints { $0.top.equalTo(scoreContainerView.snp.bottom).offset(30) $0.leading.trailing.equalToSuperview().inset(20) } - + originalContentContainer.snp.makeConstraints { $0.top.equalTo(originalSectionTitleLabel.snp.bottom).offset(12) $0.leading.trailing.equalToSuperview().inset(20) } - + originalContentLabel.snp.makeConstraints { $0.edges.equalToSuperview().inset(16) } - + // 화살표 arrowImageView.snp.makeConstraints { $0.top.equalTo(originalContentContainer.snp.bottom).offset(16) $0.centerX.equalToSuperview() $0.width.height.equalTo(20) } - + // 맞춤법 교정 결과 섹션 revisedSectionTitleLabel.snp.makeConstraints { $0.top.equalTo(arrowImageView.snp.bottom).offset(16) $0.leading.trailing.equalToSuperview().inset(20) } - + revisedContentContainer.snp.makeConstraints { $0.top.equalTo(revisedSectionTitleLabel.snp.bottom).offset(12) $0.leading.trailing.equalToSuperview().inset(20) } - + revisedContentLabel.snp.makeConstraints { $0.edges.equalToSuperview().inset(16) } - + // 맞춤법 오류 섹션 errorSectionTitleLabel.snp.makeConstraints { $0.top.equalTo(revisedContentContainer.snp.bottom).offset(20) $0.leading.trailing.equalToSuperview().inset(20) } - + errorStackView.snp.makeConstraints { $0.top.equalTo(errorSectionTitleLabel.snp.bottom).offset(12) $0.leading.trailing.equalToSuperview().inset(20) } - + // AI 피드백 섹션 feedbackSectionTitleLabel.snp.makeConstraints { $0.top.equalTo(errorStackView.snp.bottom).offset(20) $0.leading.trailing.equalToSuperview().inset(20) } - + feedbackContainer.snp.makeConstraints { $0.top.equalTo(feedbackSectionTitleLabel.snp.bottom).offset(12) $0.leading.trailing.equalToSuperview().inset(20) @@ -277,7 +321,7 @@ open class DiaryDetailView: UIView { $0.edges.equalToSuperview().inset(16) } } - + // MARK: Public Methods public func configure(with diary: DiaryVO) { self.diaryData = diary @@ -330,8 +374,12 @@ open class DiaryDetailView: UIView { for revision in revisions { let range = (revisedContent as NSString).range(of: revision.revisedContent) if range.location != NSNotFound { - attributedString.addAttribute(.backgroundColor, - value: CommonUIAssets.LMOrange1?.withAlphaComponent(0.3) ?? UIColor.orange.withAlphaComponent(0.3), + let categoryColor = SpellingCategoryColor.color(for: revision.category) + attributedString.addAttribute(.foregroundColor, + value: categoryColor.color ?? .black, + range: range) + attributedString.addAttribute(.font, + value: UIFont.systemFont(ofSize: 16, weight: .medium), range: range) } } @@ -363,11 +411,12 @@ open class DiaryDetailView: UIView { private func createErrorView(_ revision: RevisionVO) -> UIView { let containerView = UIView() + let categoryColor = SpellingCategoryColor.color(for: revision.category) let originalBox = UIView().then { $0.backgroundColor = .clear $0.layer.borderWidth = 1 - $0.layer.borderColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.3).cgColor + $0.layer.borderColor = categoryColor.borderColor?.cgColor $0.layer.cornerRadius = 8 } @@ -380,12 +429,12 @@ open class DiaryDetailView: UIView { let arrowImageView = UIImageView().then { $0.image = UIImage(systemName: "arrow.right") - $0.tintColor = CommonUIAssets.LMOrange1 + $0.tintColor = categoryColor.color $0.contentMode = .scaleAspectFit } let revisedBox = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.3) + $0.backgroundColor = categoryColor.lightColor $0.layer.cornerRadius = 8 } diff --git a/Projects/CommonUI/Sources/View/Diary/DiaryLoadingView.swift b/Projects/CommonUI/Sources/View/Diary/DiaryLoadingView.swift index 1b05880..d3c3288 100644 --- a/Projects/CommonUI/Sources/View/Diary/DiaryLoadingView.swift +++ b/Projects/CommonUI/Sources/View/Diary/DiaryLoadingView.swift @@ -23,10 +23,19 @@ open class DiaryLoadingView: UIView { } private let subLabel = UILabel().then { - $0.text = "잠시만 기다려 주세요" + $0.text = "최대 15초 소요될 수 있습니다\n잠시만 기다려 주세요" $0.textColor = CommonUIAssets.LMGray3 $0.font = UIFont.systemFont(ofSize: 14, weight: .regular) $0.textAlignment = .center + $0.numberOfLines = 2 + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 4 + paragraphStyle.alignment = .center + $0.attributedText = NSAttributedString( + string: $0.text ?? "", + attributes: [.paragraphStyle: paragraphStyle] + ) } public override init(frame: CGRect) { @@ -55,7 +64,7 @@ open class DiaryLoadingView: UIView { subLabel.snp.makeConstraints { $0.centerX.equalToSuperview() - $0.top.equalTo(mainLabel.snp.bottom).offset(8) + $0.top.equalTo(mainLabel.snp.bottom).offset(10) } } } diff --git a/Projects/CommonUI/Sources/View/Diary/DiaryResultView.swift b/Projects/CommonUI/Sources/View/Diary/DiaryResultView.swift deleted file mode 100644 index a0559f3..0000000 --- a/Projects/CommonUI/Sources/View/Diary/DiaryResultView.swift +++ /dev/null @@ -1,522 +0,0 @@ -// -// DiaryResultView.swift -// CommonUI -// -// Created by 박지윤 on 1/7/25. -// - -import UIKit -import SnapKit -import Then -import Domain -import RxSwift - -open class DiaryResultView: UIView { - - // MARK: UI Components - private let scrollView = UIScrollView().then { - $0.showsVerticalScrollIndicator = true - $0.showsHorizontalScrollIndicator = true - } - - private let contentView = UIView() - - private(set) var dateLabel = UILabel().then { - $0.text = Date().getToday() - $0.textColor = .black - $0.font = UIFont.systemFont(ofSize: 20, weight: .semibold) - } - - private let dateUnderlineView = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange1 - } - - // 원형 점수 표시기 - private let scoreContainerView = UIView() - private let scoreCircleView = UIView().then { - $0.backgroundColor = .yellow - $0.layer.borderWidth = 8 - $0.layer.borderColor = CommonUIAssets.LMOrange1?.cgColor - $0.layer.cornerRadius = 60 - } - - private let scoreProgressView = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange3 - $0.layer.cornerRadius = 60 - } - - private let scoreLabel = UILabel().then { - $0.font = UIFont.systemFont(ofSize: 24, weight: .bold) - $0.textColor = CommonUIAssets.LMBlack - $0.textAlignment = .center - } - - // 내가 쓴 일기 섹션 - private let originalSectionTitleLabel = UILabel().then { - $0.text = "내가 쓴 일기" - $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) - $0.textColor = CommonUIAssets.LMBlack - } - - private let originalContentContainer = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.1) - $0.layer.cornerRadius = 12 - } - - private let originalContentLabel = UILabel().then { - $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) - $0.textColor = CommonUIAssets.LMBlack - $0.numberOfLines = 0 - } - - // 화살표 - private let arrowImageView = UIImageView().then { - $0.image = UIImage(systemName: "arrow.down") - $0.tintColor = CommonUIAssets.LMGray3 - $0.contentMode = .scaleAspectFit - } - - // 맞춤법 교정 결과 섹션 - private let revisedSectionTitleLabel = UILabel().then { - $0.text = "맞춤법 교정 결과" - $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) - $0.textColor = CommonUIAssets.LMBlack - } - - private let revisedContentContainer = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.1) - $0.layer.cornerRadius = 12 - } - - private let revisedContentLabel = UILabel().then { - $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) - $0.textColor = CommonUIAssets.LMBlack - $0.numberOfLines = 0 - } - - // 맞춤법 오류 섹션 - private let errorSectionTitleLabel = UILabel().then { - $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) - $0.textColor = CommonUIAssets.LMBlack - } - - private let errorStackView = UIStackView().then { - $0.axis = .vertical - $0.spacing = 12 - $0.distribution = .fill - } - - // AI 피드백 섹션 - private let feedbackSectionTitleLabel = UILabel().then { - $0.text = "AI 피드백" - $0.font = UIFont.systemFont(ofSize: 16, weight: .semibold) - $0.textColor = CommonUIAssets.LMBlack - } - - private let feedbackContainer = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.1) - $0.layer.cornerRadius = 12 - } - - private let feedbackLabel = UILabel().then { - $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) - $0.textColor = CommonUIAssets.LMBlack - $0.numberOfLines = 0 - } - - private var confirmButton = LMButton(textColor: CommonUIAssets.LMBlack, - bgColor: CommonUIAssets.LMOrange1) - - // MARK: Properties - private var diaryData: DiaryVO? - private var isButtonVisible: Bool = true - let disposeBag = DisposeBag() - - // MARK: Public Properties - public var onSaveButtonTapped: (() -> Void)? - - public override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - bindEvents() - } - - public convenience init(frame: CGRect, showButton: Bool) { - self.init(frame: frame) - self.isButtonVisible = showButton - updateButtonVisibility() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Setup - private func setupUI() { - backgroundColor = CommonUIAssets.LMWhite - - // 버튼 설정 - confirmButton = confirmButton.then { - $0.setTitle("일기 저장하기", for: .normal) - $0.layer.cornerRadius = 12 - } - - addSubview(scrollView) - if isButtonVisible { - addSubview(confirmButton) - } - scrollView.addSubview(contentView) - - // 원형 점수 표시기 설정 - scoreContainerView.addSubview(scoreCircleView) - scoreContainerView.addSubview(scoreProgressView) - scoreContainerView.addSubview(scoreLabel) - - // 원문 컨테이너 설정 - originalContentContainer.addSubview(originalContentLabel) - - // 수정문 컨테이너 설정 - revisedContentContainer.addSubview(revisedContentLabel) - - // 피드백 컨테이너 설정 - feedbackContainer.addSubview(feedbackLabel) - - [dateLabel, dateUnderlineView, scoreContainerView, - originalSectionTitleLabel, originalContentContainer, - arrowImageView, revisedSectionTitleLabel, revisedContentContainer, - errorSectionTitleLabel, errorStackView, - feedbackSectionTitleLabel, feedbackContainer] - .forEach { contentView.addSubview($0) } - - setupConstraints() - } - - private func bindEvents() { - confirmButton.rx.tap - .subscribe(onNext: { [weak self] in - self?.onSaveButtonTapped?() - }) - .disposed(by: disposeBag) - } - - private func setupConstraints() { - if isButtonVisible { - scrollView.snp.makeConstraints { - $0.top.leading.trailing.equalToSuperview() - $0.bottom.equalTo(confirmButton.snp.top).offset(-20) - } - - contentView.snp.makeConstraints { - $0.top.leading.trailing.bottom.equalToSuperview() - $0.width.equalToSuperview() - } - - confirmButton.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(20) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) - } - } else { - scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - contentView.snp.makeConstraints { - $0.top.leading.trailing.equalToSuperview() - $0.bottom.greaterThanOrEqualToSuperview() - $0.width.equalToSuperview() - } - } - - dateLabel.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).inset(20) - $0.leading.equalToSuperview().inset(20) - } - - dateUnderlineView.snp.makeConstraints { - $0.top.equalTo(dateLabel.snp.bottom) - $0.centerX.width.equalTo(dateLabel) - $0.height.equalTo(3) - } - - scoreContainerView.snp.makeConstraints { - $0.top.equalTo(dateUnderlineView.snp.bottom).offset(30) - $0.centerX.equalToSuperview() - $0.width.height.equalTo(120) - } - - scoreCircleView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - scoreProgressView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - scoreLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - - // 내가 쓴 일기 섹션 - originalSectionTitleLabel.snp.makeConstraints { - $0.top.equalTo(scoreContainerView.snp.bottom).offset(30) - $0.leading.trailing.equalToSuperview().inset(20) - } - - originalContentContainer.snp.makeConstraints { - $0.top.equalTo(originalSectionTitleLabel.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview().inset(20) - } - - originalContentLabel.snp.makeConstraints { - $0.edges.equalToSuperview().inset(16) - } - - // 화살표 - arrowImageView.snp.makeConstraints { - $0.top.equalTo(originalContentContainer.snp.bottom).offset(16) - $0.centerX.equalToSuperview() - $0.width.height.equalTo(20) - } - - // 맞춤법 교정 결과 섹션 - revisedSectionTitleLabel.snp.makeConstraints { - $0.top.equalTo(arrowImageView.snp.bottom).offset(16) - $0.leading.trailing.equalToSuperview().inset(20) - } - - revisedContentContainer.snp.makeConstraints { - $0.top.equalTo(revisedSectionTitleLabel.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview().inset(20) - } - - revisedContentLabel.snp.makeConstraints { - $0.edges.equalToSuperview().inset(16) - } - - // 맞춤법 오류 섹션 - errorSectionTitleLabel.snp.makeConstraints { - $0.top.equalTo(revisedContentContainer.snp.bottom).offset(20) - $0.leading.trailing.equalToSuperview().inset(20) - } - - errorStackView.snp.makeConstraints { - $0.top.equalTo(errorSectionTitleLabel.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview().inset(20) - } - - // AI 피드백 섹션 - feedbackSectionTitleLabel.snp.makeConstraints { - $0.top.equalTo(errorStackView.snp.bottom).offset(20) - $0.leading.trailing.equalToSuperview().inset(20) - } - - feedbackContainer.snp.makeConstraints { - $0.top.equalTo(feedbackSectionTitleLabel.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview().inset(20) - $0.bottom.equalToSuperview().inset(20) - } - - feedbackLabel.snp.makeConstraints { - $0.edges.equalToSuperview().inset(16) - } - } - - // MARK: Public Methods - public func configure(with diary: DiaryVO) { - self.diaryData = diary - - // 날짜 설정 - dateLabel.text = diary.createdAt - - // 원문 설정 - originalContentLabel.text = diary.originContent - - // 수정문 설정 (하이라이트 포함) - setupRevisedContent(diary.spellingDto.revisedContent, revisions: diary.spellingDto.revisions) - - // 점수 설정 - scoreLabel.text = "\(diary.spellingDto.score)점" - setupScoreProgress(score: diary.spellingDto.score) - - // 오류 섹션 설정 - setupErrorSection(revisions: diary.spellingDto.revisions) - - // 피드백 설정 - feedbackLabel.text = diary.feedback - } - - public func setButtonVisibility(_ isVisible: Bool) { - self.isButtonVisible = isVisible - updateButtonVisibility() - } - - private func updateButtonVisibility() { - if isButtonVisible { - if confirmButton.superview == nil { - addSubview(confirmButton) - confirmButton.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(20) - $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) - } - } - confirmButton.isHidden = false - - // 스크롤뷰 제약조건 업데이트 - scrollView.snp.remakeConstraints { - $0.top.leading.trailing.equalToSuperview() - $0.bottom.equalTo(confirmButton.snp.top).offset(-20) - } - } else { - confirmButton.isHidden = true - confirmButton.removeFromSuperview() - - // 스크롤뷰 제약조건 업데이트 - scrollView.snp.remakeConstraints { - $0.edges.equalToSuperview() - } - } - } - - private func setupScoreProgress(score: Int) { - let progress = CGFloat(score) / 100.0 - let angle = progress * 2 * .pi - .pi / 2 // -90도부터 시작 - - // 원형 프로그레스 애니메이션 - let path = UIBezierPath(arcCenter: CGPoint(x: 60, y: 60), - radius: 52, - startAngle: -.pi / 2, - endAngle: angle, - clockwise: true) - - let shapeLayer = CAShapeLayer() - shapeLayer.path = path.cgPath - shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.strokeColor = CommonUIAssets.LMOrange1?.cgColor - shapeLayer.lineWidth = 8 - shapeLayer.lineCap = .round - - scoreProgressView.layer.sublayers?.removeAll() - scoreProgressView.layer.addSublayer(shapeLayer) - } - - private func setupRevisedContent(_ revisedContent: String, revisions: [RevisionVO]) { - let attributedString = NSMutableAttributedString(string: revisedContent) - - for revision in revisions { - let range = (revisedContent as NSString).range(of: revision.revisedContent) - if range.location != NSNotFound { - attributedString.addAttribute(.backgroundColor, - value: CommonUIAssets.LMOrange1?.withAlphaComponent(0.3) ?? UIColor.orange.withAlphaComponent(0.3), - range: range) - } - } - - revisedContentLabel.attributedText = attributedString - } - - private func setupErrorSection(revisions: [RevisionVO]) { - errorStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - if revisions.isEmpty { - errorSectionTitleLabel.text = "맞춤법 오류 0개" - let noErrorsLabel = UILabel().then { - $0.text = "완벽한 문장이에요! 🎉" - $0.font = UIFont.systemFont(ofSize: 16, weight: .medium) - $0.textColor = CommonUIAssets.LMOrange1 - $0.textAlignment = .center - } - errorStackView.addArrangedSubview(noErrorsLabel) - } else { - errorSectionTitleLabel.text = "맞춤법 오류 \(revisions.count)개" - - for revision in revisions { - let errorView = createErrorView(revision) - errorStackView.addArrangedSubview(errorView) - } - } - } - - private func createErrorView(_ revision: RevisionVO) -> UIView { - let containerView = UIView() - - let originalBox = UIView().then { - $0.backgroundColor = .clear - $0.layer.borderWidth = 1 - $0.layer.borderColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.3).cgColor - $0.layer.cornerRadius = 8 - } - - let originalLabel = UILabel().then { - $0.text = revision.originContent - $0.font = UIFont.systemFont(ofSize: 14, weight: .medium) - $0.textColor = CommonUIAssets.LMBlack - $0.textAlignment = .center - } - - let arrowImageView = UIImageView().then { - $0.image = UIImage(systemName: "arrow.right") - $0.tintColor = CommonUIAssets.LMOrange1 - $0.contentMode = .scaleAspectFit - } - - let revisedBox = UIView().then { - $0.backgroundColor = CommonUIAssets.LMOrange1?.withAlphaComponent(0.3) - $0.layer.cornerRadius = 8 - } - - let revisedLabel = UILabel().then { - $0.text = revision.revisedContent - $0.font = UIFont.systemFont(ofSize: 14, weight: .medium) - $0.textColor = CommonUIAssets.LMBlack - $0.textAlignment = .center - } - - [originalBox, originalLabel, arrowImageView, revisedBox, revisedLabel] - .forEach { containerView.addSubview($0) } - - originalBox.snp.makeConstraints { - $0.leading.equalToSuperview() - $0.centerY.equalToSuperview() - $0.height.equalTo(32) - } - - originalLabel.snp.makeConstraints { - $0.edges.equalTo(originalBox).inset(8) - } - - // originalBox의 너비를 originalLabel의 내용에 따라 동적으로 설정 - originalBox.snp.makeConstraints { - $0.width.greaterThanOrEqualTo(60) // 최소 너비 - $0.width.lessThanOrEqualTo(120) // 최대 너비 - } - - arrowImageView.snp.makeConstraints { - $0.leading.equalTo(originalBox.snp.trailing).offset(8) - $0.centerY.equalToSuperview() - $0.width.height.equalTo(16) - } - - revisedBox.snp.makeConstraints { - $0.leading.equalTo(arrowImageView.snp.trailing).offset(8) - $0.centerY.equalToSuperview() - $0.height.equalTo(32) - } - - revisedLabel.snp.makeConstraints { - $0.edges.equalTo(revisedBox).inset(8) - } - - // revisedBox의 너비를 revisedLabel의 내용에 따라 동적으로 설정 - revisedBox.snp.makeConstraints { - $0.width.greaterThanOrEqualTo(60) // 최소 너비 - $0.width.lessThanOrEqualTo(120) // 최대 너비 - } - - containerView.snp.makeConstraints { - $0.height.equalTo(32) - } - - return containerView - } - -} diff --git a/Projects/CommonUI/Sources/View/Diary/DiaryTodayView.swift b/Projects/CommonUI/Sources/View/Diary/DiaryTodayView.swift index 4efb4e3..6883a1e 100644 --- a/Projects/CommonUI/Sources/View/Diary/DiaryTodayView.swift +++ b/Projects/CommonUI/Sources/View/Diary/DiaryTodayView.swift @@ -57,7 +57,6 @@ open class DiaryTodayView: UIView { public override init(frame: CGRect) { super.init(frame: frame) - print("initttt") configureSubviews() makeConstraints() } diff --git a/Projects/CommonUI/Sources/View/Home/AnswerView.swift b/Projects/CommonUI/Sources/View/Home/AnswerView.swift index 31c6a40..3f5243b 100644 --- a/Projects/CommonUI/Sources/View/Home/AnswerView.swift +++ b/Projects/CommonUI/Sources/View/Home/AnswerView.swift @@ -45,7 +45,7 @@ open class AnswerView: UIView { switch type { case .correct: correctView = correctView.then { - $0.backgroundColor = CommonUIAssets.LMGreen + $0.backgroundColor = CommonUIAssets.LMGreen2 $0.layer.cornerRadius = 12 } @@ -81,8 +81,9 @@ open class AnswerView: UIView { correctView.addSubview(correctLabel) correctView.snp.makeConstraints { - $0.verticalEdges.equalToSuperview() + $0.top.bottom.equalToSuperview() $0.leading.equalToSuperview().inset(20) + $0.width.lessThanOrEqualToSuperview().multipliedBy(0.7) } correctLabel.snp.makeConstraints { @@ -94,8 +95,9 @@ open class AnswerView: UIView { wrongView.addSubview(wrongLabel) wrongView.snp.makeConstraints { - $0.verticalEdges.equalToSuperview() + $0.top.bottom.equalToSuperview() $0.leading.equalToSuperview().inset(20) + $0.width.lessThanOrEqualToSuperview().multipliedBy(0.7) } wrongLabel.snp.makeConstraints { diff --git a/Projects/CommonUI/Sources/View/Home/HomeProgressView.swift b/Projects/CommonUI/Sources/View/Home/HomeProgressView.swift index 2f5f640..9522a9a 100644 --- a/Projects/CommonUI/Sources/View/Home/HomeProgressView.swift +++ b/Projects/CommonUI/Sources/View/Home/HomeProgressView.swift @@ -21,24 +21,92 @@ open class HomeProgressView: UIView { var buttonStackView = UIStackView() var restartButton = UIButton() var continueButton = UIButton() - var courseList: CourseVO? + var nextButton = UIButton() + var previousButton = UIButton() + public var courseList: [CourseVO] = [] + public var currentCourseIndex: Int = 0 + public var onNextCourseTapped: (() -> Void)? + public var onPreviousCourseTapped: (() -> Void)? public override init(frame: CGRect) { super.init(frame: frame) initAttribute() initUI() + bindActions() + } + + private func bindActions() { + nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) + previousButton.addTarget(self, action: #selector(previousButtonTapped), for: .touchUpInside) + } + + @objc private func nextButtonTapped() { + guard nextButton.isEnabled else { return } + if currentCourseIndex < courseList.count - 1 { + currentCourseIndex += 1 + updateCurrentCourse() + onNextCourseTapped?() + } + } + + @objc private func previousButtonTapped() { + guard previousButton.isEnabled else { return } + if currentCourseIndex > 0 { + currentCourseIndex -= 1 + updateCurrentCourse() + onPreviousCourseTapped?() + } } - public func setCourseList(_ list: CourseVO) { + public func setCourseList(_ list: [CourseVO]) { self.courseList = list - courseLabel.text = "한국어 훈련 \(courseList?.courseLv ?? 0)단계" - progressLabel.text = "진행률 \(courseList?.progress ?? 0)%" - updateProgress(progress: courseList?.progress ?? 0) + updateCurrentCourse() + } + + private func updateCurrentCourse() { + guard currentCourseIndex < courseList.count else { return } + let currentCourse = courseList[currentCourseIndex] + courseLabel.text = "한국어 훈련 \(currentCourse.courseLv)단계" + progressLabel.text = "진행률 \(currentCourse.progress)%" + updateProgress(progress: currentCourse.progress) + updateButtonVisibility() + } + + public func updateButtonVisibility() { + guard currentCourseIndex < courseList.count else { + nextButton.isEnabled = false + previousButton.isEnabled = false + updateButtonAppearance() + return + } + + let currentCourse = courseList[currentCourseIndex] + let allStepsSolved = currentCourse.stepList.allSatisfy { $0.stepStatus == "SOLVED" } + let hasNextCourse = currentCourseIndex < courseList.count - 1 + let hasPreviousCourse = currentCourseIndex > 0 + + // nextButton: 모든 스텝이 완료되고 다음 코스가 있을 때만 활성화 + nextButton.isEnabled = allStepsSolved && hasNextCourse + + // previousButton: 이전 코스가 있을 때만 활성화 + previousButton.isEnabled = hasPreviousCourse + + updateButtonAppearance() + } + + private func updateButtonAppearance() { + // nextButton 스타일 업데이트 + nextButton.backgroundColor = nextButton.isEnabled ? CommonUIAssets.LMOrange1 : CommonUIAssets.LMGray5 + nextButton.tintColor = nextButton.isEnabled ? .white : CommonUIAssets.LMGray3 + + // previousButton 스타일 업데이트 + previousButton.backgroundColor = previousButton.isEnabled ? CommonUIAssets.LMOrange1 : CommonUIAssets.LMGray5 + previousButton.tintColor = previousButton.isEnabled ? .white : CommonUIAssets.LMGray3 } public func updateProgress(progress: Int) { let progressEntireWidth = 344 - 40 - let progressWidth = Int(CGFloat(progress)) / 100 * progressEntireWidth + let progressWidth = CGFloat(progress) / 100.0 * CGFloat(progressEntireWidth) progressView.snp.updateConstraints { make in make.width.equalTo(progressWidth) @@ -69,7 +137,7 @@ open class HomeProgressView: UIView { } progressView = progressView.then { - $0.backgroundColor = .red + $0.backgroundColor = CommonUIAssets.LMOrange1 $0.layer.cornerRadius = 3 $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] // TODO: 100% 달성 시 모든 corner에 적용 @@ -97,13 +165,27 @@ open class HomeProgressView: UIView { $0.backgroundColor = CommonUIAssets.LMOrange1 $0.layer.cornerRadius = 10 } + + nextButton = nextButton.then { + $0.setImage(CommonUIAssets.IconNext, for: .normal) + $0.backgroundColor = CommonUIAssets.LMOrange1 + $0.layer.cornerRadius = 15 + $0.tintColor = .white + } + + previousButton = previousButton.then { + $0.setImage(CommonUIAssets.IconPrevious, for: .normal) + $0.backgroundColor = CommonUIAssets.LMOrange1 + $0.layer.cornerRadius = 15 + $0.tintColor = .white + } } func initUI() { [restartButton, continueButton] .forEach { buttonStackView.addArrangedSubview($0) } - [courseLabel, progressLabel, progressEntireView, progressView, buttonStackView] + [courseLabel, progressLabel, progressEntireView, progressView, buttonStackView, nextButton, previousButton] .forEach { self.addSubview($0) } self.snp.makeConstraints { @@ -139,6 +221,18 @@ open class HomeProgressView: UIView { $0.width.bottom.equalToSuperview().inset(20) $0.height.equalTo(38) } + + nextButton.snp.makeConstraints { + $0.width.height.equalTo(30) + $0.trailing.equalToSuperview().inset(20) + $0.centerY.equalTo(courseLabel) + } + + previousButton.snp.makeConstraints { + $0.width.height.equalTo(30) + $0.trailing.equalTo(nextButton.snp.leading).offset(-10) + $0.centerY.equalTo(courseLabel) + } } required public init?(coder: NSCoder) { diff --git a/Projects/CommonUI/Sources/View/Home/HomeQuizView.swift b/Projects/CommonUI/Sources/View/Home/HomeQuizView.swift index c919c3d..17dd337 100644 --- a/Projects/CommonUI/Sources/View/Home/HomeQuizView.swift +++ b/Projects/CommonUI/Sources/View/Home/HomeQuizView.swift @@ -90,8 +90,12 @@ open class HomeQuizView: UIView, UICollectionViewDataSource, UICollectionViewDel return UICollectionViewCell() } let step = stepList[indexPath.item] - cell.quizTitleLabel.text = step.stepTitle - cell.quizSubtitleLabel.text = step.stepDescription + cell.configure(with: step) + + // 셀 재사용 시 이전 subscription 정리하고 버튼 이벤트 재설정 + cell.disposeBag = DisposeBag() + cell.bindActions() // 버튼 이벤트 재설정 + cell.onStartButtonTapped .subscribe(onNext: { [weak self] in self?.onStartButtonTapped?(indexPath) diff --git a/Projects/CommonUI/Sources/View/Home/QuizCollectionViewCell.swift b/Projects/CommonUI/Sources/View/Home/QuizCollectionViewCell.swift index 8f4a9ff..293a24c 100644 --- a/Projects/CommonUI/Sources/View/Home/QuizCollectionViewCell.swift +++ b/Projects/CommonUI/Sources/View/Home/QuizCollectionViewCell.swift @@ -16,7 +16,7 @@ final class HomeQuizCell: UICollectionViewCell { var quizSubtitleLabel = UILabel() var startButton = UIButton() - let disposeBag = DisposeBag() + var disposeBag = DisposeBag() let onStartButtonTapped = PublishSubject() override init(frame: CGRect) { @@ -95,12 +95,35 @@ final class HomeQuizCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func configure(with course: CourseVO) { - quizTitleLabel.text = "처음 보는 사람과 인사하기" - quizSubtitleLabel.text = "인사와 대화의 첫걸음을 배워요" + func configure(with step: StepVO) { + quizTitleLabel.text = step.stepTitle + quizSubtitleLabel.text = step.stepDescription + + // stepStatus에 따라 버튼 텍스트 변경 + let buttonTitle = step.stepStatus == "SOLVED" ? "완료" : "시작하기" + updateButtonTitle(buttonTitle) + } + + private func updateButtonTitle(_ title: String) { + var config = startButton.configuration ?? UIButton.Configuration.plain() + let attributedTitle = AttributedString(title, attributes: AttributeContainer([ + .font: UIFont.systemFont(ofSize: 12, weight: .regular) + ])) + config.attributedTitle = attributedTitle + + if title == "완료" { + config.image = nil + config.baseBackgroundColor = CommonUIAssets.LMOrange4 + config.baseForegroundColor = CommonUIAssets.LMGray1 + } else { + config.image = CommonUIAssets.IconPlay + config.baseForegroundColor = CommonUIAssets.LMGray1 + } + + startButton.configuration = config } - func bindActions() { + public func bindActions() { startButton.rx.tap .bind(to: onStartButtonTapped) .disposed(by: disposeBag) diff --git a/Projects/CommonUI/Sources/View/Home/QuizCompleteAlertView.swift b/Projects/CommonUI/Sources/View/Home/QuizCompleteAlertView.swift index b4ea857..7d472f8 100644 --- a/Projects/CommonUI/Sources/View/Home/QuizCompleteAlertView.swift +++ b/Projects/CommonUI/Sources/View/Home/QuizCompleteAlertView.swift @@ -10,6 +10,9 @@ import SnapKit import Then public final class QuizCompleteAlertView: UIView { + + // MARK: - Properties + public var onConfirmButtonTapped: (() -> Void)? private let backgroundView = UIView().then { $0.backgroundColor = UIColor.black.withAlphaComponent(0.4) @@ -116,6 +119,9 @@ public final class QuizCompleteAlertView: UIView { } @objc private func dismiss() { + // 확인 버튼 콜백 호출 + onConfirmButtonTapped?() + UIView.animate(withDuration: 0.3, animations: { self.backgroundView.alpha = 0.0 }) { _ in diff --git a/Projects/CommonUI/Sources/View/Home/QuizView.swift b/Projects/CommonUI/Sources/View/Home/QuizView.swift index e03a329..d3600bd 100644 --- a/Projects/CommonUI/Sources/View/Home/QuizView.swift +++ b/Projects/CommonUI/Sources/View/Home/QuizView.swift @@ -59,7 +59,7 @@ open class QuizView: UIView { } case .question: questionView = questionView.then { - $0.backgroundColor = CommonUIAssets.LMBlue + $0.backgroundColor = CommonUIAssets.LMBlue2 $0.layer.cornerRadius = 12 } @@ -80,8 +80,9 @@ open class QuizView: UIView { situationView.addSubview(situationLabel) situationView.snp.makeConstraints { - $0.verticalEdges.equalToSuperview() + $0.top.bottom.equalToSuperview() $0.centerX.equalToSuperview() + $0.width.lessThanOrEqualToSuperview().multipliedBy(0.8) } situationLabel.snp.makeConstraints { @@ -93,8 +94,9 @@ open class QuizView: UIView { questionView.addSubview(questionLabel) questionView.snp.makeConstraints { - $0.verticalEdges.equalToSuperview() + $0.top.bottom.equalToSuperview() $0.leading.equalToSuperview().inset(20) + $0.width.lessThanOrEqualToSuperview().multipliedBy(0.7) } questionLabel.snp.makeConstraints { diff --git a/Projects/CommonUI/Sources/View/Login/SignUpView.swift b/Projects/CommonUI/Sources/View/Login/SignUpView.swift index fc76e7f..c05133f 100644 --- a/Projects/CommonUI/Sources/View/Login/SignUpView.swift +++ b/Projects/CommonUI/Sources/View/Login/SignUpView.swift @@ -30,10 +30,12 @@ open class SignUpView: UIView { public let passwordInputField = LMInputField(inputType: .password, inputText: "비밀번호", inputPlaceholder: "비밀번호를 입력하세요", - warningText: " 비밀번호 형식이 올바르지 않습니다") + warningText: " 비밀번호 형식이 올바르지 않습니다", + isSecureTextEntry: true) public let passwordCheckInputField = LMInputField(inputText: "비밀번호 확인", inputPlaceholder: "비밀번호를 한번 더 입력하세요", - warningText: " 비밀번호가 일치하지 않습니다") + warningText: " 비밀번호가 일치하지 않습니다", + isSecureTextEntry: true) public override init(frame: CGRect) { super.init(frame: frame) diff --git a/Projects/CommonUI/Sources/View/MyPageView.swift b/Projects/CommonUI/Sources/View/MyPageView.swift index fbda76e..a3c3443 100644 --- a/Projects/CommonUI/Sources/View/MyPageView.swift +++ b/Projects/CommonUI/Sources/View/MyPageView.swift @@ -10,32 +10,353 @@ import SnapKit import Then open class MyPageView: UIView { - public let logoutButton = UIButton().then { - $0.setTitle("로그아웃", for: .normal) - $0.setTitleColor(.white, for: .normal) - $0.backgroundColor = .systemRed - $0.layer.cornerRadius = 8 - $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + + // MARK: UI Components + private let tableView = UITableView().then { + $0.backgroundColor = .clear + $0.separatorStyle = .none + $0.register(MyPageTableViewCell.self, forCellReuseIdentifier: MyPageTableViewCell.identifier) } + // MARK: Properties + private var sections: [MyPageSection] = [] + + // MARK: Public Properties + public var onGetUser: (() -> Void)? + public var onPostLogout: (() -> Void)? + public var onDeleteUser: (() -> Void)? + public override init(frame: CGRect) { super.init(frame: frame) - initUI() + setupUI() + setupData() } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func initUI() { - backgroundColor = .systemBackground + private func setupUI() { + backgroundColor = CommonUIAssets.LMOrange4 + + self.addSubview(tableView) + + tableView.delegate = self + tableView.dataSource = self + + setupConstraints() + } + + private func setupConstraints() { + tableView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + private func setupData() { + sections = [ + MyPageSection( + title: "내 정보", + items: [ + MyPageItem(title: "내 정보", icon: "person.fill", hasChevron: false), + MyPageItem(title: "사용자 이름", subtitle: "", hasChevron: false) + ] + ), + MyPageSection( + title: "앱 정보", + items: [ + MyPageItem(title: "앱 정보", icon: "info.circle.fill", hasChevron: false), + MyPageItem(title: "앱 버전", subtitle: "v 1.0.0", hasChevron: false) + ] + ), + MyPageSection( + title: "계정 관리", + items: [ + MyPageItem(title: "로그아웃", hasChevron: true), + MyPageItem(title: "회원탈퇴", hasChevron: true) + ] + ) + ] + + tableView.reloadData() + } + + // MARK: - Public Methods + public func updateUserName(_ name: String) { + // "내 정보" 섹션의 두 번째 아이템(사용자 이름) 업데이트 + if sections.count > 0 && sections[0].items.count > 1 { + var updatedItems = sections[0].items + updatedItems[1] = MyPageItem(title: "사용자 이름", subtitle: name, hasChevron: false) + sections[0] = MyPageSection(title: sections[0].title, items: updatedItems) + tableView.reloadData() + } + } + + // MARK: - Modal Methods + private func showLogoutConfirmation() { + let alert = LMAlert( + title: "정말로 로그아웃하시겠습니까?", + cancelTitle: "취소", + confirmTitle: "확인" + ) + + alert.setCancelAction { [weak self] in + // 취소 액션 - 아무것도 하지 않음 + } + + alert.setConfirmAction { [weak self] in + self?.onPostLogout?() + } + + // 현재 뷰 컨트롤러 찾기 + if let topViewController = findTopViewController() { + alert.show(in: topViewController.view) + } + } + + private func showWithdrawalConfirmation() { + let alert = LMAlert( + title: "정말로 회원탈퇴하시겠습니까?\n탈퇴 후에는 모든 데이터가 삭제되며 복구할 수 없습니다.", + cancelTitle: "취소", + confirmTitle: "확인" + ) + + alert.setCancelAction { [weak self] in + // 취소 액션 - 아무것도 하지 않음 + } + + alert.setConfirmAction { [weak self] in + self?.onDeleteUser?() + } + + // 현재 뷰 컨트롤러 찾기 + if let topViewController = findTopViewController() { + alert.show(in: topViewController.view) + } + } + + private func findTopViewController() -> UIViewController? { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first else { + return nil + } + + var topViewController = window.rootViewController + + while let presentedViewController = topViewController?.presentedViewController { + topViewController = presentedViewController + } + + return topViewController + } +} + +// MARK: - UITableViewDataSource +extension MyPageView: UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].items.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: MyPageTableViewCell.identifier, for: indexPath) as! MyPageTableViewCell + let item = sections[indexPath.section].items[indexPath.row] + + // 마지막 섹션의 마지막 셀이거나, 섹션의 마지막 셀이 아닌 경우 디바이더 숨김 + let isLastSection = indexPath.section == sections.count - 1 + let isLastRowInSection = indexPath.row == sections[indexPath.section].items.count - 1 + let shouldHideDivider = isLastSection || !isLastRowInSection + + cell.configure(with: item, isLastCell: shouldHideDivider) + return cell + } +} + +// MARK: - UITableViewDelegate +extension MyPageView: UITableViewDelegate { + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) - addSubview(logoutButton) + let item = sections[indexPath.section].items[indexPath.row] - logoutButton.snp.makeConstraints { - $0.center.equalToSuperview() - $0.width.equalTo(200) - $0.height.equalTo(50) + switch item.title { + case "내 정보": + onGetUser?() + case "로그아웃": + showLogoutConfirmation() + case "회원탈퇴": + showWithdrawalConfirmation() + default: + break } } + + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 56 + } + + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + if section == 0 { + return nil + } + + let headerView = UIView() + headerView.backgroundColor = .clear + + let separatorView = UIView().then { + $0.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) + } + + headerView.addSubview(separatorView) + separatorView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(20) + $0.centerY.equalToSuperview() + $0.height.equalTo(0.5) + } + + return headerView + } + + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 0 + } +} + +// MARK: - Data Models +private struct MyPageSection { + let title: String + let items: [MyPageItem] +} + +private struct MyPageItem { + let title: String + let subtitle: String? + let icon: String? + let hasChevron: Bool + + init(title: String, subtitle: String? = nil, icon: String? = nil, hasChevron: Bool = false) { + self.title = title + self.subtitle = subtitle + self.icon = icon + self.hasChevron = hasChevron + } +} + +// MARK: - Custom Cell +private class MyPageTableViewCell: UITableViewCell { + static let identifier = "MyPageTableViewCell" + + private let iconImageView = UIImageView().then { + $0.contentMode = .scaleAspectFit + $0.tintColor = CommonUIAssets.LMOrange1 + } + + private let titleLabel = UILabel().then { + $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) + $0.textColor = CommonUIAssets.LMBlack + } + + private let subtitleLabel = UILabel().then { + $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) + $0.textColor = CommonUIAssets.LMBlack + } + + private let chevronImageView = UIImageView().then { + $0.image = UIImage(systemName: "chevron.right") + $0.tintColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0) + $0.contentMode = .scaleAspectFit + } + + private let dividerView = UIView().then { + $0.backgroundColor = CommonUIAssets.LMGray5 + } + + private let containerView = UIView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + backgroundColor = .clear + selectionStyle = .none + + contentView.addSubview(containerView) + contentView.addSubview(dividerView) + containerView.addSubview(iconImageView) + containerView.addSubview(titleLabel) + containerView.addSubview(subtitleLabel) + containerView.addSubview(chevronImageView) + + setupConstraints() + } + + private func setupConstraints() { + containerView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)) + } + + iconImageView.snp.makeConstraints { + $0.leading.equalToSuperview() + $0.centerY.equalToSuperview() + $0.width.height.equalTo(20) + } + + titleLabel.snp.makeConstraints { + $0.leading.equalTo(iconImageView.snp.trailing).offset(12) + $0.centerY.equalToSuperview() + } + + subtitleLabel.snp.makeConstraints { + $0.trailing.equalTo(chevronImageView.snp.leading).offset(-8) + $0.centerY.equalToSuperview() + } + + chevronImageView.snp.makeConstraints { + $0.trailing.equalToSuperview() + $0.centerY.equalToSuperview() + $0.width.height.equalTo(12) + } + + dividerView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(20) + $0.bottom.equalToSuperview() + $0.height.equalTo(0.5) + } + } + + func configure(with item: MyPageItem, isLastCell: Bool = false) { + titleLabel.text = item.title + + if let icon = item.icon { + iconImageView.image = UIImage(systemName: icon) + iconImageView.isHidden = false + titleLabel.snp.updateConstraints { + $0.leading.equalTo(iconImageView.snp.trailing).offset(12) + } + } else { + iconImageView.isHidden = true + titleLabel.snp.updateConstraints { + $0.leading.equalTo(iconImageView.snp.trailing).offset(0) + } + } + + if let subtitle = item.subtitle { + subtitleLabel.text = subtitle + subtitleLabel.isHidden = false + } else { + subtitleLabel.isHidden = true + } + + chevronImageView.isHidden = !item.hasChevron + dividerView.isHidden = isLastCell + } } diff --git a/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift b/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift index 0b0ca19..d5e2995 100644 --- a/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift +++ b/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift @@ -16,7 +16,8 @@ public final class DefaultNavigationBar: UIView { public init(leftImage: UIImage?, rightImage: UIImage?, - title: String?) { + title: String?, + isBack: Bool = true) { super.init(frame: .zero) setupUI() setupLayout() @@ -25,7 +26,9 @@ public final class DefaultNavigationBar: UIView { rightButton.setImage(rightImage, for: .normal) titleLabel.text = title - leftButton.addTarget(self, action: #selector(leftButtonTapped), for: .touchUpInside) + if (isBack) { + leftButton.addTarget(self, action: #selector(leftButtonTapped), for: .touchUpInside) + } rightButton.addTarget(self, action: #selector(rightButtonTapped), for: .touchUpInside) } diff --git a/Projects/Data/Data.xcodeproj/project.pbxproj b/Projects/Data/Data.xcodeproj/project.pbxproj index b76edae..59b54d7 100644 --- a/Projects/Data/Data.xcodeproj/project.pbxproj +++ b/Projects/Data/Data.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 9532C7AC2E786F3F00B4BADE /* DiaryDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9532C7AB2E786F3A00B4BADE /* DiaryDTO.swift */; }; 9532C7AE2E78707D00B4BADE /* DiaryCalendarDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9532C7AD2E78707600B4BADE /* DiaryCalendarDTO.swift */; }; 956C4D8F2E77142700E32F93 /* DiaryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D8E2E77140A00E32F93 /* DiaryRepository.swift */; }; + 95EDE8D72E7E6F550091ED75 /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95EDE8D62E7E6F4C0091ED75 /* UserRepository.swift */; }; + 95EDE8DB2E7E70620091ED75 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95EDE8DA2E7E705F0091ED75 /* UserDTO.swift */; }; 99E14C3BCF2FDBB8004B27AC /* ChatRoomDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298B5F633729122951DB918 /* ChatRoomDTO.swift */; }; A334985695DC9388841BBC43 /* QuizRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCA951B89F2C3D20AA31F7F /* QuizRepository.swift */; }; B2F8FBFA915F696CCCA4152A /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3B0D3D8C7049B6856791C1D /* Alamofire.framework */; }; @@ -57,6 +59,8 @@ 9532C7AB2E786F3A00B4BADE /* DiaryDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryDTO.swift; sourceTree = ""; }; 9532C7AD2E78707600B4BADE /* DiaryCalendarDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryCalendarDTO.swift; sourceTree = ""; }; 956C4D8E2E77140A00E32F93 /* DiaryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryRepository.swift; sourceTree = ""; }; + 95EDE8D62E7E6F4C0091ED75 /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = ""; }; + 95EDE8DA2E7E705F0091ED75 /* UserDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = ""; }; A3B0D3D8C7049B6856791C1D /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A44BC1E2E75FC256F832CA38 /* LoginDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginDTO.swift; sourceTree = ""; }; AAAC2885ACFE998578DC25E8 /* CourseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDTO.swift; sourceTree = ""; }; @@ -107,6 +111,7 @@ 377C66458E6219D981E14335 /* DefaultDTO.swift */, A44BC1E2E75FC256F832CA38 /* LoginDTO.swift */, 77810122262C6CB16D4D47DA /* QuizDTO.swift */, + 95EDE8DA2E7E705F0091ED75 /* UserDTO.swift */, ); path = DTO; sourceTree = ""; @@ -114,6 +119,7 @@ 73F3ED55BFDC2EF15878F5B6 /* Repository */ = { isa = PBXGroup; children = ( + 95EDE8D62E7E6F4C0091ED75 /* UserRepository.swift */, DC5F0E176D612624E2C929B7 /* SignRepository.swift */, 3D7624DE90CFBBEF778E120E /* LoginRepository.swift */, 050263DDA587409393A9328A /* TokenRepository.swift */, @@ -251,6 +257,7 @@ files = ( C69F1794E1B0FB5F7B7CBDA5 /* ChatDTO.swift in Sources */, C9C427C4DD6EB1B302F5F219 /* ChatDetailDTO.swift in Sources */, + 95EDE8D72E7E6F550091ED75 /* UserRepository.swift in Sources */, 99E14C3BCF2FDBB8004B27AC /* ChatRoomDTO.swift in Sources */, 43E9C2380F425520C1FA1AD2 /* CourseDTO.swift in Sources */, 0C1776E871E0CB4A493E8354 /* DefaultDTO.swift in Sources */, @@ -262,6 +269,7 @@ E1BFC73FB539432F6E12CD94 /* CourseRepository.swift in Sources */, 34FD760EB97BB96E9D770BF0 /* LoginRepository.swift in Sources */, A334985695DC9388841BBC43 /* QuizRepository.swift in Sources */, + 95EDE8DB2E7E70620091ED75 /* UserDTO.swift in Sources */, 59034BF673F78A457B681967 /* SignRepository.swift in Sources */, E6785667C8E247C344474CBB /* TokenRepository.swift in Sources */, 9532C7AE2E78707D00B4BADE /* DiaryCalendarDTO.swift in Sources */, diff --git a/Projects/Data/Sources/DTO/UserDTO.swift b/Projects/Data/Sources/DTO/UserDTO.swift new file mode 100644 index 0000000..2839e13 --- /dev/null +++ b/Projects/Data/Sources/DTO/UserDTO.swift @@ -0,0 +1,27 @@ +// +// UserDTO.swift +// Data +// +// Created by 박지윤 on 9/20/25. +// + +import Foundation +import Domain + +public struct UserDTO: Decodable { + public let is_success: Bool + public let code: String + public let message: String + public let data: UserDataDTO +} + +public struct UserDataDTO: Decodable { + public let userId: Int + public let name: String +} + +extension UserDataDTO { + func toDomain() -> UserVO { + return UserVO(userId: userId, name: name) + } +} diff --git a/Projects/Data/Sources/Repository/QuizRepository.swift b/Projects/Data/Sources/Repository/QuizRepository.swift index 29b019d..28c5534 100644 --- a/Projects/Data/Sources/Repository/QuizRepository.swift +++ b/Projects/Data/Sources/Repository/QuizRepository.swift @@ -17,37 +17,76 @@ public class DefaultQuizRepository: QuizRepository { } public func startStep(course: Int, step: Int) -> Single { - return request( - endpoint: "/api/courses", - course: course, - step: step, - responseType: QuizResponseDTO.self + let params: Parameters = [ + "course": course, + "step": step + ] + + return request(method: .post, + parameters: params, + endpoint: "/api/courses", + encoding: URLEncoding.default, + responseType: QuizResponseDTO.self ) .map { dto in return dto.data.toDomain() } } - - private func request(endpoint: String, course: Int, step: Int, responseType: T.Type) -> Single { + + public func patchStep(stepProgressId: Int) -> Single { + return request(method: .patch, + endpoint: "/api/courses/\(stepProgressId)", + responseType: DefaultDTO.self + ) + .map { dto in + return dto.getMessage() + } + } + + public func deleteStep(stepProgressId: Int) -> Single { + return request(method: .delete, + endpoint: "/api/courses/\(stepProgressId)", + responseType: DefaultDTO.self + ) + .map { dto in + return dto.getMessage() + } + } + + private func request( + method: HTTPMethod = .get, + parameters: [String: Any]? = nil, + endpoint: String, + encoding: ParameterEncoding = JSONEncoding.default, + responseType: T.Type + ) -> Single { return Single.create { single in - let url = "\(NetworkConfiguration.baseUrl)\(endpoint)?course=\(course)&step=\(step)" + let url = "\(NetworkConfiguration.baseUrl)\(endpoint)" var headers: HTTPHeaders = [:] + if let token = self.tokenRepository.getAccessToken() { + print("🔑 사용할 토큰: \(token)") headers.add(name: "Authorization", value: "Bearer \(token)") } else { print("❌ 토큰이 없습니다!") } - print("🌐 API 요청 URL: \(url)") print("🔑 Authorization 헤더: \(headers)") + print("📤 요청 파라미터: \(parameters ?? [:])") + print("📤 요청 메서드: \(method)") + print("📤 인코딩: \(encoding)") let request = AF.request(url, - method: .post, - parameters: nil, - encoding: JSONEncoding.default, + method: method, + parameters: parameters, + encoding: encoding, headers: headers) - .validate() .responseDecodable(of: responseType) { response in + print("📊 HTTP 상태 코드: \(response.response?.statusCode ?? -1)") + if let data = response.data { + print("📊 응답 데이터: \(String(data: data, encoding: .utf8) ?? "데이터 파싱 실패")") + } + switch response.result { case .success(let value): print("✅ API 응답 성공: \(value)") diff --git a/Projects/Data/Sources/Repository/UserRepository.swift b/Projects/Data/Sources/Repository/UserRepository.swift new file mode 100644 index 0000000..3326dab --- /dev/null +++ b/Projects/Data/Sources/Repository/UserRepository.swift @@ -0,0 +1,92 @@ +// +// UserRepository.swift +// Data +// +// Created by 박지윤 on 9/20/25. +// + +import Domain +import RxSwift +import Alamofire + +public class DefaultUserRepository: UserRepository { + private let tokenRepository: TokenRepository + + public init(tokenRepository: TokenRepository) { + self.tokenRepository = tokenRepository + } + + public func getUser() -> Single { + return request(endpoint: "/api/users/{userId}", + responseType: UserDTO.self) + .map { dto in + return dto.data.toDomain() + } + } + + public func postLogout() -> Single { + return request(method: .post, + endpoint: "/api/auth/logout", + responseType: DefaultDTO.self) + .map { dto in + return dto.getMessage() + } + } + + public func deleteUser() -> Single { + return request(method: .delete, + endpoint: "/api/users", + responseType: DefaultDTO.self) + .map { dto in + return dto.getMessage() + } + } + + private func request( + method: HTTPMethod = .get, + parameters: [String: Any]? = nil, + endpoint: String, + encoding: ParameterEncoding = JSONEncoding.default, + responseType: T.Type + ) -> Single { + return Single.create { single in + let url = "\(NetworkConfiguration.baseUrl)\(endpoint)" + var headers: HTTPHeaders = [:] + + if let token = self.tokenRepository.getAccessToken() { + print("🔑 사용할 토큰: \(token)") + headers.add(name: "Authorization", value: "Bearer \(token)") + } else { + print("❌ 토큰이 없습니다!") + } + print("🌐 API 요청 URL: \(url)") + print("🔑 Authorization 헤더: \(headers)") + print("📤 요청 파라미터: \(parameters ?? [:])") + print("📤 요청 메서드: \(method)") + print("📤 인코딩: \(encoding)") + + let request = AF.request(url, + method: method, + parameters: parameters, + encoding: encoding, + headers: headers) + .responseDecodable(of: responseType) { response in + print("📊 HTTP 상태 코드: \(response.response?.statusCode ?? -1)") + if let data = response.data { + print("📊 응답 데이터: \(String(data: data, encoding: .utf8) ?? "데이터 파싱 실패")") + } + + switch response.result { + case .success(let value): + print("✅ API 응답 성공: \(value)") + single(.success(value)) + case .failure(let error): + print("❌ API 응답 실패: \(error)") + single(.failure(error)) + } + } + + return Disposables.create { request.cancel() } + } + } +} diff --git a/Projects/Diary/Sources/View/DiaryAddViewController.swift b/Projects/Diary/Sources/View/DiaryAddViewController.swift index 71bcc28..f1be2fe 100644 --- a/Projects/Diary/Sources/View/DiaryAddViewController.swift +++ b/Projects/Diary/Sources/View/DiaryAddViewController.swift @@ -31,6 +31,12 @@ public class DiaryAddViewController: BaseViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: false) + self.tabBarController?.tabBar.isHidden = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.tabBarController?.tabBar.isHidden = false } public override func viewDidLoad() { @@ -131,9 +137,7 @@ public class DiaryAddViewController: BaseViewController { print("📊 분석 결과 화면으로 이동") let diaryResultViewController = DiaryResultViewController(diaryViewModel: viewModel, diaryData: diaryData) - diaryResultViewController.modalPresentationStyle = .fullScreen - - present(diaryResultViewController, animated: true) + navigationController?.pushViewController(diaryResultViewController, animated: true) } private func bindData() { diff --git a/Projects/Diary/Sources/View/DiaryDetailViewController.swift b/Projects/Diary/Sources/View/DiaryDetailViewController.swift index 623759b..00a8a66 100644 --- a/Projects/Diary/Sources/View/DiaryDetailViewController.swift +++ b/Projects/Diary/Sources/View/DiaryDetailViewController.swift @@ -10,7 +10,7 @@ import CommonUI import Domain public class DiaryDetailViewController: BaseViewController { - let diaryDetailView = DiaryDetailView() + let diaryDetailView = DiaryDetailView(isResult: false) let viewModel: DiaryViewModel let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, diff --git a/Projects/Diary/Sources/View/DiaryResultViewController.swift b/Projects/Diary/Sources/View/DiaryResultViewController.swift index a4496cb..b6ef14a 100644 --- a/Projects/Diary/Sources/View/DiaryResultViewController.swift +++ b/Projects/Diary/Sources/View/DiaryResultViewController.swift @@ -10,7 +10,7 @@ import CommonUI import Domain public class DiaryResultViewController: BaseViewController { - let diaryResultView = DiaryResultView() + let diaryResultView = DiaryDetailView(isResult: true) let viewModel: DiaryViewModel let navigationBar = DefaultNavigationBar(leftImage: nil, @@ -29,6 +29,16 @@ public class DiaryResultViewController: BaseViewController { fatalError("init(coder:) has not been implemented") } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.tabBarController?.tabBar.isHidden = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.tabBarController?.tabBar.isHidden = false + } + public override func viewDidLoad() { super.viewDidLoad() setupViewProperty() @@ -66,9 +76,7 @@ public class DiaryResultViewController: BaseViewController { } public func setupActions() { - // 기존 타겟들을 모두 제거 navigationBar.rightButton.removeTarget(navigationBar, action: nil, for: .touchUpInside) - // 새로운 타겟 추가 navigationBar.rightButton.addTarget(self, action: #selector(handleRightButtonTapped), for: .touchUpInside) } @@ -91,26 +99,20 @@ public class DiaryResultViewController: BaseViewController { private func deleteDiaryAndExit() { viewModel.deleteDiaryDetail(diaryId: diaryData.diaryId) - - // 모달 닫기 - dismiss(animated: true) + self.navigationController?.popToRootViewController(animated: true) } private func bindEvents() { - // leftButton이 nil이므로 이벤트 바인딩 제거 - // navigationBar.leftButton.rx.tap - // .subscribe(onNext: { [weak self] in - // self?.navigationController?.popViewController(animated: true) - // }) - // .disposed(by: disposeBag) - - diaryResultView.onSaveButtonTapped = { [weak self] in + diaryResultView.onSaveButtonTapped.subscribe(onNext: { [weak self] in self?.handleSaveDiary() - } + }).disposed(by: disposeBag) } private func handleSaveDiary() { - self.navigationController?.popViewController(animated: true) + print("🔄 handleSaveDiary 호출됨") + print("🔄 navigationController: \(String(describing: navigationController))") + + self.navigationController?.popToRootViewController(animated: true) } private func configureData() { diff --git a/Projects/Diary/Sources/View/DiaryViewController.swift b/Projects/Diary/Sources/View/DiaryViewController.swift index a81954a..cb4b79a 100644 --- a/Projects/Diary/Sources/View/DiaryViewController.swift +++ b/Projects/Diary/Sources/View/DiaryViewController.swift @@ -30,21 +30,20 @@ public class DiaryViewController: BaseViewController { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: false) - // 첫 진입 시에만 데이터 로드 + let today = Date() + let calendar = Calendar.current + let year = calendar.component(.year, from: today) + let month = calendar.component(.month, from: today) + + // 첫 진입 시에만 오늘 날짜의 일기 데이터 로드 if isFirstAppearance { - let today = Date() - let calendar = Calendar.current - let year = calendar.component(.year, from: today) - let month = calendar.component(.month, from: today) - - // 캘린더 데이터 로드 - viewModel.getDiaryCalendar(year: year, month: month) - // 오늘 날짜의 일기 데이터 로드 viewModel.getDiary(date: today) - isFirstAppearance = false } + + // 매번 캘린더 데이터 리로드 (일기 저장 후 돌아올 때 포함) + viewModel.getDiaryCalendar(year: year, month: month) } public override func viewDidLoad() { @@ -163,6 +162,7 @@ public class DiaryViewController: BaseViewController { private func presentNewDiaryView() { let diaryAddViewController = DiaryAddViewController(diaryViewModel: viewModel) + diaryAddViewController.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(diaryAddViewController, animated: true) } diff --git a/Projects/Domain/Domain.xcodeproj/project.pbxproj b/Projects/Domain/Domain.xcodeproj/project.pbxproj index 80e7544..9b66eaa 100644 --- a/Projects/Domain/Domain.xcodeproj/project.pbxproj +++ b/Projects/Domain/Domain.xcodeproj/project.pbxproj @@ -28,6 +28,9 @@ 956C4D8B2E77104800E32F93 /* DiaryUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D8A2E77104500E32F93 /* DiaryUseCase.swift */; }; 956C4D8D2E7710BC00E32F93 /* DiaryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D8C2E7710B900E32F93 /* DiaryRepository.swift */; }; 956C4D922E771DD300E32F93 /* TokenValidationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D912E771DCB00E32F93 /* TokenValidationUseCase.swift */; }; + 95EDE8D92E7E6F6E0091ED75 /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95EDE8D82E7E6F6A0091ED75 /* UserRepository.swift */; }; + 95EDE8DD2E7E70A70091ED75 /* UserVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95EDE8DC2E7E70A40091ED75 /* UserVO.swift */; }; + 95EDE8DF2E7E90900091ED75 /* UserUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95EDE8DE2E7E908B0091ED75 /* UserUseCase.swift */; }; 9DBF2CB00F16FBC09602488A /* ChatDetailVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 097F5EF085884AD8C9591CE5 /* ChatDetailVO.swift */; }; A0CEC5A2E5A76EBD987CE37D /* StepVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798CFC331192C9FE77F3446A /* StepVO.swift */; }; B0B0382EF5DFDACDD3EB8A75 /* QuizUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4565F6A02AFBF3D9A0D41B /* QuizUseCase.swift */; }; @@ -71,6 +74,9 @@ 956C4D8A2E77104500E32F93 /* DiaryUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryUseCase.swift; sourceTree = ""; }; 956C4D8C2E7710B900E32F93 /* DiaryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryRepository.swift; sourceTree = ""; }; 956C4D912E771DCB00E32F93 /* TokenValidationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenValidationUseCase.swift; sourceTree = ""; }; + 95EDE8D82E7E6F6A0091ED75 /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = ""; }; + 95EDE8DC2E7E70A40091ED75 /* UserVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserVO.swift; sourceTree = ""; }; + 95EDE8DE2E7E908B0091ED75 /* UserUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUseCase.swift; sourceTree = ""; }; 9A2E30672E510822AAF38EAD /* RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9B2CBEE7AC445460FAE28675 /* ChatUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUseCase.swift; sourceTree = ""; }; A89402CD621AD4E2A5B16175 /* SignUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUseCase.swift; sourceTree = ""; }; @@ -145,6 +151,7 @@ F904C836B5CD2D4DA5EAE38D /* CourseUseCase.swift */, 6F4565F6A02AFBF3D9A0D41B /* QuizUseCase.swift */, ABB8C62A6FE12783AD3819BE /* TokenUseCase.swift */, + 95EDE8DE2E7E908B0091ED75 /* UserUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -188,6 +195,7 @@ FD08A7186FB9676854B7AEAC /* LoginVO.swift */, 24F9958F3DAC8013B440CEEA /* QuizVO.swift */, 798CFC331192C9FE77F3446A /* StepVO.swift */, + 95EDE8DC2E7E70A40091ED75 /* UserVO.swift */, ); path = VO; sourceTree = ""; @@ -195,11 +203,12 @@ BC1A55AF7DC675AFEA6E7C12 /* RepositoryProtocol */ = { isa = PBXGroup; children = ( + DCA3E06ED2A8180A63919B6C /* CourseRepository.swift */, + 1BA1471CB01ECE73BAD3D595 /* QuizRepository.swift */, CFB20675E9BA27548003EA55 /* ChatRepository.swift */, 956C4D8C2E7710B900E32F93 /* DiaryRepository.swift */, - DCA3E06ED2A8180A63919B6C /* CourseRepository.swift */, + 95EDE8D82E7E6F6A0091ED75 /* UserRepository.swift */, 8A2AB67DE6AA50B3229C7A86 /* LoginRepository.swift */, - 1BA1471CB01ECE73BAD3D595 /* QuizRepository.swift */, B437170C7511425C025E4D08 /* SignRepository.swift */, 1A1F66123A81636A913985E6 /* TokenRepository.swift */, ); @@ -279,10 +288,12 @@ buildActionMask = 2147483647; files = ( 69DAD609572D32F2BA3845AE /* String+Extension.swift in Sources */, + 95EDE8DD2E7E70A70091ED75 /* UserVO.swift in Sources */, 16748FBAA3739FB31DEB293A /* UIImage+Extension.swift in Sources */, 92BD46EE48F6C63B6E43D069 /* UIView+Extension.swift in Sources */, 6D73D9043F7C3AA14AED7068 /* ChatRepository.swift in Sources */, 9532C7B02E7872A400B4BADE /* DiaryVO.swift in Sources */, + 95EDE8DF2E7E90900091ED75 /* UserUseCase.swift in Sources */, D510BC17C4583615CB60439E /* CourseRepository.swift in Sources */, D7012CC494E56CD8CE58176F /* LoginRepository.swift in Sources */, 1782D2A1A7FB2BD6FFA1DFEA /* QuizRepository.swift in Sources */, @@ -297,6 +308,7 @@ 3E835D4F2E5F639557AC7C06 /* TokenUseCase.swift in Sources */, 956C4D8B2E77104800E32F93 /* DiaryUseCase.swift in Sources */, 9DBF2CB00F16FBC09602488A /* ChatDetailVO.swift in Sources */, + 95EDE8D92E7E6F6E0091ED75 /* UserRepository.swift in Sources */, 72077A10B09B16423036D020 /* ChatRoomListVO.swift in Sources */, 6A879D6F3FB2FB826037933B /* ChatVO.swift in Sources */, 956C4D8D2E7710BC00E32F93 /* DiaryRepository.swift in Sources */, diff --git a/Projects/Domain/Sources/RepositoryProtocol/QuizRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/QuizRepository.swift index 2c678ec..6181ada 100644 --- a/Projects/Domain/Sources/RepositoryProtocol/QuizRepository.swift +++ b/Projects/Domain/Sources/RepositoryProtocol/QuizRepository.swift @@ -9,4 +9,6 @@ import RxSwift public protocol QuizRepository { func startStep(course: Int, step: Int) -> Single + func patchStep(stepProgressId: Int) -> Single + func deleteStep(stepProgressId: Int) -> Single } diff --git a/Projects/Domain/Sources/RepositoryProtocol/UserRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/UserRepository.swift new file mode 100644 index 0000000..f88e7df --- /dev/null +++ b/Projects/Domain/Sources/RepositoryProtocol/UserRepository.swift @@ -0,0 +1,14 @@ +// +// UserRepository.swift +// Domain +// +// Created by 박지윤 on 9/20/25. +// + +import RxSwift + +public protocol UserRepository { + func getUser() -> Single + func postLogout() -> Single + func deleteUser() -> Single +} diff --git a/Projects/Domain/Sources/UseCase/QuizUseCase.swift b/Projects/Domain/Sources/UseCase/QuizUseCase.swift index 1122158..d8503a3 100644 --- a/Projects/Domain/Sources/UseCase/QuizUseCase.swift +++ b/Projects/Domain/Sources/UseCase/QuizUseCase.swift @@ -9,6 +9,8 @@ import RxSwift public protocol QuizUseCase { func startStep(course: Int, step: Int) -> Single + func patchStep(stepProgressId: Int) -> Single + func deleteStep(stepProgressId: Int) -> Single } public final class DefaultQuizUseCase: QuizUseCase { @@ -21,4 +23,12 @@ public final class DefaultQuizUseCase: QuizUseCase { public func startStep(course: Int, step: Int) -> Single { return repository.startStep(course: course, step: step) } + + public func patchStep(stepProgressId: Int) -> Single { + return repository.patchStep(stepProgressId: stepProgressId) + } + + public func deleteStep(stepProgressId: Int) -> Single { + return repository.deleteStep(stepProgressId: stepProgressId) + } } diff --git a/Projects/Domain/Sources/UseCase/UserUseCase.swift b/Projects/Domain/Sources/UseCase/UserUseCase.swift new file mode 100644 index 0000000..3265f60 --- /dev/null +++ b/Projects/Domain/Sources/UseCase/UserUseCase.swift @@ -0,0 +1,34 @@ +// +// UserUseCase.swift +// Domain +// +// Created by 박지윤 on 9/20/25. +// + +import RxSwift + +public protocol UserUseCase { + func getUser() -> Single + func postLogout() -> Single + func deleteUser() -> Single +} + +public final class DefaultUserUseCase: UserUseCase { + private let repository: UserRepository + + public init(repository: UserRepository) { + self.repository = repository + } + + public func getUser() -> Single { + return repository.getUser() + } + + public func postLogout() -> Single { + return repository.postLogout() + } + + public func deleteUser() -> Single { + return repository.deleteUser() + } +} diff --git a/Projects/Domain/Sources/VO/UserVO.swift b/Projects/Domain/Sources/VO/UserVO.swift new file mode 100644 index 0000000..8e4e209 --- /dev/null +++ b/Projects/Domain/Sources/VO/UserVO.swift @@ -0,0 +1,17 @@ +// +// UserVO.swift +// Domain +// +// Created by 박지윤 on 9/20/25. +// + +public struct UserVO { + public let userId: Int + public let name: String + + public init(userId: Int, + name: String) { + self.userId = userId + self.name = name + } +} diff --git a/Projects/Home/Home.xcodeproj/project.pbxproj b/Projects/Home/Home.xcodeproj/project.pbxproj index 49de54c..8952c87 100644 --- a/Projects/Home/Home.xcodeproj/project.pbxproj +++ b/Projects/Home/Home.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -241,8 +241,6 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - TargetAttributes = { - }; }; buildConfigurationList = AA685C1613CFBDFB4ED6BAE7 /* Build configuration list for PBXProject "Home" */; compatibilityVersion = "Xcode 14.0"; @@ -338,13 +336,7 @@ "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.Home; PRODUCT_NAME = Home; SDKROOT = iphoneos; @@ -352,10 +344,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -450,13 +439,7 @@ "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.Home; PRODUCT_NAME = Home; SDKROOT = iphoneos; @@ -493,13 +476,7 @@ "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.HomeTests; PRODUCT_NAME = HomeTests; SDKROOT = iphoneos; @@ -533,23 +510,14 @@ "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/checkouts/FSCalendar/FSCalendar/include/module.modulemap -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.HomeTests; PRODUCT_NAME = HomeTests; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/Projects/Home/Sources/View/HomeViewController.swift b/Projects/Home/Sources/View/HomeViewController.swift index 9d5d245..5e7e1e9 100644 --- a/Projects/Home/Sources/View/HomeViewController.swift +++ b/Projects/Home/Sources/View/HomeViewController.swift @@ -9,6 +9,7 @@ import CommonUI import UIKit import SnapKit import RxSwift +import Domain public class HomeViewController: BaseViewController { let viewModel: HomeViewModel @@ -21,6 +22,10 @@ public class HomeViewController: BaseViewController { let homeView = HomeView() let homeProgressView = HomeProgressView() let homeQuizView = HomeQuizView() + + // 코스와 스텝 정보 저장 + private var currentCourse: CourseVO? + private var stepList: [StepVO] = [] public init(homeViewModel: HomeViewModel) { self.viewModel = homeViewModel @@ -45,7 +50,9 @@ public class HomeViewController: BaseViewController { bindTransition() bindStepList() bindCourseData() + bindCourseList() bindQuiz() + bindPatchStepSuccess() } private func bindActions() { @@ -54,10 +61,13 @@ public class HomeViewController: BaseViewController { private func bindTransition() { homeQuizView.onStartButtonTapped = { [weak self] indexPath in - // course와 step 정보를 가져와서 퀴즈 시작 - let course = indexPath.item + 1 // 1시작 - let step = indexPath.item + 1 // 1작 - self?.viewModel.startStep(course: course, step: step) + // getCourses에서 받아온 courseLv와 stepLv 사용 + guard let self = self, + let course = self.currentCourse, + indexPath.item < self.stepList.count else { return } + + let step = self.stepList[indexPath.item] + self.viewModel.startStep(course: course.courseLv, step: step.stepLv) } } @@ -65,7 +75,7 @@ public class HomeViewController: BaseViewController { viewModel.quizSubject .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] quiz in - let quizViewController = QuizViewController(quizData: quiz) + let quizViewController = QuizViewController(homeViewModel: self!.viewModel, quizData: quiz) quizViewController.hidesBottomBarWhenPushed = true self?.navigationController?.pushViewController(quizViewController, animated: true) }) @@ -76,6 +86,7 @@ public class HomeViewController: BaseViewController { viewModel.stepListSubject .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] stepList in + self?.stepList = stepList self?.homeQuizView.setQuizList(stepList) }) .disposed(by: disposeBag) @@ -83,9 +94,43 @@ public class HomeViewController: BaseViewController { private func bindCourseData() { viewModel.courseSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] course in + self?.currentCourse = course + }) + .disposed(by: disposeBag) + } + + private func bindCourseList() { + viewModel.courseListSubject .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] courseList in self?.homeProgressView.setCourseList(courseList) + self?.homeProgressView.onNextCourseTapped = { [weak self] in + self?.updateCurrentCourseAndSteps() + } + self?.homeProgressView.onPreviousCourseTapped = { [weak self] in + self?.updateCurrentCourseAndSteps() + } + }) + .disposed(by: disposeBag) + } + + private func updateCurrentCourseAndSteps() { + guard homeProgressView.currentCourseIndex < homeProgressView.courseList.count else { return } + let currentCourse = homeProgressView.courseList[homeProgressView.currentCourseIndex] + self.currentCourse = currentCourse + homeQuizView.setQuizList(currentCourse.stepList) + } + + private func bindPatchStepSuccess() { + viewModel.patchStepSuccessSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + print("🔄 patchStep 성공 감지, getCourses 호출하여 뷰 재로딩") + self?.viewModel.getCourses() + // HomeProgressView의 버튼 가시성 업데이트 + self?.homeProgressView.updateButtonVisibility() }) .disposed(by: disposeBag) } @@ -118,7 +163,8 @@ public class HomeViewController: BaseViewController { scrollView.snp.makeConstraints { $0.top.equalTo(logoImageView.snp.bottom).offset(10) - $0.horizontalEdges.bottom.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview().inset(10) } contentView.snp.makeConstraints { diff --git a/Projects/Home/Sources/View/QuizViewController.swift b/Projects/Home/Sources/View/QuizViewController.swift index f1f6f3d..fc4a75f 100644 --- a/Projects/Home/Sources/View/QuizViewController.swift +++ b/Projects/Home/Sources/View/QuizViewController.swift @@ -12,9 +12,13 @@ import SnapKit import RxSwift public class QuizViewController: UIViewController { + let viewModel: HomeViewModel + private let disposeBag = DisposeBag() + let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, - title: nil) + title: nil, + isBack: false) let progressView = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange1 $0.layer.cornerRadius = 3 @@ -40,15 +44,12 @@ public class QuizViewController: UIViewController { var quizData: QuizVO? var currentQuiz: QuizDetailVO? - public init(quizData: QuizVO) { + public init(homeViewModel: HomeViewModel, quizData: QuizVO) { + self.viewModel = homeViewModel self.quizData = quizData super.init(nibName: nil, bundle: nil) } - init() { - super.init(nibName: nil, bundle: nil) - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -64,6 +65,8 @@ public class QuizViewController: UIViewController { setupHierarchy() setupLayout() bindDatas() + bindEvents() + bindPatchStepSuccess() if let quizData = quizData { showSituation(index: 0) } @@ -109,7 +112,7 @@ public class QuizViewController: UIViewController { scrollView.snp.makeConstraints { $0.top.equalTo(progressEntireView.snp.bottom).offset(20) - $0.horizontalEdges.bottom.equalToSuperview() + $0.horizontalEdges.bottom.equalToSuperview().inset(10) } quizStackView.snp.makeConstraints { @@ -127,6 +130,47 @@ public class QuizViewController: UIViewController { navigationBar.setupViewProperty(title: "처음 보는 사람과 인사하기") } } + + func bindEvents() { + navigationBar.leftButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.showExitConfirmationAlert() + }) + .disposed(by: disposeBag) + } + + private func showExitConfirmationAlert() { + let lmAlert = LMAlert(title: "퀴즈를 중단하시겠습니까?\n진행 상황이 저장되지 않습니다.") + + lmAlert.setCancelAction { + // 취소 시 아무것도 하지 않음 + } + + lmAlert.setConfirmAction { [weak self] in + self?.deleteStepAndExit() + } + + lmAlert.show(in: view) + } + + private func deleteStepAndExit() { + guard let quizData = quizData else { return } + + viewModel.deleteStep(stepProgressId: quizData.stepProgressId) + + // 네비게이션에서 뒤로가기 + navigationController?.popViewController(animated: true) + } + + func bindPatchStepSuccess() { + viewModel.patchStepSuccessSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + // patchStep 성공 시 네비게이션 뒤로 가기 + self?.navigationController?.popViewController(animated: true) + }) + .disposed(by: disposeBag) + } func showSituation(index: Int) { guard let quizData = quizData, index < quizData.quizList.count else { @@ -140,6 +184,7 @@ public class QuizViewController: UIViewController { if !situationText.isEmpty { let situationView = QuizView(text: situationText, type: .situation) quizStackView.addArrangedSubview(situationView) + scrollToBottom() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.showQuestion(index: index) @@ -158,6 +203,7 @@ public class QuizViewController: UIViewController { let questionView = QuizView(text: currentQuiz.quiz, type: .question) quizStackView.addArrangedSubview(questionView) + scrollToBottom() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { let optionStack = UIStackView().then { @@ -176,6 +222,7 @@ public class QuizViewController: UIViewController { self.quizStackView.addArrangedSubview(optionStack) self.currentQuestionIndex = index self.currentOptionStackView = optionStack + self.scrollToBottom() } } @@ -200,6 +247,7 @@ public class QuizViewController: UIViewController { let feedbackText = currentQuiz.quizOptions[selectedIndex].description let feedback = AnswerView(text: feedbackText, type: .correct) quizStackView.addArrangedSubview(feedback) + scrollToBottom() updateProgress() @@ -209,8 +257,10 @@ public class QuizViewController: UIViewController { self.showSituation(index: nextIndex) } } else { - let feedback = AnswerView(text: "다시 한 번 생각해보세요.", type: .wrong) + let feedbackText = currentQuiz.quizOptions[selectedIndex].description + let feedback = AnswerView(text: feedbackText, type: .wrong) quizStackView.addArrangedSubview(feedback) + scrollToBottom() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { let currentQuiz = quizData.quizList[self.currentQuestionIndex] @@ -229,6 +279,7 @@ public class QuizViewController: UIViewController { self.quizStackView.addArrangedSubview(optionStack) self.currentOptionStackView = optionStack + self.scrollToBottom() } } } @@ -239,6 +290,12 @@ public class QuizViewController: UIViewController { func showQuizCompleteAlert() { let alertView = QuizCompleteAlertView() + alertView.onConfirmButtonTapped = { [weak self] in + // 퀴즈 완료 시 stepProgressId를 사용하여 patchStep 호출 + if let quizData = self?.quizData { + self?.viewModel.patchStep(stepProgressId: quizData.stepProgressId) + } + } alertView.show(in: view) } @@ -260,4 +317,16 @@ public class QuizViewController: UIViewController { self.view.layoutIfNeeded() } } + + private func scrollToBottom() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let bottomOffset = CGPoint( + x: 0, + y: self.scrollView.contentSize.height - self.scrollView.bounds.height + ) + if bottomOffset.y > 0 { + self.scrollView.setContentOffset(bottomOffset, animated: true) + } + } + } } diff --git a/Projects/Home/Sources/ViewModel/HomeViewModel.swift b/Projects/Home/Sources/ViewModel/HomeViewModel.swift index acdb2a9..2d6e61e 100644 --- a/Projects/Home/Sources/ViewModel/HomeViewModel.swift +++ b/Projects/Home/Sources/ViewModel/HomeViewModel.swift @@ -10,6 +10,9 @@ import RxSwift protocol HomeViewModelProtocol { func getCourses() + func startStep(course: Int, step: Int) + func patchStep(stepProgressId: Int) + func deleteStep(stepProgressId: Int) } public class HomeViewModel: HomeViewModelProtocol { @@ -20,7 +23,9 @@ public class HomeViewModel: HomeViewModelProtocol { let stepListSubject = PublishSubject<[StepVO]>() let courseSubject = PublishSubject() + let courseListSubject = PublishSubject<[CourseVO]>() let quizSubject = PublishSubject() + let patchStepSuccessSubject = PublishSubject() public init(courseUseCase: CourseUseCase, tokenUseCase: TokenUseCase, quizUseCase: QuizUseCase) { self.courseUseCase = courseUseCase @@ -33,6 +38,7 @@ public class HomeViewModel: HomeViewModelProtocol { courseUseCase.getCourses() .subscribe(onSuccess: { [weak self] response in print("✅ 코스 정보: \(response)") + self?.courseListSubject.onNext(response.list) if let firstCourse = response.list.first { self?.stepListSubject.onNext(firstCourse.stepList) self?.courseSubject.onNext(firstCourse) @@ -43,6 +49,9 @@ public class HomeViewModel: HomeViewModelProtocol { } func startStep(course: Int, step: Int) { + print(course) + print(step) + print("ddddd") quizUseCase.startStep(course: course, step: step) .subscribe(onSuccess: { [weak self] quiz in print("✅ 퀴즈 시작 성공: \(quiz)") @@ -51,4 +60,23 @@ public class HomeViewModel: HomeViewModelProtocol { print("❌ 퀴즈 시작 실패: \(error)") }).disposed(by: disposeBag) } + + func patchStep(stepProgressId: Int) { + quizUseCase.patchStep(stepProgressId: stepProgressId) + .subscribe(onSuccess: { [weak self] result in + print("✅ 퀴즈 완료 성공: \(result)") + self?.patchStepSuccessSubject.onNext(()) + }, onFailure: { error in + print("❌ 퀴즈 완료 실패: \(error)") + }).disposed(by: disposeBag) + } + + func deleteStep(stepProgressId: Int) { + quizUseCase.deleteStep(stepProgressId: stepProgressId) + .subscribe(onSuccess: { [weak self] result in + print("✅ 퀴즈 삭제 성공: \(result)") + }, onFailure: { error in + print("❌ 퀴즈 삭제 실패: \(error)") + }).disposed(by: disposeBag) + } } diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/1024.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..8a44bbe Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/114.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..c882055 Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/120.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..792b664 Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/180.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..bc889fe Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/29.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..f4008ac Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/40.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..1ff83bb Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/57.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..cc92541 Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/58.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..601ebf5 Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/60.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..f0e4960 Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/80.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..614c34c Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/87.png b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..54da158 Binary files /dev/null and b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/Contents.json b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..af727e0 --- /dev/null +++ b/Projects/LearnMate/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,80 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/LearnMate/Assets.xcassets/Contents.json b/Projects/LearnMate/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Projects/LearnMate/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj b/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj index 5b71397..e9804ab 100644 --- a/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj +++ b/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 53EE0AA2C503F809334F8412 /* Diary.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7459B5071C1C3E12A6519672 /* Diary.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5EB1027699ED6A51784245ED /* HomeAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AEA88AC00680DC27F375FB6 /* HomeAssembly.swift */; }; 94D749E4159727E7A20216D4 /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0FF6CED28AB52ACEA2B7ED2A /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 95EDE8E12E7EA4090091ED75 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95EDE8E02E7EA4090091ED75 /* Assets.xcassets */; }; 9713AE7BE3A3D893CE4835B5 /* Then.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 69B232119647519D26142C64 /* Then.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9ED6C34369DCA2A469096622 /* Diary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7459B5071C1C3E12A6519672 /* Diary.framework */; }; A12C0E946F134ED9ED2BB84C /* DiaryAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D786F720EBD14634E77E3C1 /* DiaryAssembly.swift */; }; @@ -109,6 +110,7 @@ 7C422FE2DDF9127C2F4B2DF0 /* RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 89F58ED76FC4C0973305766F /* DependencyInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyInjector.swift; sourceTree = ""; }; 8A00D2BDD6B9A0B6E62CF3CC /* LearnMateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LearnMateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 95EDE8E02E7EA4090091ED75 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9ACFF0B3105009DD7646173E /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9AEA88AC00680DC27F375FB6 /* HomeAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAssembly.swift; sourceTree = ""; }; A4DC536F09A4A8284AA20CE7 /* CommonUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CommonUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -198,6 +200,7 @@ E290CD2FA6D313265F045E73 = { isa = PBXGroup; children = ( + 95EDE8E02E7EA4090091ED75 /* Assets.xcassets */, B074F9F8E730729626040CA5 /* Project */, ED0EE4EA4327EC4E4D07675E /* Products */, ); @@ -329,6 +332,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 95EDE8E12E7EA4090091ED75 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -379,6 +383,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 4P6BZ8CR69; ENABLE_PREVIEWS = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -415,6 +420,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 4P6BZ8CR69; ENABLE_PREVIEWS = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", diff --git a/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift b/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift index d1bef94..8e17d95 100644 --- a/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift +++ b/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift @@ -13,7 +13,7 @@ import RxSwift import Domain protocol AppCoordinator: Coordinator { - // func showLoginFlow() + func showLoginFlow() func showTabbarFlow() func setTabBarCoordinator() func getChildCoordinator(_ type: CoordinatorType) -> Coordinator? @@ -82,23 +82,36 @@ final class DefaultAppCoordinator: AppCoordinator{ .disposed(by: disposeBag) } - private func showLoginFlow() { - let loginViewController = dependency.injector.resolve(LoginViewController.self) - loginViewController.onPresentLmLogin = { [weak self] in - guard let self else { return } - let signInViewController = self.dependency.injector.resolve(SignInViewController.self) - signInViewController.onPresentSignUp = { [weak self] in + func showLoginFlow() { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + print("🔄 로그인 플로우 시작") + + let loginViewController = self.dependency.injector.resolve(LoginViewController.self) + loginViewController.onPresentLmLogin = { [weak self] in guard let self else { return } - let signUpViewController = self.dependency.injector.resolve(SignUpViewController.self) - self.navigationController.pushViewController(signUpViewController, animated: true) - } - signInViewController.onLoginSuccess = { [weak self] in - guard let self else { return } - self.showHomeAfterLogin() + let signInViewController = self.dependency.injector.resolve(SignInViewController.self) + signInViewController.onPresentSignUp = { [weak self] in + guard let self else { return } + let signUpViewController = self.dependency.injector.resolve(SignUpViewController.self) + self.navigationController.pushViewController(signUpViewController, animated: true) + } + signInViewController.onLoginSuccess = { [weak self] in + guard let self else { return } + self.showHomeAfterLogin() + } + self.navigationController.pushViewController(signInViewController, animated: true) } - self.navigationController.pushViewController(signInViewController, animated: true) + + // 네비게이션 바 숨기기 (로그인 화면에서 필요) + self.navigationController.setNavigationBarHidden(true, animated: false) + + // 애니메이션과 함께 로그인 화면 표시 + self.navigationController.setViewControllers([loginViewController], animated: true) + + print("✅ 로그인 화면 표시 완료") } - self.navigationController.pushViewController(loginViewController, animated: true) } /// 탭바 컨트롤러 플로우 @@ -148,10 +161,13 @@ final class DefaultAppCoordinator: AppCoordinator{ /// 자식 코디네이터가 종료되었을 때 실행할 메서드 extension DefaultAppCoordinator: CoordinatorFinishDelegate { func coordinatorDidFinish(childCoordinator: Coordinator) { + print("🔄 coordinatorDidFinish 호출됨 - childCoordinator: \(Swift.type(of: childCoordinator))") + childCoordinators.removeAll { $0 === childCoordinator } // TabBarCoordinator가 종료되면 로그인 화면으로 이동 if childCoordinator is TabBarCoordinator { + print("🔄 TabBarCoordinator 종료, 로그인 화면으로 이동") showLoginFlow() } } diff --git a/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift b/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift index 2160fbd..bab2e5c 100644 --- a/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift +++ b/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift @@ -34,6 +34,7 @@ final class DefaultTabBarController: TabBarCoordinator { init(dependency: Dependency) { self.dependency = dependency self.navigationController = dependency.navigationController + self.finishDelegate = dependency.finishDelegate } /// 탭바 flow 시작 @@ -119,9 +120,24 @@ final class DefaultTabBarController: TabBarCoordinator { } private func handleLogout() { - // 모든 뷰 컨트롤러 제거하고 로그인 화면으로 이동 - navigationController.viewControllers.removeAll() - finishDelegate?.coordinatorDidFinish(childCoordinator: self) + // 애니메이션과 함께 로그인 화면으로 전환 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + print("🔄 로그아웃 처리 시작") + + // 네비게이션 바 숨기기 (로그인 화면에서 필요) + self.navigationController.setNavigationBarHidden(true, animated: false) + + // 모든 뷰 컨트롤러 제거 + self.navigationController.viewControllers.removeAll() + + // 코디네이터 종료 알림 + print("🔄 finishDelegate 호출 시도 - finishDelegate: \(self.finishDelegate != nil ? "존재" : "nil")") + self.finishDelegate?.coordinatorDidFinish(childCoordinator: self) + + print("✅ 로그아웃 처리 완료") + } } } diff --git a/Projects/LearnMate/Sources/DI/DataAssembly.swift b/Projects/LearnMate/Sources/DI/DataAssembly.swift index aa3abf7..526d706 100644 --- a/Projects/LearnMate/Sources/DI/DataAssembly.swift +++ b/Projects/LearnMate/Sources/DI/DataAssembly.swift @@ -42,5 +42,10 @@ public struct DataAssembly: Assembly { let tokenRepository = resolver.resolve(TokenRepository.self)! return DefaultDiaryRepository(tokenRepository: tokenRepository) } + + container.register(UserRepository.self) { resolver in + let tokenRepository = resolver.resolve(TokenRepository.self)! + return DefaultUserRepository(tokenRepository: tokenRepository) + } } } diff --git a/Projects/LearnMate/Sources/DI/DomainAssembly.swift b/Projects/LearnMate/Sources/DI/DomainAssembly.swift index d20bc18..f6936a5 100644 --- a/Projects/LearnMate/Sources/DI/DomainAssembly.swift +++ b/Projects/LearnMate/Sources/DI/DomainAssembly.swift @@ -49,5 +49,10 @@ public struct DomainAssembly: Assembly { let repository = resolver.resolve(DiaryRepository.self)! return DefaultDiaryUseCase(repository: repository) } + + container.register(UserUseCase.self) { resolver in + let repository = resolver.resolve(UserRepository.self)! + return DefaultUserUseCase(repository: repository) + } } } diff --git a/Projects/LearnMate/Sources/DI/MyPageAssembly.swift b/Projects/LearnMate/Sources/DI/MyPageAssembly.swift index 22e6dc3..e20a5aa 100644 --- a/Projects/LearnMate/Sources/DI/MyPageAssembly.swift +++ b/Projects/LearnMate/Sources/DI/MyPageAssembly.swift @@ -12,8 +12,9 @@ import Swinject public struct MyPageAssembly: Assembly { public func assemble(container: Container) { container.register(MyPageViewModel.self) { resolver in + let userUseCase = resolver.resolve(UserUseCase.self)! let tokenUseCase = resolver.resolve(TokenUseCase.self)! - return MyPageViewModel(tokenUseCase: tokenUseCase) + return MyPageViewModel(userUseCase: userUseCase, tokenUseCase: tokenUseCase) } container.register(MyPageViewController.self) { resolver in @@ -21,4 +22,4 @@ public struct MyPageAssembly: Assembly { return MyPageViewController(myPageViewModel: viewModel) } } -} \ No newline at end of file +} diff --git a/Projects/Login/Sources/View/SignInViewController.swift b/Projects/Login/Sources/View/SignInViewController.swift index 7ac1a2c..9df6205 100644 --- a/Projects/Login/Sources/View/SignInViewController.swift +++ b/Projects/Login/Sources/View/SignInViewController.swift @@ -38,6 +38,7 @@ public class SignInViewController: BaseViewController { super.viewDidLoad() bindActions() bindTransition() + setupTapGesture() } private func bindActions() { @@ -75,6 +76,16 @@ public class SignInViewController: BaseViewController { } } } + + private func setupTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tapGesture.cancelsTouchesInView = false + view.addGestureRecognizer(tapGesture) + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + } public override func setupViewProperty() { view.backgroundColor = CommonUIAssets.LMWhite diff --git a/Projects/Login/Sources/View/SignUpViewController.swift b/Projects/Login/Sources/View/SignUpViewController.swift index 125234a..04b8995 100644 --- a/Projects/Login/Sources/View/SignUpViewController.swift +++ b/Projects/Login/Sources/View/SignUpViewController.swift @@ -64,6 +64,12 @@ public class SignUpViewController: BaseViewController { self?.signUpView.confirmInputField.showWarning() } } + + viewModel.onSignUpSuccess = { [weak self] in + DispatchQueue.main.async { + self?.showSignUpSuccessModal() + } + } } private func bindActions() { @@ -207,4 +213,18 @@ public class SignUpViewController: BaseViewController { $0.width.equalToSuperview() } } + + private func showSignUpSuccessModal() { + let alertView = LMAlert(title: "회원가입이 완료되었습니다!", + cancelTitle: "", + confirmTitle: "확인") + alertView.setConfirmAction { [weak self] in + self?.navigateToSignIn() + } + alertView.show(in: view) + } + + private func navigateToSignIn() { + navigationController?.popViewController(animated: true) + } } diff --git a/Projects/Login/Sources/ViewModel/SignViewModel.swift b/Projects/Login/Sources/ViewModel/SignViewModel.swift index 35a4f59..0df1d5c 100644 --- a/Projects/Login/Sources/ViewModel/SignViewModel.swift +++ b/Projects/Login/Sources/ViewModel/SignViewModel.swift @@ -26,6 +26,7 @@ public class SignViewModel: SignViewModelProtocol { public var onConfirmSuccess: (() -> Void)? public var onConfirmFailure: (() -> Void)? public var onSignInSuccess: (() -> Void)? + public var onSignUpSuccess: (() -> Void)? public let emailVerified = BehaviorRelay(value: false) public init(signUseCase: SignUseCase, tokenRepository: TokenRepository) { @@ -86,6 +87,7 @@ public class SignViewModel: SignViewModelProtocol { .subscribe(onSuccess: { [weak self] response in guard let self = self else { return } print("회원가입 성공: \(response.message)") + self.onSignUpSuccess?() }, onFailure: { [weak self] error in guard let self = self else { return } print("회원가입 실패: \(error)") diff --git a/Projects/MyPage/Sources/View/MyPageViewController.swift b/Projects/MyPage/Sources/View/MyPageViewController.swift index a905fa4..18d4b15 100644 --- a/Projects/MyPage/Sources/View/MyPageViewController.swift +++ b/Projects/MyPage/Sources/View/MyPageViewController.swift @@ -12,11 +12,11 @@ import RxSwift public class MyPageViewController: BaseViewController { let viewModel: MyPageViewModel - - let scrollView = UIScrollView() - let contentView = UIView() let myPageView = MyPageView() - + let navigationBar = DefaultNavigationBar(leftImage: nil, + rightImage: nil, + title: "마이페이지") + public var onLogout: (() -> Void)? public init(myPageViewModel: MyPageViewModel) { @@ -30,64 +30,112 @@ public class MyPageViewController: BaseViewController { public override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = CommonUIAssets.LMOrange4 bindActions() } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // 마이페이지 진입 시 사용자 정보 로드 + viewModel.getUser() + } + public override func setupViewProperty() { view.backgroundColor = .systemBackground - - scrollView.do { - $0.showsVerticalScrollIndicator = false - $0.showsHorizontalScrollIndicator = false - } } public override func setupHierarchy() { - view.addSubview(scrollView) - scrollView.addSubview(contentView) - contentView.addSubview(myPageView) + [navigationBar, myPageView] + .forEach { view.addSubview($0) } } public override func setupDelegate() { } public override func setupLayout() { - scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.width.centerX.equalToSuperview() } - - contentView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.width.equalToSuperview() - } - + myPageView.snp.makeConstraints { - $0.edges.equalToSuperview() + $0.top.equalTo(navigationBar.snp.bottom) + $0.horizontalEdges.bottom.equalToSuperview() } } public override func setupBind() { - viewModel.onLogoutSuccess = { [weak self] in - self?.onLogout?() - } - } - - private func bindActions() { - myPageView.logoutButton.rx.tap - .bind { [weak self] in - self?.showLogoutAlert() - } + // User data binding + viewModel.userSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] user in + print("📱 사용자 정보 로드 성공: \(user)") + // 사용자 이름을 MyPageView에 업데이트 + self?.myPageView.updateUserName(user.name) + }) + .disposed(by: disposeBag) + + viewModel.userErrorSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { error in + print("❌ 사용자 정보 로드 실패: \(error)") + // 에러 처리 로직 추가 + }) + .disposed(by: disposeBag) + + // Logout binding + viewModel.logoutSuccessSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + print("📱 로그아웃 성공") + // 토큰 지우기 + self?.viewModel.clearTokens() + // 로그인 화면으로 이동 + self?.onLogout?() + }) + .disposed(by: disposeBag) + + viewModel.logoutErrorSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { error in + print("❌ 로그아웃 실패: \(error)") + // 에러 처리 로직 추가 + }) + .disposed(by: disposeBag) + + // Delete user binding + viewModel.deleteUserSuccessSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + print("📱 회원탈퇴 성공") + // 토큰 지우기 + self?.viewModel.clearTokens() + // 로그인 화면으로 이동 + self?.onLogout?() + }) + .disposed(by: disposeBag) + + viewModel.deleteUserErrorSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { error in + print("❌ 회원탈퇴 실패: \(error)") + // 에러 처리 로직 추가 + }) .disposed(by: disposeBag) } - private func showLogoutAlert() { - let alert = UIAlertController(title: "로그아웃", message: "정말 로그아웃 하시겠습니까?", preferredStyle: .alert) + private func bindActions() { + myPageView.onGetUser = { [weak self] in + self?.viewModel.getUser() + } - alert.addAction(UIAlertAction(title: "취소", style: .cancel)) - alert.addAction(UIAlertAction(title: "로그아웃", style: .destructive) { [weak self] _ in + myPageView.onPostLogout = { [weak self] in self?.viewModel.logout() - }) + } - present(alert, animated: true) + myPageView.onDeleteUser = { [weak self] in + self?.viewModel.deleteUser() + } } -} \ No newline at end of file + +} diff --git a/Projects/MyPage/Sources/ViewModel/MyPageViewModel.swift b/Projects/MyPage/Sources/ViewModel/MyPageViewModel.swift index db4298f..325bcf2 100644 --- a/Projects/MyPage/Sources/ViewModel/MyPageViewModel.swift +++ b/Projects/MyPage/Sources/ViewModel/MyPageViewModel.swift @@ -8,20 +8,67 @@ import RxSwift import Domain +protocol MyPageViewModelProtocol { + func getUser() + func logout() + func deleteUser() +} + public class MyPageViewModel { - private let tokenUseCase: TokenUseCase private let disposeBag = DisposeBag() + private let userUseCase: UserUseCase + private let tokenUseCase: TokenUseCase + + // Subject for success/failure handling + public let userSubject = PublishSubject() + public let userErrorSubject = PublishSubject() - public var onLogoutSuccess: (() -> Void)? + public let logoutSuccessSubject = PublishSubject() + public let logoutErrorSubject = PublishSubject() - public init(tokenUseCase: TokenUseCase) { + public let deleteUserSuccessSubject = PublishSubject() + public let deleteUserErrorSubject = PublishSubject() + + public init(userUseCase: UserUseCase, tokenUseCase: TokenUseCase) { + self.userUseCase = userUseCase self.tokenUseCase = tokenUseCase } - + + public func getUser() { + userUseCase.getUser() + .subscribe(onSuccess: { [weak self] user in + print("✅ getUser 성공: \(user)") + self?.userSubject.onNext(user) + }, onFailure: { [weak self] error in + print("❌ getUser 실패: \(error)") + self?.userErrorSubject.onNext(error) + }).disposed(by: disposeBag) + } + public func logout() { - // 토큰 삭제 + userUseCase.postLogout() + .subscribe(onSuccess: { [weak self] _ in + print("✅ logout 성공") + self?.logoutSuccessSubject.onNext(()) + }, onFailure: { [weak self] error in + print("❌ logout 실패: \(error)") + self?.logoutErrorSubject.onNext(error) + }).disposed(by: disposeBag) + } + + public func deleteUser() { + userUseCase.deleteUser() + .subscribe(onSuccess: { [weak self] _ in + print("✅ deleteUser 성공") + self?.deleteUserSuccessSubject.onNext(()) + }, onFailure: { [weak self] error in + print("❌ deleteUser 실패: \(error)") + self?.deleteUserErrorSubject.onNext(error) + }).disposed(by: disposeBag) + } + + public func clearTokens() { tokenUseCase.clearAccessToken() - print("🔓 로그아웃 완료") - onLogoutSuccess?() + print("🔑 토큰 삭제 완료") } -} \ No newline at end of file +}