diff --git a/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswerDetailResponseDTO.swift b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswerDetailResponseDTO.swift new file mode 100644 index 00000000..42fdd755 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswerDetailResponseDTO.swift @@ -0,0 +1,46 @@ +// +// CommonQuestAnswerDetailResponseDTO.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/5/26. +// + +import Foundation + +struct CommonQuestAnswerDetailResponseDTO: Decodable { + let question: String + let answer: CommonQuestAnswerResponseDTO + let comments: [CommonQuestCommentResponseDTO] +} + +struct CommonQuestCommentResponseDTO: Decodable { + let commentId: Int + let replyCount: Int + let writerId: Int + let writer: String + let profileIcon: String + let writtenAt: String + let content: String +} + +extension CommonQuestAnswerDetailResponseDTO { + func toEntity() -> [CommonQuestCommentEntity] { + comments.map { + $0.toEntity() + } + } +} + +extension CommonQuestCommentResponseDTO { + func toEntity() -> CommonQuestCommentEntity { + .init( + commentID: commentId, + replyCount: replyCount, + writerID: writerId, + writer: writer, + profileIcon: profileIcon, + writtenAt: writtenAt, + content: content + ) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswerRepliesResponseDTO.swift b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswerRepliesResponseDTO.swift new file mode 100644 index 00000000..2c798e0a --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswerRepliesResponseDTO.swift @@ -0,0 +1,54 @@ +// +// CommonQuestAnswerRepliesResponseDTO.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/5/26. +// + +import Foundation + +struct CommonQuestAnswerRepliesResponseDTO: Decodable { + let totalCount: Int + let comment: CommonQuestAnswerCommentResponseDTO + let replies: [CommonQuestAnswerReplyResponseDTO] +} + +struct CommonQuestAnswerCommentResponseDTO: Decodable { + let content: String + let writer: String + let createdAt: String + let profileIcon: String + let commentId: Int + let writerId: Int +} + +struct CommonQuestAnswerReplyResponseDTO: Decodable { + let content: String + let writer: String + let createdAt: String + let profileIcon: String + let commentId: Int + let writerId: Int +} + +extension CommonQuestAnswerRepliesResponseDTO { + func toEntity() -> [CommonQuestCommentEntity] { + replies.map { + $0.toEntity() + } + } +} + +extension CommonQuestAnswerReplyResponseDTO { + func toEntity() -> CommonQuestCommentEntity { + .init( + commentID: commentId, + replyCount: nil, + writerID: writerId, + writer: writer, + profileIcon: profileIcon, + writtenAt: createdAt, + content: content + ) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswersResponseDTO.swift b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswersResponseDTO.swift index 8c3e1aae..32b563fc 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswersResponseDTO.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Data/Model/CommonQuestAnswersResponseDTO.swift @@ -16,12 +16,17 @@ struct CommonQuestAnswersResponseDTO: Decodable { } struct CommonQuestAnswerResponseDTO: Decodable { +// let likeCount: Int +// let commentCount: Int +// let isLiked: Bool let answerId: Int +// let userId: Int +// let writerId: String let writer: String + let writerId: Int let profileIcon: String let writtenAt: String let content: String - let writerId: Int } extension CommonQuestAnswersResponseDTO { @@ -41,13 +46,26 @@ extension CommonQuestAnswersResponseDTO { extension CommonQuestAnswerResponseDTO { func toEntity(userName: String) -> CommonQuestAnswerEntity { .init( +// isMyAnswer: userName == writerId ? true : false, +// answerID: answerId, +// writerID: writerId, +// profileIcon: profileIcon, +// writtenAt: writtenAt, +// content: content, +// userID: userId, +// likeCount: likeCount, +// commentCount: commentCount, +// isLiked: isLiked isMyAnswer: userName == writer ? true : false, answerID: answerId, - writer: writer, + writerID: writer, profileIcon: profileIcon, writtenAt: writtenAt, content: content, - writerID: writerId + userID: writerId, + likeCount: 3, + commentCount: 4, + isLiked: false, ) } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestAnswersEntity.swift b/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestAnswersEntity.swift index 8edb6a71..a0d251ff 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestAnswersEntity.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestAnswersEntity.swift @@ -20,11 +20,14 @@ struct CommonQuestAnswersEntity { struct CommonQuestAnswerEntity { let isMyAnswer: Bool let answerID: Int - let writer: String + let writerID: String let profileIcon: String let writtenAt: String let content: String - let writerID: Int + let userID: Int + let likeCount: Int + let commentCount: Int + let isLiked: Bool } extension CommonQuestAnswersEntity { @@ -35,11 +38,14 @@ extension CommonQuestAnswersEntity { CommonQuestAnswerEntity( isMyAnswer: false, answerID: $0, - writer: "유저\($0)", + writerID: "유저\($0)", profileIcon: profileIcons[$0 % 4], writtenAt: Date.now.toString(), content: "\($0)번째 테스트 답변", - writerID: 1 + userID: $0, + likeCount: 1, + commentCount: 1, + isLiked: false ) } @@ -61,11 +67,14 @@ extension CommonQuestAnswerEntity { .init( isMyAnswer: false, answerID: 1, - writer: "장원영", + writerID: "장원영", profileIcon: "SO_SO", writtenAt: "2025-10-12", content: "헤어진 지 벌써 일주일이 지났습니다. 처음에는 실감이 안 나서 눈물조차 나오지 않았어요. 그저 멍하니 천장만 바라보며 시간을 보냈습니다. 그런데 오늘 아침, 습관적으로 휴대폰을 확인하다가 더 이상 '굿모닝' 인사를 보낼 사람이 없다는 사실을 깨닫고 그제야 무너져 내렸습니다. 밥알이 모래알 같아서 잘 넘어가지도 않네요. 친구들은 시간이 약이라고, 더 좋은 사람 만날 거라고 위로하지만 지금 당장은 그 어떤 말도 귀에 들어오지 않습니다.", - writerID: 1 + userID: 12, + likeCount: 3, + commentCount: 4, + isLiked: false ) } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestCommentEntity.swift b/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestCommentEntity.swift new file mode 100644 index 00000000..16c286e6 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Domain/Entity/CommonQuestCommentEntity.swift @@ -0,0 +1,73 @@ +// +// CommonQuestCommentEntity.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/5/26. +// + +import Foundation + +struct CommonQuestCommentEntity: Hashable { + let commentID: Int + let replyCount: Int? + let writerID: Int + let writer: String + let profileIcon: String + let writtenAt: String + let content: String +} + +extension CommonQuestCommentEntity { + static func toCommentListStub() -> [Self] { + return (1...5).map { + .init( + commentID: $0, + replyCount: $0 - 1, + writerID: 2, + writer: "장원영", + profileIcon: "SADNESS", + writtenAt: "2026-02-19T02:09:43.735730", + content: "나도여ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ냐냐냐냐냐냐냐냐냐" + ) + } + } + + static func toCommentStub() -> Self { + .init( + commentID: 1, + replyCount: 2, + writerID: 2, + writer: "가을이", + profileIcon: "SELF_UNDERSTANDING", + writtenAt: "2026-02-19T02:09:43.735730", + content: "나도여ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ냐냐냐냐냐냐냐냐냐" + ) + } + + static func toReplyListStub() -> [Self] { + return (6...10).map { + .init( + commentID: $0, + replyCount: nil, + writerID: 2, + writer: "안유진", + profileIcon: "SADNESS", + writtenAt: "2026-02-19T02:09:43.735730", + content: "헤어짐ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ" + ) + } + } + + + static func toReplyStub() -> Self { + .init( + commentID: 1, + replyCount: nil, + writerID: 2, + writer: "가을이", + profileIcon: "SELF_UNDERSTANDING", + writtenAt: "2026-02-19T02:09:43.735730", + content: "헤어짐" + ) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/ProfileIcon.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/ProfileIcon.swift new file mode 100644 index 00000000..da77f388 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/ProfileIcon.swift @@ -0,0 +1,32 @@ +// +// ProfileIcon.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/5/26. +// + +import UIKit + +enum ProfileIcon: String, CaseIterable { + case sad = "SADNESS" + case selfUnderstanding = "SELF_UNDERSTANDING" + case soso = "SO_SO" + case relieved = "RELIEVED" + + var image: UIImage { + switch self { + case .sad: + return .sadnessBadge + case .selfUnderstanding: + return .selfUnderstandingBadge + case .soso: + return .sosoBadge + case .relieved: + return .relievedBadge + } + } + + static func image(for iconString: String) -> UIImage? { + ProfileIcon(rawValue: iconString)?.image + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITableView+.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITableView+.swift index d3a196f4..fc2a002b 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITableView+.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITableView+.swift @@ -7,8 +7,22 @@ import UIKit +final class SelfSizingTableView: UITableView { + override var contentSize: CGSize { + didSet { + if oldValue != contentSize { + invalidateIntrinsicContentSize() + } + } + } + + override var intrinsicContentSize: CGSize { + return contentSize + } +} + extension UITableView { - + func dequeueReusableCell(for indexPath: IndexPath) -> T { guard let cell = self.dequeueReusableCell( withIdentifier: T.identifier, diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift index b98111bf..74c2481a 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Extension/UITextView+.swift @@ -37,4 +37,31 @@ extension UITextView { attributedText = NSAttributedString(string: targetText, attributes: attributes) typingAttributes = attributes } + + func applyTextViewStyle(style: FontManager, text: String, color: UIColor) { + self.applyByeBooFont( + style: style, + text: text, + color: color + ) + } + + func numberOfLine() -> Int { + guard !text.isEmpty else { return 0 } + let size = CGSize(width: frame.width, height: .infinity) + let estimatedSize = sizeThatFits(size) + + let lineHeight: CGFloat + if let paragraphStyle = attributedText?.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, + paragraphStyle.minimumLineHeight > 0 { + lineHeight = paragraphStyle.minimumLineHeight + } else if let font = attributedText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont { + lineHeight = font.lineHeight + } else { + lineHeight = self.font?.lineHeight ?? 0 + } + + guard lineHeight > 0 else { return 0 } + return Int(estimatedSize.height / lineHeight) + } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommentTableViewCell.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommentTableViewCell.swift new file mode 100644 index 00000000..eed1bea4 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommentTableViewCell.swift @@ -0,0 +1,278 @@ +// +// CommentCellTableViewCell.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/4/26. +// + +import UIKit + +protocol CommentProtocol: AnyObject { + func moreLabelDidTap(commentID: Int) + func replyIconDidTap(commentID: Int) +} + +extension CommentProtocol { + func replyIconDidTap(commentID: Int) { } +} + +final class CommentTableViewCell: UITableViewCell { + + weak var delegate: CommentProtocol? + + private var replyCommentArrow = UIImageView() + private var replyCommentContainer = UIView() + private let profileIcon = UIImageView(image: .relievedBadge) + private let nicknameLabel = UILabel() + private let dateLabel = UILabel() + private let menuButton = UIImageView() + private let commentTextView = UITextView() + private var moreLabel = UILabel() + private var replyCountContainer = UIStackView() + private var replyCommentIcon = UIImageView() + private var replyCountLabel = UILabel() + + private var replyCount: Int = 0 + private var commentID: Int = 0 + private var content: String = "" + private var didSetReplyLayout = false + private var didSetCommentLayout = false + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setUI() + setStyle() + setLayout() + setAddTarget() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUI() { + contentView.addSubviews(profileIcon, nicknameLabel, dateLabel, menuButton, commentTextView, moreLabel) + } + + private func setStyle() { + backgroundColor = .grayscale900 + selectionStyle = .none + + replyCommentArrow.do { + $0.image = .replyArrow + } + + nicknameLabel.do { + $0.applyByeBooFont(style: .body5M14, color: .grayscale200) + } + + dateLabel.do { + $0.applyByeBooFont(style: .cap2R12, color: .grayscale400) + } + + menuButton.do { + $0.image = .menu + } + + commentTextView.do { + $0.isEditable = false + $0.isScrollEnabled = false + $0.isUserInteractionEnabled = false + $0.isSelectable = false + $0.backgroundColor = .clear + $0.textContainerInset = .zero + $0.textContainer.lineBreakMode = .byTruncatingTail + $0.textContainer.lineFragmentPadding = 0 + $0.applyByeBooFont(style: .body6R14, color: .grayscale100) + } + + moreLabel.do { + $0.applyByeBooFont(style: .body6R14, text: "더보기", color: .grayscale400) + $0.backgroundColor = .grayscale900 + $0.isUserInteractionEnabled = true + } + + replyCountContainer.do { + $0.axis = .horizontal + $0.spacing = 4.adjustedW + } + + replyCommentIcon.do { + $0.image = .comment.withRenderingMode(.alwaysTemplate) + $0.isUserInteractionEnabled = true + $0.tintColor = .grayscale100 + } + + replyCountLabel.do { + $0.applyByeBooFont(style: .cap2R12, text: String(replyCount), color: .grayscale100) + } + } + + private func setLayout() { + nicknameLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(16.adjustedH) + $0.leading.equalTo(profileIcon.snp.trailing).offset(4.adjustedW) + } + dateLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(18.5.adjustedH) + $0.leading.equalTo(nicknameLabel.snp.trailing).offset(4) + } + menuButton.snp.makeConstraints { + $0.top.equalToSuperview().inset(16.adjustedH) + $0.trailing.equalToSuperview() + $0.size.equalTo(24.adjustedH) + } + moreLabel.snp.makeConstraints { + $0.bottom.equalTo(commentTextView.snp.bottom) + $0.trailing.equalTo(commentTextView.snp.trailing).offset(-20.adjustedW) + } + } + private func setCommentListLayout() { + guard !didSetCommentLayout else { return } + didSetCommentLayout = true + + contentView.addSubview(replyCountContainer) + replyCountContainer.addArrangedSubviews(replyCommentIcon, replyCountLabel) + + profileIcon.snp.makeConstraints { + $0.top.equalToSuperview().inset(16.adjustedH) + $0.leading.equalToSuperview() + $0.size.equalTo(20.adjustedH) + } + commentTextView.snp.makeConstraints { + $0.top.equalTo(profileIcon.snp.bottom).offset(4.adjustedH) + $0.leading.equalTo(profileIcon.snp.leading).offset(20) + $0.trailing.equalToSuperview().inset(24.adjustedW) + $0.bottom.equalToSuperview().inset(44.adjustedH) + } + replyCountContainer.snp.makeConstraints { + $0.top.equalTo(commentTextView.snp.bottom).offset(8.adjustedH) + $0.leading.equalTo(commentTextView.snp.leading) + $0.height.equalTo(20.adjustedH) + $0.bottom.equalToSuperview().inset(16.adjustedH) + } + replyCommentIcon.snp.makeConstraints { + $0.size.equalTo(20.adjustedH) + } + } + + private func setReplySheetLayout() { + guard !didSetReplyLayout else { return } + didSetReplyLayout = true + + contentView.addSubviews(replyCommentArrow) + + replyCommentArrow.snp.makeConstraints { + $0.top.equalToSuperview().inset(16.adjustedH) + $0.leading.equalToSuperview() + $0.size.equalTo(24.adjustedH) + } + profileIcon.snp.makeConstraints { + $0.top.equalToSuperview().inset(16.adjustedH) + $0.leading.equalTo(replyCommentArrow.snp.trailing) + $0.size.equalTo(20.adjustedH) + } + commentTextView.snp.remakeConstraints { + $0.top.equalTo(profileIcon.snp.bottom).offset(4.adjustedH) + $0.leading.equalTo(profileIcon.snp.leading).offset(20) + $0.trailing.equalToSuperview().inset(24.adjustedW) + $0.bottom.equalToSuperview().inset(16.adjustedH) + } + } + + private func setAddTarget() { + let moreLabelTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(moreLabelDidTap(_:))) + moreLabel.addGestureRecognizer(moreLabelTapRecognizer) + + let replyTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(replyIconDidTap(_:))) + replyCommentIcon.addGestureRecognizer(replyTapRecognizer) + } +} + +extension CommentTableViewCell { + func configure( + commentID: Int, + replyCount: Int? = nil, + writer: String, + profileIcon: UIImage, + writtenAt: String, + content: String, + showAllText: Bool, + isReplySheet: Bool + ) { + if let _ = replyCount { + setCommentListLayout() + } else { + setReplySheetLayout() + } + + if isReplySheet { + replyCommentIcon.tintColor = .grayscale600 + replyCountLabel.textColor = .grayscale600 + } + + self.commentID = commentID + nicknameLabel.text = writer + self.profileIcon.image = profileIcon + dateLabel.text = writtenAt + self.content = content + commentTextView.applyTextViewStyle(style: .body6R14, text: content, color: .grayscale100) + + layoutIfNeeded() + + commentTextView.textContainer.maximumNumberOfLines = 0 + commentTextView.textContainer.exclusionPaths = [] + + if showAllText { + commentTextView.invalidateIntrinsicContentSize() + moreLabel.isHidden = true + } else { + let numberOfLines = commentTextView.numberOfLine() + applyStyleWhenHideText(numberOfLines) + } + } +} + +extension CommentTableViewCell { + private func applyStyleWhenHideText(_ numberOfLines: Int) { + moreLabel.isHidden = numberOfLines > 5 ? false : true + commentTextView.textContainer.maximumNumberOfLines = 5 + + let moreLabelWidth: CGFloat = moreLabel.intrinsicContentSize.width + let contentHeight = commentTextView.sizeThatFits( + CGSize(width: commentTextView.bounds.width, height: .infinity) + ).height + + + let lineHeight: CGFloat + if let paragraphStyle = commentTextView.attributedText?.attribute(.paragraphStyle, at: 0, effectiveRange: nil) + as? NSParagraphStyle, + paragraphStyle.minimumLineHeight > 0 { + lineHeight = paragraphStyle.minimumLineHeight + } else { + lineHeight = commentTextView.font?.lineHeight ?? 0 + } + + let exclusionRect = CGRect( + x: commentTextView.bounds.width - moreLabelWidth - 10, + y: contentHeight - lineHeight, + width: moreLabelWidth, + height: lineHeight + ) + ByeBooLogger.debug("가로 : \(exclusionRect.width), 세로: \(exclusionRect.height)" ) + commentTextView.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionRect)] + } + + @objc + private func moreLabelDidTap(_ tapRecognizer: UITapGestureRecognizer) { + delegate?.moreLabelDidTap(commentID: commentID) + } + + @objc private func replyIconDidTap(_ tapRecognizer: UITapGestureRecognizer) { + delegate?.replyIconDidTap(commentID: commentID) + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift index 7419c854..4cf66ccc 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestAnswerCell.swift @@ -16,8 +16,7 @@ final class CommonQuestAnswerCell: UITableViewCell { private let containerView = UIView() private let userIconView = UIImageView() private let userNicknameLabel = UILabel() - private let answerContentLabel = UILabel() - private let writtenDateLabel = UILabel() + private(set) var questContentView = QuestContentView() override init( style: UITableViewCell.CellStyle, @@ -47,24 +46,14 @@ final class CommonQuestAnswerCell: UITableViewCell { style: .body6R14, color: .grayscale200 ) - answerContentLabel.applyByeBooFont( - style: .body3R16, - color: .grayscale100, - numberOfLines: 0 - ) - writtenDateLabel.applyByeBooFont( - style: .body6R14, - color: .grayscale400 - ) } private func setUI() { - addSubview(containerView) + contentView.addSubview(containerView) containerView.addSubviews( userIconView, userNicknameLabel, - answerContentLabel, - writtenDateLabel + questContentView ) } @@ -84,14 +73,9 @@ final class CommonQuestAnswerCell: UITableViewCell { $0.leading.equalTo(userIconView.snp.trailing).offset(4.adjustedW) $0.centerY.equalTo(userIconView.snp.centerY) } - answerContentLabel.snp.makeConstraints { - $0.top.equalTo(userIconView.snp.bottom).offset(12.adjustedH) + questContentView.snp.makeConstraints { + $0.top.equalTo(userNicknameLabel.snp.bottom).offset(12.adjustedH) $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.height.equalTo(47.adjustedH) - } - writtenDateLabel.snp.makeConstraints { - $0.top.equalTo(answerContentLabel.snp.bottom).offset(20.adjustedH) - $0.leading.equalToSuperview().inset(24.adjustedW) $0.bottom.equalToSuperview().inset(16.adjustedH) } } @@ -107,10 +91,16 @@ extension CommonQuestAnswerCell { if let profileIcon { userIconView.image = profileIcon } - userNicknameLabel.text = answer.writer - answerContentLabel.text = answer.content - writtenDateLabel.text = writtenAt + userNicknameLabel.text = answer.writerID answerID = answer.answerID + questContentView.configure( + content: answer.content, + writtenAt: writtenAt, + isLiked: false, + likeCount: 4, + commentCount: 3, + showAllText: false + ) } func getAnswewrID() -> Int? { diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift index 1df86b35..650ce1ec 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Cells/CommonQuestMyAnswerCell.swift @@ -13,8 +13,7 @@ final class CommonQuestMyAnswerCell: UITableViewCell { private let questionView = UIView() private let questionMarkLabel = UILabel() private let questionContentLabel = UILabel() - private let answerContentLabel = UILabel() - private let writtenDateLabel = UILabel() + private(set) var questContentView = QuestContentView() override init( style: UITableViewCell.CellStyle, @@ -53,23 +52,13 @@ final class CommonQuestMyAnswerCell: UITableViewCell { ) $0.lineBreakStrategy = [] } - answerContentLabel.applyByeBooFont( - style: .body3R16, - color: .grayscale100, - numberOfLines: 2 - ) - writtenDateLabel.applyByeBooFont( - style: .cap2R12, - color: .grayscale400 - ) } private func setUI() { contentView.addSubview(containerView) containerView.addSubviews( questionView, - answerContentLabel, - writtenDateLabel + questContentView ) questionView.addSubviews( questionMarkLabel, @@ -86,25 +75,19 @@ final class CommonQuestMyAnswerCell: UITableViewCell { $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) } questionMarkLabel.snp.makeConstraints { - $0.top.equalTo(questionContentLabel.snp.top) + $0.top.equalToSuperview() $0.leading.equalToSuperview() $0.width.equalTo(17.adjustedW) } questionContentLabel.snp.makeConstraints { - $0.verticalEdges.equalToSuperview() + $0.top.equalToSuperview() $0.leading.equalTo(questionMarkLabel.snp.trailing).offset(4.adjustedW) $0.trailing.equalToSuperview() } - answerContentLabel.snp.makeConstraints { - $0.top.equalTo(questionView.snp.bottom).offset(12.adjustedH) + questContentView.snp.makeConstraints { + $0.top.equalTo(questionContentLabel.snp.bottom).offset(12.adjustedH) $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.height.equalTo(47.adjustedH) - } - writtenDateLabel.snp.makeConstraints { - $0.top.equalTo(answerContentLabel.snp.bottom).offset(20.adjustedH) - $0.leading.equalToSuperview().inset(24.adjustedW) $0.bottom.equalToSuperview().inset(16.adjustedH) - $0.height.equalTo(16.adjustedH) } } } @@ -114,10 +97,19 @@ extension CommonQuestMyAnswerCell { func bind( question: String, content: String, - writtenAt: String + writtenAt: String, + isLiked: Bool, + likeCount: Int, + commentCount: Int ) { questionContentLabel.text = question - answerContentLabel.text = content - writtenDateLabel.text = writtenAt + questContentView.configure( + content: content, + writtenAt: writtenAt, + isLiked: isLiked, + likeCount: likeCount, + commentCount: commentCount, + showAllText: true + ) } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/CommentDataSource.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/CommentDataSource.swift new file mode 100644 index 00000000..3b32e980 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/CommentDataSource.swift @@ -0,0 +1,22 @@ +// +// CommentDataSource.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/8/26. +// + +import Foundation + +enum CommentSection: Hashable { + case main +} + +enum ReplySection: Hashable { + case comment + case replies +} + +struct CommentItem: Hashable { + let entity: CommonQuestCommentEntity + var showAllText: Bool +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/CommentTextFieldView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/CommentTextFieldView.swift new file mode 100644 index 00000000..dab606a8 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/CommentTextFieldView.swift @@ -0,0 +1,214 @@ +// +// CommentTextFieldView.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/4/26. +// + +import UIKit + +final class CommentTextView: BaseView { + private let topBorderLine = UIView() + private let textFieldContainer = UIView() + private let textView = UITextView() + private let countConfirmContainer = UIView() + private let textCountLabel = UILabel() + private let confirmButton = UIButton() + + private let placeholder: String = "댓글로 위로를 남겨보세요." + private var isPlaceholderActive: Bool = true + private let textCountLimit: Int = 500 + + override init(frame: CGRect) { + super.init(frame: frame) + textView.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setUI() { + addSubviews(topBorderLine,textFieldContainer, confirmButton) + textFieldContainer.addSubviews(textView) + } + + override func setStyle() { + backgroundColor = .grayscale900 + + topBorderLine.do { + $0.backgroundColor = .grayscale800 + } + + textFieldContainer.do { + $0.backgroundColor = .white5 + $0.layer.cornerRadius = 8 + } + + textView.do { + let offset = (FontManager.body6R14.lineHeight - FontManager.body6R14.font.lineHeight) / 2 + $0.backgroundColor = .clear + $0.text = placeholder + $0.font = FontManager.body6R14.font + $0.textColor = .grayscale600 + $0.textContainerInset = .init( + top: offset, + left: 0, + bottom: offset, + right: 0 + ) + $0.isScrollEnabled = false + $0.textContainer.maximumNumberOfLines = 1 + $0.textContainer.lineBreakMode = .byTruncatingTail + } + + textCountLabel.do { + $0.text = "0/\(textCountLimit)" + $0.applyByeBooFont(style: .cap2R12, color: .grayscale400) + } + + confirmButton.do { + $0.applyByeBooFont(style: .body2M16, text: "완료", color: .grayscale600) + } + } + + override func setLayout() { + textFieldContainer.snp.makeConstraints { + $0.top.equalToSuperview().inset(8.adjustedH) + $0.leading.equalToSuperview().inset(24.adjustedW) + $0.trailing.equalTo(confirmButton.snp.leading).offset(-20) + $0.height.equalTo(40.adjustedH) + $0.bottom.equalToSuperview().inset(6.adjustedH) + } + + topBorderLine.snp.makeConstraints { + $0.top.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(1.adjustedH) + } + + textView.snp.makeConstraints { + $0.verticalEdges.equalToSuperview().inset(9.5.adjustedH) + $0.horizontalEdges.equalToSuperview().inset(12.adjustedW) + } + + confirmButton.snp.makeConstraints { + $0.top.equalToSuperview().inset(17.5.adjustedH) + $0.trailing.equalToSuperview().inset(24.adjustedW) + $0.height.equalTo(21.adjustedH) + } + } + +} + +extension CommentTextView: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + if isPlaceholderActive { + isPlaceholderActive = false + textView.text = "" + } + textView.applyTextViewStyle(style: .body6R14, text: textView.text, color: .grayscale100) + updateTextViewLayoutWhenEditing() + } + + func textViewDidChange(_ textView: UITextView) { + textCountLabel.text = "\(textView.text.count)/500" + let hasText = !textView.text.isEmpty + confirmButton.isEnabled = hasText + confirmButton.applyByeBooFont(style: .body2M16, text: "완료", color: hasText ? .primary300 : .grayscale600) + + if textView.text.count > textCountLimit { + textView.deleteBackward() + return + } + + let maxHeight = 105.adjustedH + let fittingHeight = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: .infinity)).height + + let shouldScroll = fittingHeight > maxHeight + textView.isScrollEnabled = shouldScroll + if !shouldScroll { textView.contentOffset = .zero } + } + + func textViewDidEndEditing(_ textView: UITextView) { + isPlaceholderActive = textView.text.isEmpty + updateTextViewLayoutWhenEndEditing() + } +} + +extension CommentTextView { + private func updateTextViewLayoutWhenEditing() { + textView.textContainer.maximumNumberOfLines = 0 + textView.textContainer.lineBreakMode = .byWordWrapping + textFieldContainer.backgroundColor = .clear + confirmButton.removeFromSuperview() + addSubviews(countConfirmContainer) + countConfirmContainer.addSubviews(textCountLabel, confirmButton) + + textFieldContainer.snp.remakeConstraints { + $0.top.equalToSuperview().inset(8.adjustedH) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalTo(countConfirmContainer.snp.top) + } + + textView.snp.remakeConstraints { + $0.top.equalToSuperview().inset(9.adjustedH) + $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) + $0.height.greaterThanOrEqualTo(42.adjustedH) + $0.height.lessThanOrEqualTo(105.adjustedH) + $0.bottom.equalToSuperview().inset(9.adjustedH) + } + + countConfirmContainer.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(24.adjustedW) + $0.bottom.equalToSuperview().inset(8.adjustedH) + $0.height.equalTo(40.adjustedH) + } + + textCountLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalTo(confirmButton.snp.leading).offset(-12.adjustedW) + } + + confirmButton.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview() + $0.height.equalTo(40.adjustedH) + } + } + + private func updateTextViewLayoutWhenEndEditing() { + textFieldContainer.backgroundColor = .white5 + countConfirmContainer.removeFromSuperview() + addSubviews(confirmButton) + + textFieldContainer.snp.remakeConstraints { + $0.top.equalToSuperview().inset(8.adjustedH) + $0.leading.equalToSuperview().inset(24.adjustedW) + $0.trailing.equalTo(confirmButton.snp.leading).offset(-20) + $0.height.equalTo(40.adjustedH) + $0.bottom.equalToSuperview().inset(6.adjustedH) + } + + textView.snp.remakeConstraints { + $0.verticalEdges.equalToSuperview().inset(9.5.adjustedH) + $0.horizontalEdges.equalToSuperview().inset(12.adjustedW) + } + + confirmButton.snp.makeConstraints { + $0.top.equalToSuperview().inset(17.5.adjustedH) + $0.trailing.equalToSuperview().inset(24.adjustedW) + $0.height.equalTo(21.adjustedH) + } + + textView.isScrollEnabled = false + textView.textContainer.maximumNumberOfLines = 1 + textView.textContainer.lineBreakMode = .byTruncatingTail + + let text = isPlaceholderActive ? placeholder : (textView.text ?? "") + let color: UIColor = isPlaceholderActive ? .grayscale600 : .grayscale100 + textView.typingAttributes = [.font: FontManager.body6R14.font, .foregroundColor: color] + textView.text = text + textView.textColor = color + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift new file mode 100644 index 00000000..67a40bb9 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/Common/QuestContentView.swift @@ -0,0 +1,145 @@ +// +// QuestContentView.swift +// ByeBoo-iOS +// +// Created by 이나연 on 4/29/26. +// + +import UIKit + +protocol CommonQuestLikeCommentProtocol: AnyObject { + func likeButtonDidTap() +} + +final class QuestContentView: UIView { + + private let answerContentTextView = UITextView() + private let writtenDateLabel = UILabel() + + private let likeCommentStackView = UIStackView() + private let likeContainerView = UIStackView() + private(set) var likeButton = UIButton() + private let likeCountLabel = UILabel() + private let commentContainerView = UIStackView() + private let commentIcon = UIImageView() + private let commentCountLabel = UILabel() + + weak var delegate: CommonQuestLikeCommentProtocol? + + private var likeCounts: Int = 0 + + init() { + super.init(frame: .zero) + + setUI() + setStyle() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUI() { + self.addSubviews( + answerContentTextView, + writtenDateLabel, + likeCommentStackView + ) + likeCommentStackView.addArrangedSubviews(likeContainerView, commentContainerView) + likeContainerView.addArrangedSubviews(likeButton, likeCountLabel) + commentContainerView.addArrangedSubviews(commentIcon, commentCountLabel) + } + + private func setStyle() { + answerContentTextView.do { + $0.applyByeBooFont(style: .body3R16, color: .grayscale100) + $0.isScrollEnabled = false + $0.isEditable = false + $0.isSelectable = false + $0.isUserInteractionEnabled = false + $0.backgroundColor = .clear + $0.textContainer.lineBreakMode = .byTruncatingTail + } + writtenDateLabel.do { + $0.applyByeBooFont(style: .cap2R12, color: .grayscale400) + $0.isHidden = true + } + likeCommentStackView.do { + $0.axis = .horizontal + $0.spacing = 16.adjustedW + } + [likeContainerView, commentContainerView].forEach { + $0.do { + $0.axis = .horizontal + $0.spacing = 4.adjustedW + } + } + likeButton.do { + $0.setImage(.heartOff, for: .normal) + $0.setImage(.heartOn, for: .selected) + $0.addTarget(self, action: #selector(likeButtonDidTap), for: .touchUpInside) + } + commentIcon.do { + $0.image = .comment + } + [likeCountLabel, commentCountLabel].forEach { + $0.do { + $0.text = "0" + $0.applyByeBooFont(style: .cap2R12, color: .grayscale100) + } + } + } + + private func setLayout() { + answerContentTextView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + } + writtenDateLabel.snp.makeConstraints { + $0.top.equalTo(answerContentTextView.snp.bottom).offset(20.adjustedH) + $0.leading.equalToSuperview() + $0.height.equalTo(16.adjustedH) + } + likeCommentStackView.snp.makeConstraints { + $0.top.equalTo(answerContentTextView.snp.bottom).offset(20) + $0.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + [likeButton, commentIcon].forEach { + $0.snp.makeConstraints { + $0.size.equalTo(20) + } + } + } + + func configure( + content: String, + writtenAt: String? = nil, + isLiked: Bool, + likeCount: Int, + commentCount: Int, + showAllText: Bool + ) { + self.likeCounts = likeCount + answerContentTextView.do { + $0.textContainer.maximumNumberOfLines = showAllText ? 0 : 2 + $0.applyTextViewStyle(style: .body3R16, text: content, color: .grayscale100) + } + if let writtenAt { + writtenDateLabel.isHidden = false + writtenDateLabel.text = writtenAt + } + likeButton.isSelected = isLiked + likeCountLabel.text = String(likeCount) + commentCountLabel.text = String(commentCount) + } + + @objc + private func likeButtonDidTap() { + likeButton.isSelected.toggle() + likeCounts += likeButton.isSelected ? 1 : -1 + likeCountLabel.text = String(likeCounts) + delegate?.likeButtonDidTap() + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift index 724b42c6..d807133c 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestHistoryView.swift @@ -7,12 +7,13 @@ import UIKit +import SnapKit + final class CommonQuestHistoryView: BaseView { + + private var commentTextViewBottomConstraint: Constraint? - private var profileIcon: UIImage? - private var nickname: String? - - private let scrollView = UIScrollView() + private(set) var scrollView = UIScrollView() private let contentView = UIView() private let commonQuestLabel = UILabel() private let dateLabel = UILabel() @@ -22,9 +23,14 @@ final class CommonQuestHistoryView: BaseView { private let answerView = UIView() private let profileIconImageView = UIImageView() private let userNicknameLabel = UILabel() - private let answerContentLabel = UILabel() + private let questContentView = QuestContentView() + private(set) var commentListView = SelfSizingTableView() + private let commentTextView = CommentTextView() override func setStyle() { + scrollView.do { + $0.showsVerticalScrollIndicator = false + } commonQuestLabel.applyByeBooFont( style: .body6R14, text: "공통퀘스트", @@ -51,44 +57,35 @@ final class CommonQuestHistoryView: BaseView { $0.layer.cornerRadius = 12 $0.backgroundColor = .white5 } + commentListView.do { + $0.isScrollEnabled = false + $0.rowHeight = UITableView.automaticDimension + $0.estimatedRowHeight = 100 + } userNicknameLabel.applyByeBooFont( style: .body6R14, color: .grayscale200 ) - answerContentLabel.do { - $0.applyByeBooFont( - style: .body3R16, - color: .grayscale100, - numberOfLines: 0 - ) - } } override func setUI() { - addSubview(scrollView) + addSubviews(scrollView, commentTextView) scrollView.addSubview(contentView) contentView.addSubviews( commonQuestLabel, dateLabel, questionView, - answerView + answerView, + commentListView ) questionView.addSubviews( questionMarkLabel, questionContentLabel ) - - guard let _ = profileIcon, - let _ = nickname - else { - answerView.addSubview(answerContentLabel) - return - } - answerView.addSubviews( profileIconImageView, userNicknameLabel, - answerContentLabel + questContentView ) } @@ -96,7 +93,7 @@ final class CommonQuestHistoryView: BaseView { scrollView.snp.makeConstraints { $0.top.equalTo(safeAreaLayoutGuide).offset(16.adjustedH) $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.bottom.equalToSuperview().inset(24.adjustedH) + $0.bottom.equalTo(commentTextView.snp.top) } contentView.snp.makeConstraints { $0.edges.equalTo(scrollView.contentLayoutGuide) @@ -129,20 +126,7 @@ final class CommonQuestHistoryView: BaseView { answerView.snp.makeConstraints { $0.top.equalTo(questionView.snp.bottom).offset(30.adjustedH) $0.horizontalEdges.equalToSuperview() - $0.bottom.equalToSuperview().inset(24.adjustedH) } - - guard let _ = profileIcon, - let _ = nickname - else { - answerContentLabel.snp.makeConstraints { - $0.top.equalToSuperview().inset(16.adjustedH) - $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) - $0.bottom.equalToSuperview().inset(16.adjustedH) - } - return - } - profileIconImageView.snp.makeConstraints { $0.top.equalToSuperview().inset(16.adjustedH) $0.leading.equalToSuperview().inset(24.adjustedW) @@ -153,44 +137,55 @@ final class CommonQuestHistoryView: BaseView { $0.centerY.equalTo(profileIconImageView) $0.trailing.equalToSuperview().inset(24.adjustedW) } - answerContentLabel.snp.makeConstraints { + questContentView.snp.makeConstraints { $0.top.equalTo(profileIconImageView.snp.bottom).offset(12.adjustedH) $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) $0.bottom.equalToSuperview().inset(16.adjustedH) } + commentListView.snp.makeConstraints { + $0.top.equalTo(answerView.snp.bottom).offset(24.adjustedH) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview().inset(24.adjustedH) + } + commentTextView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + commentTextViewBottomConstraint = $0.bottom.equalToSuperview().inset(34.adjustedH).constraint + } } } extension CommonQuestHistoryView { - + + func updateLayout(keyboardHeight: CGFloat) { + let inset = keyboardHeight > 0 ? keyboardHeight : 34.adjustedH + commentTextViewBottomConstraint?.update(inset: inset) + } +} + +extension CommonQuestHistoryView { + func configure( question: String, writtenAt: String, - profileIcon: UIImage?, - nickname: String?, - content: String + profileIcon: UIImage, + nickname: String, + content: String, + isLiked: Bool, + likeCount: Int, + commentCount: Int ) { questionContentLabel.text = question dateLabel.text = writtenAt - answerContentLabel.text = content - - answerContentLabel.do { - $0.applyByeBooFont( - style: .body3R16, - text: content, - color: .grayscale100, - numberOfLines: 0 - ) - } - - if let profileIcon { - self.profileIcon = profileIcon - profileIconImageView.image = profileIcon - } + questContentView.configure( + content: content, + isLiked: isLiked, + likeCount: likeCount, + commentCount: commentCount, + showAllText: true + ) + + profileIconImageView.image = profileIcon + userNicknameLabel.text = nickname - if let nickname { - self.nickname = nickname - userNicknameLabel.text = nickname - } } } diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestReplyView.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestReplyView.swift new file mode 100644 index 00000000..8a2c149a --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/CommonQuest/CommonQuestReplyView.swift @@ -0,0 +1,76 @@ +// +// CommonQuestReplyView.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/8/26. +// + +import UIKit + +import SnapKit + +final class CommonQuestReplyView: BaseView { + private let headerView = UIView() + private(set) var backButton = UIButton() + private let headerLabel = UILabel() + private(set) var commentListView = UITableView() + private let commentTextView = CommentTextView() + + private var commentTextViewBottomConstraint: Constraint? + + override func setUI() { + addSubviews(headerView, commentListView, commentTextView) + headerView.addSubviews(backButton, headerLabel) + } + + override func setStyle() { + backgroundColor = .grayscale900 + + backButton.do { + $0.setImage(.left.withRenderingMode(.alwaysTemplate), for: .normal) + $0.tintColor = .white + } + + headerLabel.do { + $0.applyByeBooFont(style: .sub1Sb20, text: "답글", color: .white) + } + + commentListView.do { + $0.backgroundColor = .grayscale900 + $0.showsVerticalScrollIndicator = false + } + } + + override func setLayout() { + headerView.snp.makeConstraints { + $0.top.equalToSuperview().inset(26.adjustedH) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(40.adjustedH) + } + backButton.snp.makeConstraints { + $0.top.equalToSuperview().inset(8.adjustedH) + $0.leading.equalToSuperview().inset(24.adjustedH) + $0.size.equalTo(24.adjustedH) + } + headerLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(8.adjustedH) + $0.centerX.equalToSuperview() + } + commentListView.snp.makeConstraints { + $0.top.equalTo(headerView.snp.bottom).offset(6.adjustedH) + $0.horizontalEdges.equalToSuperview().inset(20.adjustedW) + $0.bottom.equalTo(commentTextView.snp.top) + } + commentTextView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + commentTextViewBottomConstraint = $0.bottom.equalToSuperview().inset(34.adjustedH).constraint + } + } +} + +extension CommonQuestReplyView { + func updateLayout(keyboardHeight: CGFloat) { + let inset = keyboardHeight > 0 ? keyboardHeight : 34.adjustedH + commentTextViewBottomConstraint?.update(inset: inset) + } +} 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 index 7b5e00dc..591dfd99 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/QuestTextField.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/View/Write/QuestTextField.swift @@ -77,17 +77,17 @@ extension QuestTextField: UITextViewDelegate { func textViewDidBeginEditing(_ textView: UITextView) { if isPlaceholderActive == true { isPlaceholderActive = false - applyTextViewStyle(text: "", color: .grayscale100) + textView.applyTextViewStyle(style: .body3R16 ,text: "", color: .grayscale100) } textView.textColor = .grayscale100 } func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { - applyTextViewStyle(text: placeholder, color: .grayscale300) + textView.applyTextViewStyle(style: .body3R16 ,text: placeholder, color: .grayscale300) isPlaceholderActive = true } else { - applyTextViewStyle(text: textView.text, color: .grayscale100) + textView.applyTextViewStyle(style: .body3R16, text: textView.text, color: .grayscale100) } questTextViewDelegate?.textViewDidEndEditing() } @@ -102,14 +102,6 @@ extension QuestTextField: UITextViewDelegate { } extension QuestTextField { - func applyTextViewStyle(text: String, color: UIColor) { - textView.applyByeBooFont( - style: .body3R16, - text: text, - color: color - ) - } - func updateTextViewHeight() -> CGFloat { let width = self.frame.width let fittingSize = CGSize(width: width, height: .greatestFiniteMagnitude) diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift index 7c80aab0..ba8a7828 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestHistoryViewController.swift @@ -11,7 +11,11 @@ import Mixpanel final class CommonQuestHistoryViewController: BaseViewController { + private var dataSource: UITableViewDiffableDataSource! + private let rootView = CommonQuestHistoryView() + private let viewModel = CommonQuestHistoryViewModel() + private var answerID: Int? private var answer: String? private var question: String? @@ -26,6 +30,7 @@ final class CommonQuestHistoryViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.tabBarController?.tabBar.isHidden = true + addKeyboardObservers() Mixpanel.mainInstance().track(event: CommonJourneyEvents.Name.commonJourneyOthersAnswerPageview) } @@ -40,6 +45,27 @@ final class CommonQuestHistoryViewController: BaseViewController { action: #selector(back), secondAction: #selector(bottomUp) ) + + configureDataSource() + applySnapshot() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + removeKeyboardObservers() + } + + override func setAddTarget() { + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tapGestureRecognizer.cancelsTouchesInView = false + rootView.scrollView.addGestureRecognizer(tapGestureRecognizer) + } + + override func setDelegate() { + rootView.commentListView.do { + $0.separatorStyle = .none + $0.register(CommentTableViewCell.self) + } } } @@ -50,6 +76,41 @@ extension CommonQuestHistoryViewController: BackNavigable { } } +extension CommonQuestHistoryViewController { + private func configureDataSource() { + dataSource = UITableViewDiffableDataSource( + tableView: rootView.commentListView + ) { tableView, indexPath, item in + guard let cell = tableView.dequeueReusableCell( + withIdentifier: CommentTableViewCell.identifier, + for: indexPath + ) as? CommentTableViewCell else { return UITableViewCell() } + + let entity = item.entity + cell.configure( + commentID: entity.commentID, + replyCount: entity.replyCount, + writer: entity.writer, + profileIcon: ProfileIcon.image(for: entity.profileIcon) ?? .relievedBadge, + writtenAt: entity.writtenAt, + content: entity.content, + showAllText: item.showAllText, + isReplySheet: false + ) + cell.delegate = self + return cell + } + } + + private func applySnapshot() { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let items = viewModel.commentLists.map { CommentItem(entity: $0, showAllText: false) } + snapshot.appendItems(items, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false) + } +} + extension CommonQuestHistoryViewController: CommonQuestBottomSheetDelegate { func didTapEdit( @@ -79,7 +140,7 @@ extension CommonQuestHistoryViewController: CommonQuestBottomSheetDelegate { writtenAt: writtenAt ) setDelegate(bottomSheet: commonQuestBottomSheet) - + if let sheet = commonQuestBottomSheet.sheetPresentationController{ sheet.detents = [.custom { _ in 224.adjustedH }] sheet.prefersGrabberVisible = true @@ -104,13 +165,39 @@ extension CommonQuestHistoryViewController: DeleteCommonQuestDelegate { } } +extension CommonQuestHistoryViewController: CommentProtocol { + func moreLabelDidTap(commentID: Int) { + let updatedItems = dataSource.snapshot().itemIdentifiers.map { item -> CommentItem in + guard item.entity.commentID == commentID else { return item } + return CommentItem(entity: item.entity, showAllText: true) + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(updatedItems, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in + self?.rootView.commentListView.performBatchUpdates(nil) + } + } + + func replyIconDidTap(commentID: Int) { + let viewController = CommonQuestReplyViewController() + if let sheet = viewController.sheetPresentationController{ + sheet.detents = [.large()] + sheet.prefersGrabberVisible = true + sheet.prefersScrollingExpandsWhenScrolledToEdge = false + } + self.present(viewController, animated: true) + } +} + extension CommonQuestHistoryViewController { func configure( question: String, writtenAt: String, - profileIcon: UIImage? = nil, - nickname: String? = nil, + profileIcon: UIImage, + nickname: String, content: String, answerID: Int? = nil, writerID: Int? = nil, @@ -120,7 +207,7 @@ extension CommonQuestHistoryViewController { self.answer = content self.question = question self.writtenAt = writtenAt - + if let isMyAnswer { commonQuestArchiveType = isMyAnswer ? .mine : .other } @@ -134,7 +221,10 @@ extension CommonQuestHistoryViewController { writtenAt: writtenAt, profileIcon: profileIcon, nickname: nickname, - content: content + content: content, + isLiked: false, + likeCount: 4, + commentCount: 5 ) } } @@ -150,3 +240,26 @@ extension CommonQuestHistoryViewController: BlockReportProtocol { } } } + +extension CommonQuestHistoryViewController { + @objc + private func dismissKeyboard() { + view.endEditing(true) + } +} + +extension CommonQuestHistoryViewController: KeyboardHandleProtocol { + func keyboardWillShow(height: CGFloat, duration: Double) { + UIView.animate(withDuration: duration) { + self.rootView.updateLayout(keyboardHeight: height) + self.rootView.layoutIfNeeded() + } + } + + func keyboardWillHide(duration: Double) { + UIView.animate(withDuration: duration) { + self.rootView.updateLayout(keyboardHeight: 0) + self.rootView.layoutIfNeeded() + } + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift index 97473a87..0e592468 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestMyAnswersViewController.swift @@ -115,6 +115,8 @@ extension CommonQuestMyAnswersViewController: UITableViewDelegate { historyViewController.configure( question: answer.question, writtenAt: answer.writtenAt, + profileIcon: .relievedBadge, + nickname: "냐냐냐", content: answer.content, answerID: answer.answerID ) @@ -194,12 +196,21 @@ extension CommonQuestMyAnswersViewController: UITableViewDataSource { } let cell: CommonQuestMyAnswerCell = tableView.dequeueReusableCell(for: indexPath) - + cell.questContentView.delegate = self cell.bind( question: answer.question, content: answer.content, - writtenAt: answer.writtenAt + writtenAt: answer.writtenAt, + isLiked: true, + likeCount: 3, + commentCount: 3 ) return cell } } + +extension CommonQuestMyAnswersViewController: CommonQuestLikeCommentProtocol { + func likeButtonDidTap() { + // TODO: like button + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestReplyViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestReplyViewController.swift new file mode 100644 index 00000000..620c2f03 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestReplyViewController.swift @@ -0,0 +1,134 @@ +// +// CommonQuestReplyViewController.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/8/26. +// + +import UIKit + +final class CommonQuestReplyViewController: BaseViewController { + + private let rootView = CommonQuestReplyView() + private let viewModel = CommonQuestReplyViewModel() + + private var dataSource: UITableViewDiffableDataSource! + + override func loadView() { + view = rootView + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.tabBarController?.tabBar.isHidden = true + addKeyboardObservers() + } + + override func viewDidLoad() { + super.viewDidLoad() + + configureDataSource() + applySnapshot() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + removeKeyboardObservers() + } + + override func setAddTarget() { + rootView.backButton.addTarget(self, action: #selector(backButtonDidTap), for: .touchUpInside) + } + + override func setDelegate() { + rootView.commentListView.do { + $0.separatorStyle = .none + $0.register(CommentTableViewCell.self, forCellReuseIdentifier: "comment") + $0.register(CommentTableViewCell.self, forCellReuseIdentifier: "reply") + } + } +} + +extension CommonQuestReplyViewController { + private func configureDataSource() { + dataSource = UITableViewDiffableDataSource( + tableView: rootView.commentListView + ) { tableView, indexPath, item in + let identifier = item.entity.replyCount != nil ? "comment" : "reply" + guard let cell = tableView.dequeueReusableCell( + withIdentifier: identifier, + for: indexPath + ) as? CommentTableViewCell else { return UITableViewCell() } + + let entity = item.entity + cell.configure( + commentID: entity.commentID, + replyCount: entity.replyCount, + writer: entity.writer, + profileIcon: ProfileIcon.image(for: entity.profileIcon) ?? .relievedBadge, + writtenAt: entity.writtenAt, + content: entity.content, + showAllText: item.showAllText, + isReplySheet: true + ) + cell.delegate = self + return cell + } + } + + private func applySnapshot() { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.comment, .replies]) + let commentItem = CommentItem(entity: viewModel.getCommentContent(), showAllText: false) + snapshot.appendItems([commentItem], toSection: .comment) + + let replyItems = viewModel.getReplyList().map { CommentItem(entity: $0, showAllText: false) } + snapshot.appendItems(replyItems, toSection: .replies) + + dataSource.apply(snapshot, animatingDifferences: false) + } + + @objc + private func backButtonDidTap() { + self.dismiss(animated: true) + } +} + +extension CommonQuestReplyViewController: CommentProtocol { + func moreLabelDidTap(commentID: Int) { + let currentSnapshot = dataSource.snapshot() + + let commentItems = currentSnapshot.itemIdentifiers(inSection: .comment).map { item -> CommentItem in + guard item.entity.commentID == commentID else { return item } + return CommentItem(entity: item.entity, showAllText: true) + } + let replyItems = currentSnapshot.itemIdentifiers(inSection: .replies).map { item -> CommentItem in + guard item.entity.commentID == commentID else { return item } + return CommentItem(entity: item.entity, showAllText: true) + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.comment, .replies]) + snapshot.appendItems(commentItems, toSection: .comment) + snapshot.appendItems(replyItems, toSection: .replies) + dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in + self?.rootView.commentListView.performBatchUpdates(nil) + } + } +} + +extension CommonQuestReplyViewController: KeyboardHandleProtocol { + func keyboardWillShow(height: CGFloat, duration: Double) { + UIView.animate(withDuration: duration) { + self.rootView.updateLayout(keyboardHeight: height) + self.rootView.layoutIfNeeded() + } + } + + func keyboardWillHide(duration: Double) { + UIView.animate(withDuration: duration) { + self.rootView.updateLayout(keyboardHeight: 0) + self.rootView.layoutIfNeeded() + } + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift index 924c64bd..f3ca30c7 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/CommonQuestViewController.swift @@ -137,19 +137,18 @@ extension CommonQuestViewController: UITableViewDelegate { DateFormatter.toDisplayDateString(from: $0) } - guard let formattedWrittenAt else { - return - } + guard let formattedWrittenAt else { return } + guard let profileIcon = viewModel.getProfileIcon(at: answerIndex) else { return } let historyViewController = ViewControllerFactory.shared.makeCommonQuestHistoryViewController() historyViewController.configure( question: viewModel.question, writtenAt: formattedWrittenAt, - profileIcon: viewModel.getProfileIcon(at: answerIndex), - nickname: answer.writer, + profileIcon: profileIcon, + nickname: answer.writerID, content: answer.content, answerID: answer.answerID, - writerID: answer.writerID, + writerID: answer.userID, isMyAnswer: answer.isMyAnswer ) historyViewController.navigationItem.hidesBackButton = true @@ -163,7 +162,7 @@ extension CommonQuestViewController: UITableViewDelegate { _ tableView: UITableView, heightForRowAt indexPath: IndexPath ) -> CGFloat { - indexPath.row == 0 ? UITableView.automaticDimension : 171.adjustedH + UITableView.automaticDimension } func tableView( @@ -260,7 +259,7 @@ extension CommonQuestViewController: UITableViewDataSource { let answer = viewModel.getAnswer(at: indexPath.row - 1) let profileIcon = viewModel.getProfileIcon(at: indexPath.row - 1) let writtenAt = viewModel.getWrittenAt(at: indexPath.row - 1) - + cell.questContentView.delegate = self if let answer, let writtenAt { cell.bind( @@ -281,3 +280,9 @@ extension CommonQuestViewController: UITableViewDataSource { return cell } } + +extension CommonQuestViewController: CommonQuestLikeCommentProtocol { + func likeButtonDidTap() { + // TODO: like button + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift index 0dbd56b0..e027c8dd 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewController/WriteQuestionTypeQuestViewController.swift @@ -217,6 +217,8 @@ extension WriteQuestionTypeQuestViewController: ToastPresentable, ToastErrorHand historyVC.configure( question: questTitle, writtenAt: writtenAt, + profileIcon: .relievedBadge, // TODO: 서버 수정 후 고치기 + nickname: "냐냐냐", content: self.rootView.questTextField.textView.text ) } @@ -307,7 +309,7 @@ extension WriteQuestionTypeQuestViewController { private func setQuestTextField(answer: String) { rootView.questTextField.do { - $0.applyTextViewStyle(text: answer, color: .grayscale100) + $0.textView.applyTextViewStyle(style: .body3R16 ,text: answer, color: .grayscale100) $0.isPlaceholderActive = false } rootView.layoutIfNeeded() diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift new file mode 100644 index 00000000..eb0625b5 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestHistoryViewModel.swift @@ -0,0 +1,17 @@ +// +// CommonQuestHistoryViewModel.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/5/26. +// + +import Combine +import Foundation + +final class CommonQuestHistoryViewModel { + private(set) var commentLists: [CommonQuestCommentEntity] = CommonQuestCommentEntity.toCommentListStub() + + func getCommentsCount() -> Int { + return commentLists.count + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestReplyViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestReplyViewModel.swift new file mode 100644 index 00000000..e0d5c0cd --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestReplyViewModel.swift @@ -0,0 +1,18 @@ +// +// CommonQuestReplyViewModel.swift +// ByeBoo-iOS +// +// Created by 이나연 on 5/8/26. +// + +import Foundation + +final class CommonQuestReplyViewModel { + func getCommentContent() -> CommonQuestCommentEntity { + return CommonQuestCommentEntity.toCommentStub() + } + + func getReplyList() -> [CommonQuestCommentEntity] { + return CommonQuestCommentEntity.toReplyListStub() + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift index 06b6f30f..82f1dc2b 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Quest/ViewModel/CommonQuestViewModel.swift @@ -90,27 +90,6 @@ extension CommonQuestViewModel: ViewModelType { } extension CommonQuestViewModel { - - private enum ProfileIcon: String, CaseIterable { - case sad = "SADNESS" - case selfUnderstanding = "SELF_UNDERSTANDING" - case soso = "SO_SO" - case relieved = "RELIEVED" - - var image: UIImage { - switch self { - case .sad: - return .sadnessBadge - case .selfUnderstanding: - return .selfUnderstandingBadge - case .soso: - return .sosoBadge - case .relieved: - return .relievedBadge - } - } - } - var question: String { commonQuest?.question ?? "" } @@ -141,17 +120,10 @@ extension CommonQuestViewModel { } return answers[index] } - + func getProfileIcon(at index: Int) -> UIImage? { - guard index >= 0 && index < answers.count else { - return nil - } - - let iconString = self.answers[index].profileIcon - let profileIcon = ProfileIcon.allCases - .first { $0.rawValue == iconString }? - .image - return profileIcon + guard index >= 0 && index < answers.count else { return nil } + return ProfileIcon.image(for: answers[index].profileIcon) } func getWrittenAt(at index: Int) -> String? { diff --git a/ByeBoo-iOS/ByeBoo-iOS/Presentation/Protocol/KeyboardHandleProtocol.swift b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Protocol/KeyboardHandleProtocol.swift new file mode 100644 index 00000000..07fcee60 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Presentation/Protocol/KeyboardHandleProtocol.swift @@ -0,0 +1,41 @@ +// +// KeyboardHandlable+.swift +// ByeBoo-iOS +// + +import UIKit + +protocol KeyboardHandleProtocol: UIViewController { + func keyboardWillShow(height: CGFloat, duration: Double) + func keyboardWillHide(duration: Double) +} + +extension KeyboardHandleProtocol { + func addKeyboardObservers() { + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillShowNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, + let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double + else { return } + self?.keyboardWillShow(height: keyboardFrame.height, duration: duration) + } + + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillHideNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double + else { return } + self?.keyboardWillHide(duration: duration) + } + } + + func removeKeyboardObservers() { + 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/Resource/Assets.xcassets/IconSystem/comment.imageset/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/comment.imageset/Contents.json new file mode 100644 index 00000000..d4e54c51 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/comment.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "comment.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/comment.imageset/comment.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/comment.imageset/comment.svg new file mode 100644 index 00000000..44e8b841 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/comment.imageset/comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart.imageset/Property 1=heart.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart.imageset/Property 1=heart.svg deleted file mode 100644 index b35689a1..00000000 --- a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart.imageset/Property 1=heart.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_off.imageset/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_off.imageset/Contents.json new file mode 100644 index 00000000..d022b3c5 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_off.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "heart_off.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_off.imageset/heart_off.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_off.imageset/heart_off.svg new file mode 100644 index 00000000..6a2d39c7 --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_off.imageset/heart_off.svg @@ -0,0 +1,3 @@ + + + diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_on.imageset/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_on.imageset/Contents.json new file mode 100644 index 00000000..b5e8cbaf --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_on.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "heart_on.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_on.imageset/heart_on.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_on.imageset/heart_on.svg new file mode 100644 index 00000000..70bf32ba --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart_on.imageset/heart_on.svg @@ -0,0 +1,3 @@ + + + diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart.imageset/Contents.json b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/replyArrow.imageset/Contents.json similarity index 74% rename from ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart.imageset/Contents.json rename to ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/replyArrow.imageset/Contents.json index de556b1e..52ca28db 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/heart.imageset/Contents.json +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/replyArrow.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Property 1=heart.svg", + "filename" : "icon.svg", "idiom" : "universal" } ], diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/replyArrow.imageset/icon.svg b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/replyArrow.imageset/icon.svg new file mode 100644 index 00000000..4f6764ab --- /dev/null +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Assets.xcassets/IconSystem/replyArrow.imageset/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ByeBoo-iOS/ByeBoo-iOS/Resource/Info.plist b/ByeBoo-iOS/ByeBoo-iOS/Resource/Info.plist index 9687993c..616e9eb9 100644 --- a/ByeBoo-iOS/ByeBoo-iOS/Resource/Info.plist +++ b/ByeBoo-iOS/ByeBoo-iOS/Resource/Info.plist @@ -58,5 +58,7 @@ remote-notification + UIDesignRequiresCompatibility +