diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/ModalBuilder.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/ModalBuilder.swift index 5c43dff0..6cca959c 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/ModalBuilder.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/ModalBuilder.swift @@ -32,4 +32,8 @@ struct ModalBuilder { modalViewController.modalPresentationStyle = .overFullScreen rootViewController.present(modalViewController, animated: false) } + + func dismiss() { + modalViewController.dismiss(animated: false) + } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/CongratSquare.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/QuestCompleteModal.swift similarity index 83% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/CongratSquare.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/QuestCompleteModal.swift index 7750e77b..b7ca3ead 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/CongratSquare.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/QuestCompleteModal.swift @@ -1,8 +1,8 @@ // -// CongratSqusre.swift +// QuestCompleteModal.swift // ByeBoo-iOS // -// Created by 이나연 on 7/10/25. +// Created by 이나연 on 2/22/26. // import UIKit @@ -11,7 +11,7 @@ import Lottie import SnapKit import Then -final class CongratSquare: BaseView { +final class QuestCompleteModal: BaseView { private let titleLabel = UILabel() private let imageLottie = LottieAnimationView(name: "bori_congrate") private let descriptionLabel = UILabel() @@ -23,20 +23,21 @@ final class CongratSquare: BaseView { override func setStyle() { self.do { $0.layer.cornerRadius = 12 - $0.backgroundColor = .white5 + $0.backgroundColor = .grayscale900 } titleLabel.do { - $0.attributedText = "QUEST\nCOMPLETE!".makeTitle( - rangedText: "QUEST", - originalTitleColor: .primary100 - ) $0.applyByeBooFont( style: .head1M24, color: .primary100, textAlignment: .center, numberOfLines: 2 ) + + $0.attributedText = "QUEST\nCOMPLETE!".makeTitle( + rangedText: "QUEST", + originalTitleColor: .primary100 + ) } imageLottie.do { @@ -57,13 +58,12 @@ final class CongratSquare: BaseView { override func setLayout() { self.snp.makeConstraints { $0.width.equalTo(325.adjustedW) - $0.height.equalTo(365.adjustedH) + $0.height.equalTo(371.adjustedH) } titleLabel.snp.makeConstraints { $0.top.equalToSuperview().inset(24.adjustedH) - $0.leading.trailing.equalToSuperview().inset(61.adjustedH) - $0.width.equalTo(203.adjustedW) + $0.centerX.equalToSuperview() } imageLottie.snp.makeConstraints { @@ -75,7 +75,7 @@ final class CongratSquare: BaseView { descriptionLabel.snp.makeConstraints { $0.top.equalTo(imageLottie.snp.bottom).offset(16.adjustedH) - $0.leading.trailing.equalToSuperview().inset(61.adjustedH) + $0.centerX.equalToSuperview() } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooNavigationBar.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooNavigationBar.swift index 6275764a..14a10965 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooNavigationBar.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Navigation/ByeBooNavigationBar.swift @@ -21,6 +21,7 @@ enum NavigationBarType: Equatable { case titleAndClose(String, header: NavigationHeaderType = .clear) case titleAndBack(String, header: NavigationHeaderType = .clear) case editAndClose(header: NavigationHeaderType = .clear) + case confirmAndBack(String, header: NavigationHeaderType = .clear) case none(header: NavigationHeaderType = .clear) } @@ -76,7 +77,8 @@ struct ByeBooNavigationBar { .title(_, let header), .titleAndClose(_, let header), .editAndClose(let header), - .titleAndBack(_, let header): + .titleAndBack(_, let header), + .confirmAndBack(_, let header): headerType = header } return headerType @@ -96,6 +98,16 @@ struct ByeBooNavigationBar { .font: FontManager.sub1Sb20.font, .foregroundColor: UIColor.white ] + + $0.buttonAppearance.normal.titleTextAttributes = [ + .font: FontManager.body2M16.font, + .foregroundColor: UIColor.primary300 + ] + + $0.buttonAppearance.disabled.titleTextAttributes = [ + .font: FontManager.body2M16.font, + .foregroundColor: UIColor.grayscale600 + ] } return barAppearance } @@ -158,6 +170,19 @@ struct ByeBooNavigationBar { navigationItem: navigationItem, action: action ) + case .confirmAndBack: + let backButtonItem = makeBarButtonItem( + image: .left.withTintColor(.white), + target: topViewController, + action: action + ) + let confirmButtonItem = makeBarButtonItem( + title: "완료", + target: topViewController, + action: secondAction + ) + navigationItem.leftBarButtonItem = backButtonItem + navigationItem.rightBarButtonItem = confirmButtonItem case .none: let emptyItem = makeBarButtonItem( image: UIImage(), @@ -179,18 +204,37 @@ struct ByeBooNavigationBar { } private static func makeBarButtonItem( - image: UIImage, + image: UIImage? = nil, + title: String? = nil, target: BaseViewController, action: Selector? ) -> UIBarButtonItem { + if let image { + return UIBarButtonItem( + image: image.withTintColor(.white).withRenderingMode(.alwaysOriginal), + style: .plain, + target: target, + action: action + ) + } + + if let title { + return UIBarButtonItem( + title: title, + style: .plain, + target: target, + action: action + ) + } + return UIBarButtonItem( - image: image.withTintColor(.white).withRenderingMode(.alwaysOriginal), + title: "기본", style: .plain, target: target, action: action ) } - + private static func makeCloseButtonItem( image: UIImage, target: BaseViewController, diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/QuestScope.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/QuestScope.swift new file mode 100644 index 00000000..dba7af19 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/QuestScope.swift @@ -0,0 +1,13 @@ +// +// QuestScope.swift +// ByeBoo-iOS +// +// Created by 이나연 on 2/23/26. +// + +import Foundation + +enum QuestScope { + case common + case personal +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift index fc1ada6e..b98111bf 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift @@ -27,7 +27,6 @@ extension UITextView { var attributes: [NSAttributedString.Key: Any] = [ .font: style.font, .paragraphStyle: paragraphStyle, - .baselineOffset: (style.lineHeight - style.font.lineHeight) / 2, .kern: style.kern ] diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ActionView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ActionView.swift deleted file mode 100644 index 2077a078..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ActionView.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// ActionView.swift -// ByeBoo-iOS -// -// Created by 최주리 on 7/9/25. -// - -import UIKit - -import Kingfisher -import SnapKit - -final class ActionView: BaseView { - - private let photoView = UIImageView() - private let descriptionView: TextBoxView - private let placeholderView = UIImageView() - private let thinkTextView = IconOneLineTextView(iconType: .action,text: "이렇게 완료했어요" ) - private let descriptionText: String - private let photoURL: String - - init( - descriptionText: String, - photoURL: String - ) { - self.descriptionText = descriptionText - self.photoURL = photoURL - - descriptionView = TextBoxView(title: descriptionText) - - super.init(frame: .zero) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func setStyle() { - photoView.do { - $0.layer.cornerRadius = 12 - $0.clipsToBounds = true - $0.backgroundColor = .gray - $0.contentMode = .scaleAspectFill - guard let url = URL(string: photoURL) else { - ByeBooLogger.error(ByeBooError.URLError) - return - } - $0.kf.setImage(with: url) - } - } - - override func setUI() { - addSubviews( - photoView, - thinkTextView - ) - - addSubview(descriptionView) - } - - override func setLayout() { - thinkTextView.snp.makeConstraints { - $0.top.equalToSuperview().inset(24.5.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - } - - photoView.snp.makeConstraints { - $0.top.equalTo(thinkTextView.snp.bottom).offset(12.adjustedH) - $0.size.equalTo(327.adjustedW) - $0.centerX.equalToSuperview() - if descriptionText.isEmpty { - $0.bottom.equalToSuperview().inset(24.5.adjustedH) - } - } - - descriptionView.snp.makeConstraints { - $0.top.equalTo(photoView.snp.bottom).offset(12.adjustedH) - $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.bottom.equalToSuperview().inset(24.5.adjustedH) - } - } -} - -extension ActionView { - func updateUI(description: String, photoURL: String) { - if description.isEmpty { - descriptionView.removeFromSuperview() - photoView.snp.remakeConstraints { - $0.top.equalTo(thinkTextView.snp.bottom).offset(12.adjustedH) - $0.size.equalTo(327.adjustedW) - $0.centerX.equalToSuperview() - $0.bottom.equalToSuperview().inset(24.5.adjustedH) - } - } else { - photoView.snp.remakeConstraints { - $0.top.equalTo(thinkTextView.snp.bottom).offset(12.adjustedH) - $0.size.equalTo(327.adjustedW) - $0.centerX.equalToSuperview() - } - } - self.descriptionView.updateText(description) - - if let url = URL(string: photoURL) { - photoView.kf.setImage(with: url) - } - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestHeaderView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestHeaderView.swift index 610d5a01..bd7ae307 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestHeaderView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestHeaderView.swift @@ -10,38 +10,33 @@ import UIKit import SnapKit import Then -enum QuestHeaderType { - case complete - case archive -} - final class ArchiveQuestHeaderView: BaseView { - - private let type: QuestHeaderType + private let stepStackView = UIStackView() private let stepLabel = ByeBooTextTag(type: .gray, text: "STEP 0") private let questNumberLabel = UILabel() private let dateLabel = UILabel() + private let qLaebl = UILabel() private(set) var questTitleLabel = UILabel() private let stepNumber: Int private let questNumber: Int private let date: String + + private let questStackView = UIStackView() private let questTitle: String init( - type: QuestHeaderType, stepNumber: Int, questNumber: Int, date: String, questTitle: String ) { - self.type = type self.stepNumber = stepNumber self.questNumber = questNumber self.date = date self.questTitle = questTitle - + super.init(frame: .zero) questNumberLabel.text = "\(questNumber)번째 퀘스트" @@ -56,18 +51,28 @@ final class ArchiveQuestHeaderView: BaseView { override func setStyle() { stepStackView.do { $0.axis = .horizontal - $0.spacing = 8 - $0.distribution = .equalCentering + $0.spacing = 4 } questNumberLabel.applyByeBooFont(style: .body6R14, color: .grayscale500) dateLabel.applyByeBooFont(style: .body6R14, color: .grayscale500) + questStackView.do { + $0.axis = .horizontal + $0.spacing = 4.adjustedW + $0.alignment = .firstBaseline + } + + qLaebl.applyByeBooFont( + style: .head2M22, + text: "Q.", + color: .primary200 + ) + questTitleLabel.do { $0.applyByeBooFont ( style: .head1M24, color: .grayscale100, - textAlignment: type == .complete ? .center : .left, numberOfLines: 0 ) $0.lineBreakMode = .byWordWrapping @@ -78,38 +83,27 @@ final class ArchiveQuestHeaderView: BaseView { addSubviews( stepStackView, dateLabel, - questTitleLabel + questStackView ) stepStackView.addArrangedSubviews(stepLabel, questNumberLabel) + questStackView.addArrangedSubviews(qLaebl, questTitleLabel) } override func setLayout() { stepStackView.snp.makeConstraints { - $0.top.equalToSuperview() - - switch type { - case .complete: - $0.centerX.equalToSuperview() - case .archive: - $0.leading.equalToSuperview().inset(24.adjustedW) - } + $0.top.equalToSuperview().inset(10.adjustedH) + $0.leading.equalToSuperview().inset(24.adjustedW) } dateLabel.snp.makeConstraints { $0.top.equalTo(stepLabel.snp.bottom).offset(12.adjustedH) - - switch type { - case .complete: - $0.centerX.equalToSuperview() - case .archive: - $0.leading.equalToSuperview().inset(24.adjustedW) - } + $0.leading.equalToSuperview().inset(24.adjustedW) } - questTitleLabel.snp.makeConstraints { + questStackView.snp.makeConstraints { $0.top.equalTo(dateLabel.snp.bottom).offset(12.adjustedH) $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.bottom.equalToSuperview().inset(9.6.adjustedH) + $0.bottom.equalToSuperview().inset(10.adjustedH) } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestView.swift index e5a2a298..af4b44c1 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ArchiveQuestView.swift @@ -8,6 +8,7 @@ import UIKit import Then +import Kingfisher import SnapKit final class ArchiveQuestView: BaseView { @@ -15,20 +16,21 @@ final class ArchiveQuestView: BaseView { private let scrollView = UIScrollView() private let contentView = UIView() private let headerView = ArchiveQuestHeaderView( - type: .archive, stepNumber: 0, questNumber: 0, date: "", questTitle:"" ) - private let thinkView: ThinkView? - private let actionView: ActionView? - private let feelView = FeelView(emotionType: "", descriptionText: "") + private let textBoxView = TextBoxView(title: "") + private let photoBoxView: UIImageView? + private let feelView = FeelView(emotionType: "", descriptionText: "") private let AIAnswerButton = ByeBooButton( titleText: "보리의 답장 보러가기", type: .enabled ) + private var descriptionText: String = "" + private var photoURL: String = "" private(set) var type: QuestType @@ -37,11 +39,9 @@ final class ArchiveQuestView: BaseView { switch type { case .question: - thinkView = ThinkView(descriptionText: "") - actionView = nil + photoBoxView = nil case .activation: - thinkView = nil - actionView = ActionView(descriptionText: "", photoURL: "") + photoBoxView = UIImageView() } super.init(frame: .zero) @@ -54,30 +54,31 @@ final class ArchiveQuestView: BaseView { override func setStyle() { backgroundColor = .grayscale900 - actionView?.do { - $0.isUserInteractionEnabled = false - } - thinkView?.do { - $0.isUserInteractionEnabled = false + photoBoxView?.do { + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + $0.backgroundColor = .gray + $0.contentMode = .scaleAspectFill + guard let url = URL(string: photoURL) else { + ByeBooLogger.error(ByeBooError.URLError) + return + } + $0.kf.setImage(with: url) } } override func setUI() { - addSubview(scrollView) + addSubviews(scrollView) scrollView.addSubview(contentView) contentView.addSubviews( headerView, + textBoxView, feelView, AIAnswerButton ) - switch type { - case .question: - guard let thinkView else { return } - contentView.addSubview(thinkView) - case .activation: - guard let actionView else { return } - contentView.addSubview(actionView) + if let photoBoxView { + contentView.addSubview(photoBoxView) } } @@ -97,28 +98,30 @@ final class ArchiveQuestView: BaseView { $0.horizontalEdges.equalToSuperview() } - if let thinkView { - thinkView.snp.makeConstraints { - $0.top.equalTo(headerView.snp.bottom) - $0.horizontalEdges.equalToSuperview() - } - feelView.snp.makeConstraints { - $0.top.equalTo(thinkView.snp.bottom) - $0.horizontalEdges.equalToSuperview() + if let photoBoxView { + photoBoxView.snp.makeConstraints { + $0.top.equalTo(headerView.snp.bottom).offset(20.adjustedH) + $0.size.equalTo(327.adjustedW) + $0.centerX.equalToSuperview() } } - if let actionView { - actionView.snp.makeConstraints { - $0.top.equalTo(headerView.snp.bottom) - $0.horizontalEdges.equalToSuperview() - } - feelView.snp.makeConstraints { - $0.top.equalTo(actionView.snp.bottom) - $0.horizontalEdges.equalToSuperview() + textBoxView.snp.makeConstraints { + if let photoBoxView { + if !descriptionText.isEmpty { + $0.top.equalTo(photoBoxView.snp.bottom).offset(20.adjustedH) + } + } else { + $0.top.equalTo(headerView.snp.bottom).offset(20.adjustedH) } + $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) } + feelView.snp.makeConstraints { + $0.top.equalTo(textBoxView.snp.bottom).offset(20.adjustedH) + $0.horizontalEdges.equalToSuperview() + } + AIAnswerButton.snp.makeConstraints { $0.top.greaterThanOrEqualTo(feelView.snp.bottom).offset(44.adjustedH) $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) @@ -136,17 +139,36 @@ extension ArchiveQuestView { title: entity.question ) - self.feelView.updateUI( + feelView.updateUI( emotionType: entity.questEmotionState, descriptionText: entity.emotionDescription ) - switch self.type { + switch type { case .question: - self.thinkView?.updateUI(description: entity.answer) + textBoxView.updateText(entity.answer) case .activation: - self.actionView?.updateUI(description: entity.answer, photoURL: entity.imageUrl ?? "") + guard let photoBoxView else { return } + if let url = URL(string: entity.imageUrl!) { + photoBoxView.kf.setImage(with: url) + } + + if entity.answer.isEmpty { + textBoxView.removeFromSuperview() + + feelView.snp.remakeConstraints { + $0.top.equalTo(photoBoxView.snp.bottom).offset(20.adjustedH) + $0.horizontalEdges.equalToSuperview() + } + } else { + textBoxView.updateText(entity.answer) + textBoxView.snp.remakeConstraints { + $0.top.equalTo(photoBoxView.snp.bottom).offset(20.adjustedH) + $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) + $0.centerX.equalToSuperview() + } + } } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/FeelView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/FeelView.swift index 78adf891..dcc974e3 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/FeelView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/FeelView.swift @@ -15,8 +15,6 @@ final class FeelView: BaseView { private let emotionType: String private let descriptionText: String - private let titleTextView = IconOneLineTextView(iconType: .change, text: "퀘스트 완료 후, 이런 감정을 느꼈어요") - init(emotionType: String, descriptionText: String) { self.emotionType = emotionType self.descriptionText = descriptionText @@ -35,19 +33,13 @@ final class FeelView: BaseView { override func setUI() { addSubviews( - titleTextView, descriptionView ) } override func setLayout() { - titleTextView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - } - descriptionView.snp.makeConstraints { - $0.top.equalTo(titleTextView.snp.bottom).offset(12.adjustedH) + $0.top.equalToSuperview() $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) $0.bottom.equalToSuperview().inset(24.5.adjustedH) } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ThinkView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ThinkView.swift deleted file mode 100644 index e12f8d78..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Archive/ThinkView.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ThinkView.swift -// ByeBoo-iOS -// -// Created by 최주리 on 7/7/25. -// - -import UIKit - -import SnapKit - -final class ThinkView: BaseView { - - private let titleTextView = IconOneLineTextView(iconType: .think, text: "이렇게 생각했어요") - private let descriptionView: TextBoxView - private let descriptionText: String - - init(descriptionText: String) { - self.descriptionText = descriptionText - descriptionView = TextBoxView(title: descriptionText) - - super.init(frame: .zero) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func setStyle() { - - } - - override func setUI() { - addSubviews( - titleTextView, - descriptionView - ) - } - - override func setLayout() { - titleTextView.snp.makeConstraints { - $0.top.equalToSuperview().inset(24.5.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - } - - descriptionView.snp.makeConstraints { - $0.top.equalTo(titleTextView.snp.bottom).offset(12.adjustedH) - $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.bottom.equalToSuperview().inset(24.5.adjustedH) - } - } -} - -extension ThinkView { - func updateUI(description: String) { - self.descriptionView.updateText(description) - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/QuestStart/QuestStartView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/QuestStart/QuestStartView.swift index a8ad086a..02500730 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/QuestStart/QuestStartView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/QuestStart/QuestStartView.swift @@ -28,16 +28,16 @@ final class QuestStartView: BaseView { $0.isUserInteractionEnabled = true } titleLabel.do { - $0.attributedText = "QUEST JOURNEY\nSTART!".makeTitle( - rangedText: "QUEST JOURNEY", - originalTitleColor: .primary100 - ) $0.applyByeBooFont( style: .head1M24, color: .primary100, textAlignment: .center, numberOfLines: 2 ) + $0.attributedText = "QUEST JOURNEY\nSTART!".makeTitle( + rangedText: "QUEST JOURNEY", + originalTitleColor: .primary100 + ) } cloverImageView.do { $0.image = .clover diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/ImagePickerContainer.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/ImagePickerContainer.swift similarity index 100% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/ImagePickerContainer.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/ImagePickerContainer.swift diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/QuestTextField.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/QuestTextField.swift new file mode 100644 index 00000000..6eb089fa --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/QuestTextField.swift @@ -0,0 +1,179 @@ +// +// QuestTextField.swift +// ByeBoo-iOS +// +// Created by 이나연 on 7/8/25. +// + +import UIKit + +import SnapKit +import Then + +final class QuestTextField: BaseView { + private(set) var textView = UITextView() + private var descriptionStackView: UIStackView? + private let errorIcon = UIImageView() + private let descriptionLabel = UILabel() + private(set) var textCountLabel = UILabel() + + private var questType: QuestType + var isPlaceholderActive: Bool = true + var count: Int = 0 + private(set) var placeholder: String + private(set) var limitCount: Int + private var containerHeightConsraint: Constraint? + private var textViewHeightConstraint: Constraint? + + weak var delegate: QuestCompleteProtocol? + + init(type: QuestType) { + self.questType = type + placeholder = type.plaeholder + limitCount = type.textLimit + + switch type { + case .question: + descriptionStackView = UIStackView() + case .activation: + descriptionStackView = nil + } + + super.init(frame: .zero) + textView.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setUI() { + addSubviews(textView, textCountLabel) + + if let descriptionStackView { + addSubviews(descriptionStackView) + descriptionStackView.addArrangedSubviews(errorIcon, descriptionLabel) + } + } + + override func setStyle() { + self.do { + $0.backgroundColor = .clear + } + + textView.do { + $0.backgroundColor = .clear + $0.tintColor = .grayscale100 + $0.isScrollEnabled = false + $0.applyByeBooFont( + style: .body3R16, + text: placeholder, + color: .grayscale300 + ) + } + + textCountLabel.applyByeBooFont ( + style: .cap2R12, + text: "(\(count)/\(limitCount))", + color: .grayscale400 + ) + + if let descriptionStackView { + descriptionStackView.do { + $0.axis = .horizontal + $0.spacing = 3.adjustedW + } + } + + errorIcon.do { + $0.image = .error + $0.contentMode = .scaleAspectFit + } + + descriptionLabel.applyByeBooFont( + style: .cap2R12, + text: "10글자 이상 작성해 주세요.", + color: .grayscale400, + textAlignment: .center + ) + } + + override func setLayout() { + self.snp.makeConstraints { + $0.width.equalTo(327.adjustedW) + containerHeightConsraint = $0.height.equalTo(268.adjustedH).constraint + } + + textView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.leading.trailing.equalToSuperview() + textViewHeightConstraint = $0.height.equalTo(196.adjustedH).constraint + } + + if let descriptionStackView { + descriptionStackView.snp.makeConstraints { + $0.leading.equalToSuperview() + $0.bottom.equalToSuperview().inset(24.adjustedW) + } + } + + textCountLabel.snp.makeConstraints { + $0.trailing.equalToSuperview() + $0.bottom.equalToSuperview().inset(24.adjustedW) + } + } +} + +extension QuestTextField: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + if isPlaceholderActive == true { + isPlaceholderActive = false + applyTextViewStyle(text: "", color: .grayscale100) + } + textView.textColor = .grayscale100 + } + + func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.isEmpty { + applyTextViewStyle(text: placeholder, color: .grayscale300) + isPlaceholderActive = true + } else { + applyTextViewStyle(text: textView.text, color: .grayscale100) + } + textCountLabel.textColor = .grayscale300 + updateTextViewHeight() + } + + func textViewDidChange(_ textView: UITextView) { + if textView.text.count > limitCount { + textView.deleteBackward() + } + count = textView.text.count + textCountLabel.text = "(\(count)/\(limitCount))" + updateTextViewHeight() + delegate?.updateButtonWhenWriting(text: textView.text) + } +} + +extension QuestTextField { + func applyTextViewStyle(text: String, color: UIColor) { + textView.applyByeBooFont( + style: .body3R16, + text: text, + color: color + ) + } + + func updateTextViewHeight() { + let width = self.frame.width + let fittingSize = CGSize(width: width, height: .greatestFiniteMagnitude) + let estimatedHeight = ceil(textView.sizeThatFits(fittingSize).height) + let containerMinHeight = 268.adjustedH + let textViewMinHeight = 196.adjustedH + + containerHeightConsraint?.update(offset: max(containerMinHeight, estimatedHeight + 72.adjustedH)) + textViewHeightConstraint?.update(offset: max(textViewMinHeight, estimatedHeight)) + superview?.layoutIfNeeded() + layoutIfNeeded() + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/ActivationType/WriteActiveTypeQuestView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/View/ActivationType/WriteActiveTypeQuestView.swift similarity index 83% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/ActivationType/WriteActiveTypeQuestView.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/View/ActivationType/WriteActiveTypeQuestView.swift index 575ddcbd..f8525510 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/ActivationType/WriteActiveTypeQuestView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/View/ActivationType/WriteActiveTypeQuestView.swift @@ -13,12 +13,12 @@ import Then final class WriteActiveTypeQuestView: BaseView { private(set) var scrollView = UIScrollView() private let contentView = UIView() - private(set) var title = WriteQuestTitleView( - stepNum: "", - stepTitle: "", + + private(set) var headerView = WriteQuestTitleView( questNum: 0, title: "" ) + private let divider = UIView() private let imgTitleContainerView = UIView() private let yellowTag = ByeBooFilledTag(tagType: .yelloFilled, text: "필수") @@ -31,19 +31,18 @@ final class WriteActiveTypeQuestView: BaseView { private let grayTag = ByeBooFilledTag(tagType: .smallGray, text: "선택") private let thinkTitleLabel = UILabel() private(set) var questTextField = QuestTextField(type: .activation) - private(set) var confirmButton = ByeBooButton(titleText: "완료하기", type: .disabled) override func setUI() { addSubviews(scrollView) scrollView.addSubviews(contentView) contentView.addSubviews( - title, + headerView, + divider, imgTitleContainerView, textStackView, imageContainer, - questTextField, - confirmButton + questTextField ) imgTitleContainerView.addSubviews( @@ -70,6 +69,10 @@ final class WriteActiveTypeQuestView: BaseView { $0.isUserInteractionEnabled = true } + divider.do { + $0.backgroundColor = .grayscale800 + } + imgTitleLabel.applyByeBooFont( style: .body2M16, text: "사진 첨부", @@ -103,15 +106,22 @@ final class WriteActiveTypeQuestView: BaseView { contentView.snp.makeConstraints { $0.edges.equalTo(scrollView.contentLayoutGuide) $0.width.equalTo(scrollView.frameLayoutGuide) + $0.bottom.equalTo(questTextField) } - title.snp.makeConstraints { - $0.top.equalTo(0) + headerView.snp.makeConstraints { + $0.top.equalTo(self.safeAreaLayoutGuide) $0.leading.trailing.equalToSuperview() } + divider.snp.makeConstraints { + $0.top.equalTo(headerView.snp.bottom) + $0.leading.trailing.equalToSuperview().inset(24.adjustedW) + $0.height.equalTo(1.adjustedH) + } + imgTitleContainerView.snp.makeConstraints { - $0.top.equalTo(title.snp.bottom).offset(8.adjustedH) + $0.top.equalTo(divider.snp.bottom).offset(20.adjustedH) $0.height.equalTo(24.adjustedH) $0.leading.equalToSuperview().inset(24.adjustedW) } @@ -150,15 +160,7 @@ final class WriteActiveTypeQuestView: BaseView { questTextField.snp.makeConstraints { $0.top.equalTo(textStackView.snp.bottom).offset(8.adjustedH) $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - $0.height.equalTo(290.adjustedH) - } - - confirmButton.snp.makeConstraints { - $0.top.equalTo(questTextField.snp.bottom).offset(28.adjustedH) - $0.height.equalTo(53.adjustedH) - $0.width.equalTo(311.adjustedW) - $0.bottom.equalToSuperview().inset(24.adjustedH) - $0.centerX.equalToSuperview() + $0.height.greaterThanOrEqualTo(290.adjustedH) } } @@ -167,17 +169,23 @@ final class WriteActiveTypeQuestView: BaseView { } } +extension WriteActiveTypeQuestView: WriteQuestBaseProtocol { + var questTextView: UITextView { + questTextField.textView + } + + var tipTagView: UIView { + headerView.tipTag + } +} + extension WriteActiveTypeQuestView { func updateQuestTitle( - step: String, - stepNum: Int, questNumber: Int, questStyle: String, question: String ) { - title.bind( - stepNum: String(stepNum), - stepTitle: step, + headerView.bind( questNum: questNumber, title: question ) diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/View/QuestionType/WriteQuestionTypeQuestView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/View/QuestionType/WriteQuestionTypeQuestView.swift new file mode 100644 index 00000000..4bc5f6d9 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/View/QuestionType/WriteQuestionTypeQuestView.swift @@ -0,0 +1,105 @@ +// +// WriteQuestionTypeView.swift +// ByeBoo-iOS +// +// Created by 이나연 on 7/8/25. +// + +import UIKit + +import SnapKit +import Then + +final class WriteQuestionTypeQuestView: BaseView { + private(set) var scrollView = UIScrollView() + private let contentView = UIView() + private(set) var headerView = WriteQuestTitleView(questNum: 0, title: "") + private let divider = UIView() + private(set) var questTextField = QuestTextField(type: .question) + + override func setUI() { + addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubviews( + headerView, + divider, + questTextField + ) + } + + override func setStyle() { + backgroundColor = .grayscale900 + + scrollView.do { + $0.isScrollEnabled = true + $0.keyboardDismissMode = .onDrag + $0.backgroundColor = .clear + $0.isUserInteractionEnabled = true + } + + contentView.do { + $0.backgroundColor = .grayscale900 + $0.isUserInteractionEnabled = true + } + + divider.do { + $0.backgroundColor = .grayscale800 + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.endEditing(true) + } + + override func setLayout() { + scrollView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.edges.equalTo(scrollView.contentLayoutGuide) + $0.width.equalTo(scrollView.frameLayoutGuide) + $0.height.greaterThanOrEqualTo(scrollView.frameLayoutGuide) + } + + headerView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.leading.trailing.equalToSuperview() + } + + divider.snp.makeConstraints { + $0.top.equalTo(headerView.snp.bottom) + $0.leading.trailing.equalToSuperview().inset(24.adjustedW) + $0.height.equalTo(1.adjustedH) + } + + questTextField.snp.makeConstraints { + $0.top.equalTo(divider.snp.bottom).offset(20.adjustedH) + $0.leading.trailing.equalToSuperview().inset(24.adjustedW) + $0.height.greaterThanOrEqualTo(268.adjustedH) + } + } +} + +extension WriteQuestionTypeQuestView: WriteQuestBaseProtocol { + var questTextView: UITextView { + questTextField.textView + } + + var tipTagView: UIView { + headerView.tipTag + } +} + +extension WriteQuestionTypeQuestView { + func updateQuestTitle( + questNumber: Int, + questStyle: String, + question: String + ) { + headerView.bind( + questNum: questNumber, + title: question + ) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/WriteQuestTitleView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/WriteQuestTitleView.swift similarity index 65% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/WriteQuestTitleView.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/WriteQuestTitleView.swift index f3307d20..dbb0368b 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/WriteQuestTitleView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/WriteQuestTitleView.swift @@ -11,10 +11,6 @@ import SnapKit import Then final class WriteQuestTitleView: BaseView { - private let stepStackView = UIStackView() - private let stepNum = ByeBooTextTag(type: .gray, text: "STEP 0") - private let stepTitle = UILabel() - private var questNum: Int private let questNumLabel = UILabel() @@ -22,8 +18,7 @@ final class WriteQuestTitleView: BaseView { let tipTag = ByeBooTipTag(text: "작성 TIP") - init(stepNum: String, stepTitle: String, questNum: Int, title: String) { - self.stepTitle.text = stepTitle + init(questNum: Int, title: String) { self.questNum = questNum self.titleLabel.text = title self.questNumLabel.text = "\(questNum)번째 퀘스트" @@ -35,17 +30,10 @@ final class WriteQuestTitleView: BaseView { } override func setUI() { - addSubviews(stepStackView, questNumLabel, titleLabel, tipTag) - stepStackView.addArrangedSubviews(stepNum, stepTitle) + addSubviews(questNumLabel, titleLabel, tipTag) } override func setStyle() { - stepStackView.do { - $0.axis = .horizontal - $0.spacing = 8.adjustedW - } - - stepTitle.applyByeBooFont(style: .body2M16, color: .grayscale500) questNumLabel.applyByeBooFont(style: .body6R14, color: .grayscale500) titleLabel.do { @@ -64,19 +52,14 @@ final class WriteQuestTitleView: BaseView { } override func setLayout() { - stepStackView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.centerX.equalToSuperview() - } - questNumLabel.snp.makeConstraints { - $0.top.equalTo(stepStackView.snp.bottom).offset(12.adjustedH) + $0.top.equalToSuperview() $0.centerX.equalToSuperview() } titleLabel.snp.makeConstraints { $0.top.equalTo(questNumLabel.snp.bottom).offset(12.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedH) + $0.width.equalTo(327.adjustedW) $0.centerX.equalToSuperview() } @@ -91,11 +74,9 @@ final class WriteQuestTitleView: BaseView { } extension WriteQuestTitleView { - func bind(stepNum: String, stepTitle: String, questNum: Int, title: String) { - self.stepNum.updateText("STEP \(stepNum)") - self.stepTitle.text = stepTitle + func bind(questNum: Int, title: String) { self.questNum = questNum self.titleLabel.text = title questNumLabel.text = "\(questNum)번째 퀘스트" - } + } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/AIAnswerViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/AIAnswerViewController.swift similarity index 100% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/AIAnswerViewController.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/AIAnswerViewController.swift diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ArchiveQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ArchiveQuestViewController.swift index 35cfbfc3..f6248c99 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ArchiveQuestViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/ArchiveQuestViewController.swift @@ -11,20 +11,21 @@ import UIKit enum ArchiveViewControllerEntryPoint { case mypage case questMain + case writeQuest case edit } final class ArchiveQuestViewController: BaseViewController { private var rootView = ArchiveQuestView(type: .activation) - private let viewModel: CompleteQuestViewModel + private let viewModel: ArchiveQuestViewModel private var cancellables = Set() private var questID: Int = 1 private var questType: QuestType = .activation var entryViewController: ArchiveViewControllerEntryPoint? - init(viewModel: CompleteQuestViewModel + init(viewModel: ArchiveQuestViewModel ) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) @@ -47,13 +48,24 @@ final class ArchiveQuestViewController: BaseViewController { super.viewDidLoad() self.navigationItem.hidesBackButton = true - ByeBooNavigationBar.makeNavigationBar( - navigationItem: self.navigationItem, - navigationController: self.navigationController, - type: .editAndClose(header: .black), - action: #selector(close), - secondAction: #selector(editButtonDidTap) - ) + guard let entryViewController else { return } + switch entryViewController { + case .writeQuest: + ByeBooNavigationBar.makeNavigationBar( + navigationItem: self.navigationItem, + navigationController: self.navigationController, + type: .close(header: .black), + action: #selector(close) + ) + case .mypage, .questMain, .edit: + ByeBooNavigationBar.makeNavigationBar( + navigationItem: self.navigationItem, + navigationController: self.navigationController, + type: .editAndClose(header: .black), + action: #selector(close), + secondAction: #selector(editButtonDidTap) + ) + } viewModel.action(.questAnswerDidLoad(questID: questID)) bind() } @@ -66,8 +78,9 @@ extension ArchiveQuestViewController: ToastPresentable, ToastErrorHandler { .sink { [weak self] result in switch result { case .success(let entity): - ByeBooLogger.debug("퀘스트 아이디 \(self?.questID)") - self?.rootView.updateUI(entity) + guard let self else { return } + ByeBooLogger.debug("퀘스트 아이디 \(self.questID)") + self.rootView.updateUI(entity) case .failure(let error): self?.handleError(error) } @@ -96,7 +109,7 @@ extension ArchiveQuestViewController: Dismissible { switch entryViewController { case .mypage, .questMain: self.navigationController?.popViewController(animated: true) - case .edit: + case .writeQuest, .edit: let viewController = ByeBooTabBar() viewController.selectedIndex = 1 diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/WriteActiveTypeQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteActiveTypeQuestViewController.swift similarity index 67% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/WriteActiveTypeQuestViewController.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteActiveTypeQuestViewController.swift index f28adfd9..ef7acd2c 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/WriteActiveTypeQuestViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteActiveTypeQuestViewController.swift @@ -11,9 +11,8 @@ import UIKit import Kingfisher import Mixpanel -final class WriteActiveTypeQuestViewController: BaseViewController { - - private let rootView = WriteActiveTypeQuestView() +final class WriteActiveTypeQuestViewController: WriteQuestBaseViewController { + private let viewModel: WriteActiveTypeViewModel private var cancellables = Set() var questMode: QuestMode = .write @@ -25,49 +24,24 @@ final class WriteActiveTypeQuestViewController: BaseViewController { private var answerText: String = "" private var emotionState: String = "" private var image: UIImage = UIImage() - private var isKeyboardUsed: Bool = false + private var isImageChanged: Bool = false private var originalImageKey: String = "" private let bottomSheetViewController = EmotionBottomSheetViewController() - override func loadView() { - view = rootView - } - init(viewModel: WriteActiveTypeViewModel){ self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + super.init(rootView: WriteActiveTypeQuestView()) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewWillAppear(_ animated: Bool) { - NotificationCenter.default.addObserver( - self, - selector: #selector(textViewMoveUp), - name: UIResponder.keyboardWillShowNotification, object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(textViewMoveDown), - name: UIResponder.keyboardWillHideNotification, object: nil - ) - isKeyboardUsed = false - } - override func viewDidLoad() { super.viewDidLoad() - ByeBooNavigationBar.makeNavigationBar( - navigationItem: self.navigationItem, - navigationController: self.navigationController, - type: .back(header: .black), - action: #selector(back) - ) - setGesture() setDelegate() bind() presentPhotoPicker() @@ -87,72 +61,21 @@ final class WriteActiveTypeQuestViewController: BaseViewController { ) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - override func setAddTarget() { - rootView.confirmButton.addTarget(self, action: #selector(confirmButtonDidTap), for: .touchUpInside) - } - override func setDelegate() { rootView.questTextField.delegate = self } - private func setGesture() { - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(endEditingOnTap)) - let tipTagGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tipTagDidTap)) - - tapGestureRecognizer.isEnabled = true - tapGestureRecognizer.delegate = self - tapGestureRecognizer.cancelsTouchesInView = false - - - tipTagGestureRecognizer.isEnabled = true - - self.rootView.title.tipTag.addGestureRecognizer(tipTagGestureRecognizer) - self.rootView.scrollView.addGestureRecognizer(tapGestureRecognizer) - } - - private func presentPhotoPicker() { - rootView.imageContainer.didTapAddImage = { [weak self] in - guard let self = self else { return } - self.openPhotosButtenPressed() - } - } -} - -extension WriteActiveTypeQuestViewController { - @objc - private func textViewMoveUp(_ notification: NSNotification) { - if self.view.window?.frame.origin.y == 0 && !isKeyboardUsed{ - if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { - let keyboardHeight = keyboardFrame.cgRectValue.height - let safeAreaBottom = view.safeAreaInsets.bottom - let offsetY = keyboardHeight - safeAreaBottom - - UIView.animate(withDuration: 0.3) { - self.rootView.transform = CGAffineTransform(translationX: 0, y: -offsetY) - } - - isKeyboardUsed = true - } - } - - } - @objc - private func textViewMoveDown(_ notification: NSNotification) { - UIView.animate(withDuration: 0.3) { - self.rootView.transform = .identity - } - isKeyboardUsed = false + override func tipTagDidTap() { + let viewController = ViewControllerFactory.shared.makeQuestTipViewController() + viewController.bind(questID: questID, questType: questType, questNumber: questNumber) + viewController.navigationItem.hidesBackButton = true + viewController.modalPresentationStyle = .fullScreen + self.present(viewController, animated: false) } @objc - private func confirmButtonDidTap() { + override func confirmButtonDidTap() { if rootView.questTextField.textView.text == rootView.questTextField.placeholder || rootView.questTextField.textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { @@ -186,22 +109,16 @@ extension WriteActiveTypeQuestViewController { properties: property.dictionary ) } - - @objc - private func endEditingOnTap(sender: UITapGestureRecognizer){ - self.view.endEditing(true) - } - - @objc - private func tipTagDidTap() { - let viewController = ViewControllerFactory.shared.makeQuestTipViewController() - viewController.bind(questID: questID, questType: questType, questNumber: questNumber) - viewController.navigationItem.hidesBackButton = true - viewController.modalPresentationStyle = .fullScreen - self.present(viewController, animated: false) + + private func presentPhotoPicker() { + rootView.imageContainer.didTapAddImage = { [weak self] in + guard let self = self else { return } + self.openPhotosButtenPressed() + } } } + extension WriteActiveTypeQuestViewController: ToastPresentable, ToastErrorHandler { private func bind() { @@ -212,8 +129,6 @@ extension WriteActiveTypeQuestViewController: ToastPresentable, ToastErrorHandle case .success(let quest): self?.questNumber = quest.questNumber self?.rootView.updateQuestTitle( - step: quest.step, - stepNum: quest.stepNumber, questNumber: quest.questNumber, questStyle: quest.questStyle, question: quest.question @@ -230,11 +145,24 @@ extension WriteActiveTypeQuestViewController: ToastPresentable, ToastErrorHandle switch result { case .success(()): guard let self else { return } - ByeBooLogger.debug("퀘스트 아이디 \(self.questID)") - let viewController = ViewControllerFactory.shared.makeCompleteActiveTypeQuestViewController() - viewController.configure(questID: self.questID, questNumber: self.questNumber) - self.bottomSheetViewController.dismiss(animated: true) - self.navigationController?.pushViewController(viewController, animated: true) + self.bottomSheetViewController.dismiss(animated: true) { + let modal = ModalBuilder( + modalView: QuestCompleteModal(), + action: nil, + rootViewController: self + ) + modal.present() + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + modal.dismiss() + + ByeBooLogger.debug("퀘스트 아이디 \(self.questID)") + let viewController = ViewControllerFactory.shared.makeArchiveQuestViewController() + viewController.entryViewController = .writeQuest + viewController.configure(questID: self.questID, questType: .activation) + self.navigationController?.pushViewController(viewController, animated: true) + } + } case .failure(let error): self?.handleError(error) } @@ -247,8 +175,6 @@ extension WriteActiveTypeQuestViewController: ToastPresentable, ToastErrorHandle switch result { case .success(let quest): self?.rootView.updateQuestTitle( - step: quest.step, - stepNum: quest.stepNumber, questNumber: quest.questNumber, questStyle: quest.questStyle, question: quest.question @@ -279,43 +205,18 @@ extension WriteActiveTypeQuestViewController: ToastPresentable, ToastErrorHandle viewModel.output.isValidTextPublisher .receive(on: DispatchQueue.main) .sink { [weak self] result in + guard let self else { return } switch result { case true: - self?.rootView.confirmButton.updateType(.enabled) + self.navigationItem.rightBarButtonItem?.isEnabled = true case false: - self?.rootView.confirmButton.updateType(.disabled) + self.navigationItem.rightBarButtonItem?.isEnabled = false } } .store(in: &cancellables) } } -extension WriteActiveTypeQuestViewController: BackNavigable { - func back() { - tabBarController?.tabBar.isHidden = true - - let action: (() -> Void) = { - self.navigationController?.popViewController(animated: true) - self.tabBarController?.tabBar.isHidden = false - } - - ModalBuilder( - modalView: QuitModalView(), - action: action, - rootViewController: self - ).present() - } -} - -extension WriteActiveTypeQuestViewController: UIGestureRecognizerDelegate { - func gestureRecognizer( - _ agestureRecognizer: UIGestureRecognizer, - shouldReceive touch: UITouch) - -> Bool { - return true - } -} - extension WriteActiveTypeQuestViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func openPhotosButtenPressed() { let imagePicker = UIImagePickerController() @@ -399,14 +300,14 @@ extension WriteActiveTypeQuestViewController: EditQuestProtocol { rootView.imgCount = 1 rootView.updateImageCountLabel(count: 1) rootView.imageContainer.changeIconHidden() - rootView.confirmButton.updateType(.disabled) + self.navigationItem.rightBarButtonItem?.isEnabled = false if questAnswer.isEmpty { rootView.questTextField.textView.text = "꼭 적지 않아도 괜찮지만, 글로 정리해 보면 스스로에게 한 걸음 더 가까워질 수 있어요." } else { rootView.questTextField.textView.text = questAnswer - rootView.questTextField.textCount.text = "(\(questAnswer.count)/\(rootView.questTextField.limitCount))" + rootView.questTextField.textCountLabel.text = "(\(questAnswer.count)/\(rootView.questTextField.limitCount))" rootView.questTextField.isPlaceholderActive = false } } @@ -415,13 +316,16 @@ extension WriteActiveTypeQuestViewController: EditQuestProtocol { extension WriteActiveTypeQuestViewController: QuestCompleteProtocol { func changeCount(count: Int) { if count == 1 { - rootView.confirmButton.updateType(.enabled) + self.navigationItem.rightBarButtonItem?.isEnabled = true } else { - rootView.confirmButton.updateType(.disabled) + self.navigationItem.rightBarButtonItem?.isEnabled = false } } func updateButtonWhenWriting(text: String) { viewModel.action(.textFieldEditing(answerText: self.answerText, text: text, imgCount: rootView.imgCount)) + if isKeyboardUsed { + adjustViewForKeyboard(mode: .textGrowth) + } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestBaseViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestBaseViewController.swift new file mode 100644 index 00000000..3e7e4b04 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestBaseViewController.swift @@ -0,0 +1,217 @@ +// +// BaseWriteQuestViewController.swift +// ByeBoo-iOS +// +// Created by 이나연 on 2/24/26. +// + +import UIKit + +protocol WriteQuestBaseProtocol where Self: UIView { + var scrollView: UIScrollView { get } + var questTextView: UITextView { get } + var tipTagView: UIView { get } +} + +class WriteQuestBaseViewController: + BaseViewController, UIGestureRecognizerDelegate, BackNavigable { + + let rootView: RootView + var isKeyboardUsed: Bool = false + private var keyboardFrameInWindow: CGRect = .zero + private var currentKeyboardOffset: CGFloat = 0 + private var previousTextViewHeight: CGFloat = 0 + + + init(rootView: RootView) { + self.rootView = rootView + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = rootView + } + + override func viewWillAppear(_ animated: Bool) { + registerKeyboardNotificationCenter() + isKeyboardUsed = false + } + + override func viewDidLoad() { + super.viewDidLoad() + + ByeBooNavigationBar.makeNavigationBar( + navigationItem: self.navigationItem, + navigationController: self.navigationController, + type: .confirmAndBack("완료", header: .clear), + action: #selector(back), + secondAction: #selector(confirmButtonDidTap) + ) + + self.navigationItem.rightBarButtonItem?.isEnabled = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + removeKeyboardNotifiationCneter() + } + + override func viewDidDisappear(_ animated: Bool) { + removeKeyboardNotifiationCneter() + } + + override func setAddTarget() { + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(endEditingOnTap)) + let tipTagGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tipTagDidTap)) + + tapGestureRecognizer.isEnabled = true + tapGestureRecognizer.delegate = self + tapGestureRecognizer.cancelsTouchesInView = false + + rootView.scrollView.addGestureRecognizer(tapGestureRecognizer) + rootView.tipTagView.addGestureRecognizer(tipTagGestureRecognizer) + rootView.tipTagView.isUserInteractionEnabled = true + } + + func back() { + tabBarController?.tabBar.isHidden = true + + let action: (() -> Void) = { + self.navigationController?.popViewController(animated: true) + self.tabBarController?.tabBar.isHidden = false + } + + ModalBuilder( + modalView: QuitModalView(), + action: action, + rootViewController: self + ).present() + } + + @objc func tipTagDidTap() {} + @objc func confirmButtonDidTap() { } + + @objc + private func textViewMoveUp(_ notification: NSNotification) { + guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { + return + } + keyboardFrameInWindow = keyboardFrame.cgRectValue + currentKeyboardOffset = 0 + previousTextViewHeight = rootView.questTextView.bounds.height + isKeyboardUsed = true + + DispatchQueue.main.async { [weak self] in + self?.adjustViewForKeyboard(mode: .caretTracking) + } + } + + @objc + private func textViewMoveDown(_ notification: NSNotification) { + keyboardFrameInWindow = .zero + currentKeyboardOffset = 0 + previousTextViewHeight = 0 + UIView.animate(withDuration: 0.3) { + self.rootView.transform = .identity + } + isKeyboardUsed = false + } + + @objc + private func endEditingOnTap(sender: UITapGestureRecognizer){ + self.view.endEditing(true) + } +} + +extension WriteQuestBaseViewController { + enum KeyboardAdjustMode { + case textGrowth + case caretTracking + } + + func adjustViewForKeyboard(mode: KeyboardAdjustMode) { + guard isKeyboardUsed else { return } + guard !keyboardFrameInWindow.isEmpty else { return } + + switch mode { + case .textGrowth: + adjustViewForTextGrowth() + case .caretTracking: + adjustViewForCurrentCaret() + } + } + + private func adjustViewForTextGrowth() { + let currentHeight = rootView.questTextView.bounds.height + guard currentHeight > 0 else { return } + + if previousTextViewHeight == 0 { + previousTextViewHeight = currentHeight + return + } + + let diff = currentHeight - previousTextViewHeight + previousTextViewHeight = currentHeight + + guard abs(diff) > 0.5 else { return } + + let textView = rootView.questTextView + let textViewFrameInWindow = textView.convert(textView.bounds, to: nil) + let overlap = keyboardOverlap(for: textViewFrameInWindow) + let targetOffset = max(0, currentKeyboardOffset + overlap) + + applyKeyboardOffset(targetOffset) + } + + private func adjustViewForCurrentCaret() { + let textView = rootView.questTextView + guard textView.isFirstResponder, + let selectedRange = textView.selectedTextRange else { return } + + let caretRect = textView.caretRect(for: selectedRange.end) + let caretInWindow = textView.convert(caretRect, to: nil) + let overlap = keyboardOverlap(for: caretInWindow) + let targetOffset = max(0, overlap) + + applyKeyboardOffset(targetOffset) + } + + private func keyboardOverlap(for rectInWindow: CGRect) -> CGFloat { + let keyboardTop = keyboardFrameInWindow.minY + let padding = 12.adjustedH + return rectInWindow.maxY + padding - keyboardTop + } + + private func applyKeyboardOffset(_ targetOffset: CGFloat) { + guard abs(targetOffset - currentKeyboardOffset) > 0.5 else { return } + currentKeyboardOffset = targetOffset + + UIView.animate(withDuration: 0.2) { + self.rootView.transform = CGAffineTransform(translationX: 0, y: -self.currentKeyboardOffset) + } + } +} + +extension WriteQuestBaseViewController { + private func registerKeyboardNotificationCenter() { + NotificationCenter.default.addObserver( + self, + selector: #selector(textViewMoveUp), + name: UIResponder.keyboardWillShowNotification, object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(textViewMoveDown), + name: UIResponder.keyboardWillHideNotification, object: nil + ) + } + + private func removeKeyboardNotifiationCneter() { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/WriteQuestionTypeQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift similarity index 60% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/WriteQuestionTypeQuestViewController.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift index c249f928..8d0c3a3c 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/WriteQuestionTypeQuestViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift @@ -10,9 +10,8 @@ import UIKit import Mixpanel -final class WriteQuestionTypeQuestViewController: BaseViewController { +final class WriteQuestionTypeQuestViewController: WriteQuestBaseViewController { - private let rootView = WriteQuestionTypeQuestView() private let viewModel: WriteQuestionTypeViewModel private var cancellables = Set() var questMode: QuestMode = .write @@ -23,45 +22,20 @@ final class WriteQuestionTypeQuestViewController: BaseViewController { private var answerText: String = "" private var emotionState: String = "" - private var isKeyboardUsed: Bool = false private let bottomSheetViewController = EmotionBottomSheetViewController() init(viewModel: WriteQuestionTypeViewModel){ self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + super.init(rootView: WriteQuestionTypeQuestView()) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func loadView() { - view = rootView - } - - override func viewWillAppear(_ animated: Bool) { - NotificationCenter.default.addObserver( - self, - selector: #selector(textViewMoveUp), - name: UIResponder.keyboardWillShowNotification, object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(textViewMoveDown), - name: UIResponder.keyboardWillHideNotification, object: nil - ) - isKeyboardUsed = false - } - override func viewDidLoad() { super.viewDidLoad() - ByeBooNavigationBar.makeNavigationBar( - navigationItem: self.navigationItem, - navigationController: self.navigationController, - type: .back(header: .black), - action: #selector(back) - ) bind() setDelegate() @@ -81,58 +55,21 @@ final class WriteQuestionTypeQuestViewController: BaseViewController { ) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - override func setAddTarget() { - rootView.confirmButton.addTarget(self, action: #selector(confirmButtonDidTap), for: .touchUpInside) - - let tipTagGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tipTagDidTap)) - self.rootView.title.tipTag.addGestureRecognizer(tipTagGestureRecognizer) - self.rootView.title.tipTag.isUserInteractionEnabled = true - } - - override func viewDidDisappear(_ animated: Bool) { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - override func setDelegate() { rootView.questTextField.delegate = self } -} - -extension WriteQuestionTypeQuestViewController { - @objc - private func textViewMoveUp(_ notification: NSNotification) { - if self.view.window?.frame.origin.y == 0 && !isKeyboardUsed{ - if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { - let keyboardHeight = keyboardFrame.cgRectValue.height - let safeAreaBottom = view.safeAreaInsets.bottom - let offsetY = keyboardHeight - safeAreaBottom - - UIView.animate(withDuration: 0.3) { - self.rootView.transform = CGAffineTransform(translationX: 0, y: -offsetY * 0.5) - } - - isKeyboardUsed = true - } - } - } @objc - private func textViewMoveDown(_ notification: NSNotification) { - UIView.animate(withDuration: 0.3) { - self.rootView.transform = .identity - } - isKeyboardUsed = false + override func tipTagDidTap() { + let viewController = ViewControllerFactory.shared.makeQuestTipViewController() + viewController.bind(questID: questID, questType: questType, questNumber: questNumber) + viewController.navigationItem.hidesBackButton = true + viewController.modalPresentationStyle = .fullScreen + self.present(viewController, animated: false) } @objc - private func confirmButtonDidTap() { + override func confirmButtonDidTap() { answerText = rootView.questTextField.textView.text if questMode == .edit { @@ -159,17 +96,9 @@ extension WriteQuestionTypeQuestViewController { properties: property.dictionary ) } - - @objc - private func tipTagDidTap() { - let viewController = ViewControllerFactory.shared.makeQuestTipViewController() - viewController.bind(questID: questID, questType: questType, questNumber: questNumber) - viewController.navigationItem.hidesBackButton = true - viewController.modalPresentationStyle = .fullScreen - self.present(viewController, animated: false) - } } + extension WriteQuestionTypeQuestViewController: ToastPresentable, ToastErrorHandler { private func bind() { @@ -180,8 +109,6 @@ extension WriteQuestionTypeQuestViewController: ToastPresentable, ToastErrorHand case .success(let quest): self?.questNumber = quest.questNumber self?.rootView.updateQuestTitle( - step: quest.step, - stepNum: quest.stepNumber, questNumber: quest.questNumber, questStyle: quest.questStyle, question: quest.question @@ -197,10 +124,25 @@ extension WriteQuestionTypeQuestViewController: ToastPresentable, ToastErrorHand .sink { [weak self] result in switch result { case .success(()): - let viewController = ViewControllerFactory.shared.makeCompleteQuestionTypeQuestViewController() - viewController.configure(questID: self?.questID ?? 1, questNumber: self?.questNumber ?? 1) - self?.bottomSheetViewController.dismiss(animated: true) - self?.navigationController?.pushViewController(viewController, animated: true) + guard let self else { return } + self.bottomSheetViewController.dismiss(animated: true) { + let modal = ModalBuilder( + modalView: QuestCompleteModal(), + action: nil, + rootViewController: self + ) + modal.present() + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + modal.dismiss() + + ByeBooLogger.debug("퀘스트 아이디 \(self.questID)") + let viewController = ViewControllerFactory.shared.makeArchiveQuestViewController() + viewController.entryViewController = .writeQuest + viewController.configure(questID: self.questID, questType: .question) + self.navigationController?.pushViewController(viewController, animated: true) + } + } case .failure(let error): self?.handleError(error) } @@ -226,34 +168,18 @@ extension WriteQuestionTypeQuestViewController: ToastPresentable, ToastErrorHand viewModel.output.isValidTextPublisher .receive(on: DispatchQueue.main) .sink { [weak self] result in + guard let self else { return } switch result { case true: - self?.rootView.confirmButton.updateType(.enabled) + self.navigationItem.rightBarButtonItem?.isEnabled = true case false: - self?.rootView.confirmButton.updateType(.disabled) + self.navigationItem.rightBarButtonItem?.isEnabled = false } } .store(in: &cancellables) } } -extension WriteQuestionTypeQuestViewController: BackNavigable { - func back() { - tabBarController?.tabBar.isHidden = true - - let action: (() -> Void) = { - self.navigationController?.popViewController(animated: true) - self.tabBarController?.tabBar.isHidden = false - } - - ModalBuilder( - modalView: QuitModalView(), - action: action, - rootViewController: self - ).present() - } -} - extension WriteQuestionTypeQuestViewController: BottomSheetProtocol { func saveEmotionState(emotionState: ByeBooEmotion) { self.emotionState = emotionState.key @@ -288,17 +214,20 @@ extension WriteQuestionTypeQuestViewController: EditQuestProtocol { self.viewModel.action(.viewDidLoadWhenEditMode(questID: questID)) guard let questAnswer = questAnswer else { return } self.answerText = questAnswer - rootView.questTextField.textView.text = questAnswer + rootView.questTextField.applyTextViewStyle(text: answerText, color: .grayscale100) let textCount = questAnswer.count - rootView.questTextField.textCount.text = "(\(textCount)/\(rootView.questTextField.limitCount))" + rootView.questTextField.textCountLabel.text = "(\(textCount)/\(rootView.questTextField.limitCount))" rootView.questTextField.isPlaceholderActive = false - rootView.confirmButton.updateType(.disabled) + self.navigationItem.rightBarButtonItem?.isEnabled = false } } extension WriteQuestionTypeQuestViewController: QuestCompleteProtocol { func updateButtonWhenWriting(text: String) { viewModel.action(.textFieldEditing(answerText: self.answerText, text: text)) + if isKeyboardUsed { + adjustViewForKeyboard(mode: .textGrowth) + } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/CompleteQuestViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/ArchiveQuestViewModel.swift similarity index 94% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/CompleteQuestViewModel.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/ArchiveQuestViewModel.swift index 5069585d..ab10b861 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/CompleteQuestViewModel.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/ArchiveQuestViewModel.swift @@ -8,7 +8,7 @@ import Combine import Foundation -final class CompleteQuestViewModel: ViewModelType { +final class ArchiveQuestViewModel: ViewModelType { private var cancellables = Set() @@ -34,7 +34,7 @@ final class CompleteQuestViewModel: ViewModelType { } } -extension CompleteQuestViewModel { +extension ArchiveQuestViewModel { enum Input { case questAnswerDidLoad(questID: Int) } @@ -53,7 +53,7 @@ extension CompleteQuestViewModel { } } -extension CompleteQuestViewModel { +extension ArchiveQuestViewModel { private func fetchQuestAnswer(questID: Int) { Task { do { diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/WriteActiveTypeViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/WriteActiveTypeViewModel.swift similarity index 100% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/WriteActiveTypeViewModel.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/WriteActiveTypeViewModel.swift diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/WriteQuestTypeViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/WriteQuestTypeViewModel.swift similarity index 100% rename from ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewModel/WriteQuestTypeViewModel.swift rename to ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/WriteQuestTypeViewModel.swift diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/QuestTextField.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/QuestTextField.swift deleted file mode 100644 index ca1f19a0..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/Common/QuestTextField.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// QuestTextField.swift -// ByeBoo-iOS -// -// Created by 이나연 on 7/8/25. -// - -import UIKit - -import SnapKit -import Then - -final class QuestTextField: BaseView { - let textView = UITextView() - var textCount = UILabel() - let placeholder: String - var isPlaceholderActive: Bool = true - let limitCount: Int - var count: Int = 0 - weak var delegate: QuestCompleteProtocol? - - init(type: QuestType) { - placeholder = type.plaeholder - limitCount = type.textLimit - super.init(frame: .zero) - textView.delegate = self - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func setUI() { - addSubviews(textView, textCount) - } - - override func setStyle() { - self.do { - $0.backgroundColor = .white5 - $0.layer.cornerRadius = 12 - } - - textView.do { - $0.applyByeBooFont( - style: .body3R16, - text: placeholder, - color: .grayscale300 - ) - $0.backgroundColor = .clear - $0.tintColor = .white - } - - textCount.applyByeBooFont ( - style: .body6R14, - text: "(\(count)/\(limitCount))", - color: .grayscale300 - ) - } - - override func setLayout() { - self.snp.makeConstraints { - $0.width.equalTo(327.adjustedW) - $0.height.equalTo(290.adjustedH) - } - - textView.snp.makeConstraints { - $0.top.equalToSuperview().inset(16.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - $0.bottom.equalToSuperview().inset(34.adjustedH) - } - - textCount.snp.makeConstraints { - $0.trailing.equalToSuperview().inset(24) - $0.bottom.equalToSuperview().inset(16.adjustedW) - } - } -} - -extension QuestTextField: UITextViewDelegate { - func textViewDidBeginEditing(_ textView: UITextView) { - if isPlaceholderActive == true { - isPlaceholderActive = false - textView.textColor = .white - textView.text = nil - } - self.layer.borderColor = UIColor.primary300.cgColor - self.layer.borderWidth = 1 - textCount.textColor = .primary300 - textView.textColor = .white - } - - func textViewDidEndEditing(_ textView: UITextView) { - if textView.text.isEmpty { - textView.text = placeholder - textView.textColor = .grayscale300 - isPlaceholderActive = true - } - textView.textColor = .grayscale300 - self.layer.borderWidth = 0 - textCount.textColor = .grayscale300 - } - - func textViewDidChange(_ textView: UITextView) { - if textView.text.count > limitCount { - textView.deleteBackward() - } - count = textView.text.count - textCount.text = "(\(count)/\(limitCount))" - delegate?.updateButtonWhenWriting(text: textView.text) - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/ActivationType/CompleteActiveTypeQuestView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/ActivationType/CompleteActiveTypeQuestView.swift deleted file mode 100644 index 58ec00e4..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/ActivationType/CompleteActiveTypeQuestView.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// CompleteActiveTypeQuestView.swift -// ByeBoo-iOS -// -// Created by 이나연 on 7/10/25. -// - -import UIKit - -import SnapKit -import Then - -final class CompleteActiveTypeQuestView: BaseView { - - private let scrollView = UIScrollView() - private let contentView = UIView() - private let congratSquare = CongratSquare() - private var headerView = ArchiveQuestHeaderView( - type: .complete, - stepNumber: 0, - questNumber: 0, - date: "", - questTitle:"" - ) - - override func setUI() { - addSubviews(scrollView) - scrollView.addSubview(contentView) - - contentView.addSubviews( - congratSquare, - headerView - ) - } - - override func setStyle() { - backgroundColor = .grayscale900 - - contentView.do { - $0.backgroundColor = .grayscale900 - } - } - - func bind(with entity: QuestAnswerEntity) { - headerView.updateUI( - stepNumber: entity.stepNumber, - questNumber: entity.questNumber, - date: entity.createdAt, - title: entity.question - ) - - guard let imageUrl = entity.imageUrl else { return } - let actionView = ActionView(descriptionText: entity.answer, photoURL: imageUrl) - - let feelView = FeelView( - emotionType: entity.questEmotionState, - descriptionText: entity.emotionDescription - ) - - contentView.addSubviews(actionView, feelView) - - actionView.snp.makeConstraints { - $0.top.equalTo(headerView.questTitleLabel.snp.bottom) - $0.leading.trailing.equalToSuperview() - } - - feelView.snp.makeConstraints { - $0.top.equalTo(actionView.snp.bottom) - $0.leading.trailing.equalToSuperview() - $0.bottom.equalToSuperview().inset(24.adjustedH) - } - } - - override func setLayout() { - scrollView.snp.makeConstraints { - $0.edges.equalTo(self.safeAreaLayoutGuide) - } - - contentView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.width.equalToSuperview() - $0.height.greaterThanOrEqualToSuperview() - } - - congratSquare.snp.makeConstraints { - $0.top.equalToSuperview().inset(16.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - } - - headerView.snp.makeConstraints { - $0.top.equalTo(congratSquare.snp.bottom).offset(32.adjustedH) - $0.horizontalEdges.equalToSuperview() - } - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/QuestionType/CompleteQuestionTypeQuestView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/QuestionType/CompleteQuestionTypeQuestView.swift deleted file mode 100644 index 09e60858..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/QuestionType/CompleteQuestionTypeQuestView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// CompleteQuestionTypeQuestView.swift -// ByeBoo-iOS -// -// Created by 이나연 on 7/10/25. -// - -import UIKit - -import SnapKit -import Then - -final class CompleteQuestionTypeQuestView: BaseView { - - private let scrollView = UIScrollView() - private let contentView = UIView() - private let congratSquare = CongratSquare() - private var headerView = ArchiveQuestHeaderView( - type: .complete, - stepNumber: 0, - questNumber: 0, - date: "", - questTitle:"" - ) - - override func setUI() { - addSubviews(scrollView) - scrollView.addSubview(contentView) - - contentView.addSubviews( - congratSquare, - headerView - ) - } - - override func setStyle() { - backgroundColor = .grayscale900 - - contentView.do { - $0.backgroundColor = .grayscale900 - } - } - - func bind(with entity: QuestAnswerEntity) { - headerView.updateUI( - stepNumber: entity.stepNumber, - questNumber: entity.questNumber, - date: entity.createdAt, - title: entity.question - ) - - let thinkView = ThinkView(descriptionText: entity.answer) - let feelView = FeelView(emotionType: entity.questEmotionState, descriptionText: entity.emotionDescription) - - contentView.addSubviews(thinkView, feelView) - - thinkView.isUserInteractionEnabled = false - feelView.isUserInteractionEnabled = false - - thinkView.snp.makeConstraints { - $0.top.equalTo(headerView.questTitleLabel.snp.bottom) - $0.leading.trailing.equalToSuperview() - } - - feelView.snp.makeConstraints { - $0.top.equalTo(thinkView.snp.bottom) - $0.leading.trailing.equalToSuperview() - $0.bottom.equalToSuperview().inset(24.adjustedH) - } - } - - override func setLayout() { - scrollView.snp.makeConstraints { - $0.edges.equalTo(self.safeAreaLayoutGuide) - } - - contentView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.width.equalToSuperview() - $0.height.greaterThanOrEqualToSuperview() - } - - congratSquare.snp.makeConstraints { - $0.top.equalToSuperview().inset(16.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - } - - headerView.snp.makeConstraints { - $0.top.equalTo(congratSquare.snp.bottom).offset(32.adjustedH) - $0.horizontalEdges.equalToSuperview() - } - } -} - diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/QuestionType/WriteQuestionTypeQuestView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/QuestionType/WriteQuestionTypeQuestView.swift deleted file mode 100644 index 2983226b..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/View/QuestionType/WriteQuestionTypeQuestView.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// WriteQuestionTypeView.swift -// ByeBoo-iOS -// -// Created by 이나연 on 7/8/25. -// - -import UIKit - -import SnapKit -import Then - -final class WriteQuestionTypeQuestView: BaseView { - private(set) var title = WriteQuestTitleView( - stepNum: "", - stepTitle: "", - questNum: 0, - title: "" - ) - private(set) var questTextField = QuestTextField(type: .question) - private let descriptionView = UIStackView() - private let descriptionLabel = UILabel() - private let errorIcon = UIImageView() - private(set) var confirmButton = ByeBooButton(titleText: "완료하기", type: .disabled) - - override func setUI() { - addSubviews( - title, - questTextField, - descriptionView, - confirmButton - ) - - descriptionView.addArrangedSubviews( - errorIcon, - descriptionLabel - ) - } - - override func setStyle() { - backgroundColor = .grayscale900 - - descriptionView.do { - $0.axis = .horizontal - $0.spacing = 3.adjustedW - } - - errorIcon.do { - $0.image = .error - $0.contentMode = .scaleAspectFit - } - - descriptionLabel.applyByeBooFont( - style: .cap2R12, - text: "10글자 이상 작성해 주세요.", - color: .grayscale400, - textAlignment: .center - ) - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - self.endEditing(true) - } - - override func setLayout() { - title.snp.makeConstraints { - $0.top.equalTo(self.safeAreaLayoutGuide) - $0.leading.trailing.equalToSuperview() - } - - questTextField.snp.makeConstraints { - $0.top.equalTo(title.snp.bottom).offset(8.adjustedH) - $0.leading.trailing.equalToSuperview().inset(24.adjustedW) - $0.height.equalTo(290.adjustedH) - } - - descriptionView.snp.makeConstraints { - $0.top.equalTo(questTextField.snp.bottom).offset(16.adjustedH) - $0.leading.equalToSuperview().inset(24.adjustedW) - } - - confirmButton.snp.makeConstraints { - $0.width.equalTo(311.adjustedW) - $0.bottom.equalTo(self.safeAreaLayoutGuide) - $0.centerX.equalToSuperview() - } - } -} - -extension WriteQuestionTypeQuestView { - func updateQuestTitle( - step: String, - stepNum: Int, - questNumber: Int, - questStyle: String, - question: String - ) { - title.bind( - stepNum: String(stepNum), - stepTitle: step, - questNum: questNumber, - title: question - ) - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/CompleteActiveTypeQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/CompleteActiveTypeQuestViewController.swift deleted file mode 100644 index 8cdba483..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/CompleteActiveTypeQuestViewController.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// CompleteActiveTypeQuestViewController.swift -// ByeBoo-iOS -// -// Created by 이나연 on 7/10/25. -// - -import Combine -import UIKit - -final class CompleteActiveTypeQuestViewController: BaseViewController { - - private let rootView = CompleteActiveTypeQuestView() - private var viewModel: CompleteQuestViewModel - private var cancellables = Set() - - private var questID: Int = 31 - private var questNumber: Int = 1 - - override func loadView() { - view = rootView - } - - init(viewModel: CompleteQuestViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.navigationItem.hidesBackButton = true - viewModel.action(.questAnswerDidLoad(questID: questID)) - - ByeBooNavigationBar.makeNavigationBar( - navigationItem: self.navigationItem, - navigationController: self.navigationController, - type: .close(header: .black), - action: #selector(close) - ) - - bind() - } -} - -extension CompleteActiveTypeQuestViewController: ToastPresentable, ToastErrorHandler { - - private func bind() { - viewModel.output.resultPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - switch result { - case .success(let entity): - self?.questNumber = entity.questNumber - self?.rootView.bind(with: entity) - case .failure(let error): - self?.handleError(error) - } - } - .store(in: &cancellables) - - viewModel.output.loadingPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - if result { - self?.view.alpha = 0 - } else { - UIView.animate(withDuration: 0.2) { - self?.view.alpha = 1 - } - } - } - .store(in: &cancellables) - } -} - -extension CompleteActiveTypeQuestViewController: Dismissible, ReviewRequestProtocol { - - func close() { - tabBarController?.tabBar.isHidden = false - self.navigationController?.popToRootViewController(animated: false) - - if requestQuestNumber.contains(questNumber) { - reviewRequest() - } - } - - private func modalAction() { - tabBarController?.tabBar.isHidden = false - self.navigationController?.popToRootViewController(animated: false) - } -} - -extension CompleteActiveTypeQuestViewController { - func configure( - questID: Int, - questNumber: Int - ) { - self.questID = questID - self.questNumber = questNumber - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/CompleteQuestionTypeQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/CompleteQuestionTypeQuestViewController.swift deleted file mode 100644 index df424f84..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/WriteQuest/ViewController/CompleteQuestionTypeQuestViewController.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// CompleteQuestionTypeQuestViewController.swift -// ByeBoo-iOS -// -// Created by 이나연 on 7/10/25. -// - -import Combine -import UIKit - -final class CompleteQuestionTypeQuestViewController: BaseViewController { - - private let rootView = CompleteQuestionTypeQuestView() - private var viewModel: CompleteQuestViewModel - private var cancellables = Set() - - private var questID: Int = 1 - private var questNumber: Int = 1 - - override func loadView() { - view = rootView - } - - init(viewModel: CompleteQuestViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.navigationItem.hidesBackButton = true - viewModel.action(.questAnswerDidLoad(questID: questID)) - - ByeBooNavigationBar.makeNavigationBar( - navigationItem: self.navigationItem, - navigationController: self.navigationController, - type: .close(header: .black), - action: #selector(close) - ) - - bind() - } -} - -extension CompleteQuestionTypeQuestViewController: ToastPresentable, ToastErrorHandler { - - private func bind() { - viewModel.output.resultPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - switch result { - case .success(let entity): - self?.questNumber = entity.questNumber - self?.rootView.bind(with: entity) - case .failure(let error): - self?.handleError(error) - } - } - .store(in: &cancellables) - - viewModel.output.loadingPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - if result { - self?.view.alpha = 0 - } else { - UIView.animate(withDuration: 0.2) { - self?.view.alpha = 1 - } - } - } - .store(in: &cancellables) - } -} - -extension CompleteQuestionTypeQuestViewController: Dismissible, ReviewRequestProtocol { - - func close() { - tabBarController?.tabBar.isHidden = false - self.navigationController?.popToRootViewController(animated: false) - - if requestQuestNumber.contains(questNumber) { - reviewRequest() - } - } -} - -extension CompleteQuestionTypeQuestViewController { - func configure( - questID: Int, - questNumber: Int - ) { - self.questID = questID - self.questNumber = questNumber - } -} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift index 52e94048..d69eda7a 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/PresentationDependencyAssembler.swift @@ -78,13 +78,13 @@ struct PresentationDependencyAssembler: DependencyAssembler { ) } - DIContainer.shared.register(type: CompleteQuestViewModel.self) { container in + DIContainer.shared.register(type: ArchiveQuestViewModel.self) { container in guard let questAnswerUseCase = container.resolve(type: QuestAnswerUseCase.self) else { ByeBooLogger.error(ByeBooError.DIFailedError) return } - return CompleteQuestViewModel( + return ArchiveQuestViewModel( questAnswerCase: questAnswerUseCase ) } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift index d1e54e42..9e44e2be 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/ViewControllerFactory.swift @@ -24,8 +24,6 @@ protocol ViewControllerFactoryProtocol { func makeQuestTipViewController() -> QuestTipViewController func makeWriteQuestionTypeQuestViewController() -> WriteQuestionTypeQuestViewController func makeWriteActiveTypeQuestViewController() -> WriteActiveTypeQuestViewController - func makeCompleteActiveTypeQuestViewController() -> CompleteActiveTypeQuestViewController - func makeCompleteQuestionTypeQuestViewController() -> CompleteQuestionTypeQuestViewController func makeFinishJourneyViewController() -> FinishJourneyViewController } @@ -126,7 +124,7 @@ final class ViewControllerFactory: ViewControllerFactoryProtocol { } func makeArchiveQuestViewController() -> ArchiveQuestViewController { - guard let viewModel = DIContainer.shared.resolve(type: CompleteQuestViewModel.self) else { + guard let viewModel = DIContainer.shared.resolve(type: ArchiveQuestViewModel.self) else { DIErrorHandle() fatalError() } @@ -157,22 +155,6 @@ final class ViewControllerFactory: ViewControllerFactoryProtocol { return WriteActiveTypeQuestViewController(viewModel: viewModel) } - func makeCompleteActiveTypeQuestViewController() -> CompleteActiveTypeQuestViewController { - guard let viewModel = DIContainer.shared.resolve(type: CompleteQuestViewModel.self) else { - DIErrorHandle() - fatalError() - } - return CompleteActiveTypeQuestViewController(viewModel: viewModel) - } - - func makeCompleteQuestionTypeQuestViewController() -> CompleteQuestionTypeQuestViewController { - guard let viewModel = DIContainer.shared.resolve(type: CompleteQuestViewModel.self) else { - DIErrorHandle() - fatalError() - } - return CompleteQuestionTypeQuestViewController(viewModel: viewModel) - } - func makeFinishJourneyViewController() -> FinishJourneyViewController { guard let viewModel = DIContainer.shared.resolve(type: FinishJourneyViewModel.self) else {