From 48aa978d5e17f79ec756675aaf6a4d41801b1d12 Mon Sep 17 00:00:00 2001 From: Funital Date: Fri, 3 Oct 2025 23:23:17 +0900 Subject: [PATCH 01/69] =?UTF-8?q?[#321]=20develop-3.0.0=EC=97=90=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=20=EC=88=98=EC=A0=95=EB=90=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0v2=20UI=20=EC=9E=91=EC=97=85=20=EC=98=AE?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/RateReview/MenuLikeCell.swift | 115 +++++ .../View/SeeReview/RateNumberView.swift | 54 +- .../View/SeeReview/ReviewEmptyViewCell.swift | 52 +- .../View/SeeReview/ReviewRateViewCell.swift | 473 +++++++++++------- .../View/SeeReview/ReviewTableCell.swift | 200 +++++--- .../ReviewTagCollectionViewCell.swift | 94 ++++ .../ViewController/ReviewViewController.swift | 37 +- .../SetRateViewController.swift | 258 +++++++--- .../ic_restaurant.imageset/Contents.json | 12 + .../ic_restaurant.imageset/ic_restaurant.png | Bin 0 -> 1225 bytes .../noReview.imageset/Contents.json | 12 + .../noReview.imageset/noReview.png | Bin 0 -> 4445 bytes .../thumb-up_gray.imageset/Contents.json | 12 + .../thumb-up_gray.imageset/thumb-up_gray.png | Bin 0 -> 1168 bytes 14 files changed, 976 insertions(+), 343 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift create mode 100644 EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/ic_restaurant.png create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/noReview.png create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/thumb-up_gray.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/thumb-up_gray.imageset/thumb-up_gray.png diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift new file mode 100644 index 00000000..c238b4fb --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -0,0 +1,115 @@ +// +// MenuLikeCell.swift +// EATSSU +// +// Created by 한금준 on 9/28/25. +// + +import UIKit +import SnapKit + +import EATSSUDesign + +final class MenuLikeCell: UITableViewCell { + static let identifier = "MenuLikeCell" + + // MARK: - Properties + var onLikeTapped: (() -> Void)? + var isLiked: Bool = false { + didSet { + tapped() // 상태값 변경 시 UI 갱신 + } + } + + // MARK: - UI Components + private let menuLabel: UILabel = { + let label = UILabel() + label.font = .body3 + label.textColor = .black + return label + }() + + private let likeButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .gray + button.backgroundColor = .clear + button.isUserInteractionEnabled = false + return button + }() + + private let likeContainer: UIView = { + let view = UIView() + view.layer.cornerRadius = 14 + view.layer.borderWidth = 1 + view.layer.borderColor = UIColor.lightGray.cgColor + return view + }() + + private lazy var hStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [menuLabel, likeContainer]) + stack.axis = .horizontal + stack.spacing = 12 + stack.alignment = .center + return stack + }() + + // MARK: - Init + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + + contentView.addSubview(hStack) + likeContainer.addSubview(likeButton) + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(likeTapped)) + likeContainer.isUserInteractionEnabled = true + likeContainer.addGestureRecognizer(tapGesture) + + hStack.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) + } + + likeContainer.snp.makeConstraints { + $0.height.equalTo(28) + $0.width.equalTo(58) + } + + likeButton.snp.makeConstraints { + $0.center.equalToSuperview() + $0.size.equalTo(18) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Actions + @objc private func likeTapped() { + onLikeTapped?() + } + + // MARK: - Public Functions + func dataBind(menu: String, isLiked: Bool) { + menuLabel.text = menu + self.isLiked = isLiked + } + + private func tapped() { + print("tapped 실행됨 → isLiked:", isLiked) + let image = isLiked ? EATSSUDesignAsset.Images.thumbUp.image : EATSSUDesignAsset.Images.thumbUpGray.image + DispatchQueue.main.async { + self.likeButton.setImage(image.withRenderingMode(.alwaysOriginal), for: .normal) + + // Container 스타일 업데이트 + if self.isLiked { + self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor + } else { + self.likeContainer.backgroundColor = .clear + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor + } + } + } +} + diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift index c5354c58..ba033af8 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift @@ -14,9 +14,16 @@ import EATSSUDesign final class RateNumberView: BaseUIView { // MARK: - UI Components - let starImageView = UIImageView() +// let starImageView = UIImageView() + private var starImageViews: [UIImageView] = [] + private lazy var starsStackView = UIStackView() lazy var rateNumberLabel = UILabel() - private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starImageView,rateNumberLabel]) +// private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starImageView, + private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView, + rateNumberLabel]) + + var filledStarImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image + var emptyStarImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image // MARK: - init @@ -38,25 +45,56 @@ final class RateNumberView: BaseUIView { override func configureUI() { addSubviews(rateNumberStackView) - starImageView.image = EATSSUDesignAsset.Images.icStarYellow.image +// starImageView.image = EATSSUDesignAsset.Images.icStarYellow.image + starImageViews = (0..<5).map { _ in + let imageView = UIImageView() +// imageView.image = EATSSUDesignAsset.Images.icStarYellow.image.withRenderingMode(.alwaysTemplate) +// imageView.tintColor = EATSSUDesignAsset.Color.gray3.color + imageView.image = emptyStarImage + return imageView + } + + starsStackView.axis = .horizontal + starsStackView.spacing = 3 + starsStackView.alignment = .bottom + starImageViews.forEach { starsStackView.addArrangedSubview($0) } + rateNumberLabel.text = "5" rateNumberLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 14) rateNumberLabel.textColor = EATSSUDesignAsset.Color.Main.primary.color rateNumberStackView.axis = .horizontal - rateNumberStackView.spacing = 3 - rateNumberStackView.alignment = .center +// rateNumberStackView.spacing = 3 + rateNumberStackView.spacing = 6 + rateNumberStackView.alignment = .bottom } override func setLayout() { - starImageView.snp.makeConstraints { - $0.height.equalTo(12.adjusted) - $0.width.equalTo(12.adjusted) +// starImageView.snp.makeConstraints { +// $0.height.equalTo(12.adjusted) +// $0.width.equalTo(12.adjusted) + starImageViews.forEach { + $0.snp.makeConstraints { + $0.height.equalTo(12.adjusted) + $0.width.equalTo(12.adjusted) + } } rateNumberStackView.snp.makeConstraints { $0.edges.equalToSuperview() } } + + + func setRating(_ rating: Int) { + for (index, star) in starImageViews.enumerated() { + if index < rating { + star.image = filledStarImage + } else { + star.image = emptyStarImage + } + } + rateNumberLabel.text = "\(rating)" + } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index 30fa1a59..c5526095 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -9,24 +9,50 @@ import UIKit import SnapKit +import EATSSUDesign + final class ReviewEmptyViewCell: UITableViewCell { // MARK: - Properties - static let identifier = "ReviewEmptyViewCell" // MARK: - UI Components - private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() - imageView.image = ImageLiteral.noReview +// imageView.image = EATSSUDesignAsset.Images.noReview.image + imageView.tintColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return imageView }() - // MARK: - Functions + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = "아직 작성된 리뷰가 없어요" + label.font = .subtitle2 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + label.textAlignment = .center + return label + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = "메뉴에 가장 먼저 리뷰를 남겨주세요!" + label.font = .caption2 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + label.textAlignment = .center + return label + }() + private lazy var stackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [noReviewImageView, titleLabel, descriptionLabel]) + stack.axis = .vertical + stack.alignment = .center + stack.spacing = 16 + return stack + }() + + // MARK: - Init override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(noReviewImageView) + contentView.addSubview(stackView) setLayout() } @@ -35,17 +61,27 @@ final class ReviewEmptyViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - func setLayout() { - noReviewImageView.snp.makeConstraints { + // MARK: - Layout + private func setLayout() { + stackView.snp.makeConstraints { $0.center.equalToSuperview() } + noReviewImageView.snp.makeConstraints { + $0.size.equalTo(48) + } } + // MARK: - Configure func configure(isTokenExist: Bool) { if isTokenExist { - noReviewImageView.image = ImageLiteral.noReview +// noReviewImageView.image = ImageLiteral.noReview + noReviewImageView.image = EATSSUDesignAsset.Images.noReview.image + titleLabel.text = "아직 작성된 리뷰가 없어요" + descriptionLabel.text = "메뉴에 가장 먼저 리뷰를 남겨주세요!" } else { noReviewImageView.image = ImageLiteral.pleaseLogin +// titleLabel.text = "로그인이 필요합니다" +// descriptionLabel.text = "리뷰 작성을 위해 먼저 로그인 해주세요" } } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 2132724d..47653f33 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -27,6 +27,14 @@ final class ReviewRateViewCell: UITableViewCell { var reviewData: ReviewRateResponse? // MARK: - UI Components + + private let menuContainer: UIView = { + let view = UIView() + view.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color // EATSSUDesignAsset.Color.Gray.gray100.color 같은 컬러 사용 가능 + view.layer.cornerRadius = 12 + view.layer.masksToBounds = true + return view + }() private var menuLabel: UILabel = { let label = UILabel() @@ -37,80 +45,118 @@ final class ReviewRateViewCell: UITableViewCell { label.textAlignment = .center return label }() - - private let bigStarImageView: UIImageView = { + + private let menuIcon: UIImageView = { let imageView = UIImageView() - imageView.image = EATSSUDesignAsset.Images.icStarYellow.image + imageView.image = EATSSUDesignAsset.Images.icRestaurant.image +// imageView.tintColor = EATSSUDesignAsset.Color.Main.primary.color return imageView }() - private let rateNumLabel: UILabel = { + private let menuTitleLabel: UILabel = { let label = UILabel() - label.text = "4.3" - label.font = .bold(size: 36) + label.text = "오늘의 메뉴" + label.font = .body1 label.textColor = .black return label }() - private let tasteStarImageView: UIImageView = { - let imageView = UIImageView() - imageView.image = EATSSUDesignAsset.Images.icStarYellow.image - return imageView + private lazy var menuTitleStackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [menuIcon, menuTitleLabel]) + stack.axis = .horizontal + stack.alignment = .center + stack.spacing = 6 + return stack }() - private let quantityStarImageView: UIImageView = { + +// private lazy var rateSectionStackView: UIStackView = { +// let stack = UIStackView(arrangedSubviews: [totalRateStackView, yAxisStackView]) +// stack.axis = .horizontal +// stack.alignment = .center // 높이를 맞추고 싶다면 .top / .bottom으로 바꿀 수도 있음 +//// stack.distribution = .equalCentering +// stack.distribution = .equalSpacing +// stack.spacing = 36 // 기존에 yAxisStackView.leading = totalRateStackView.trailing.offset(36) 대체 +// return stack +// }() + private let rateSectionContainer: UIView = { + let view = UIView() + return view + }() + + private let bigStarImageView: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.icStarYellow.image return imageView }() - private let tasteLabel: UILabel = { - let label = UILabel() - label.text = "맛" - label.font = .body3 - label.textColor = .black - return label - }() - - private let tasteRateLabel: UILabel = { - let label = UILabel() - label.text = "5" - label.font = .body2 - label.textColor = EATSSUDesignAsset.Color.Main.primary.color - return label - }() - - private let quantityLabel: UILabel = { - let label = UILabel() - label.text = "양" - label.font = .body3 - label.textColor = .black - return label - }() - - private let quantityRateLabel: UILabel = { - let label = UILabel() - label.text = "5" - label.font = .body2 - label.textColor = EATSSUDesignAsset.Color.Main.primary.color - return label - }() - - private let totalReviewLabel: UILabel = { + private let rateNumLabel: UILabel = { let label = UILabel() - label.text = "총 리뷰 수" - label.font = .caption2 + label.text = "4.3" + label.font = .bold(size: 36) label.textColor = .black return label }() - private let totalReviewCount: UILabel = { - let label = UILabel() - label.text = "15" - label.font = .caption1 - label.textColor = EATSSUDesignAsset.Color.Main.primary.color - return label - }() +// private let tasteStarImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.image = EATSSUDesignAsset.Images.icStarYellow.image +// return imageView +// }() +// +// private let quantityStarImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.image = EATSSUDesignAsset.Images.icStarYellow.image +// return imageView +// }() +// +// private let tasteLabel: UILabel = { +// let label = UILabel() +// label.text = "맛" +// label.font = .body3 +// label.textColor = .black +// return label +// }() +// +// private let tasteRateLabel: UILabel = { +// let label = UILabel() +// label.text = "5" +// label.font = .body2 +// label.textColor = EATSSUDesignAsset.Color.Main.primary.color +// return label +// }() +// +// private let quantityLabel: UILabel = { +// let label = UILabel() +// label.text = "양" +// label.font = .body3 +// label.textColor = .black +// return label +// }() +// +// private let quantityRateLabel: UILabel = { +// let label = UILabel() +// label.text = "5" +// label.font = .body2 +// label.textColor = EATSSUDesignAsset.Color.Main.primary.color +// return label +// }() + +// private let totalReviewLabel: UILabel = { +// let label = UILabel() +// label.text = "총 리뷰 수" +// label.font = .caption2 +// label.textColor = .black +// return label +// }() +// +// private let totalReviewCount: UILabel = { +// let label = UILabel() +// label.text = "15" +// label.font = .caption1 +// label.textColor = EATSSUDesignAsset.Color.Main.primary.color +// return label +// }() private let fivePointLabel: UILabel = { let label = UILabel() @@ -197,14 +243,14 @@ final class ReviewRateViewCell: UITableViewCell { stackView.alignment = .trailing return stackView }() - - lazy var totalLabelStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [totalReviewLabel, - totalReviewCount]) - stackView.axis = .horizontal - stackView.spacing = 7 - return stackView - }() +// +// lazy var totalLabelStackView: UIStackView = { +// let stackView = UIStackView(arrangedSubviews: [totalReviewLabel, +// totalReviewCount]) +// stackView.axis = .horizontal +// stackView.spacing = 7 +// return stackView +// }() lazy var totalRateStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [bigStarImageView, @@ -215,36 +261,36 @@ final class ReviewRateViewCell: UITableViewCell { return stackView }() - lazy var tasteStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [tasteLabel, - tasteStarImageView, - tasteRateLabel]) - stackView.axis = .horizontal - stackView.spacing = 5 - stackView.alignment = .center - return stackView - }() - - lazy var quantityStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [quantityLabel, - quantityStarImageView, - quantityRateLabel]) - stackView.axis = .horizontal - stackView.spacing = 5 - stackView.alignment = .center - return stackView - }() - - private var addReviewButton: UIButton = { - let button = UIButton() - button.setTitle("리뷰 작성하기", for: .normal) - button.setTitleColor(.white, for: .normal) - button.titleLabel?.font = .bold(size: 14) - button.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color - button.layer.cornerRadius = 10 - button.layer.masksToBounds = false - return button - }() +// lazy var tasteStackView: UIStackView = { +// let stackView = UIStackView(arrangedSubviews: [tasteLabel, +// tasteStarImageView, +// tasteRateLabel]) +// stackView.axis = .horizontal +// stackView.spacing = 5 +// stackView.alignment = .center +// return stackView +// }() +// +// lazy var quantityStackView: UIStackView = { +// let stackView = UIStackView(arrangedSubviews: [quantityLabel, +// quantityStarImageView, +// quantityRateLabel]) +// stackView.axis = .horizontal +// stackView.spacing = 5 +// stackView.alignment = .center +// return stackView +// }() + +// private var addReviewButton: UIButton = { +// let button = UIButton() +// button.setTitle("리뷰 작성하기", for: .normal) +// button.setTitleColor(.white, for: .normal) +// button.titleLabel?.font = .bold(size: 14) +// button.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color +// button.layer.cornerRadius = 10 +// button.layer.masksToBounds = false +// return button +// }() // MARK: FIX ME - charts 추가 나중에 하기 @@ -260,7 +306,7 @@ final class ReviewRateViewCell: UITableViewCell { configureUI() setLayout() - addTarget() +// addTarget() } @available(*, unavailable) @@ -272,50 +318,114 @@ final class ReviewRateViewCell: UITableViewCell { func configureUI() { contentView.addSubviews( - menuLabel, - totalRateStackView, - addReviewButton, - totalLabelStackView, - yAxisStackView, +// menuLabel, + menuContainer, +// rateSectionStackView, + rateSectionContainer, +// totalRateStackView, +// addReviewButton, +// totalLabelStackView, +// yAxisStackView, oneChartBar, twoChartBar, threeChartBar, fourChartBar, fiveChartBar, - tasteStackView, - quantityStackView +// tasteStackView, +// quantityStackView ) + +// menuContainer.addSubview(menuLabel) + + menuContainer.addSubviews(menuTitleStackView, menuLabel) + + + // Add totalRateStackView and yAxisStackView to rateSectionContainer with constraints + rateSectionContainer.addSubviews(totalRateStackView, yAxisStackView) + + totalRateStackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview().offset(35.5) + make.leading.equalToSuperview().offset(36) + } + + yAxisStackView.snp.makeConstraints { make in +// make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) +// make.top.bottom.equalToSuperview().offset(12) +// make.trailing.equalToSuperview().offset(-37) + make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) + make.centerY.equalTo(totalRateStackView) + } } func setLayout() { backgroundColor = .white - menuLabel.snp.makeConstraints { make in +// menuLabel.snp.makeConstraints { make in +// make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) +// make.centerX.equalToSuperview() +// make.width.equalTo(290.adjusted) +// } + menuContainer.snp.makeConstraints { make in make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) make.centerX.equalToSuperview() - make.width.equalTo(290.adjusted) - } - - totalRateStackView.snp.makeConstraints { make in - make.top.equalTo(menuLabel.snp.bottom).offset(40) - make.leading.equalToSuperview().inset(60) + make.width.equalTo(320.adjusted) + make.height.greaterThanOrEqualTo(100) // 라벨 줄 수에 따라 자동으로 늘어나게 } - - tasteStackView.snp.makeConstraints { make in - make.top.equalTo(totalRateStackView.snp.bottom).offset(8) - make.leading.equalTo(totalRateStackView).offset(-5) + + menuTitleStackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(16) + make.centerX.equalToSuperview() } - - quantityStackView.snp.makeConstraints { make in - make.top.equalTo(tasteStackView) - make.leading.equalTo(tasteStackView.snp.trailing).offset(5) + + menuIcon.snp.makeConstraints { make in + make.width.height.equalTo(20) } - yAxisStackView.snp.makeConstraints { make in - make.top.equalTo(totalReviewLabel.snp.bottom).offset(8) - make.leading.equalTo(totalReviewLabel) + menuLabel.snp.makeConstraints { make in +// make.edges.equalToSuperview().inset(12) // 안쪽 여백 + make.top.equalTo(menuTitleStackView.snp.bottom).offset(12) + make.leading.trailing.equalToSuperview().inset(28) + make.bottom.equalToSuperview().inset(16) } - + + rateSectionContainer.snp.makeConstraints { make in + make.top.equalTo(menuLabel.snp.bottom).offset(40) +// make.centerX.equalToSuperview().inset(100) // 중앙 배치 + make.leading.trailing.equalToSuperview().inset(60) +// make.leading.equalToSuperview().offset(60) + } + + + + +// totalRateStackView.snp.makeConstraints { make in +//// make.top.equalTo(menuLabel.snp.bottom).offset(40) +//// make.leading.equalToSuperview().inset(60) +// make.centerY.equalTo(rateSectionStackView) +// make.leading.equalTo(rateSectionStackView).offset(37) +// } + +// tasteStackView.snp.makeConstraints { make in +// make.top.equalTo(totalRateStackView.snp.bottom).offset(8) +// make.leading.equalTo(totalRateStackView).offset(-5) +// } + +// quantityStackView.snp.makeConstraints { make in +// make.top.equalTo(tasteStackView) +// make.leading.equalTo(tasteStackView.snp.trailing).offset(5) +// } + +// yAxisStackView.snp.makeConstraints { make in +// make.top.equalTo(totalReviewLabel.snp.bottom).offset(8) +// make.leading.equalTo(totalReviewLabel) +// } + +// yAxisStackView.snp.makeConstraints { make in +//// make.top.equalTo(menuContainer.snp.bottom).offset(12) +// make.centerY.equalTo(rateSectionStackView) +// make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) +// } + oneChartBar.snp.makeConstraints { make in make.centerY.equalTo(onePointLabel) make.leading.equalTo(onePointLabel.snp.trailing).offset(7) @@ -357,45 +467,46 @@ final class ReviewRateViewCell: UITableViewCell { } } - addReviewButton.snp.makeConstraints { make in - make.top.equalTo(tasteStackView.snp.bottom).offset(35) - make.horizontalEdges.equalToSuperview().inset(60) - make.height.equalTo(36) - } - - totalLabelStackView.snp.makeConstraints { make in - make.top.equalTo(menuLabel.snp.bottom).offset(15) - make.leading.equalTo(quantityStackView.snp.trailing).offset(44) - } +// addReviewButton.snp.makeConstraints { make in +//// make.top.equalTo(tasteStackView.snp.bottom).offset(35) +// make.top.equalTo(rateSectionContainer.snp.bottom).offset(35) +// make.horizontalEdges.equalToSuperview().inset(60) +// make.height.equalTo(36) +// } - tasteStarImageView.snp.makeConstraints { make in - make.height.equalTo(11.19) - make.width.equalTo(11.71) - } +// totalLabelStackView.snp.makeConstraints { make in +// make.top.equalTo(menuLabel.snp.bottom).offset(15) +// make.leading.equalTo(quantityStackView.snp.trailing).offset(44) +// } - quantityStarImageView.snp.makeConstraints { make in - make.height.equalTo(11.19) - make.width.equalTo(11.71) - } +// tasteStarImageView.snp.makeConstraints { make in +// make.height.equalTo(11.19) +// make.width.equalTo(11.71) +// } +// +// quantityStarImageView.snp.makeConstraints { make in +// make.height.equalTo(11.19) +// make.width.equalTo(11.71) +// } bigStarImageView.snp.makeConstraints { $0.height.width.equalTo(24.adjusted) } - tasteStarImageView.snp.makeConstraints { - $0.height.width.equalTo(12.adjusted) - } - - quantityStarImageView.snp.makeConstraints { - $0.height.width.equalTo(12.adjusted) - } +// tasteStarImageView.snp.makeConstraints { +// $0.height.width.equalTo(12.adjusted) +// } +// +// quantityStarImageView.snp.makeConstraints { +// $0.height.width.equalTo(12.adjusted) +// } } - func addTarget() { - addReviewButton.addTarget(self, - action: #selector(touchAddReviewButton), - for: .touchUpInside) - } +// func addTarget() { +// addReviewButton.addTarget(self, +// action: #selector(touchAddReviewButton), +// for: .touchUpInside) +// } @objc func touchAddReviewButton() { @@ -409,24 +520,24 @@ extension ReviewRateViewCell { let taste = String(format: "%.1f", data.tasteRating ?? 0) let amount = String(format: "%.1f", data.amountRating ?? 0) menuLabel.text = data.menuName - totalReviewCount.text = "\(data.totalReviewCount)" +// totalReviewCount.text = "\(data.totalReviewCount)" rateNumLabel.text = "\(total)" totalRate = data.mainRating ?? 0 - if data.tasteRating == nil || data.tasteRating == 0.0 { - tasteStackView.isHidden = true - } else { - tasteStackView.isHidden = false - let taste = String(format: "%.1f", data.tasteRating ?? 0) - tasteRateLabel.text = "\(taste)" - } - - if data.amountRating == nil || data.amountRating == 0.0 { - quantityStackView.isHidden = true - } else { - quantityStackView.isHidden = false - let amount = String(format: "%.1f", data.amountRating ?? 0) - quantityRateLabel.text = "\(amount)" - } +// if data.tasteRating == nil || data.tasteRating == 0.0 { +// tasteStackView.isHidden = true +// } else { +// tasteStackView.isHidden = false +// let taste = String(format: "%.1f", data.tasteRating ?? 0) +// tasteRateLabel.text = "\(taste)" +// } + +// if data.amountRating == nil || data.amountRating == 0.0 { +// quantityStackView.isHidden = true +// } else { +// quantityStackView.isHidden = false +// let amount = String(format: "%.1f", data.amountRating ?? 0) +// quantityRateLabel.text = "\(amount)" +// } fiveChartBar.snp.updateConstraints { if data.reviewRatingCount.fiveStarCount == 0 { $0.width.equalTo(0) @@ -468,25 +579,25 @@ extension ReviewRateViewCell { let total = String(format: "%.1f", data.mainRating ?? 0) let taste = String(format: "%.1f", data.tasteRating ?? 0) let amount = String(format: "%.1f", data.amountRating ?? 0) - menuLabel.text = data.menuNames.joined(separator: ", ") - totalReviewCount.text = "\(data.totalReviewCount)" + menuLabel.text = data.menuNames.joined(separator: " + ") +// totalReviewCount.text = "\(data.totalReviewCount)" rateNumLabel.text = "\(total)" totalRate = data.mainRating ?? 0 - if data.tasteRating == nil || data.tasteRating == 0.0 { - tasteStackView.isHidden = true - } else { - tasteStackView.isHidden = false - let taste = String(format: "%.1f", data.tasteRating ?? 0) - tasteRateLabel.text = "\(taste)" - } - - if data.amountRating == nil || data.amountRating == 0.0 { - quantityStackView.isHidden = true - } else { - quantityStackView.isHidden = false - let amount = String(format: "%.1f", data.amountRating ?? 0) - quantityRateLabel.text = "\(amount)" - } +// if data.tasteRating == nil || data.tasteRating == 0.0 { +// tasteStackView.isHidden = true +// } else { +// tasteStackView.isHidden = false +// let taste = String(format: "%.1f", data.tasteRating ?? 0) +// tasteRateLabel.text = "\(taste)" +// } +// +// if data.amountRating == nil || data.amountRating == 0.0 { +// quantityStackView.isHidden = true +// } else { +// quantityStackView.isHidden = false +// let amount = String(format: "%.1f", data.amountRating ?? 0) +// quantityRateLabel.text = "\(amount)" +// } fiveChartBar.snp.updateConstraints { if data.reviewRatingCount.fiveStarCount == 0 { $0.width.equalTo(0) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 30addeb6..38b5eae1 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -12,6 +12,7 @@ import SnapKit import EATSSUDesign final class ReviewTableCell: UITableViewCell { + // MARK: - Properties static let identifier = "ReviewTableCell" @@ -22,24 +23,50 @@ final class ReviewTableCell: UITableViewCell { // MARK: - UI Components lazy var totalRateView = RateNumberView() - lazy var tasteRateView = RateNumberView() - lazy var quantityRateView = RateNumberView() - - private let tasteLabel: UILabel = { - let label = UILabel() - label.text = "맛" - label.font = .body3 - label.textColor = .black - return label - }() - - private let quantityLabel: UILabel = { - let label = UILabel() - label.text = "양" - label.font = .body3 - label.textColor = .black - return label - }() +// lazy var tasteRateView = RateNumberView() +// lazy var quantityRateView = RateNumberView() + +// private let tasteLabel: UILabel = { +// let label = UILabel() +// label.text = "맛" +// label.font = .body3 +// label.textColor = .black +// return label +// }() + +// private let quantityLabel: UILabel = { +// let label = UILabel() +// label.text = "양" +// label.font = .body3 +// label.textColor = .black +// return label +// }() + + // 태그 표시용 컬렉션 뷰 + private lazy var tagCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.minimumInteritemSpacing = 8 + layout.minimumLineSpacing = 8 + + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.isScrollEnabled = false + cv.register(ReviewTagCollectionViewCell.self, forCellWithReuseIdentifier: ReviewTagCollectionViewCell.identifier) + cv.dataSource = self + return cv + }() + + private var tags: [(name: String, isLiked: Bool)] = [] + + lazy var contentStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [tagCollectionView, reviewTextView, foodImageView]) + stackView.axis = .vertical + stackView.spacing = 8.adjusted + stackView.alignment = .leading + return stackView + }() private var dateLabel: UILabel = { let label = UILabel() @@ -52,15 +79,16 @@ final class ReviewTableCell: UITableViewCell { private var userNameLabel: UILabel = { let label = UILabel() label.text = "hellosoongsil1234" - label.font = .caption3 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + label.font = .caption1 +// label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + label.textColor = .black return label }() private var menuNameLabel: UILabel = { let label = UILabel() label.text = "계란국" - label.font = .caption1 + label.font = .caption3 label.textColor = .black return label }() @@ -98,26 +126,26 @@ final class ReviewTableCell: UITableViewCell { }() /// 맛 별점 - lazy var tasteStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [tasteLabel, tasteRateView]) - stackView.axis = .horizontal - stackView.spacing = 4.adjusted - stackView.alignment = .center - return stackView - }() +// lazy var tasteStackView: UIStackView = { +// let stackView = UIStackView(arrangedSubviews: [tasteLabel, tasteRateView]) +// stackView.axis = .horizontal +// stackView.spacing = 4.adjusted +// stackView.alignment = .center +// return stackView +// }() /// 양 별점 - lazy var quantityStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [quantityLabel, quantityRateView]) - stackView.axis = .horizontal - stackView.spacing = 4.adjusted - stackView.alignment = .center - return stackView - }() +// lazy var quantityStackView: UIStackView = { +// let stackView = UIStackView(arrangedSubviews: [quantityLabel, quantityRateView]) +// stackView.axis = .horizontal +// stackView.spacing = 4.adjusted +// stackView.alignment = .center +// return stackView +// }() /// 별점 lazy var rateStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [totalRateView, tasteStackView, quantityStackView]) + let stackView = UIStackView(arrangedSubviews: [totalRateView/*, tasteStackView, quantityStackView*/]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center @@ -159,13 +187,13 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - lazy var contentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [reviewTextView, foodImageView]) - stackView.axis = .vertical - stackView.spacing = 8.adjusted - stackView.alignment = .leading - return stackView - }() +// lazy var contentStackView: UIStackView = { +// let stackView = UIStackView(arrangedSubviews: [reviewTextView, foodImageView]) +// stackView.axis = .vertical +// stackView.spacing = 8.adjusted +// stackView.alignment = .leading +// return stackView +// }() // MARK: - Functions @@ -185,6 +213,8 @@ final class ReviewTableCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + tags = [] + tagCollectionView.reloadData() sideButton.setTitle("", for: .normal) sideButton.setImage(UIImage(), for: .normal) foodImageView.image = UIImage() @@ -221,6 +251,11 @@ final class ReviewTableCell: UITableViewCell { sideButton.snp.makeConstraints { $0.height.equalTo(12.adjusted) } + + tagCollectionView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.greaterThanOrEqualTo(30) + } } @objc @@ -229,6 +264,25 @@ final class ReviewTableCell: UITableViewCell { } } +// MARK: - CollectionView DataSource +extension ReviewTableCell: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return tags.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: ReviewTagCollectionViewCell.identifier, + for: indexPath + ) as? ReviewTagCollectionViewCell else { + return UICollectionViewCell() + } + let tag = tags[indexPath.item] + cell.configure(tagName: tag.name, isLiked: tag.isLiked) + return cell + } +} + // MARK: - Data Bind extension ReviewTableCell { @@ -236,20 +290,22 @@ extension ReviewTableCell { menuNameLabel.text = response.menu menuName = response.menu userNameLabel.text = response.writerNickname - totalRateView.rateNumberLabel.text = "\(response.mainRating)" - if response.tasteRating == nil { - tasteStackView.isHidden = true - } else { - tasteStackView.isHidden = false - tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" - } + totalRateView.setRating(response.mainRating) +// totalRateView.rateNumberLabel.text = "\(response.mainRating)" - if response.amountRating == nil { - quantityStackView.isHidden = true - } else { - quantityStackView.isHidden = false - quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" - } +// if response.tasteRating == nil { +// tasteStackView.isHidden = true +// } else { +// tasteStackView.isHidden = false +// tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" +// } + +// if response.amountRating == nil { +// quantityStackView.isHidden = true +// } else { +// quantityStackView.isHidden = false +// quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" +// } dateLabel.text = response.writedAt reviewTextView.text = response.content reviewId = response.reviewID @@ -261,25 +317,33 @@ extension ReviewTableCell { } sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) + + +// tags = (response.tags ?? []).map { ($0.name, $0.isLiked) } +// tags = (response.tags ?? [Tag(name: "기본태그", isLiked: true), +// Tag(name: "추천", isLiked: false)]) +// .map { ($0.name, $0.isLiked) } + tagCollectionView.reloadData() + } func myPageDataBind(response: MyDataList, nickname: String) { userNameLabel.text = "\(nickname)" menuNameLabel.text = response.menuName totalRateView.rateNumberLabel.text = "\(response.mainRating)" - if response.tasteRating == nil { - tasteStackView.isHidden = true - } else { - tasteStackView.isHidden = false - tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" - } +// if response.tasteRating == nil { +// tasteStackView.isHidden = true +// } else { +// tasteStackView.isHidden = false +// tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" +// } - if response.amountRating == nil { - quantityStackView.isHidden = true - } else { - quantityStackView.isHidden = false - quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" - } +// if response.amountRating == nil { +// quantityStackView.isHidden = true +// } else { +// quantityStackView.isHidden = false +// quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" +// } dateLabel.text = response.writeDate reviewTextView.text = response.content if response.imgURLList.count != 0 { diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift new file mode 100644 index 00000000..6ca3b8f8 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -0,0 +1,94 @@ +// +// ReviewTagCollectionViewCell.swift +// EATSSU +// +// Created by 한금준 on 10/3/25. +// + +import UIKit +import SnapKit + +final class ReviewTagCollectionViewCell: UICollectionViewCell { + static let identifier = "ReviewTagCollectionViewCell" + + private let iconImageView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage(systemName: "hand.thumbsup") // 기본 좋아요 아이콘 + iv.tintColor = .systemTeal + iv.isHidden = true + iv.contentMode = .scaleAspectFit + return iv + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 10, weight: .medium) + label.textColor = .systemTeal + return label + }() + + private let stackView: UIStackView = { + let sv = UIStackView() + sv.axis = .horizontal + sv.spacing = 4 + sv.alignment = .center + return sv + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + contentView.layer.cornerRadius = contentView.bounds.height / 2 + } + + + private func setupViews() { + contentView.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.1) +// contentView.layer.cornerRadius = 12 + contentView.layer.borderColor = UIColor.systemTeal.cgColor + contentView.layer.borderWidth = 1 + + stackView.addArrangedSubview(iconImageView) + stackView.addArrangedSubview(titleLabel) + + contentView.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + +// NSLayoutConstraint.activate([ +// iconImageView.widthAnchor.constraint(equalToConstant: 10), +// iconImageView.heightAnchor.constraint(equalToConstant: 10), +// stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), +// stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), +// stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 2), +// stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -2) +// ]) + + iconImageView.snp.makeConstraints { make in + make.width.height.equalTo(10) + } + stackView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(8) + make.trailing.equalToSuperview().inset(8) + make.top.equalToSuperview().offset(2) + make.bottom.equalToSuperview().inset(2) + } + } + + func configure(tagName: String, isLiked: Bool) { + titleLabel.text = tagName + if isLiked { + iconImageView.isHidden = false + iconImageView.image = UIImage(systemName: "hand.thumbsup") + } else { + iconImageView.isHidden = true + } + } +} diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 352dc1b1..97c27651 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -64,10 +64,13 @@ final class ReviewViewController: BaseViewController { getReviewList(type: type, menuId: menuID) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - logScreenView(screenID: FirebaseScreenID.Review.V1.review_v1_1) + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) +// if self.isMovingFromParent { +// if let tabContainer = navigationController?.parent as? CustomTabBarContainerController { +// tabContainer.setTabBarHidden(false) +// } +// } } // MARK: - Functions @@ -102,6 +105,11 @@ final class ReviewViewController: BaseViewController { private func setFirebaseTask() { FirebaseRemoteConfig.shared.fetchRestaurantInfo() + + #if DEBUG + #else + Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) + #endif } func setTableView() { @@ -188,9 +196,8 @@ final class ReviewViewController: BaseViewController { // MARK: - Action Method + // @objc func userTapReviewButton() { - // firebase - write_review_v1 이벤트 호출 - ReviewAnalyticsManager.shared.logWriteReviewV1() if RealmService.shared.isAccessTokenPresent() { activityIndicatorView.isHidden = false DispatchQueue.global().async { // 백그라운드 스레드에서 작업을 수행 @@ -217,10 +224,16 @@ final class ReviewViewController: BaseViewController { activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) } else { - let choiceMenuViewController = ChoiceMenuViewController() - choiceMenuViewController.menuDataBind(menuList: menuNameList, idList: menuIDList ?? []) +// let choiceMenuViewController = ChoiceMenuViewController() + let setRateViewController = SetRateViewController() +// choiceMenuViewController.menuDataBind(menuList: menuNameList, idList: menuIDList ?? []) + setRateViewController.dataBind(list: menuNameList, + idList: menuIDList ?? [], + reviewList: nil, + currentPage: 0) activityIndicatorView.stopAnimating() - navigationController?.pushViewController(choiceMenuViewController, animated: true) +// navigationController?.pushViewController(choiceMenuViewController, animated: true) + navigationController?.pushViewController(setRateViewController, animated: true) } } } @@ -247,6 +260,12 @@ final class ReviewViewController: BaseViewController { } } +//extension ReviewViewController: AddReviewButtonDelegate { +// func didTapAddReviewButton() { +// userTapReviewButton() +// } +//} + // MARK: - UITableView Delegate, DataSource extension ReviewViewController: UITableViewDelegate { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 3f990527..730d59c6 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -9,7 +9,6 @@ import UIKit import SnapKit import Moya -import FirebaseAnalytics import EATSSUDesign @@ -20,7 +19,7 @@ final class SetRateViewController: BaseViewController { private let reviewProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private var currentPage: Int = 0 { didSet { - menuLabel.text = "\(selectedList[currentPage]) 을/를 추천하시겠어요?" +// menuLabel.text = "\(selectedList[currentPage]) 을/를 추천하시겠어요?" if currentPage == selectedList.count - 1 { nextButton.setTitle("리뷰 남기기", for: .normal) } @@ -32,6 +31,10 @@ final class SetRateViewController: BaseViewController { private var selectedIDList: [Int] = [] private var selectedList: [String] = [] private var reviewId: Int? + + // 좋아요 상태를 보관 (selectedList와 같은 인덱스) + private var likedStates: [Bool] = [] + private var menuTableViewHeightConstraint: Constraint? // MARK: - UI Components @@ -60,7 +63,8 @@ final class SetRateViewController: BaseViewController { private var menuLabel: UILabel = { let label = UILabel() - label.text = "김치볶음밥 & 계란국을 추천하시겠어요?" +// label.text = "김치볶음밥 & 계란국을 추천하시겠어요?" + label.text = "오늘의 식사는 어떠셨나요?" label.font = .subtitle1 label.textColor = .black return label @@ -68,43 +72,51 @@ final class SetRateViewController: BaseViewController { private var detailLabel: UILabel = { let label = UILabel() - label.text = "해당 메뉴에 대한 상세한 평가를 남겨주세요." - label.font = .body3 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - return label - }() - - private var tasteLabel: UILabel = { - let label = UILabel() - label.text = "맛" - label.font = .subtitle1 - label.textColor = .black - return label - }() - - private var quantityLabel: UILabel = { - let label = UILabel() - label.text = "양" + label.text = "추천하고 싶은 메뉴가 있나요?" label.font = .subtitle1 label.textColor = .black return label }() - - private lazy var tasteStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 16.adjusted - stackView.alignment = .center - return stackView + + private let menuTableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.isScrollEnabled = false + return tableView }() - private lazy var quantityStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 16.adjusted - stackView.alignment = .center - return stackView - }() +// private var tasteLabel: UILabel = { +// let label = UILabel() +// label.text = "맛" +// label.font = .subtitle1 +// label.textColor = .black +// return label +// }() +// +// private var quantityLabel: UILabel = { +// let label = UILabel() +// label.text = "양" +// label.font = .subtitle1 +// label.textColor = .black +// return label +// }() + +// private lazy var tasteStackView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.spacing = 16.adjusted +// stackView.alignment = .center +// return stackView +// }() +// +// private lazy var quantityStackView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.spacing = 16.adjusted +// stackView.alignment = .center +// return stackView +// }() private let userReviewTextView: UITextView = { let textView = UITextView() @@ -114,7 +126,8 @@ final class SetRateViewController: BaseViewController { textView.layer.borderWidth = 1.adjusted textView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor textView.textContainerInset = UIEdgeInsets(top: 16.0.adjusted, left: 16.0.adjusted, bottom: 16.0.adjusted, right: 16.0.adjusted) - textView.text = "3글자 이상 작성해주세요!" +// textView.text = "3글자 이상 작성해주세요!" + textView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" textView.textColor = .gray500 return textView }() @@ -128,6 +141,22 @@ final class SetRateViewController: BaseViewController { imageView.addGestureRecognizer(tapGesture) return imageView }() + + private lazy var closeButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) + button.tintColor = .lightGray + button.addTarget(self, action: #selector(didTappedimageView), for: .touchUpInside) + button.isHidden = true // Hide close button initially + return button + }() + +// private lazy var imageContainerView: UIView = { +// let view = UIView() +// view.layer.cornerRadius = 10 +// view.clipsToBounds = true +// return view +// }() private lazy var imageContainer: UIView = { let view = UIView() @@ -188,24 +217,29 @@ final class SetRateViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() setDelegate() + + // 더미데이터 지정 + selectedList = ["김치볶음밥", "돈까스", "된장찌개", "샐러드", "라면"] + + // 좋아요 상태 배열도 맞춰서 초기화 + likedStates = Array(repeating: false, count: selectedList.count) + + // 테이블 갱신 + menuTableView.reloadData() } override func viewWillAppear(_: Bool) { addKeyboardNotifications() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - Analytics.logEvent(AnalyticsEventScreenView, - parameters: [AnalyticsParameterScreenName: FirebaseScreenID.Review.V1.review_v1_3, - AnalyticsParameterScreenClass: "SetRateViewController"]) - } - override func viewWillDisappear(_: Bool) { removeKeyboardNotifications() } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + menuTableViewHeightConstraint?.update(offset: menuTableView.contentSize.height) + } // MARK: - Functions @@ -216,24 +250,30 @@ final class SetRateViewController: BaseViewController { scrollView.addSubview(contentView) contentView.addSubviews(rateView, menuLabel, - tasteLabel, - quantityLabel, +// tasteLabel, +// quantityLabel, detailLabel, - tasteStackView, - quantityStackView, + + menuTableView, +// tasteStackView, +// quantityStackView, userReviewTextView, maximumWordLabel, selectImageButton, imageCountLabel, userReviewImageView, + closeButton, +// imageContainerView, deleteMethodLabel, nextButton) - tasteStackView.addArrangedSubviews([tasteLabel, - tasteRateView]) +// tasteStackView.addArrangedSubviews([tasteLabel, +// tasteRateView]) - quantityStackView.addArrangedSubviews([quantityLabel, - quantityRateView]) +// quantityStackView.addArrangedSubviews([quantityLabel, +// quantityRateView]) +// imageContainerView.addSubview(userReviewImageView) +// imageContainerView.addSubview(closeButton) } override func setLayout() { @@ -261,16 +301,25 @@ final class SetRateViewController: BaseViewController { make.top.equalTo(rateView.snp.bottom).offset(35) make.centerX.equalToSuperview() } + + menuTableView.snp.makeConstraints { + $0.top.equalTo(detailLabel.snp.bottom).offset(20) + $0.leading.equalToSuperview().offset(32) + $0.trailing.equalToSuperview().offset(-32) +// $0.bottom.equalToSuperview() +// $0.height.equalTo(200) + menuTableViewHeightConstraint = $0.height.equalTo(0).constraint // 처음엔 0으로 + } - tasteStackView.snp.makeConstraints { make in - make.top.equalTo(detailLabel.snp.bottom).offset(30) - make.centerX.equalToSuperview() - } - - quantityStackView.snp.makeConstraints { make in - make.top.equalTo(tasteStackView.snp.bottom).offset(30) - make.centerX.equalToSuperview() - } +// tasteStackView.snp.makeConstraints { make in +// make.top.equalTo(detailLabel.snp.bottom).offset(30) +// make.centerX.equalToSuperview() +// } +// +// quantityStackView.snp.makeConstraints { make in +// make.top.equalTo(tasteStackView.snp.bottom).offset(30) +// make.centerX.equalToSuperview() +// } nextButton.snp.makeConstraints { make in make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) @@ -291,7 +340,8 @@ final class SetRateViewController: BaseViewController { } userReviewTextView.snp.makeConstraints { make in - make.top.equalTo(quantityStackView.snp.bottom).offset(40) +// make.top.equalTo(quantityStackView.snp.bottom).offset(40) + make.top.equalTo(menuTableView.snp.bottom).offset(40) make.leading.equalToSuperview().offset(16) make.trailing.equalToSuperview().offset(-16) make.height.equalTo(181) @@ -321,6 +371,24 @@ final class SetRateViewController: BaseViewController { $0.width.equalTo(60) $0.height.equalTo(60) } +// imageContainerView.snp.makeConstraints { +// $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) +// $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) +// $0.width.height.equalTo(70) // 원하는 크기로 조절 +// } + +// userReviewImageView.snp.makeConstraints { +//// $0.edges.equalToSuperview() +// $0.size.equalTo(60) +// +// } + + + closeButton.snp.makeConstraints { + $0.top.equalTo(userReviewImageView.snp.top).offset(-6) + $0.trailing.equalTo(userReviewImageView.snp.trailing).offset(6) + $0.size.equalTo(24) + } deleteMethodLabel.snp.makeConstraints { $0.top.equalTo(selectImageButton.snp.bottom).offset(7) @@ -355,6 +423,18 @@ final class SetRateViewController: BaseViewController { } self.currentPage = currentPage } + + // 좋아요 토글 메서드 (여기가 없으면 'Cannot find toggleLike' 에러) + private func toggleLike(for index: Int) { + likedStates[index].toggle() + let idx = IndexPath(row: index, section: 0) + + if let cell = menuTableView.cellForRow(at: idx) as? MenuLikeCell { + cell.dataBind(menu: selectedList[index], isLiked: likedStates[index]) + } else { + menuTableView.reloadRows(at: [idx], with: .none) + } + } func dataBindForFix(list: [String], reivewId: Int) { selectedList = list @@ -366,6 +446,10 @@ final class SetRateViewController: BaseViewController { } func setDelegate() { + menuTableView.register(MenuLikeCell.self, forCellReuseIdentifier: MenuLikeCell.identifier) + menuTableView.dataSource = self + menuTableView.delegate = self + imagePickerController.delegate = self imagePickerController.sourceType = .photoLibrary imagePickerController.allowsEditing = false @@ -413,13 +497,6 @@ final class SetRateViewController: BaseViewController { private func sendDataIfCurrentPageIsLast() { for (index, review) in reviewList.enumerated() { let (reviewDTO, image) = review - - // firebase - complete_review_v1 이벤트 호출 - let photoAttached = (image != nil) ? 1 : 0 - let rating = reviewDTO.mainRating - let selection = self.selectedList.count - ReviewAnalyticsManager.shared.logCompleteReviewV1(photoAttached: photoAttached, rating: rating, selection: selection) - if image != nil { postReviewImage(param: reviewDTO, image: image, @@ -442,6 +519,7 @@ final class SetRateViewController: BaseViewController { userReviewImageView.image = nil // 이미지 삭제 userPickedImage = nil imageCountLabel.text = "사진 0/1" + closeButton.isHidden = true // Hide close button when image is cleared } private func prepareForNextReview() { @@ -473,6 +551,8 @@ final class SetRateViewController: BaseViewController { userReviewTextView.text = data.content userReviewTextView.textColor = .black } + + } // MARK: - Server @@ -558,6 +638,7 @@ extension SetRateViewController: UIImagePickerControllerDelegate { userReviewImageView.image = image userPickedImage = image imageCountLabel.text = "사진 1/1" + closeButton.isHidden = false // Show close button when image is selected } picker.dismiss(animated: true, completion: nil) } @@ -648,3 +729,42 @@ extension SetRateViewController: UINavigationControllerDelegate { object: nil) } } + +extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return selectedList.count // 리뷰 대상 메뉴 리스트 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: MenuLikeCell.identifier, for: indexPath) as? MenuLikeCell else { + return UITableViewCell() + } + + let menuName = selectedList[indexPath.row] + let isLiked = likedStates[indexPath.row] + cell.dataBind(menu: menuName, isLiked: isLiked) + + // 인덱스 캡처 대신, 셀로부터 현재 indexPath를 찾아서 토글 (재사용 안전) + cell.onLikeTapped = { [weak self, weak cell, weak tableView] in + guard + let self = self, + let tableView = tableView, + let cell = cell, + let tappedIndexPath = tableView.indexPath(for: cell) + else { return } + self.toggleLike(for: tappedIndexPath.row) + } + + return cell + } + + // 선택 이벤트 (좋아요 버튼 눌렀을 때 등) + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + print("\(selectedList[indexPath.row]) 선택됨") + // 선택 효과 제거 (회색 하이라이트 방지) + tableView.deselectRow(at: indexPath, animated: false) + + // 행을 눌렀을 때도 토글 실행 + toggleLike(for: indexPath.row) + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/Contents.json new file mode 100644 index 00000000..e01bb10f --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_restaurant.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/ic_restaurant.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_restaurant.imageset/ic_restaurant.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd9ec7bd4713229f71ecaae6760e2d2b3b299ac GIT binary patch literal 1225 zcmV;)1UCDLP)@~0drDELIAGL9O(c600d`2O+f$vv5yP_h@po)673@G zvDr63f}{xW%+7?gLW$GZp0VRL-|qlYoY;y#&mYf>0}v4r5fKp)(I^2&8po~L{2l*k z(zd0$@xk_k_0!?kH~#%lkEIGmKjF9EJo=?Q`u@LySYFkU;LW4e5E(T;9@j2=z2FM* zvM)tv;oiOW_JfsElxz-;EG(cVW9%nwyEsLp|BZY(c_kad6TgpqKwWR?zat^w`Z^FI z`50P=l+4NR_0BiD-Ls~?3Tx&{ltQ7`J;RHWqfq$sTe}Z`?w}+dW=ubT?s*GJy)*#~ zZ&$id%lN8gN*zivb0K`{p1snidU?Xz0x2)wp(HaEg1NfQm7eO&N-RG=IeLW>%v1>S z`m%v+nj8OosH0%BRUIh=$VfNX@0uT5H56#(+JqcxpL+g23d9gnPzq0rtdAWE--!}?c7`d2VyU2@$k3L zSqR}uf7Nwj5K1WP*(kGyR49zuk80x^=4@5Ap-)nz1V)y}o58LZCr2&iVaJQbm!Nbw z_f^t{kt59d}$`ZevRHceW0}X~Vqx6dI2Z&3hK1uZVg zm8l1yc_OwB!D+*M+u#H>bT22AN;Y)dKk3t#bFi!51L^1n`cS|-Fl`Siz?4Q87eZkn zd^R{?QPQ^Oe;a@`}kjUlAapS{cR^KNOeO_*RX zc~t1^9ZSLlrWz`G@`xdPg)RjVcYZZQq8KPiS$eyoPufL%_`(fvZXT zJQzX+FoX(V2o=B(Du5wW07Iw%hEM?vp#m5}1u%pPUuI* literal 0 HcmV?d00001 diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/Contents.json new file mode 100644 index 00000000..648b31d7 --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "noReview.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/noReview.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/noReview.imageset/noReview.png new file mode 100644 index 0000000000000000000000000000000000000000..eed035e8179734bd5d79b0474b017c6f88ea6f58 GIT binary patch literal 4445 zcmbVQhf~wb6aOZJBtU?G7^=KbB}kQyl+dey0f`_;sPdE|Rhp1crAg>02&e%?rN1Xc ziUBF2fQki>BE3j&5)jBQ?_c=c%-+uJ?#p^K7+|A)oIC!$14*HZ zEX=LmkA6)t3|+3fP7La;u;Qog*k-#1)ku8E3EL9t*M4ui=UsE?L4$GQ@DKM5j&&kaV?{n2ra&Cta)MA zNUkNOO+{Im$1fmY-R@prK7c}@WbN(kIs8_e=mD9r8Iz~Mo}3&{x*sFOVl=h2Z>SXy zsLv<%v!VkK!bhGb9e!5 zJ}4EV=V0X9H{okPWPipO-twnKg6|8`8%h^mlG6YJ9nB>dZ|ZC)W8e98W$#-sb$qUP zpMKs1$nYthc$~DnynL+a)-L%vEpG&Y?vQ8jC{E9^Sb-;RHy3~PvwI97Un;|g>vhse zEMEDWD>E}QDwe=g6{6Eb_N6!yaLbyvo}>a2C|uMH_ciF$3`;F(#`+zp&2Bre5xsZ{ zmn2$@ZCVj4g2cG&-k5LYB{X|4^sugl>vRQ2T*RM*99n<$*xS;bq|(F=l6m{(j}tc- zYdW04M=g32KYm2npsujdAksJUczc>#*zhr)06l^L5f^M#J6T_+)CoO9kt~6R z{=T@pZ(mg)%7@Z!>V(Z$_n2_%_){;RyWXe{Xt(4mp#QJ!f^ptW@7blLC9RCfz|Oqe zqS!L(y|ewl?9L5Fd$7?c3UQLR*$a5$Ce+EGS+=n+4W45EI6<0b<+4q$K{s<@Q93qIZijCG^HLQ*AJTj_COK;ph+*^?*b$Xhvc#9IQ z-t&7YI|`3EM!!#&@J@J8zOT`6-RwwJbdPZn*xg+I^7J8$0? zs}VSP)H^vj*-?Hr0!v9%K&r!%W`Nl)#!~fHmtbU{OG`|yjRiy&e(=K#{-2F-f-pAJ_Xd~em5tTOq=8g zn+tn}Y?vF=Rx`t+gUfzCJ(L3Kk~DgSDCNW7t#tKQQ5f>SPGB?(B^ZtwFQ3$;-GC~! zl>+Y|jgrs%c%CO~yaq?mHy?$sF<3xkF-zf>iGM(@pt`i@6yiiBifM8d*3s3i4*MCn z{=x^CO#qPvorMvwV>{phAkgh=uu!ClE-y;FTxY~D^Sj>6dlN(qvF1N)-dK*(t(d$J zv=pk{0>SEyJf(-ij&U0gHR+S);0$hct49?lyPpJTtY9kkfATXCi zVoG2oe`57a<3U+OBVg;i$eD}eY?l>eco8bc`1o&Pj5`lO84`R^G z*zyr9P+k{`Cz;ou8ea}o{lW>PKFR=vQnBVq&`6`3x!D)GvsA3Ctkg>XT`vwPAE67A zO6q3Rwr?|G_(t|(ggj6M($dm8$jOW|Sl{8dLi3&{=u{1_aAHi>Lq?+m69`(Xv>wLi z$8*us*`}#-iTiJb`VWXr;5z>u_CYeq%d2-UsABF(wx;h6dlcEpVMb;pQ^TaD9lgsMxl#0(QH&wmj^x%gU@z*<-k<%A zo9z!FxwwsBCl?olWyR2h_`{jt)L{jrFvChcXL0VfeTHoMpK~13%A}EqbAbBY>#Q%3 z6J(W-5Rd5!fTE-AR(+{MQ&;y`m~@4%&GPjbaWezILoK%ArT_SAO7C>u3w-TkDF;L= zAE3zX!3W94MuvXGaxX?OgZW+os13GqI-cpJO;5WguwUWGOZrJmzM|s*p1LGGtQX!m znx{$%lSP>v6@Mf&;k|Rki6asfzxAzhtE=TlK?-RG?`}8a=03a017!)7Yinz2%REyd zO#&xl6z)#{ldGRmvU&zQ@1JPUpNnOdM&=d$l`?vcy;9znL+{O*BurMoQ1Vp>mRl3a z!k+ocUMJkypvDf?anv(np!BEgS~w?cHeLGI1ZCEGjTKDb`ynvJ1Sv&_YZeywHKF|Q zId*U8rd~Y3&g9$an{-o2iw)*$pE%yiqvYU%rmBVUw*II;nr`yIo4|V z2H$vITbAeEpQ@@ba9}Q;c|ndA22>X=NhsVL!~s+D#VQbt9JzZcm<0gyxiTE2fMBB6 zK~8)^Lc+tGl2Y-Btq$g)^*)VwT=ml*hrwl}oxg#mgfiumlTI0eR&g;4^}LXVo{@4N zrOCp$*WgbDJlPNaK(NU!E#cLs+Rocq`GAeBEkB%ya38yz-=VVzwotWZr>B%l>%My+ zgn?gJ31ubxTKY#uFTcCnq#ukvRlpqi7-aS4ZzpvbMmaZy7&jsdB7^)Iow?^+BWA6E z3S5;5J4L!JPecsBb>&g3vcZ#D0ZC0CA!xnqS42LB_(wRAufC2-2npGdTae#7{=|tR zIsuf_t>c>%BJ6;_ZT}7kZg~v4)r*$v-fG zRcz?{wj4T{BO&ZL%g}nyA#piny&!99IdMGHtx$hgVg`&9Kd0+)YOaUg?YLojCx{}5 z?kYKnvRbG4*%7R57%Kigi%8L~<@+L5063kHSzEHe_+Xm!@|z*N8#{i+lm20@AnfmZ zqKlmMtY}Fwa0lF7LZ=RK)R{ql5o0kU66rU~?*K;T@RHok64BJLqSHOXr^fXbHX0U2 zzAc-0AH_QZhMnSYHfzp8?gee4%yz2s??`)1r(a>;{dh;jAFIc9SOSKY;?2a-M+H_* z&4DAUwy$G%!`j%OZDk1?;UmJ+Q&Z*sX%K#>(~tF)ExWZi&N#i(CnbZ|B-*B}>l@?jRj|6t8e#-&sP~(Bfplvr~$pcvy{_>JK~1F4A;78yaNqF&_(mr`=uF5{A7ER9O=?w z0m(;UdpgrGKYhIv>_jA!4i^TBh3A70_UC-?ih*}&Wpm2)S=R|#%2HIt%iGAlH7z~8 zlP|>@fhR^>LE0?49ygkFcEZ#N;rH&{TlYDmlU~Mb5e~l3UUJj>nXlQ>5@c=-#5jW)G_7zr9`fCFAg>1=smhc68T{qHN=R;)E!k&C5xwY%~cy(dE4Ix12UboQQOr z5D!`U@f#5)*R^;yJz*wN^5R?lKl&J!45a9vxu$LY{X7%Goiq&l_JIY)aywBD!B}Nf zBC=(J&z?PdKFy-IILNKGKxcQ%qL)RwhIjb-s=gwor~h2?fZRS_TE@t_qQ;x*uRer| zkZe||RLEJad;d)2Kq(n|qTjVAh@aITbI5z;5^1jA7YY?f?nmS}bQcO8n?DyS#wuFi zefwR?l&X!PRM)>Pm7l4)f1kqv2}+O2*|si#1?@03HY0HYBA+{pd$elSt6s}~yc!ky z;e(~~73q|?ETv_oRz{{P4a+f1G7yhi5vl%O)s#_lt@muxMT+*0}%t3>1_4#eBShCsZN}1B+w@ z-v!k!nPXpY-jz_}ZTcH>5gy~2fYyW#DPQ;TS-4HvC|3Un+WKBc9D%f79NHQFaOh4- zC;Wv4ZE(i?D^LL|l?W%t@=!4ly1r*FqVTL&Alf6Uvs|TiqE!@iDIqXrLSP8q?#WA@N+M-QRYF-TX<6|<=qFcl)JZe~~G12!=K z>6~-~gh29&6vE5odKQT!(Dv#f3S_Av^AcAvr&#N~sdh-a&Y=-dK!DyNt|C)Xy6wd^ zb#%HKp0c0;m;in$z*2lEfO5LJ!X8KXizbH2Q!wxh>P16u-IdE692jWx1dXG6l}Qc` zp7@ol8ewC;GyMSDbRBOvt?;RZ+FcoKB(}3CIQa!%h$&gzF=p>x!UXi_c#a6*I=f&w vo8i6v?7?jN+@0qW@~0drDELIAGL9O(c600d`2O+f$vv5yP0#pMPL(p0@4#SJptwfWtp%b{T|4S z{6=8Oj$}`)-wYG$*)sNLz4s(b79tQRLrc_{$z-zm^z<}XE|)Qiu{6(69(KFk+g`8t zC>&E;fv&EuqE@SQht24nLlOwJdw6)52#2&q4KWahcsYhznQ%xg1;UL_#K+>KMDzLl zL^z~&0$pEUf5*?ZYJ)5N>~uPx@w-La>OP2hBpgz!7zA<7Aiy~@&kha_cG3d5)MBx? z$L5=Bk$A2Gc5PbF&CSg~BMt@Po@(%sxm0Q)5X9Mjety1WW^I;20FQ;zdgwTpqJ0t- zLm3O=Ot`(>Zuj=~_7ZN_#GI*cNTn_4gLHlhac1vSi`4=T+N-8gfpqB{H++Ghe5pHL zx|e!<0vDVLhg2qzE}erRZutDYI=|*FT#sJw&`3`zY_N%oC{PWook5I zf-m< zhWaxkhhfoIv(8$OuJzChFWU=+81(KZ66G0D+_U}v&SDVK_cvx5*b{K-h(K82t{R(6IyOcinL($j4fFnSUcJT$M4%NtsAY5@LklWPqXa@={=!U2 zUY(}3piJj=V`C$C!m%*|(d%B3?Y}*mbY7d3<|Ae@>e)9&AY?55((bQGy~YTH!SYvT zN(`zs*Tql{E|wyP6d4!mcDq5Kl=*zlQr2FeqCqAtIy;RVAXNxWnCg=P`oj`^!;^WAP@)y i0)apv5C{YUUic3hZ|GL1-o35>0000 Date: Sat, 4 Oct 2025 00:59:55 +0900 Subject: [PATCH 02/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A7=84=EC=9E=85=20=EC=8B=9C,=20CustomTa?= =?UTF-8?q?bbar=20hidden=20=EC=B2=98=EB=A6=AC=20=ED=9B=84=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=9E=91=EC=84=B1=20=EB=B2=84=ED=8A=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=9C=84=EC=B9=98=EB=A5=BC=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=BC=9C=EC=84=9C=20=EB=9D=84=EC=9A=B0?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/ReviewListResponse.swift | 9 +- .../HomeRestaurantViewController.swift | 21 ++++- .../View/SeeReview/ReviewTableCell.swift | 6 +- .../ViewController/ReviewViewController.swift | 62 ++++++++++-- .../CustomTabBarContainerController.swift | 15 ++- .../ReviewTabBarContainerController.swift | 94 +++++++++++++++++++ 6 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift index eadbb663..7d46fde6 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift @@ -25,12 +25,19 @@ struct MenuDataList: Codable { let amountRating, tasteRating: Int? let writedAt, content: String let imgURLList: [String?] - + let tags: [Tag]? + enum CodingKeys: String, CodingKey { case reviewID = "reviewId" case menu case writerID = "writerId" case isWriter, writerNickname, mainRating, amountRating, tasteRating, writedAt, content case imgURLList = "imageUrls" + case tags } } + +struct Tag: Codable { + let name: String + let isLiked: Bool +} diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift index d82cf4cb..ad98da92 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift @@ -257,10 +257,29 @@ extension HomeRestaurantViewController: UITableViewDataSource { reviewMenuTypeInfo.menuID = fixMenuTableViewData[restaurant]?[menuIndex].menuId ?? 100 } +// let reviewViewController = ReviewViewController() +// delegate = reviewViewController +// navigationController?.pushViewController(reviewViewController, animated: true) +// delegate?.didDelegateReviewMenuTypeInfo(for: reviewMenuTypeInfo) + let reviewViewController = ReviewViewController() + + // 상위 부모에서 CustomTabBarContainerController 찾기 + var parentVC = self.parent + while parentVC != nil { + if let customTabBar = parentVC as? CustomTabBarContainerController { + customTabBar.setTabBarHidden(true, animated: false) + break + } + parentVC = parentVC?.parent + } + + // delegate 연결 delegate = reviewViewController - navigationController?.pushViewController(reviewViewController, animated: true) delegate?.didDelegateReviewMenuTypeInfo(for: reviewMenuTypeInfo) + + // push로 띄우기 + navigationController?.pushViewController(reviewViewController, animated: true) } private func presentLoginAlert() { diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 38b5eae1..e33c440e 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -320,9 +320,9 @@ extension ReviewTableCell { // tags = (response.tags ?? []).map { ($0.name, $0.isLiked) } -// tags = (response.tags ?? [Tag(name: "기본태그", isLiked: true), -// Tag(name: "추천", isLiked: false)]) -// .map { ($0.name, $0.isLiked) } + tags = (response.tags ?? [Tag(name: "기본태그", isLiked: true), + Tag(name: "추천", isLiked: false)]) + .map { ($0.name, $0.isLiked) } tagCollectionView.reloadData() } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 97c27651..1a99d7a5 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -47,6 +47,21 @@ final class ReviewViewController: BaseViewController { imageView.isHidden = true return imageView }() + + private let reviewTabBarContainer: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 0 + view.clipsToBounds = true + return view + }() + + + private let reviewTabBarView: MainButton = { + let button = MainButton() + button.title = "리뷰 작성하기" + return button + }() // MARK: - Life Cycles @@ -66,11 +81,18 @@ final class ReviewViewController: BaseViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) -// if self.isMovingFromParent { -// if let tabContainer = navigationController?.parent as? CustomTabBarContainerController { -// tabContainer.setTabBarHidden(false) -// } -// } + + // 뒤로 가기(pop) 할 때만 실행되도록 + if self.isMovingFromParent { + var parentVC = self.parent + while parentVC != nil { + if let customTabBar = parentVC as? CustomTabBarContainerController { + customTabBar.setTabBarHidden(false, animated: false) + break + } + parentVC = parentVC?.parent + } + } } // MARK: - Functions @@ -79,29 +101,51 @@ final class ReviewViewController: BaseViewController { reviewTableView.backgroundColor = .white view.addSubviews(reviewTableView, activityIndicatorView, - noReviewImageView) + noReviewImageView, + reviewTabBarContainer) + reviewTabBarContainer.addSubview(reviewTabBarView) } override func setLayout() { reviewTableView.snp.makeConstraints { make in make.top.equalToSuperview() make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() + make.bottom.equalTo(reviewTabBarContainer.snp.top) } - + activityIndicatorView.snp.makeConstraints { make in make.center.equalToSuperview() } - + noReviewImageView.snp.makeConstraints { make in make.center.equalToSuperview() } + + reviewTabBarContainer.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide) + $0.height.equalTo(80) // 탭바 높이 + } + + reviewTabBarView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) // 안쪽 여백 + } } override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = "리뷰" } + + override func setButtonEvent() { + reviewTabBarView.addTarget(self, action: #selector(handleAddReviewButtonTap), for: .touchUpInside) + } + + @objc private func handleAddReviewButtonTap() { + let reviewVC = SetRateViewController() + + navigationController?.pushViewController(reviewVC, animated: true) + } private func setFirebaseTask() { FirebaseRemoteConfig.shared.fetchRestaurantInfo() diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 4d07b3ab..6c692ec0 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -65,7 +65,6 @@ final class CustomTabBarContainerController: BaseViewController { super.viewDidLoad() switchToViewController(at: currentIndex) } - // MARK: - Navigation Control /// 탭 전환 처리 @@ -135,4 +134,18 @@ final class CustomTabBarContainerController: BaseViewController { guard index < viewControllers.count else { return nil } return viewControllers[index] as? UINavigationController } + + public func setTabBarHidden(_ hidden: Bool, animated: Bool) { + tabBarView.snp.updateConstraints { + $0.height.equalTo(hidden ? 0 : 80) + } + if animated { + UIView.animate(withDuration: 0.3) { + self.view.layoutIfNeeded() + } + } else { + view.layoutIfNeeded() + } + tabBarView.isHidden = hidden + } } diff --git a/EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift new file mode 100644 index 00000000..40015947 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift @@ -0,0 +1,94 @@ +// +// ReviewTabBarContainerController.swift +// EATSSU +// +// Created by 한금준 on 10/3/25. +// + +import UIKit + +// Review 전용 탭바 컨테이너 +final class ReviewTabBarContainerController: BaseViewController { + +// private let tabBarView = CustomTabBarView() // 혹은 ReviewTabBarView 따로 만들기 + + private let tabBarView: MainButton = { + let button = MainButton() + button.title = "리뷰 작성하기" + return button + }() + + let reviewVC = ReviewViewController() + + private lazy var viewControllers: [UIViewController] = [ + UINavigationController(rootViewController: reviewVC), +// UINavigationController(rootViewController: myReviewsVC), +// UINavigationController(rootViewController: reportVC) + ] + + private var currentIndex = 0 + + override func configureUI() { + view.addSubview(tabBarView) + +// tabBarView.buttonTapped = { [weak self] index in +// self?.switchToViewController(at: index) +// } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if self.isMovingFromParent { + var parentVC = self.parent + while parentVC != nil { + if let customTabBar = parentVC as? CustomTabBarContainerController { + customTabBar.setTabBarHidden(false, animated: false) + break + } + parentVC = parentVC?.parent + } + } + } + + override func setButtonEvent() { + tabBarView.addTarget(self, action: #selector(handleAddReviewButtonTap), for: .touchUpInside) + } + + @objc private func handleAddReviewButtonTap() { + let reviewVC = SetRateViewController() + + if let nav = viewControllers[currentIndex] as? UINavigationController { + nav.pushViewController(reviewVC, animated: true) + } + } + + override func setLayout() { + tabBarView.snp.makeConstraints { + $0.leading.trailing.bottom.equalToSuperview() + $0.height.equalTo(80) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + switchToViewController(at: currentIndex) + } + + private func switchToViewController(at index: Int) { + let selectedVC = viewControllers[index] + children.forEach { + $0.view.removeFromSuperview() + $0.removeFromParent() + } + addChild(selectedVC) + view.insertSubview(selectedVC.view, belowSubview: tabBarView) + selectedVC.view.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.bottom.equalTo(tabBarView.snp.top) + } + selectedVC.didMove(toParent: self) +// tabBarView.setSelectedIndex(index) + currentIndex = index + } +} From 8962b0d491d15ad953f9a9d1b43986a55faaa95a Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 4 Oct 2025 02:39:31 +0900 Subject: [PATCH 03/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=EA=B0=9C?= =?UTF-8?q?=EC=88=98=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewRateViewCell.swift | 503 ++++-------------- .../ReviewTabBarContainerController.swift | 94 ---- 2 files changed, 98 insertions(+), 499 deletions(-) delete mode 100644 EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 47653f33..5a702043 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -5,17 +5,8 @@ // Created by 박윤빈 on 2023/11/26. // -// -// ReviewRateView.swift -// EatSSU-iOS -// -// Created by 박윤빈 on 2023/03/15. -// - import UIKit - import SnapKit - import EATSSUDesign final class ReviewRateViewCell: UITableViewCell { @@ -30,7 +21,7 @@ final class ReviewRateViewCell: UITableViewCell { private let menuContainer: UIView = { let view = UIView() - view.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color // EATSSUDesignAsset.Color.Gray.gray100.color 같은 컬러 사용 가능 + view.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color view.layer.cornerRadius = 12 view.layer.masksToBounds = true return view @@ -49,7 +40,6 @@ final class ReviewRateViewCell: UITableViewCell { private let menuIcon: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.icRestaurant.image -// imageView.tintColor = EATSSUDesignAsset.Color.Main.primary.color return imageView }() @@ -69,16 +59,6 @@ final class ReviewRateViewCell: UITableViewCell { return stack }() - -// private lazy var rateSectionStackView: UIStackView = { -// let stack = UIStackView(arrangedSubviews: [totalRateStackView, yAxisStackView]) -// stack.axis = .horizontal -// stack.alignment = .center // 높이를 맞추고 싶다면 .top / .bottom으로 바꿀 수도 있음 -//// stack.distribution = .equalCentering -// stack.distribution = .equalSpacing -// stack.spacing = 36 // 기존에 yAxisStackView.leading = totalRateStackView.trailing.offset(36) 대체 -// return stack -// }() private let rateSectionContainer: UIView = { let view = UIView() return view @@ -98,139 +78,24 @@ final class ReviewRateViewCell: UITableViewCell { return label }() -// private let tasteStarImageView: UIImageView = { -// let imageView = UIImageView() -// imageView.image = EATSSUDesignAsset.Images.icStarYellow.image -// return imageView -// }() -// -// private let quantityStarImageView: UIImageView = { -// let imageView = UIImageView() -// imageView.image = EATSSUDesignAsset.Images.icStarYellow.image -// return imageView -// }() -// -// private let tasteLabel: UILabel = { -// let label = UILabel() -// label.text = "맛" -// label.font = .body3 -// label.textColor = .black -// return label -// }() -// -// private let tasteRateLabel: UILabel = { -// let label = UILabel() -// label.text = "5" -// label.font = .body2 -// label.textColor = EATSSUDesignAsset.Color.Main.primary.color -// return label -// }() -// -// private let quantityLabel: UILabel = { -// let label = UILabel() -// label.text = "양" -// label.font = .body3 -// label.textColor = .black -// return label -// }() -// -// private let quantityRateLabel: UILabel = { -// let label = UILabel() -// label.text = "5" -// label.font = .body2 -// label.textColor = EATSSUDesignAsset.Color.Main.primary.color -// return label -// }() - -// private let totalReviewLabel: UILabel = { -// let label = UILabel() -// label.text = "총 리뷰 수" -// label.font = .caption2 -// label.textColor = .black -// return label -// }() -// -// private let totalReviewCount: UILabel = { -// let label = UILabel() -// label.text = "15" -// label.font = .caption1 -// label.textColor = EATSSUDesignAsset.Color.Main.primary.color -// return label -// }() - - private let fivePointLabel: UILabel = { - let label = UILabel() - label.text = "5점" - label.font = .caption2 - label.textColor = .black - return label - }() - - private let fourPointLabel: UILabel = { - let label = UILabel() - label.text = "4점" - label.font = .caption2 - label.textColor = .black - return label - }() - - private let threePointLabel: UILabel = { - let label = UILabel() - label.text = "3점" - label.font = .caption2 - label.textColor = .black - return label - }() - - private let twoPointLabel: UILabel = { - let label = UILabel() - label.text = "2점" - label.font = .caption2 - label.textColor = .black - return label - }() - - private let onePointLabel: UILabel = { - let label = UILabel() - label.text = "1점" - label.font = .caption2 - label.textColor = .black - return label - }() - - var oneChartBar: UIView = { - let view = UIView() - view.backgroundColor = .gray300 - view.roundCorners(corners: [.topRight, .bottomRight], radius: 15) - return view - }() - - var twoChartBar: UIView = { - let view = UIView() - view.backgroundColor = .gray300 - view.roundCorners(corners: [.topRight, .bottomRight], radius: 15) - return view - }() - - var threeChartBar: UIView = { - let view = UIView() - view.backgroundColor = .gray300 - view.roundCorners(corners: [.topRight, .bottomRight], radius: 15) - return view - }() - - var fourChartBar: UIView = { - let view = UIView() - view.backgroundColor = .gray300 - view.roundCorners(corners: [.topRight, .bottomRight], radius: 15) - return view - }() - - var fiveChartBar: UIView = { - let view = UIView() - view.backgroundColor = .gray300 - return view - }() + private let fivePointLabel = ReviewRateViewCell.makePointLabel("5점") + private let fourPointLabel = ReviewRateViewCell.makePointLabel("4점") + private let threePointLabel = ReviewRateViewCell.makePointLabel("3점") + private let twoPointLabel = ReviewRateViewCell.makePointLabel("2점") + private let onePointLabel = ReviewRateViewCell.makePointLabel("1점") + + // Chart bar containers and foregrounds + private var oneChartBar: UIView! + private var twoChartBar: UIView! + private var threeChartBar: UIView! + private var fourChartBar: UIView! + private var fiveChartBar: UIView! + + private var oneForeground: UIView! + private var twoForeground: UIView! + private var threeForeground: UIView! + private var fourForeground: UIView! + private var fiveForeground: UIView! lazy var yAxisStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [fivePointLabel, @@ -243,14 +108,6 @@ final class ReviewRateViewCell: UITableViewCell { stackView.alignment = .trailing return stackView }() -// -// lazy var totalLabelStackView: UIStackView = { -// let stackView = UIStackView(arrangedSubviews: [totalReviewLabel, -// totalReviewCount]) -// stackView.axis = .horizontal -// stackView.spacing = 7 -// return stackView -// }() lazy var totalRateStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [bigStarImageView, @@ -261,52 +118,12 @@ final class ReviewRateViewCell: UITableViewCell { return stackView }() -// lazy var tasteStackView: UIStackView = { -// let stackView = UIStackView(arrangedSubviews: [tasteLabel, -// tasteStarImageView, -// tasteRateLabel]) -// stackView.axis = .horizontal -// stackView.spacing = 5 -// stackView.alignment = .center -// return stackView -// }() -// -// lazy var quantityStackView: UIStackView = { -// let stackView = UIStackView(arrangedSubviews: [quantityLabel, -// quantityStarImageView, -// quantityRateLabel]) -// stackView.axis = .horizontal -// stackView.spacing = 5 -// stackView.alignment = .center -// return stackView -// }() - -// private var addReviewButton: UIButton = { -// let button = UIButton() -// button.setTitle("리뷰 작성하기", for: .normal) -// button.setTitleColor(.white, for: .normal) -// button.titleLabel?.font = .bold(size: 14) -// button.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color -// button.layer.cornerRadius = 10 -// button.layer.masksToBounds = false -// return button -// }() - - // MARK: FIX ME - charts 추가 나중에 하기 - - override func layoutSubviews() { - super.layoutSubviews() - for item in [oneChartBar, twoChartBar, threeChartBar, fourChartBar, fiveChartBar] { - item.roundCorners(corners: [.topRight, .bottomRight], radius: 15) - } - } + // MARK: - Init override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - configureUI() setLayout() -// addTarget() } @available(*, unavailable) @@ -314,34 +131,62 @@ final class ReviewRateViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - // MARK: - Functions + // MARK: - Helper + + private static func makePointLabel(_ text: String) -> UILabel { + let label = UILabel() + label.text = text + label.font = .caption2 + label.textColor = .black + return label + } + + // MARK: - UI Setup func configureUI() { + // Helper to create chart bar with background and foreground + func makeChartBar() -> (container: UIView, foreground: UIView) { + let container = UIView() + container.backgroundColor = .gray200 + container.layer.cornerRadius = 5 + container.layer.masksToBounds = true + let foreground = UIView() + foreground.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color + foreground.layer.cornerRadius = 5 + foreground.layer.masksToBounds = true + container.addSubview(foreground) + foreground.snp.makeConstraints { make in + make.leading.top.bottom.equalToSuperview() + make.width.equalTo(0) + } + return (container, foreground) + } + + let oneBar = makeChartBar() + oneChartBar = oneBar.container + oneForeground = oneBar.foreground + let twoBar = makeChartBar() + twoChartBar = twoBar.container + twoForeground = twoBar.foreground + let threeBar = makeChartBar() + threeChartBar = threeBar.container + threeForeground = threeBar.foreground + let fourBar = makeChartBar() + fourChartBar = fourBar.container + fourForeground = fourBar.foreground + let fiveBar = makeChartBar() + fiveChartBar = fiveBar.container + fiveForeground = fiveBar.foreground + contentView.addSubviews( -// menuLabel, menuContainer, -// rateSectionStackView, - rateSectionContainer, -// totalRateStackView, -// addReviewButton, -// totalLabelStackView, -// yAxisStackView, - oneChartBar, - twoChartBar, - threeChartBar, - fourChartBar, - fiveChartBar, -// tasteStackView, -// quantityStackView + rateSectionContainer ) - -// menuContainer.addSubview(menuLabel) - + menuContainer.addSubviews(menuTitleStackView, menuLabel) - - - // Add totalRateStackView and yAxisStackView to rateSectionContainer with constraints - rateSectionContainer.addSubviews(totalRateStackView, yAxisStackView) + + rateSectionContainer.addSubviews(totalRateStackView, yAxisStackView, + oneChartBar, twoChartBar, threeChartBar, fourChartBar, fiveChartBar) totalRateStackView.snp.makeConstraints { make in make.top.bottom.equalToSuperview().offset(35.5) @@ -349,9 +194,6 @@ final class ReviewRateViewCell: UITableViewCell { } yAxisStackView.snp.makeConstraints { make in -// make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) -// make.top.bottom.equalToSuperview().offset(12) -// make.trailing.equalToSuperview().offset(-37) make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) make.centerY.equalTo(totalRateStackView) } @@ -360,16 +202,11 @@ final class ReviewRateViewCell: UITableViewCell { func setLayout() { backgroundColor = .white -// menuLabel.snp.makeConstraints { make in -// make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) -// make.centerX.equalToSuperview() -// make.width.equalTo(290.adjusted) -// } menuContainer.snp.makeConstraints { make in make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) make.centerX.equalToSuperview() make.width.equalTo(320.adjusted) - make.height.greaterThanOrEqualTo(100) // 라벨 줄 수에 따라 자동으로 늘어나게 + make.height.greaterThanOrEqualTo(100) } menuTitleStackView.snp.makeConstraints { make in @@ -382,7 +219,6 @@ final class ReviewRateViewCell: UITableViewCell { } menuLabel.snp.makeConstraints { make in -// make.edges.equalToSuperview().inset(12) // 안쪽 여백 make.top.equalTo(menuTitleStackView.snp.bottom).offset(12) make.leading.trailing.equalToSuperview().inset(28) make.bottom.equalToSuperview().inset(16) @@ -390,75 +226,42 @@ final class ReviewRateViewCell: UITableViewCell { rateSectionContainer.snp.makeConstraints { make in make.top.equalTo(menuLabel.snp.bottom).offset(40) -// make.centerX.equalToSuperview().inset(100) // 중앙 배치 make.leading.trailing.equalToSuperview().inset(60) -// make.leading.equalToSuperview().offset(60) } - - - - -// totalRateStackView.snp.makeConstraints { make in -//// make.top.equalTo(menuLabel.snp.bottom).offset(40) -//// make.leading.equalToSuperview().inset(60) -// make.centerY.equalTo(rateSectionStackView) -// make.leading.equalTo(rateSectionStackView).offset(37) -// } - -// tasteStackView.snp.makeConstraints { make in -// make.top.equalTo(totalRateStackView.snp.bottom).offset(8) -// make.leading.equalTo(totalRateStackView).offset(-5) -// } -// quantityStackView.snp.makeConstraints { make in -// make.top.equalTo(tasteStackView) -// make.leading.equalTo(tasteStackView.snp.trailing).offset(5) -// } - -// yAxisStackView.snp.makeConstraints { make in -// make.top.equalTo(totalReviewLabel.snp.bottom).offset(8) -// make.leading.equalTo(totalReviewLabel) -// } - -// yAxisStackView.snp.makeConstraints { make in -//// make.top.equalTo(menuContainer.snp.bottom).offset(12) -// make.centerY.equalTo(rateSectionStackView) -// make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) -// } - oneChartBar.snp.makeConstraints { make in make.centerY.equalTo(onePointLabel) make.leading.equalTo(onePointLabel.snp.trailing).offset(7) make.height.equalTo(10) - make.width.equalTo(0) + make.width.equalTo(126) } twoChartBar.snp.makeConstraints { make in make.centerY.equalTo(twoPointLabel) make.leading.equalTo(twoPointLabel.snp.trailing).offset(7) make.height.equalTo(10) - make.width.equalTo(0) + make.width.equalTo(126) } threeChartBar.snp.makeConstraints { make in make.centerY.equalTo(threePointLabel) make.leading.equalTo(threePointLabel.snp.trailing).offset(7) make.height.equalTo(10) - make.width.equalTo(0) + make.width.equalTo(126) } fourChartBar.snp.makeConstraints { make in make.centerY.equalTo(fourPointLabel) make.leading.equalTo(fourPointLabel.snp.trailing).offset(7) make.height.equalTo(10) - make.width.equalTo(0) + make.width.equalTo(126) } fiveChartBar.snp.makeConstraints { make in make.centerY.equalTo(fivePointLabel) make.leading.equalTo(fivePointLabel.snp.trailing).offset(7) make.height.equalTo(10) - make.width.equalTo(0) + make.width.equalTo(126) } for item in [onePointLabel, twoPointLabel, threePointLabel, fourPointLabel, fivePointLabel] { @@ -467,47 +270,11 @@ final class ReviewRateViewCell: UITableViewCell { } } -// addReviewButton.snp.makeConstraints { make in -//// make.top.equalTo(tasteStackView.snp.bottom).offset(35) -// make.top.equalTo(rateSectionContainer.snp.bottom).offset(35) -// make.horizontalEdges.equalToSuperview().inset(60) -// make.height.equalTo(36) -// } - -// totalLabelStackView.snp.makeConstraints { make in -// make.top.equalTo(menuLabel.snp.bottom).offset(15) -// make.leading.equalTo(quantityStackView.snp.trailing).offset(44) -// } - -// tasteStarImageView.snp.makeConstraints { make in -// make.height.equalTo(11.19) -// make.width.equalTo(11.71) -// } -// -// quantityStarImageView.snp.makeConstraints { make in -// make.height.equalTo(11.19) -// make.width.equalTo(11.71) -// } - bigStarImageView.snp.makeConstraints { $0.height.width.equalTo(24.adjusted) } - -// tasteStarImageView.snp.makeConstraints { -// $0.height.width.equalTo(12.adjusted) -// } -// -// quantityStarImageView.snp.makeConstraints { -// $0.height.width.equalTo(12.adjusted) -// } } -// func addTarget() { -// addReviewButton.addTarget(self, -// action: #selector(touchAddReviewButton), -// for: .touchUpInside) -// } - @objc func touchAddReviewButton() { handler?() @@ -517,121 +284,47 @@ final class ReviewRateViewCell: UITableViewCell { extension ReviewRateViewCell { func fixMenuDataBind(data: FixedReviewRateResponse) { let total = String(format: "%.1f", data.mainRating ?? 0) - let taste = String(format: "%.1f", data.tasteRating ?? 0) - let amount = String(format: "%.1f", data.amountRating ?? 0) menuLabel.text = data.menuName -// totalReviewCount.text = "\(data.totalReviewCount)" rateNumLabel.text = "\(total)" totalRate = data.mainRating ?? 0 -// if data.tasteRating == nil || data.tasteRating == 0.0 { -// tasteStackView.isHidden = true -// } else { -// tasteStackView.isHidden = false -// let taste = String(format: "%.1f", data.tasteRating ?? 0) -// tasteRateLabel.text = "\(taste)" -// } - -// if data.amountRating == nil || data.amountRating == 0.0 { -// quantityStackView.isHidden = true -// } else { -// quantityStackView.isHidden = false -// let amount = String(format: "%.1f", data.amountRating ?? 0) -// quantityRateLabel.text = "\(amount)" -// } - fiveChartBar.snp.updateConstraints { - if data.reviewRatingCount.fiveStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.fiveStarCount) - } + + fiveForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) } - fourChartBar.snp.updateConstraints { - if data.reviewRatingCount.fourStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.fourStarCount) - } + fourForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / max(data.totalReviewCount, 1)) } - threeChartBar.snp.updateConstraints { - if data.reviewRatingCount.threeStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.threeStarCount) - } + threeForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / max(data.totalReviewCount, 1)) } - twoChartBar.snp.updateConstraints { - if data.reviewRatingCount.twoStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.twoStarCount) - } + twoForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / max(data.totalReviewCount, 1)) } - oneChartBar.snp.updateConstraints { - if data.reviewRatingCount.oneStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.oneStarCount) - } + oneForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / max(data.totalReviewCount, 1)) } } func dataBind(data: ReviewRateResponse) { let total = String(format: "%.1f", data.mainRating ?? 0) - let taste = String(format: "%.1f", data.tasteRating ?? 0) - let amount = String(format: "%.1f", data.amountRating ?? 0) menuLabel.text = data.menuNames.joined(separator: " + ") -// totalReviewCount.text = "\(data.totalReviewCount)" rateNumLabel.text = "\(total)" totalRate = data.mainRating ?? 0 -// if data.tasteRating == nil || data.tasteRating == 0.0 { -// tasteStackView.isHidden = true -// } else { -// tasteStackView.isHidden = false -// let taste = String(format: "%.1f", data.tasteRating ?? 0) -// tasteRateLabel.text = "\(taste)" -// } -// -// if data.amountRating == nil || data.amountRating == 0.0 { -// quantityStackView.isHidden = true -// } else { -// quantityStackView.isHidden = false -// let amount = String(format: "%.1f", data.amountRating ?? 0) -// quantityRateLabel.text = "\(amount)" -// } - fiveChartBar.snp.updateConstraints { - if data.reviewRatingCount.fiveStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.fiveStarCount) - } + + fiveForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) } - fourChartBar.snp.updateConstraints { - if data.reviewRatingCount.fourStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.fourStarCount) - } + fourForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / max(data.totalReviewCount, 1)) } - threeChartBar.snp.updateConstraints { - if data.reviewRatingCount.threeStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.threeStarCount) - } + threeForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / max(data.totalReviewCount, 1)) } - twoChartBar.snp.updateConstraints { - if data.reviewRatingCount.twoStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.twoStarCount) - } + twoForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / max(data.totalReviewCount, 1)) } - oneChartBar.snp.updateConstraints { - if data.reviewRatingCount.oneStarCount == 0 { - $0.width.equalTo(0) - } else { - $0.width.equalTo(126 / data.totalReviewCount * data.reviewRatingCount.oneStarCount) - } + oneForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / max(data.totalReviewCount, 1)) } } } diff --git a/EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift deleted file mode 100644 index 40015947..00000000 --- a/EATSSU/App/Sources/Presentation/TabBar/ReviewTabBarContainerController.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// ReviewTabBarContainerController.swift -// EATSSU -// -// Created by 한금준 on 10/3/25. -// - -import UIKit - -// Review 전용 탭바 컨테이너 -final class ReviewTabBarContainerController: BaseViewController { - -// private let tabBarView = CustomTabBarView() // 혹은 ReviewTabBarView 따로 만들기 - - private let tabBarView: MainButton = { - let button = MainButton() - button.title = "리뷰 작성하기" - return button - }() - - let reviewVC = ReviewViewController() - - private lazy var viewControllers: [UIViewController] = [ - UINavigationController(rootViewController: reviewVC), -// UINavigationController(rootViewController: myReviewsVC), -// UINavigationController(rootViewController: reportVC) - ] - - private var currentIndex = 0 - - override func configureUI() { - view.addSubview(tabBarView) - -// tabBarView.buttonTapped = { [weak self] index in -// self?.switchToViewController(at: index) -// } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if self.isMovingFromParent { - var parentVC = self.parent - while parentVC != nil { - if let customTabBar = parentVC as? CustomTabBarContainerController { - customTabBar.setTabBarHidden(false, animated: false) - break - } - parentVC = parentVC?.parent - } - } - } - - override func setButtonEvent() { - tabBarView.addTarget(self, action: #selector(handleAddReviewButtonTap), for: .touchUpInside) - } - - @objc private func handleAddReviewButtonTap() { - let reviewVC = SetRateViewController() - - if let nav = viewControllers[currentIndex] as? UINavigationController { - nav.pushViewController(reviewVC, animated: true) - } - } - - override func setLayout() { - tabBarView.snp.makeConstraints { - $0.leading.trailing.bottom.equalToSuperview() - $0.height.equalTo(80) - } - } - - override func viewDidLoad() { - super.viewDidLoad() - switchToViewController(at: currentIndex) - } - - private func switchToViewController(at index: Int) { - let selectedVC = viewControllers[index] - children.forEach { - $0.view.removeFromSuperview() - $0.removeFromParent() - } - addChild(selectedVC) - view.insertSubview(selectedVC.view, belowSubview: tabBarView) - selectedVC.view.snp.makeConstraints { - $0.top.leading.trailing.equalToSuperview() - $0.bottom.equalTo(tabBarView.snp.top) - } - selectedVC.didMove(toParent: self) -// tabBarView.setSelectedIndex(index) - currentIndex = index - } -} From de50420f37537825fad2ebaa3c1a16fbdc23548a Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 4 Oct 2025 12:10:57 +0900 Subject: [PATCH 04/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EB=9D=BC?= =?UTF-8?q?=EB=B2=A8=20=EB=B0=8F=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EB=A6=AC=EB=B7=B0=EA=B0=80=20=EC=97=86=EB=8A=94?= =?UTF-8?q?=20=EA=B2=BD=EC=9A=B0=20'-'=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewDividerCell.swift | 56 +++++++++++++++++++ .../View/SeeReview/ReviewRateViewCell.swift | 28 ++++++++-- .../ViewController/ReviewViewController.swift | 17 +++++- 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift new file mode 100644 index 00000000..0c38ec0e --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift @@ -0,0 +1,56 @@ +// +// ReviewDividerCell.swift +// EATSSU +// +// Created by 한금준 on 10/4/25. +// + +import SnapKit +import UIKit + +import EATSSUDesign + +final class ReviewDividerCell: UITableViewCell { + static let identifier = "ReviewDividerCell" + + private let divider: UIView = { + let divider = UIView() + divider.backgroundColor = .gray100 + return divider + }() + + private let label: UILabel = { + let label = UILabel() + label.text = "리뷰 15" + label.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) + label.textColor = .black + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.addSubview(divider) + contentView.addSubview(label) + + divider.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.height.equalTo(12) + } + label.snp.makeConstraints { + $0.top.equalTo(divider.snp.bottom).offset(16) + $0.leading.equalToSuperview().offset(16) + $0.bottom.equalToSuperview().inset(8) + } + } + + required init?(coder: NSCoder) { fatalError() } + + func configure(reviewCount: Int) { +// label.text = "리뷰 \(reviewCount)" + let text = "리뷰 \(reviewCount)" + let attributed = NSMutableAttributedString(string: text) + let range = (text as NSString).range(of: "\(reviewCount)") + attributed.addAttribute(.foregroundColor, value: EATSSUDesignAsset.Color.Main.primary.color, range: range) + label.attributedText = attributed + } +} diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 5a702043..694f3d43 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -283,10 +283,18 @@ final class ReviewRateViewCell: UITableViewCell { extension ReviewRateViewCell { func fixMenuDataBind(data: FixedReviewRateResponse) { - let total = String(format: "%.1f", data.mainRating ?? 0) +// let total = String(format: "%.1f", data.mainRating ?? 0) + let ratingValue = data.mainRating ?? 0 + if ratingValue == 0.0 { + rateNumLabel.text = "-" + } else { + let total = String(format: "%.1f", ratingValue) + rateNumLabel.text = "\(total)" + } menuLabel.text = data.menuName - rateNumLabel.text = "\(total)" - totalRate = data.mainRating ?? 0 +// rateNumLabel.text = "\(total)" +// totalRate = data.mainRating ?? 0 + totalRate = ratingValue fiveForeground.snp.updateConstraints { $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) @@ -306,10 +314,18 @@ extension ReviewRateViewCell { } func dataBind(data: ReviewRateResponse) { - let total = String(format: "%.1f", data.mainRating ?? 0) +// let total = String(format: "%.1f", data.mainRating ?? 0) + let ratingValue = data.mainRating ?? 0 + if ratingValue == 0.0 { + rateNumLabel.text = "-" + } else { + let total = String(format: "%.1f", ratingValue) + rateNumLabel.text = "\(total)" + } menuLabel.text = data.menuNames.joined(separator: " + ") - rateNumLabel.text = "\(total)" - totalRate = data.mainRating ?? 0 +// rateNumLabel.text = "\(total)" +// totalRate = data.mainRating ?? 0 + totalRate = ratingValue fiveForeground.snp.updateConstraints { $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 1a99d7a5..9146e640 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -108,7 +108,7 @@ final class ReviewViewController: BaseViewController { override func setLayout() { reviewTableView.snp.makeConstraints { make in - make.top.equalToSuperview() + make.top.equalToSuperview().offset(24) make.leading.trailing.equalToSuperview() make.bottom.equalTo(reviewTabBarContainer.snp.top) } @@ -160,6 +160,7 @@ final class ReviewViewController: BaseViewController { reviewTableView.register(ReviewTableCell.self, forCellReuseIdentifier: ReviewTableCell.identifier) reviewTableView.register(ReviewRateViewCell.self, forCellReuseIdentifier: ReviewRateViewCell.identifier) reviewTableView.register(ReviewEmptyViewCell.self, forCellReuseIdentifier: ReviewEmptyViewCell.identifier) + reviewTableView.register(ReviewDividerCell.self, forCellReuseIdentifier: ReviewDividerCell.identifier) reviewTableView.delegate = self reviewTableView.dataSource = self @@ -320,7 +321,8 @@ extension ReviewViewController: UITableViewDelegate { extension ReviewViewController: UITableViewDataSource { func numberOfSections(in _: UITableView) -> Int { - 2 +// 2 + 3 } func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -328,6 +330,8 @@ extension ReviewViewController: UITableViewDataSource { case 0: 1 case 1: + 1 + case 2: // 두 번째 섹션에서 리뷰 개수가 하나도 없을 때 셀 변경 if reviewList.count == 0 { 1 @@ -375,6 +379,12 @@ extension ReviewViewController: UITableViewDataSource { return cell case 1: + // Divider cell + let cell = tableView.dequeueReusableCell(withIdentifier: ReviewDividerCell.identifier, for: indexPath) as? ReviewDividerCell ?? ReviewDividerCell() + cell.configure(reviewCount: reviewList.count) + cell.selectionStyle = .none + return cell + case 2: if reviewList.count == 0 { let cell = tableView.dequeueReusableCell(withIdentifier: ReviewEmptyViewCell.identifier, for: indexPath) as? ReviewEmptyViewCell ?? ReviewEmptyViewCell() if RealmService.shared.getToken() == "" { @@ -409,6 +419,9 @@ extension ReviewViewController: UITableViewDataSource { case 0: 251.adjusted case 1: + // Divider cell + UITableView.automaticDimension + case 2: if reviewList.count == 0 { 300.adjusted } else { From ccd7cb5be9fb9c56fd8c31ea686ff0b0b60fba8c Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 4 Oct 2025 12:17:20 +0900 Subject: [PATCH 05/69] =?UTF-8?q?[#321]=20=EC=9D=B4=EB=A6=84=20=EC=98=86?= =?UTF-8?q?=EC=97=90=20=EB=A9=94=EB=89=B4=EB=AA=85=20=EB=B0=8F=20rateNumbe?= =?UTF-8?q?rLabel=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/RateNumberView.swift | 12 +- .../View/SeeReview/ReviewTableCell.swift | 260 +++++++++--------- 2 files changed, 136 insertions(+), 136 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift index ba033af8..ca704542 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift @@ -17,10 +17,10 @@ final class RateNumberView: BaseUIView { // let starImageView = UIImageView() private var starImageViews: [UIImageView] = [] private lazy var starsStackView = UIStackView() - lazy var rateNumberLabel = UILabel() +// lazy var rateNumberLabel = UILabel() // private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starImageView, private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView, - rateNumberLabel]) + /*rateNumberLabel*/]) var filledStarImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image var emptyStarImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image @@ -60,9 +60,9 @@ final class RateNumberView: BaseUIView { starImageViews.forEach { starsStackView.addArrangedSubview($0) } - rateNumberLabel.text = "5" - rateNumberLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 14) - rateNumberLabel.textColor = EATSSUDesignAsset.Color.Main.primary.color +// rateNumberLabel.text = "5" +// rateNumberLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 14) +// rateNumberLabel.textColor = EATSSUDesignAsset.Color.Main.primary.color rateNumberStackView.axis = .horizontal // rateNumberStackView.spacing = 3 @@ -95,6 +95,6 @@ final class RateNumberView: BaseUIView { star.image = emptyStarImage } } - rateNumberLabel.text = "\(rating)" +// rateNumberLabel.text = "\(rating)" } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index e33c440e..14e44a9f 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -14,60 +14,60 @@ import EATSSUDesign final class ReviewTableCell: UITableViewCell { // MARK: - Properties - + static let identifier = "ReviewTableCell" var handler: (() -> Void)? var reviewId: Int = .init() var menuName: String = .init() - + // MARK: - UI Components - + lazy var totalRateView = RateNumberView() -// lazy var tasteRateView = RateNumberView() -// lazy var quantityRateView = RateNumberView() - -// private let tasteLabel: UILabel = { -// let label = UILabel() -// label.text = "맛" -// label.font = .body3 -// label.textColor = .black -// return label -// }() - -// private let quantityLabel: UILabel = { -// let label = UILabel() -// label.text = "양" -// label.font = .body3 -// label.textColor = .black -// return label -// }() + // lazy var tasteRateView = RateNumberView() + // lazy var quantityRateView = RateNumberView() + + // private let tasteLabel: UILabel = { + // let label = UILabel() + // label.text = "맛" + // label.font = .body3 + // label.textColor = .black + // return label + // }() + + // private let quantityLabel: UILabel = { + // let label = UILabel() + // label.text = "양" + // label.font = .body3 + // label.textColor = .black + // return label + // }() // 태그 표시용 컬렉션 뷰 - private lazy var tagCollectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - layout.minimumInteritemSpacing = 8 - layout.minimumLineSpacing = 8 - - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.isScrollEnabled = false - cv.register(ReviewTagCollectionViewCell.self, forCellWithReuseIdentifier: ReviewTagCollectionViewCell.identifier) - cv.dataSource = self - return cv - }() + private lazy var tagCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.minimumInteritemSpacing = 8 + layout.minimumLineSpacing = 8 + + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.isScrollEnabled = false + cv.register(ReviewTagCollectionViewCell.self, forCellWithReuseIdentifier: ReviewTagCollectionViewCell.identifier) + cv.dataSource = self + return cv + }() private var tags: [(name: String, isLiked: Bool)] = [] lazy var contentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [tagCollectionView, reviewTextView, foodImageView]) - stackView.axis = .vertical - stackView.spacing = 8.adjusted - stackView.alignment = .leading - return stackView - }() - + let stackView = UIStackView(arrangedSubviews: [tagCollectionView, reviewTextView, foodImageView]) + stackView.axis = .vertical + stackView.spacing = 8.adjusted + stackView.alignment = .leading + return stackView + }() + private var dateLabel: UILabel = { let label = UILabel() label.text = "2023.03.03" @@ -75,30 +75,30 @@ final class ReviewTableCell: UITableViewCell { label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return label }() - + private var userNameLabel: UILabel = { let label = UILabel() label.text = "hellosoongsil1234" label.font = .caption1 -// label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - label.textColor = .black - return label - }() - - private var menuNameLabel: UILabel = { - let label = UILabel() - label.text = "계란국" - label.font = .caption3 + // label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color label.textColor = .black return label }() - + + // private var menuNameLabel: UILabel = { + // let label = UILabel() + // label.text = "계란국" + // label.font = .caption3 + // label.textColor = .black + // return label + // }() + private let userProfileImageView: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.profile.image return imageView }() - + private var sideButton: BaseButton = { let button = BaseButton() button.setTitleColor(EATSSUDesignAsset.Color.GrayScale.gray400.color, for: .normal) @@ -106,7 +106,7 @@ final class ReviewTableCell: UITableViewCell { button.configuration?.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 15) return button }() - + var reviewTextView: UITextView = { let textView = UITextView() textView.textColor = UIColor.black @@ -117,32 +117,32 @@ final class ReviewTableCell: UITableViewCell { textView.text = "여기 계란국 맛집임... 김치볶음밥에 계란후라이 없어서 아쉽 다음에 또 먹어야지" return textView }() - + var foodImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.isHidden = true return imageView }() - + /// 맛 별점 -// lazy var tasteStackView: UIStackView = { -// let stackView = UIStackView(arrangedSubviews: [tasteLabel, tasteRateView]) -// stackView.axis = .horizontal -// stackView.spacing = 4.adjusted -// stackView.alignment = .center -// return stackView -// }() - + // lazy var tasteStackView: UIStackView = { + // let stackView = UIStackView(arrangedSubviews: [tasteLabel, tasteRateView]) + // stackView.axis = .horizontal + // stackView.spacing = 4.adjusted + // stackView.alignment = .center + // return stackView + // }() + /// 양 별점 -// lazy var quantityStackView: UIStackView = { -// let stackView = UIStackView(arrangedSubviews: [quantityLabel, quantityRateView]) -// stackView.axis = .horizontal -// stackView.spacing = 4.adjusted -// stackView.alignment = .center -// return stackView -// }() - + // lazy var quantityStackView: UIStackView = { + // let stackView = UIStackView(arrangedSubviews: [quantityLabel, quantityRateView]) + // stackView.axis = .horizontal + // stackView.spacing = 4.adjusted + // stackView.alignment = .center + // return stackView + // }() + /// 별점 lazy var rateStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [totalRateView/*, tasteStackView, quantityStackView*/]) @@ -151,16 +151,16 @@ final class ReviewTableCell: UITableViewCell { stackView.alignment = .center return stackView }() - + /// 이름 + 메뉴 lazy var nameMenuStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [userNameLabel, menuNameLabel]) + let stackView = UIStackView(arrangedSubviews: [userNameLabel,/* menuNameLabel*/]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center return stackView }() - + /// 이름 + 메뉴 + 별점 lazy var infoStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [nameMenuStackView, rateStackView]) @@ -169,7 +169,7 @@ final class ReviewTableCell: UITableViewCell { stackView.alignment = .leading return stackView }() - + /// 프로필 + 이름 + 메뉴 + 별점 lazy var profileStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [userProfileImageView, infoStackView]) @@ -178,7 +178,7 @@ final class ReviewTableCell: UITableViewCell { stackView.alignment = .center return stackView }() - + lazy var dateReportStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [sideButton, dateLabel]) stackView.axis = .vertical @@ -186,17 +186,17 @@ final class ReviewTableCell: UITableViewCell { stackView.alignment = .trailing return stackView }() - -// lazy var contentStackView: UIStackView = { -// let stackView = UIStackView(arrangedSubviews: [reviewTextView, foodImageView]) -// stackView.axis = .vertical -// stackView.spacing = 8.adjusted -// stackView.alignment = .leading -// return stackView -// }() - + + // lazy var contentStackView: UIStackView = { + // let stackView = UIStackView(arrangedSubviews: [reviewTextView, foodImageView]) + // stackView.axis = .vertical + // stackView.spacing = 8.adjusted + // stackView.alignment = .leading + // return stackView + // }() + // MARK: - Functions - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(profileStackView) @@ -204,15 +204,15 @@ final class ReviewTableCell: UITableViewCell { contentView.addSubview(contentStackView) setLayout() } - + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func prepareForReuse() { super.prepareForReuse() - + tags = [] tagCollectionView.reloadData() sideButton.setTitle("", for: .normal) @@ -220,7 +220,7 @@ final class ReviewTableCell: UITableViewCell { foodImageView.image = UIImage() foodImageView.isHidden = true } - + func setLayout() { userProfileImageView.snp.makeConstraints { make in make.width.height.equalTo(30) @@ -231,23 +231,23 @@ final class ReviewTableCell: UITableViewCell { make.leading.equalToSuperview().offset(16) make.height.equalTo(50) } - + dateReportStackView.snp.makeConstraints { make in make.centerY.equalTo(profileStackView) make.trailing.equalToSuperview().inset(16) } - + contentStackView.snp.makeConstraints { make in make.top.equalTo(profileStackView.snp.bottom) make.leading.equalToSuperview().offset(16) make.bottom.equalToSuperview().offset(-15) make.trailing.equalToSuperview().offset(-16) } - + foodImageView.snp.makeConstraints { make in make.height.width.equalTo(358) } - + sideButton.snp.makeConstraints { $0.height.equalTo(12.adjusted) } @@ -257,7 +257,7 @@ final class ReviewTableCell: UITableViewCell { make.height.greaterThanOrEqualTo(30) } } - + @objc func touchedSideButtonEvent() { handler?() @@ -269,7 +269,7 @@ extension ReviewTableCell: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return tags.count } - + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: ReviewTagCollectionViewCell.identifier, @@ -287,25 +287,25 @@ extension ReviewTableCell: UICollectionViewDataSource { extension ReviewTableCell { func dataBind(response: MenuDataList) { - menuNameLabel.text = response.menu + // menuNameLabel.text = response.menu menuName = response.menu userNameLabel.text = response.writerNickname totalRateView.setRating(response.mainRating) -// totalRateView.rateNumberLabel.text = "\(response.mainRating)" + // totalRateView.rateNumberLabel.text = "\(response.mainRating)" -// if response.tasteRating == nil { -// tasteStackView.isHidden = true -// } else { -// tasteStackView.isHidden = false -// tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" -// } + // if response.tasteRating == nil { + // tasteStackView.isHidden = true + // } else { + // tasteStackView.isHidden = false + // tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" + // } -// if response.amountRating == nil { -// quantityStackView.isHidden = true -// } else { -// quantityStackView.isHidden = false -// quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" -// } + // if response.amountRating == nil { + // quantityStackView.isHidden = true + // } else { + // quantityStackView.isHidden = false + // quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" + // } dateLabel.text = response.writedAt reviewTextView.text = response.content reviewId = response.reviewID @@ -319,31 +319,31 @@ extension ReviewTableCell { sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) -// tags = (response.tags ?? []).map { ($0.name, $0.isLiked) } + // tags = (response.tags ?? []).map { ($0.name, $0.isLiked) } tags = (response.tags ?? [Tag(name: "기본태그", isLiked: true), - Tag(name: "추천", isLiked: false)]) - .map { ($0.name, $0.isLiked) } + Tag(name: "추천", isLiked: false)]) + .map { ($0.name, $0.isLiked) } tagCollectionView.reloadData() } - + func myPageDataBind(response: MyDataList, nickname: String) { userNameLabel.text = "\(nickname)" - menuNameLabel.text = response.menuName - totalRateView.rateNumberLabel.text = "\(response.mainRating)" -// if response.tasteRating == nil { -// tasteStackView.isHidden = true -// } else { -// tasteStackView.isHidden = false -// tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" -// } + // menuNameLabel.text = response.menuName +// totalRateView.rateNumberLabel.text = "\(response.mainRating)" + // if response.tasteRating == nil { + // tasteStackView.isHidden = true + // } else { + // tasteStackView.isHidden = false + // tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" + // } -// if response.amountRating == nil { -// quantityStackView.isHidden = true -// } else { -// quantityStackView.isHidden = false -// quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" -// } + // if response.amountRating == nil { + // quantityStackView.isHidden = true + // } else { + // quantityStackView.isHidden = false + // quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" + // } dateLabel.text = response.writeDate reviewTextView.text = response.content if response.imgURLList.count != 0 { From dabe3574cedea1a3de7f81826178e34f3076d036 Mon Sep 17 00:00:00 2001 From: Funital Date: Fri, 10 Oct 2025 23:11:52 +0900 Subject: [PATCH 06/69] =?UTF-8?q?[#321]=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Presentation/Review/View/RateReview/MenuLikeCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift index c238b4fb..796b5134 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -41,7 +41,7 @@ final class MenuLikeCell: UITableViewCell { let view = UIView() view.layer.cornerRadius = 14 view.layer.borderWidth = 1 - view.layer.borderColor = UIColor.lightGray.cgColor + view.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor return view }() From 69ef824af9cc9d7fc28a8e76450352848bee5cc2 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 9 Nov 2025 21:44:52 +0900 Subject: [PATCH 07/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=B7=B0=20offset=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Presentation/Review/View/RerportView/ReportView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift b/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift index 7d992b98..fa53a882 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift @@ -96,7 +96,7 @@ final class ReportView: BaseUIView { override func setLayout() { reviewReportReasonLabel.snp.makeConstraints { make in make.leading.equalTo(self).inset(24) - make.top.equalTo(safeAreaLayoutGuide.snp.top) + make.top.equalTo(safeAreaLayoutGuide.snp.top).offset(12) } singleReportPerDayLabel.snp.makeConstraints { make in From b607c297d554003f6c66becf38ec874416ee9f07 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 16 Nov 2025 13:06:52 +0900 Subject: [PATCH 08/69] =?UTF-8?q?[#321]=20=EC=B6=A9=EB=8F=8C=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewEmptyViewCell.swift | 26 - .../View/SeeReview/ReviewRateViewCell.swift | 560 ++++++++---------- .../ViewController/ReviewViewController.swift | 14 + .../CustomTabBarContainerController.swift | 52 +- 4 files changed, 296 insertions(+), 356 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index 21045cc9..c5526095 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -22,32 +22,6 @@ final class ReviewEmptyViewCell: UITableViewCell { imageView.tintColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return imageView }() - - private lazy var mainLabel: UILabel = { - let label = UILabel() - label.text = "아직 작성된 리뷰가 없어요!" - label.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - label.textAlignment = .center - return label - }() - - private lazy var subLabel: UILabel = { - let label = UILabel() - label.text = "메뉴에 가장 먼저 리뷰를 남겨주세요" - label.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 12) - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - label.textAlignment = .center - return label - }() - - private lazy var contentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [reviewIconImageView, mainLabel, subLabel]) - stackView.axis = .vertical - stackView.spacing = 12 - stackView.alignment = .center - return stackView - }() private lazy var titleLabel: UILabel = { let label = UILabel() diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index ba28fa29..694f3d43 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -1,8 +1,8 @@ // // ReviewRateViewCell.swift -// EATSSU +// EatSSU-iOS // -// Created by 황상환 on 10/22/25. +// Created by 박윤빈 on 2023/11/26. // import UIKit @@ -11,11 +11,12 @@ import EATSSUDesign final class ReviewRateViewCell: UITableViewCell { // MARK: - Properties - + static let identifier = "ReviewRateViewCell" var handler: (() -> Void)? var totalRate: Double = 0 - + var reviewData: ReviewRateResponse? + // MARK: - UI Components private let menuContainer: UIView = { @@ -28,6 +29,7 @@ final class ReviewRateViewCell: UITableViewCell { private var menuLabel: UILabel = { let label = UILabel() + label.text = "김치볶음밥 & 계란국" label.font = .header2 label.textColor = .black label.numberOfLines = 0 @@ -35,113 +37,150 @@ final class ReviewRateViewCell: UITableViewCell { return label }() - // 왼쪽 평점 섹션 컨테이너 - private let leftRatingContainer = UIView() - - private let mainRatingView = MainRatingView() - - // 오른쪽 차트 섹션 컨테이너 - private let rightChartContainer = UIView() - - private let reviewCountView = ReviewCountView() - private let chartView = RatingChartView() - - // 리뷰 작성 버튼 - private let addReviewButton: UIButton = { - let button = UIButton() - button.setTitle("리뷰 작성하기", for: .normal) - button.setTitleColor(.white, for: .normal) - button.titleLabel?.font = .bold(size: 14) - button.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color - button.layer.cornerRadius = 10 - return button + private let menuIcon: UIImageView = { + let imageView = UIImageView() + imageView.image = EATSSUDesignAsset.Images.icRestaurant.image + return imageView + }() + + private let menuTitleLabel: UILabel = { + let label = UILabel() + label.text = "오늘의 메뉴" + label.font = .body1 + label.textColor = .black + return label + }() + + private lazy var menuTitleStackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [menuIcon, menuTitleLabel]) + stack.axis = .horizontal + stack.alignment = .center + stack.spacing = 6 + return stack + }() + + private let rateSectionContainer: UIView = { + let view = UIView() + return view }() - // 전체 컨텐츠를 담는 스택뷰 - private lazy var contentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [ - leftRatingContainer, - rightChartContainer - ]) + private let bigStarImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = EATSSUDesignAsset.Images.icStarYellow.image + return imageView + }() + + private let rateNumLabel: UILabel = { + let label = UILabel() + label.text = "4.3" + label.font = .bold(size: 36) + label.textColor = .black + return label + }() + + private let fivePointLabel = ReviewRateViewCell.makePointLabel("5점") + private let fourPointLabel = ReviewRateViewCell.makePointLabel("4점") + private let threePointLabel = ReviewRateViewCell.makePointLabel("3점") + private let twoPointLabel = ReviewRateViewCell.makePointLabel("2점") + private let onePointLabel = ReviewRateViewCell.makePointLabel("1점") + + // Chart bar containers and foregrounds + private var oneChartBar: UIView! + private var twoChartBar: UIView! + private var threeChartBar: UIView! + private var fourChartBar: UIView! + private var fiveChartBar: UIView! + + private var oneForeground: UIView! + private var twoForeground: UIView! + private var threeForeground: UIView! + private var fourForeground: UIView! + private var fiveForeground: UIView! + + lazy var yAxisStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [fivePointLabel, + fourPointLabel, + threePointLabel, + twoPointLabel, + onePointLabel]) + stackView.axis = .vertical + stackView.spacing = 0 + stackView.alignment = .trailing + return stackView + }() + + lazy var totalRateStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [bigStarImageView, + rateNumLabel]) stackView.axis = .horizontal - stackView.spacing = 40 + stackView.spacing = 8.adjusted stackView.alignment = .center - stackView.distribution = .fillEqually return stackView }() - - // MARK: - Initialization - + + // MARK: - Init + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - - setupUI() - setupLayout() - setupActions() + configureUI() + setLayout() } - + @available(*, unavailable) - required init?(coder: NSCoder) { + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - Setup - - private func setupUI() { - backgroundColor = .white - selectionStyle = .none - - contentView.addSubview(menuLabel) - contentView.addSubview(contentStackView) - contentView.addSubview(addReviewButton) - - leftRatingContainer.addSubview(mainRatingView) - - rightChartContainer.addSubview(reviewCountView) - rightChartContainer.addSubview(chartView) + + // MARK: - Helper + + private static func makePointLabel(_ text: String) -> UILabel { + let label = UILabel() + label.text = text + label.font = .caption2 + label.textColor = .black + return label } - - private func setupLayout() { - menuLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(10) - $0.leading.trailing.equalToSuperview().inset(16) - } - - contentStackView.snp.makeConstraints { - $0.top.equalTo(menuLabel.snp.bottom).offset(15) - $0.leading.trailing.equalToSuperview().inset(16) - } - - // 왼쪽 컨테이너 내부 레이아웃 (정중앙) - mainRatingView.snp.makeConstraints { - $0.center.equalToSuperview() - } - - // 오른쪽 컨테이너 내부 레이아웃 - reviewCountView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.leading.equalToSuperview() - } - - chartView.snp.makeConstraints { - $0.top.equalTo(reviewCountView.snp.bottom).offset(8) - $0.leading.trailing.equalToSuperview() - $0.bottom.equalToSuperview() - } - - addReviewButton.snp.makeConstraints { - $0.top.equalTo(contentStackView.snp.bottom).offset(20) - $0.leading.trailing.equalToSuperview().inset(16) - $0.height.equalTo(48) - $0.bottom.equalToSuperview().offset(-20) + + // MARK: - UI Setup + + func configureUI() { + // Helper to create chart bar with background and foreground + func makeChartBar() -> (container: UIView, foreground: UIView) { + let container = UIView() + container.backgroundColor = .gray200 + container.layer.cornerRadius = 5 + container.layer.masksToBounds = true + let foreground = UIView() + foreground.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color + foreground.layer.cornerRadius = 5 + foreground.layer.masksToBounds = true + container.addSubview(foreground) + foreground.snp.makeConstraints { make in + make.leading.top.bottom.equalToSuperview() + make.width.equalTo(0) + } + return (container, foreground) } - } - - private func setupActions() { - addReviewButton.addTarget( - self, - action: #selector(touchAddReviewButton), - for: .touchUpInside + + let oneBar = makeChartBar() + oneChartBar = oneBar.container + oneForeground = oneBar.foreground + let twoBar = makeChartBar() + twoChartBar = twoBar.container + twoForeground = twoBar.foreground + let threeBar = makeChartBar() + threeChartBar = threeBar.container + threeForeground = threeBar.foreground + let fourBar = makeChartBar() + fourChartBar = fourBar.container + fourForeground = fourBar.foreground + let fiveBar = makeChartBar() + fiveChartBar = fiveBar.container + fiveForeground = fiveBar.foreground + + contentView.addSubviews( + menuContainer, + rateSectionContainer ) menuContainer.addSubviews(menuTitleStackView, menuLabel) @@ -159,236 +198,149 @@ final class ReviewRateViewCell: UITableViewCell { make.centerY.equalTo(totalRateStackView) } } - - @objc - private func touchAddReviewButton() { - handler?() - } -} -// MARK: - Data Binding + func setLayout() { + backgroundColor = .white -extension ReviewRateViewCell { - func dataBind(data: ReviewRateResponse) { - menuLabel.text = data.menuNames.joined(separator: ", ") - - mainRatingView.configure(rating: data.mainRating ?? 0) - reviewCountView.configure(count: data.totalReviewCount) - chartView.configure(with: data.reviewRatingCount, total: data.totalReviewCount) + menuContainer.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) + make.centerX.equalToSuperview() + make.width.equalTo(320.adjusted) + make.height.greaterThanOrEqualTo(100) + } - totalRate = data.mainRating ?? 0 - } - - func fixMenuDataBind(data: FixedReviewRateResponse) { - menuLabel.text = data.menuName + menuTitleStackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(16) + make.centerX.equalToSuperview() + } - mainRatingView.configure(rating: data.mainRating ?? 0) - reviewCountView.configure(count: data.totalReviewCount) - chartView.configure(with: data.reviewRatingCount, total: data.totalReviewCount) + menuIcon.snp.makeConstraints { make in + make.width.height.equalTo(20) + } + + menuLabel.snp.makeConstraints { make in + make.top.equalTo(menuTitleStackView.snp.bottom).offset(12) + make.leading.trailing.equalToSuperview().inset(28) + make.bottom.equalToSuperview().inset(16) + } - totalRate = data.mainRating ?? 0 - } -} + rateSectionContainer.snp.makeConstraints { make in + make.top.equalTo(menuLabel.snp.bottom).offset(40) + make.leading.trailing.equalToSuperview().inset(60) + } -// MARK: - MainRatingView (큰 별점 표시) + oneChartBar.snp.makeConstraints { make in + make.centerY.equalTo(onePointLabel) + make.leading.equalTo(onePointLabel.snp.trailing).offset(7) + make.height.equalTo(10) + make.width.equalTo(126) + } -private final class MainRatingView: UIView { - private let starImageView: UIImageView = { - let imageView = UIImageView() - imageView.image = EATSSUDesignAsset.Images.icStarYellow.image - imageView.contentMode = .scaleAspectFit - return imageView - }() - - private let ratingLabel: UILabel = { - let label = UILabel() - label.font = .bold(size: 36) - label.textColor = .black - return label - }() - - private lazy var stackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: [starImageView, ratingLabel]) - stack.axis = .horizontal - stack.spacing = 8 - stack.alignment = .center - return stack - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.edges.equalToSuperview() + twoChartBar.snp.makeConstraints { make in + make.centerY.equalTo(twoPointLabel) + make.leading.equalTo(twoPointLabel.snp.trailing).offset(7) + make.height.equalTo(10) + make.width.equalTo(126) } - - starImageView.snp.makeConstraints { - $0.width.height.equalTo(24) + + threeChartBar.snp.makeConstraints { make in + make.centerY.equalTo(threePointLabel) + make.leading.equalTo(threePointLabel.snp.trailing).offset(7) + make.height.equalTo(10) + make.width.equalTo(126) } - } - - func configure(rating: Double) { - ratingLabel.text = String(format: "%.1f", rating) - } -} -// MARK: - ReviewCountView (총 리뷰 수) + fourChartBar.snp.makeConstraints { make in + make.centerY.equalTo(fourPointLabel) + make.leading.equalTo(fourPointLabel.snp.trailing).offset(7) + make.height.equalTo(10) + make.width.equalTo(126) + } -private final class ReviewCountView: UIView { - private let titleLabel: UILabel = { - let label = UILabel() - label.text = "총 리뷰 수" - label.font = .caption2 - label.textColor = .black - return label - }() - - private let countLabel: UILabel = { - let label = UILabel() - label.font = .caption1 - label.textColor = EATSSUDesignAsset.Color.Main.primary.color - return label - }() - - private lazy var stackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: [titleLabel, countLabel]) - stack.axis = .horizontal - stack.spacing = 7 - return stack - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.edges.equalToSuperview() + fiveChartBar.snp.makeConstraints { make in + make.centerY.equalTo(fivePointLabel) + make.leading.equalTo(fivePointLabel.snp.trailing).offset(7) + make.height.equalTo(10) + make.width.equalTo(126) + } + + for item in [onePointLabel, twoPointLabel, threePointLabel, fourPointLabel, fivePointLabel] { + item.snp.makeConstraints { + $0.height.equalTo(18.adjusted) + } + } + + bigStarImageView.snp.makeConstraints { + $0.height.width.equalTo(24.adjusted) } } - - func configure(count: Int) { - countLabel.text = "\(count)" + + @objc + func touchAddReviewButton() { + handler?() } } -// MARK: - RatingChartView (별점 분포 차트) +extension ReviewRateViewCell { + func fixMenuDataBind(data: FixedReviewRateResponse) { +// let total = String(format: "%.1f", data.mainRating ?? 0) + let ratingValue = data.mainRating ?? 0 + if ratingValue == 0.0 { + rateNumLabel.text = "-" + } else { + let total = String(format: "%.1f", ratingValue) + rateNumLabel.text = "\(total)" + } + menuLabel.text = data.menuName +// rateNumLabel.text = "\(total)" +// totalRate = data.mainRating ?? 0 + totalRate = ratingValue -private final class RatingChartView: UIView { - private let chartBars: [ChartBarView] = (1...5).reversed().map { ChartBarView(rating: $0) } - - private lazy var stackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: chartBars) - stack.axis = .vertical - stack.spacing = 0 - stack.distribution = .fillEqually - return stack - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - addSubview(stackView) - - stackView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.height.equalTo(90) + fiveForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) } - } - - func configure(with ratingCount: StarCount, total: Int) { - let counts = [ - ratingCount.fiveStarCount, - ratingCount.fourStarCount, - ratingCount.threeStarCount, - ratingCount.twoStarCount, - ratingCount.oneStarCount - ] - - for (index, bar) in chartBars.enumerated() { - let count = counts[index] - let ratio = total > 0 ? CGFloat(count) / CGFloat(total) : 0 - bar.configure(ratio: ratio) + fourForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / max(data.totalReviewCount, 1)) + } + threeForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / max(data.totalReviewCount, 1)) + } + twoForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / max(data.totalReviewCount, 1)) + } + oneForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / max(data.totalReviewCount, 1)) } } -} -// MARK: - ChartBarView (개별 차트 바) + func dataBind(data: ReviewRateResponse) { +// let total = String(format: "%.1f", data.mainRating ?? 0) + let ratingValue = data.mainRating ?? 0 + if ratingValue == 0.0 { + rateNumLabel.text = "-" + } else { + let total = String(format: "%.1f", ratingValue) + rateNumLabel.text = "\(total)" + } + menuLabel.text = data.menuNames.joined(separator: " + ") +// rateNumLabel.text = "\(total)" +// totalRate = data.mainRating ?? 0 + totalRate = ratingValue -private final class ChartBarView: UIView { - private let ratingLabel: UILabel = { - let label = UILabel() - label.font = .caption2 - label.textColor = .black - label.setContentHuggingPriority(.required, for: .horizontal) - return label - }() - - private let barView: UIView = { - let view = UIView() - view.backgroundColor = .gray300 - view.layer.cornerRadius = 4 - view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] - return view - }() - - private var barWidthConstraint: Constraint? - - init(rating: Int) { - super.init(frame: .zero) - ratingLabel.text = "\(rating)점" - setupView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - addSubview(ratingLabel) - addSubview(barView) - - ratingLabel.snp.makeConstraints { - $0.leading.equalToSuperview() - $0.centerY.equalToSuperview() - $0.width.equalTo(30) + fiveForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) } - - barView.snp.makeConstraints { - $0.leading.equalTo(ratingLabel.snp.trailing).offset(8) - $0.trailing.lessThanOrEqualToSuperview() - $0.centerY.equalToSuperview() - $0.height.equalTo(10) - self.barWidthConstraint = $0.width.equalTo(0).constraint + fourForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / max(data.totalReviewCount, 1)) + } + threeForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / max(data.totalReviewCount, 1)) + } + twoForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / max(data.totalReviewCount, 1)) + } + oneForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / max(data.totalReviewCount, 1)) } - } - - func configure(ratio: CGFloat) { - let maxWidth: CGFloat = 120 - let width = maxWidth * ratio - barWidthConstraint?.update(offset: max(0, width)) } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 33a68a7f..da492b1f 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -45,6 +45,20 @@ final class ReviewViewController: BaseViewController { return imageView }() + private let reviewTabBarContainer: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 0 + view.clipsToBounds = true + return view + }() + + private let reviewTabBarView: MainButton = { + let button = MainButton() + button.title = "리뷰 작성하기" + return button + }() + // MARK: - Life Cycles override func viewDidLoad() { diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 359d2c01..45cd5c35 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -191,32 +191,32 @@ final class CustomTabBarContainerController: BaseViewController { } /// 탭바를 숨기거나 표시하는 메서드 - public func setTabBarHidden(_ hidden: Bool, animated: Bool) { - guard tabBarView.isHidden != hidden else { return } - - // 제약 업데이트 - contentBottomConstraint?.deactivate() - contentContainerView.snp.makeConstraints { - if hidden { - contentBottomConstraint = $0.bottom.equalToSuperview().constraint - } else { - contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint - } - } - - if animated { - UIView.animate(withDuration: 0.3) { - self.tabBarView.alpha = hidden ? 0 : 1 - self.view.layoutIfNeeded() - } completion: { _ in - self.tabBarView.isHidden = hidden - } - } else { - self.tabBarView.alpha = hidden ? 0 : 1 - self.tabBarView.isHidden = hidden - self.view.layoutIfNeeded() - } - } +// public func setTabBarHidden(_ hidden: Bool, animated: Bool) { +// guard tabBarView.isHidden != hidden else { return } +// +// // 제약 업데이트 +// contentBottomConstraint?.deactivate() +// contentContainerView.snp.makeConstraints { +// if hidden { +// contentBottomConstraint = $0.bottom.equalToSuperview().constraint +// } else { +// contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint +// } +// } +// +// if animated { +// UIView.animate(withDuration: 0.3) { +// self.tabBarView.alpha = hidden ? 0 : 1 +// self.view.layoutIfNeeded() +// } completion: { _ in +// self.tabBarView.isHidden = hidden +// } +// } else { +// self.tabBarView.alpha = hidden ? 0 : 1 +// self.tabBarView.isHidden = hidden +// self.view.layoutIfNeeded() +// } +// } } // MARK: - UINavigationControllerDelegate From ebd4d7b07c7a101be41dac865f8bf666bc4ba6d9 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 16 Nov 2025 18:38:42 +0900 Subject: [PATCH 09/69] =?UTF-8?q?[#321]=20Meal,=20Menu=20requestDTO=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/WriteReviewMealRequest.swift | 19 ++ .../DTO/Review/WriteReviewMenuRequest.swift | 19 ++ .../Network/Router/WriteReviewRouter.swift | 20 +- .../View/SeeReview/ReviewRateViewCell.swift | 3 +- .../ViewController/ReviewViewController.swift | 233 ++++++++++-------- fastlane/README.md | 40 +++ fastlane/report.xml | 20 ++ 7 files changed, 246 insertions(+), 108 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift create mode 100644 fastlane/README.md create mode 100644 fastlane/report.xml diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift new file mode 100644 index 00000000..e734bad5 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift @@ -0,0 +1,19 @@ +// +// WriteReviewMealRequest.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +struct WriteReviewMealRequest: Encodable { + let mealId: Int + let rating: Int + let menuLikes: [MenuLike] + let content: String? + let imageUrls: [String]? +} + +struct MenuLike: Encodable { + let menuId: Int + let isLike: Bool +} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift new file mode 100644 index 00000000..1a78044c --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift @@ -0,0 +1,19 @@ +// +// WriteReviewMenuRequest.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + + +struct WriteReviewMenuRequest: Encodable { + let rating: Int + let menuLike: MenuLikeItem + let content: String? + let imageUrls: [String]? +} + +struct MenuLikeItem: Encodable { + let menuId: Int + let isLike: Bool +} diff --git a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift index dd2e4756..023361e9 100644 --- a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift @@ -13,6 +13,10 @@ enum WriteReviewRouter { case uploadImage(image: UIImage?) case writeNewReview(param: WriteReviewRequest, menuID: Int) case writeReview(param: WriteReviewRequest, image: [UIImage?], menuId: Int) + + // MARK: - New V2 APIs + case writeMenuReview(param: WriteReviewMenuRequest) + case writeMealReview(param: WriteReviewMealRequest) } extension WriteReviewRouter: TargetType { @@ -28,12 +32,18 @@ extension WriteReviewRouter: TargetType { "/reviews/upload/image" case .writeNewReview(param: _, menuID: let menuId): "/reviews/write/\(menuId)" + + // MARK: - New V2 Paths + case .writeMenuReview: + "/v2/reviews/menu" + case .writeMealReview: + "/v2/reviews/meal" } } var method: Moya.Method { switch self { - case .writeReview, .uploadImage, .writeNewReview: + case .writeReview, .uploadImage, .writeNewReview, .writeMenuReview, .writeMealReview: .post } } @@ -77,12 +87,18 @@ extension WriteReviewRouter: TargetType { case let .writeNewReview(param: param, _): return .requestJSONEncodable(param) + + // MARK: - New V2 Tasks (JSON Encoded) + case let .writeMenuReview(param: param): + return .requestJSONEncodable(param) + case let .writeMealReview(param: param): + return .requestJSONEncodable(param) } } var headers: [String: String]? { switch self { - case .writeNewReview: + case .writeNewReview, .writeMenuReview, .writeMealReview: return ["Content-Type": "application/json"] case .uploadImage, .writeReview: return ["Content-Type": "multipart/form-data"] diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 694f3d43..2b55c884 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -203,7 +203,8 @@ final class ReviewRateViewCell: UITableViewCell { backgroundColor = .white menuContainer.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) +// make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) + make.top.equalTo(contentView.snp.top).offset(0) make.centerX.equalToSuperview() make.width.equalTo(320.adjusted) make.height.greaterThanOrEqualTo(100) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index da492b1f..3ad4db28 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -1,3 +1,4 @@ + // // ReviewViewController.swift // EatSSU-iOS @@ -13,6 +14,7 @@ import Moya final class ReviewViewController: BaseViewController { // MARK: - Properties + let reviewProvider = MoyaProvider(plugins: [ESMoyaLoggingPlugin()]) var menuID: Int = .init() var type = "VARIABLE" private var menuNameList: [String] = [] @@ -21,23 +23,25 @@ final class ReviewViewController: BaseViewController { private var reviewList = [MenuDataList]() private var responseData: ReviewRateResponse? private var fixedResponseData: FixedReviewRateResponse? - private var isDataLoaded = false // MARK: - UI Component + + let refreshControl = UIRefreshControl() + let reviewTableView: UITableView = { let tableView = UITableView() tableView.separatorStyle = .none tableView.showsVerticalScrollIndicator = false return tableView }() - + private var activityIndicatorView: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .large) indicator.startAnimating() indicator.isHidden = true return indicator }() - + private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() imageView.image = ImageLiteral.noReview @@ -46,30 +50,30 @@ final class ReviewViewController: BaseViewController { }() private let reviewTabBarContainer: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 0 - view.clipsToBounds = true - return view - }() + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 0 + view.clipsToBounds = true + return view + }() - private let reviewTabBarView: MainButton = { - let button = MainButton() - button.title = "리뷰 작성하기" - return button - }() + private let reviewTabBarView: MainButton = { + let button = MainButton() + button.title = "리뷰 작성하기" + return button + }() + // MARK: - Life Cycles - + override func viewDidLoad() { super.viewDidLoad() - + setTableView() + initRefresh() setFirebaseTask() - reviewTableView.estimatedRowHeight = 300 - reviewTableView.rowHeight = UITableView.automaticDimension } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) getReviewRate() @@ -91,9 +95,9 @@ final class ReviewViewController: BaseViewController { } } } - + // MARK: - Functions - + override func configureUI() { reviewTableView.backgroundColor = .white view.addSubviews(reviewTableView, @@ -102,7 +106,7 @@ final class ReviewViewController: BaseViewController { reviewTabBarContainer) reviewTabBarContainer.addSubview(reviewTabBarView) } - + override func setLayout() { reviewTableView.snp.makeConstraints { make in make.top.equalToSuperview().offset(24) @@ -128,12 +132,22 @@ final class ReviewViewController: BaseViewController { $0.edges.equalToSuperview().inset(12) // 안쪽 여백 } } - + override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = "리뷰" } + override func setButtonEvent() { + reviewTabBarView.addTarget(self, action: #selector(handleAddReviewButtonTap), for: .touchUpInside) + } + + @objc private func handleAddReviewButtonTap() { + let reviewVC = SetRateViewController() + + navigationController?.pushViewController(reviewVC, animated: true) + } + private func setFirebaseTask() { FirebaseRemoteConfig.shared.fetchRestaurantInfo() @@ -142,20 +156,38 @@ final class ReviewViewController: BaseViewController { Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) #endif } - + func setTableView() { reviewTableView.register(ReviewTableCell.self, forCellReuseIdentifier: ReviewTableCell.identifier) reviewTableView.register(ReviewRateViewCell.self, forCellReuseIdentifier: ReviewRateViewCell.identifier) reviewTableView.register(ReviewEmptyViewCell.self, forCellReuseIdentifier: ReviewEmptyViewCell.identifier) - + reviewTableView.register(ReviewDividerCell.self, forCellReuseIdentifier: ReviewDividerCell.identifier) + reviewTableView.delegate = self reviewTableView.dataSource = self } + func initRefresh() { + refreshControl.addTarget(self, + action: #selector(refreshTable(refresh:)), + for: .valueChanged) + + reviewTableView.refreshControl = refreshControl + } + + @objc + func refreshTable(refresh: UIRefreshControl) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.getReviewRate() + self.getReviewList(type: self.type, menuId: self.menuID) + refresh.endRefreshing() + } + } + func bindMenuID(id: Int) { menuID = id } - + private func showFixOrDeleteAlert(data: MenuDataList) { let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", @@ -285,6 +317,26 @@ extension ReviewViewController: UITableViewDelegate { func tableView(_: UITableView, didSelectRowAt _: IndexPath) { print("cell did touched") } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + switch section { + case 0: + return 0 // RateViewCell 위쪽 여백 제거 + case 1: + // RateViewCell 아래 여백 (기존 16 → 6으로 줄임) + return 6 + case 2: + // Divider 아래, 리뷰 리스트 위쪽 여백 + return 8 + default: + return 0 + } + } + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let spacerView = UIView() + spacerView.backgroundColor = .clear + return spacerView + } } extension ReviewViewController: UITableViewDataSource { @@ -296,20 +348,18 @@ extension ReviewViewController: UITableViewDataSource { func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: - return 1 + 1 case 1: - // 데이터 로딩 전에는 아무것도 표시 안 함 - if !isDataLoaded { - return 0 - } - // 데이터 로딩 완료 후: 리뷰가 없으면 EmptyCell, 있으면 리뷰 개수만큼 + 1 + case 2: + // 두 번째 섹션에서 리뷰 개수가 하나도 없을 때 셀 변경 if reviewList.count == 0 { - return 1 + 1 } else { - return reviewList.count + reviewList.count } default: - return 0 + 0 } } @@ -387,7 +437,8 @@ extension ReviewViewController: UITableViewDataSource { func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { switch indexPath.section { case 0: - UITableView.automaticDimension + 251.adjusted +// UITableView.automaticDimension case 1: // Divider cell UITableView.automaticDimension @@ -408,91 +459,63 @@ extension ReviewViewController: UITableViewDataSource { extension ReviewViewController { // 상단 메뉴 별점 불러오는 API func getReviewRate() { - if type == "FIXED" { - NetworkService.shared.request( - ReviewRouter.reviewRate(type, menuID), - responseType: FixedReviewRateResponse.self, - useAuth: true - ) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let data): - self.fixedResponseData = data - self.menuNameList = [data.menuName] - self.reviewTableView.reloadData() - self.makeDictionary() - - case .failure(let error): - print("고정 메뉴 평점 조회 실패: \(error.localizedDescription)") - } - } - } else { - NetworkService.shared.request( - ReviewRouter.reviewRate(type, menuID), - responseType: ReviewRateResponse.self, - useAuth: true - ) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let data): - self.responseData = data - self.menuNameList = data.menuNames - self.reviewTableView.reloadData() + reviewProvider.request(.reviewRate(type, menuID)) { response in + switch response { + case let .success(moyaResponse): + do { + if self.type == "FIXED" { + let responseData = try moyaResponse.map(BaseResponse.self) + guard let data = responseData.result else { return } + self.fixedResponseData = data + self.reviewTableView.reloadData() + self.menuNameList = [data.menuName] + } else { + let responseData = try moyaResponse.map(BaseResponse.self) + guard let data = responseData.result else { return } + self.responseData = data + self.reviewTableView.reloadData() + self.menuNameList = data.menuNames + } self.makeDictionary() - - case .failure(let error): - print("변동 메뉴 평점 조회 실패: \(error.localizedDescription)") + } catch let err { + print(err.localizedDescription) } + case let .failure(err): + print(err.localizedDescription) } } } // 하단 리뷰 리스트 불러오는 API func getReviewList(type: String, menuId _: Int) { - NetworkService.shared.request( - ReviewRouter.reviewList(type, menuID), - responseType: ReviewListResponse.self, - useAuth: true - ) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let data): - self.reviewList = data.dataList - self.isDataLoaded = true - self.reviewTableView.reloadData() - - case .failure(let error): - print("리뷰 리스트 조회 실패: \(error.localizedDescription)") - self.isDataLoaded = true - self.reviewTableView.reloadData() + reviewProvider.request(.reviewList(type, menuID)) { response in + switch response { + case let .success(moyaResponse): + do { + let responseData = try moyaResponse.map(BaseResponse.self) + guard let data = responseData.result else { return } + self.reviewList = data.dataList + self.reviewTableView.reloadData() + + } catch let err { + print(err.localizedDescription) + } + case let .failure(err): + print(err.localizedDescription) } } } - + func deleteReview(reviewID: Int) { - NetworkService.shared.request( - ReviewRouter.deleteReview(reviewID), - responseType: Bool.self, - useAuth: true - ) { [weak self] result in - guard let self = self else { return } - - switch result { + reviewProvider.request(.deleteReview(reviewID)) { response in + switch response { case .success: self.getReviewRate() self.updateViewConstraints() self.getReviewList(type: self.type, menuId: self.menuID) - self.showToast(message: "삭제되었어요 !") - - // 네비게이션 스택에서 HomeViewController 찾아서 새로고침 - if let homeVC = navigationController?.viewControllers.first as? HomeViewController { - homeVC.refreshAfterReview() - } - case .failure(let error): - print("리뷰 삭제 실패: \(error.localizedDescription)") +// self.reviewTableView.showToast(message: "삭제되었어요 !") + case let .failure(err): + print(err.localizedDescription) } } } diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 00000000..e4b30ae7 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios setup_development + +```sh +[bundle exec] fastlane ios setup_development +``` + +개발 환경 세팅 + +### ios setup_appstore + +```sh +[bundle exec] fastlane ios setup_appstore +``` + +App Store 배포 환경 세팅 + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/report.xml b/fastlane/report.xml new file mode 100644 index 00000000..59665bd7 --- /dev/null +++ b/fastlane/report.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + From 1f3913a09f05b851f137d31dc4b1075e9b11330f Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 16 Nov 2025 19:01:05 +0900 Subject: [PATCH 10/69] =?UTF-8?q?[#321]=20mealId=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=ED=95=A0=20=EB=A9=94=EB=89=B4=20=EB=9D=84=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/ReviewValidMenuResponse.swift | 15 +++ .../Data/Network/Router/ReviewRouter.swift | 14 +- .../ViewController/ReviewViewController.swift | 31 +++-- .../SetRateViewController.swift | 126 +++++++++++++++--- 4 files changed, 151 insertions(+), 35 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift new file mode 100644 index 00000000..101f65c2 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift @@ -0,0 +1,15 @@ +// +// ReviewValidMenuResponse.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +struct ReviewValidMenusResponse: Codable { + let menuList: [ReviewValidMenu] // 메뉴 목록 배열 +} + +struct ReviewValidMenu: Codable { + let menuId: Int + let name: String +} diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index c31a91a5..2af4b5a0 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -17,6 +17,9 @@ enum ReviewRouter { case report(param: ReportRequest) case deleteReview(_ reviewId: Int) case fixReview(_ reviewId: Int, _ param: BeforeSelectedImageDTO) + + // MARK: - New V2 API: 리뷰 작성이 가능한 메뉴 목록 조회 + case getValidMenusForReview(_ mealId: Int) } extension ReviewRouter: TargetType { @@ -43,14 +46,15 @@ extension ReviewRouter: TargetType { "/reviews/\(reviewId)" case let .fixReview(reviewId, _): "/reviews/\(reviewId)" + // MARK: - New V2 Path + case let .getValidMenusForReview(mealId): + "/v2/reviews/meal/valid-for-review/\(mealId)" // Path Parameter 사용 } } var method: Moya.Method { switch self { - case .reviewRate: - .get - case .reviewList: + case .reviewRate, .reviewList, .getValidMenusForReview: .get case .report: .post @@ -100,6 +104,10 @@ extension ReviewRouter: TargetType { .requestPlain case let .fixReview(_, param): .requestJSONEncodable(param) + + // MARK: - New V2 Task + case .getValidMenusForReview: // Path에 ID가 포함되므로 Body나 QueryString 없음 + .requestPlain } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 3ad4db28..3574fdb3 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -1,4 +1,3 @@ - // // ReviewViewController.swift // EatSSU-iOS @@ -15,7 +14,7 @@ final class ReviewViewController: BaseViewController { // MARK: - Properties let reviewProvider = MoyaProvider(plugins: [ESMoyaLoggingPlugin()]) - var menuID: Int = .init() + var menuID: Int = .init() // mealId를 담고 있다고 가정 var type = "VARIABLE" private var menuNameList: [String] = [] private var menuIDList: [Int]? = [Int]() @@ -143,9 +142,20 @@ final class ReviewViewController: BaseViewController { } @objc private func handleAddReviewButtonTap() { - let reviewVC = SetRateViewController() - - navigationController?.pushViewController(reviewVC, animated: true) + // type이 VARIABLE(식단)일 때만 mealId를 전달 (menuID가 mealId 역할을 함) + if type == "VARIABLE" { + let reviewVC = SetRateViewController(mealId: menuID) // ✨ 수정: mealId 전달 + navigationController?.pushViewController(reviewVC, animated: true) + } else { + // FIXED 메뉴는 기존 로직을 따라 `userTapReviewButton()` 내에서 처리되거나 + // 이전 로직에서 dataBind를 통해 메뉴 ID를 전달해야 함 + let reviewVC = SetRateViewController() + reviewVC.dataBind(list: menuNameList, + idList: menuIDList ?? [], + reviewList: nil, + currentPage: 0) + navigationController?.pushViewController(reviewVC, animated: true) + } } private func setFirebaseTask() { @@ -250,6 +260,7 @@ final class ReviewViewController: BaseViewController { DispatchQueue.main.async { [self] in // 고정메뉴인지 판별(메뉴 ID List에 nil값 들어옴) if menuIDList == nil { + // FIXED let setRateViewController = SetRateViewController() menuIDList = [menuID] setRateViewController.dataBind(list: menuNameList, @@ -259,9 +270,11 @@ final class ReviewViewController: BaseViewController { activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) } else { + // VARIABLE (meal review) + // 고정메뉴이고, 메뉴가 1개일때 선택창으로 안가고 바로 작성창으로 가도록 if menuIDList?.count == 1 { - let setRateViewController = SetRateViewController() + let setRateViewController = SetRateViewController(mealId: menuID) // ✨ 수정: mealId 전달 setRateViewController.dataBind(list: menuNameList, idList: menuIDList ?? [], reviewList: nil, @@ -269,15 +282,13 @@ final class ReviewViewController: BaseViewController { activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) } else { -// let choiceMenuViewController = ChoiceMenuViewController() - let setRateViewController = SetRateViewController() -// choiceMenuViewController.menuDataBind(menuList: menuNameList, idList: menuIDList ?? []) + // 메뉴가 여러 개일 때 (VARIABLE) + let setRateViewController = SetRateViewController(mealId: menuID) // ✨ 수정: mealId 전달 setRateViewController.dataBind(list: menuNameList, idList: menuIDList ?? [], reviewList: nil, currentPage: 0) activityIndicatorView.stopAnimating() -// navigationController?.pushViewController(choiceMenuViewController, animated: true) navigationController?.pushViewController(setRateViewController, animated: true) } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 1f6ee0f0..e655a67b 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -26,15 +26,30 @@ final class SetRateViewController: BaseViewController { private var userPickedImage: UIImage? private var reviewList: [(BeforeSelectedImageDTO, UIImage?)] = [] - private var selectedIDList: [Int] = [] + + // ✨ 수정: selectedIDList를 validMenuIDList로 변경 + private var validMenuIDList: [Int] = [] private var selectedList: [String] = [] private var reviewId: Int? // 좋아요 상태를 보관 (selectedList와 같은 인덱스) - private var likedStates: [Bool] = [] + private var likedStates: [Bool] = [] private var menuTableViewHeightConstraint: Constraint? + + // ✨ 추가: mealId를 저장할 프로퍼티 + private var mealID: Int? + + // MARK: - Initializer + + // ✨ 추가: mealId를 받는 이니셜라이저 + convenience init(mealId: Int) { + self.init(nibName: nil, bundle: nil) + self.mealID = mealId + } + // MARK: - UI Components + // ... (기존 UI Component 코드 유지) private var rateView = RateView() private var tasteRateView = RateView() @@ -216,14 +231,20 @@ final class SetRateViewController: BaseViewController { super.viewDidLoad() setDelegate() - // 더미데이터 지정 - selectedList = ["김치볶음밥", "돈까스", "된장찌개", "샐러드", "라면"] - - // 좋아요 상태 배열도 맞춰서 초기화 - likedStates = Array(repeating: false, count: selectedList.count) - - // 테이블 갱신 - menuTableView.reloadData() + // ✨ 수정: mealId가 있으면 API 호출, 없으면 기존 로직 유지 + if let mealId = mealID { + fetchValidMenus(mealId: mealId) + } else { + // 기존 dataBind로 넘어온 데이터가 있거나, 리뷰 수정인 경우 + // selectedList가 비어있지 않다면 테이블 뷰 갱신 (리뷰 수정 등 기존 로직 유지) + if !selectedList.isEmpty { + // 좋아요 상태 배열도 맞춰서 초기화 + likedStates = Array(repeating: false, count: selectedList.count) + // 테이블 갱신 + menuTableView.reloadData() + // 높이 업데이트 (viewDidLayoutSubviews에서 실행됨) + } + } } override func viewWillAppear(_: Bool) { @@ -236,10 +257,56 @@ final class SetRateViewController: BaseViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + // API 호출 후 데이터가 로드되면 높이가 업데이트되도록 처리 menuTableViewHeightConstraint?.update(offset: menuTableView.contentSize.height) } + // MARK: - API Call + + // ✨ 추가: 리뷰 가능한 메뉴 목록을 조회하는 메서드 + private func fetchValidMenus(mealId: Int) { + // NetworkService의 request 메서드를 사용하여 ReviewRouter 호출 + NetworkService.shared.request( + ReviewRouter.getValidMenusForReview(mealId), + responseType: ReviewValidMenusResponse.self, // DTO 타입 사용 + useAuth: true + ) { [weak self] result in + guard let self = self else { return } + + DispatchQueue.main.async { + switch result { + case .success(let data): + // 메뉴 이름과 ID를 각각 selectedList와 validMenuIDList에 저장 + self.selectedList = data.menuList.map { $0.name } + self.validMenuIDList = data.menuList.map { $0.menuId } + + // 메뉴 목록 수에 맞춰 좋아요 상태 초기화 (초기값: false) + self.likedStates = Array(repeating: false, count: self.selectedList.count) + + // reviewList 초기화 (API 결과에 따라 갯수 맞춤) + self.reviewList = Array(repeating: (BeforeSelectedImageDTO(mainRating: 0, + amountRating: nil, + tasteRating: nil, + content: ""), + nil), count: self.validMenuIDList.count) + + // 테이블 뷰 리로드 + self.menuTableView.reloadData() + // viewDidLayoutSubviews를 호출하여 높이 제약조건 업데이트 + self.view.setNeedsLayout() + + // currentPage 초기화 + self.currentPage = 0 + + case .failure(let error): + print("Error fetching valid menus: \(error)") + self.showToast(message: "메뉴 목록 조회에 실패했습니다.") + } + } + } + } + // MARK: - Functions override func configureUI() { @@ -326,15 +393,19 @@ final class SetRateViewController: BaseViewController { } for i in 0 ... 4 { - tasteRateView.buttons[i].snp.makeConstraints { make in - make.height.equalTo(28) - make.width.equalTo(29.3) - } - - quantityRateView.buttons[i].snp.makeConstraints { make in + rateView.buttons[i].snp.makeConstraints { make in // rateView로 통합 make.height.equalTo(28) make.width.equalTo(29.3) } +// tasteRateView.buttons[i].snp.makeConstraints { make in +// make.height.equalTo(28) +// make.width.equalTo(29.3) +// } +// +// quantityRateView.buttons[i].snp.makeConstraints { make in +// make.height.equalTo(28) +// make.width.equalTo(29.3) +// } } userReviewTextView.snp.makeConstraints { make in @@ -407,9 +478,10 @@ final class SetRateViewController: BaseViewController { } } + // ✨ 수정: selectedIDList -> validMenuIDList로 이름 변경 반영 func dataBind(list: [String], idList: [Int], reviewList: [(BeforeSelectedImageDTO, UIImage?)]?, currentPage: Int) { selectedList = list - selectedIDList = idList + validMenuIDList = idList // ✨ 수정: selectedIDList -> validMenuIDList if let reviewList { self.reviewList = reviewList } else { @@ -460,7 +532,8 @@ final class SetRateViewController: BaseViewController { if userReviewTextView.text == "3글자 이상 작성해주세요!" || userReviewTextView.text.count < 3 { showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) } else { - if rateView.currentStar != 0, quantityRateView.currentStar != 0, tasteRateView.currentStar != 0 { + // 별점 검사: rateView는 메인 별점, quantity/tasteRateView는 사용되지 않으므로 rateView만 확인 + if rateView.currentStar != 0 /*, quantityRateView.currentStar != 0, tasteRateView.currentStar != 0*/ { // 리뷰 작성하기 버튼이 isEnabled = true일 때의 area let param = BeforeSelectedImageDTO(mainRating: rateView.currentStar, amountRating: quantityRateView.currentStar, @@ -508,7 +581,7 @@ final class SetRateViewController: BaseViewController { ReviewAnalyticsManager.shared.logCompleteReviewV1(photoAttached: photoAttached, rating: rating, selection: selection) // 순차적으로 업로드 - try await uploadReview(reviewDTO: reviewDTO, image: image, menuId: selectedIDList[index]) + try await uploadReview(reviewDTO: reviewDTO, image: image, menuId: validMenuIDList[index]) // ✨ 수정: selectedIDList -> validMenuIDList } await MainActor.run { @@ -564,13 +637,22 @@ final class SetRateViewController: BaseViewController { userReviewImageView.image = nil // 이미지 삭제 userPickedImage = nil imageCountLabel.text = "사진 0/1" - closeButton.isHidden = true // Hide close button when image is cleared + closeButton.isHidden = true // Show close button when image is selected } private func prepareForNextReview() { - let setRateVC = SetRateViewController() + let setRateVC: SetRateViewController + + // ✨ 수정: 다음 페이지로 이동할 때 현재 mealId를 전달 + if let mealId = self.mealID { + setRateVC = SetRateViewController(mealId: mealId) + } else { + // mealId가 없으면 기존처럼 인자 없이 초기화 (예: 고정 메뉴 리뷰 수정 후 다음 단계) + setRateVC = SetRateViewController() + } + setRateVC.dataBind(list: selectedList, - idList: selectedIDList, + idList: validMenuIDList, // ✨ 수정: selectedIDList -> validMenuIDList reviewList: reviewList, currentPage: currentPage + 1) navigationController?.pushViewController(setRateVC, animated: true) From 13e747e55b03b76eed11ba9e5fee0d2155ca79ec Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 16 Nov 2025 19:57:39 +0900 Subject: [PATCH 11/69] =?UTF-8?q?[#321]=20meal,=20menu=20list=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/NewMealListResponse.swift | 24 +++++++++++++ .../DTO/Review/NewMenuListResponse.swift | 32 +++++++++++++++++ .../Data/Network/Router/ReviewRouter.swift | 36 ++++++++++++++++++- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift new file mode 100644 index 00000000..aa8c6c69 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift @@ -0,0 +1,24 @@ +// +// NewMealListResponse.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +struct ReviewMealResponse: Encodable { + let reviewId: Int + let writerId: Int + let isWriter: Bool + let writerNickname: String + let rating: Int + let writtenAt: String + let content: String? + let imageUrls: [String]? + let menuList: [ReviewMealInfo]? +} + +struct ReviewMealInfo: Encodable { + let menuId: Int + let name: String + let isLike: Bool +} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift new file mode 100644 index 00000000..b5598503 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift @@ -0,0 +1,32 @@ +// +// NewReviewListResponse.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +/// 리뷰 리스트 조회 API의 result 내부 DTO (공통) +struct NewMenuListResponse: Encodable { + let numberOfElements: Int? // menu API 응답 예시에 있음 + let hasNext: Bool + let dataList: [ReviewListItem] +} + +struct ReviewListItem: Encodable { + let reviewId: Int + let menuList: [ReviewMenuInfo]? + let writerId: Int + let isWriter: Bool // 리뷰 작성자인지 여부 + let writerNickname: String + let rating: Int + let writtenAt: String + let content: String? + let imageUrls: [String]? +} + +struct ReviewMenuInfo: Encodable { + let menuId: Int + let name: String + let isLike: Bool +} + diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 2af4b5a0..f204d96f 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -20,6 +20,11 @@ enum ReviewRouter { // MARK: - New V2 API: 리뷰 작성이 가능한 메뉴 목록 조회 case getValidMenusForReview(_ mealId: Int) + case newReviewList(_ type: String, + _ id: Int, + lastReviewId: Int?, // ✨ 추가: lastReviewId 파라미터 + page: Int? = 0, // menu API용 (옵션) + size: Int? = 20) } extension ReviewRouter: TargetType { @@ -49,12 +54,21 @@ extension ReviewRouter: TargetType { // MARK: - New V2 Path case let .getValidMenusForReview(mealId): "/v2/reviews/meal/valid-for-review/\(mealId)" // Path Parameter 사용 + case .newReviewList(let type, _, _, _, _): + switch type { + case "VARIABLE": + "/v2/reviews/list/meal" // ✨ 수정: V2 Meal List API 경로 + case "FIXED": + "/v2/reviews/list/menu" // ✨ 수정: V2 Menu List API 경로 + default: + "" // 기존 경로 유지 (혹시 모를 에러 방지) + } } } var method: Moya.Method { switch self { - case .reviewRate, .reviewList, .getValidMenusForReview: + case .reviewRate, .reviewList, .getValidMenusForReview, .newReviewList: .get case .report: .post @@ -108,6 +122,26 @@ extension ReviewRouter: TargetType { // MARK: - New V2 Task case .getValidMenusForReview: // Path에 ID가 포함되므로 Body나 QueryString 없음 .requestPlain + case let .newReviewList(type, id, lastReviewId, page, size): + switch type { + case "VARIABLE": + .requestParameters( + parameters: (lastReviewId != nil) + ? ["mealId": id, "size": size ?? 20, "lastReviewId": lastReviewId!] + : ["mealId": id, "size": size ?? 20], + encoding: URLEncoding.queryString + ) + + case "FIXED": + .requestParameters( + parameters: ["menuId": id, "page": page ?? 0, "size": size ?? 20], + encoding: URLEncoding.queryString + ) + + default: + .requestPlain + + } } } From 330c1769e33b7014323b7b4be07413e4c5fa925f Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 16 Nov 2025 20:46:52 +0900 Subject: [PATCH 12/69] =?UTF-8?q?[#321]=20meal,=20menu=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=A1=B0=ED=9A=8C=20dto,=20router=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ReviewMealStatisticsResponse.swift | 29 +++++++++++++++++++ .../Review/ReviewMeuStatisticsResponse.swift | 23 +++++++++++++++ .../Data/Network/Router/ReviewRouter.swift | 12 +++++++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift new file mode 100644 index 00000000..3c5e5e50 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift @@ -0,0 +1,29 @@ +// +// ReviewMealStatisticsResponse.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +struct ReviewMealStatisticsResponse: Encodable { + // 식단에 포함된 메뉴 리스트 + let menuList: [MenuInfo] // Meal API 고유 필드 + + let totalReviewCount: Int + let rating: Double // 메인 평균 별점 + let likeCount: Int? + let reviewRatingCount: ReviewRatingCount // 별점별 카운트 +} + +struct MenuInfo: Encodable { + let id: Int + let name: String +} + +struct ReviewMealRatingCount: Encodable { + let oneStarCount: Int + let twoStarCount: Int + let threeStarCount: Int + let fourStarCount: Int + let fiveStarCount: Int +} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift new file mode 100644 index 00000000..4713731f --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift @@ -0,0 +1,23 @@ +// +// ReviewMeuStatistics.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +struct ReviewMeuStatisticsResponse: Encodable { + let menuName: String + let totalReviewCount: Int + let rating: Double + let likeCount: Int? + let dislikeCount: Int? + let reviewRatingCount: ReviewRatingCount +} + +struct ReviewRatingCount: Encodable { + let oneStarCount: Int + let twoStarCount: Int + let threeStarCount: Int + let fourStarCount: Int + let fiveStarCount: Int +} diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index f204d96f..e27d9624 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -25,6 +25,8 @@ enum ReviewRouter { lastReviewId: Int?, // ✨ 추가: lastReviewId 파라미터 page: Int? = 0, // menu API용 (옵션) size: Int? = 20) + case getFixedMenuStatistics(_ menuId: Int) + case getMealStatistics(_ mealId: Int) } extension ReviewRouter: TargetType { @@ -63,12 +65,16 @@ extension ReviewRouter: TargetType { default: "" // 기존 경로 유지 (혹시 모를 에러 방지) } + case let .getFixedMenuStatistics(menuId): + "/v2/reviews/statistics/menus/\(menuId)" + case let .getMealStatistics(mealId): + "/v2/reviews/statistics/meals/\(mealId)" } } var method: Moya.Method { switch self { - case .reviewRate, .reviewList, .getValidMenusForReview, .newReviewList: + case .reviewRate, .reviewList, .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: .get case .report: .post @@ -142,6 +148,10 @@ extension ReviewRouter: TargetType { .requestPlain } + case .getFixedMenuStatistics: + .requestPlain + case .getMealStatistics: + .requestPlain } } From 1cbe62c360218bc8986a51b09d36c9df8c383498 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 16 Nov 2025 21:23:08 +0900 Subject: [PATCH 13/69] =?UTF-8?q?[#321]=20=EB=A9=94=EB=89=B4,=20=EC=8B=9D?= =?UTF-8?q?=EB=8B=A8=20=EC=A0=95=EB=B3=B4=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ReviewMealStatisticsResponse.swift | 6 +- .../Review/ReviewMeuStatisticsResponse.swift | 4 +- .../View/SeeReview/ReviewRateViewCell.swift | 24 +- .../ViewController/ReviewViewController.swift | 234 +++++++++++------- 4 files changed, 167 insertions(+), 101 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift index 3c5e5e50..e41eb8f1 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift @@ -5,7 +5,7 @@ // Created by 한금준 on 11/16/25. // -struct ReviewMealStatisticsResponse: Encodable { +struct ReviewMealStatisticsResponse: Codable { // 식단에 포함된 메뉴 리스트 let menuList: [MenuInfo] // Meal API 고유 필드 @@ -15,12 +15,12 @@ struct ReviewMealStatisticsResponse: Encodable { let reviewRatingCount: ReviewRatingCount // 별점별 카운트 } -struct MenuInfo: Encodable { +struct MenuInfo: Codable { let id: Int let name: String } -struct ReviewMealRatingCount: Encodable { +struct ReviewMealRatingCount: Codable { let oneStarCount: Int let twoStarCount: Int let threeStarCount: Int diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift index 4713731f..e18224ab 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift @@ -5,7 +5,7 @@ // Created by 한금준 on 11/16/25. // -struct ReviewMeuStatisticsResponse: Encodable { +struct ReviewMeuStatisticsResponse: Codable { let menuName: String let totalReviewCount: Int let rating: Double @@ -14,7 +14,7 @@ struct ReviewMeuStatisticsResponse: Encodable { let reviewRatingCount: ReviewRatingCount } -struct ReviewRatingCount: Encodable { +struct ReviewRatingCount: Codable { let oneStarCount: Int let twoStarCount: Int let threeStarCount: Int diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 2b55c884..f8b6681f 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -27,7 +27,7 @@ final class ReviewRateViewCell: UITableViewCell { return view }() - private var menuLabel: UILabel = { + var menuLabel: UILabel = { let label = UILabel() label.text = "김치볶음밥 & 계란국" label.font = .header2 @@ -70,7 +70,7 @@ final class ReviewRateViewCell: UITableViewCell { return imageView }() - private let rateNumLabel: UILabel = { + var rateNumLabel: UILabel = { let label = UILabel() label.text = "4.3" label.font = .bold(size: 36) @@ -85,17 +85,17 @@ final class ReviewRateViewCell: UITableViewCell { private let onePointLabel = ReviewRateViewCell.makePointLabel("1점") // Chart bar containers and foregrounds - private var oneChartBar: UIView! - private var twoChartBar: UIView! - private var threeChartBar: UIView! - private var fourChartBar: UIView! - private var fiveChartBar: UIView! + var oneChartBar: UIView! + var twoChartBar: UIView! + var threeChartBar: UIView! + var fourChartBar: UIView! + var fiveChartBar: UIView! - private var oneForeground: UIView! - private var twoForeground: UIView! - private var threeForeground: UIView! - private var fourForeground: UIView! - private var fiveForeground: UIView! + var oneForeground: UIView! + var twoForeground: UIView! + var threeForeground: UIView! + var fourForeground: UIView! + var fiveForeground: UIView! lazy var yAxisStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [fivePointLabel, diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 3574fdb3..f1d10f68 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -14,14 +14,17 @@ final class ReviewViewController: BaseViewController { // MARK: - Properties let reviewProvider = MoyaProvider(plugins: [ESMoyaLoggingPlugin()]) - var menuID: Int = .init() // mealId를 담고 있다고 가정 + var menuID: Int = .init() var type = "VARIABLE" private var menuNameList: [String] = [] private var menuIDList: [Int]? = [Int]() private var menuDictionary: [String: Int] = [:] private var reviewList = [MenuDataList]() - private var responseData: ReviewRateResponse? - private var fixedResponseData: FixedReviewRateResponse? + + // ✨ V2 API 응답 데이터 + private var mealStatistics: ReviewMealStatisticsResponse? + private var menuStatistics: ReviewMeuStatisticsResponse? + private var totalReviewCount: Int = 0 // MARK: - UI Component @@ -75,14 +78,14 @@ final class ReviewViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - getReviewRate() + // ✨ V2 API 호출로 변경 + getStatistics() getReviewList(type: type, menuId: menuID) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - // 뒤로 가기(pop) 할 때만 실행되도록 if self.isMovingFromParent { var parentVC = self.parent while parentVC != nil { @@ -124,11 +127,11 @@ final class ReviewViewController: BaseViewController { reviewTabBarContainer.snp.makeConstraints { $0.leading.trailing.equalToSuperview() $0.bottom.equalTo(view.safeAreaLayoutGuide) - $0.height.equalTo(80) // 탭바 높이 + $0.height.equalTo(80) } reviewTabBarView.snp.makeConstraints { - $0.edges.equalToSuperview().inset(12) // 안쪽 여백 + $0.edges.equalToSuperview().inset(12) } } @@ -142,13 +145,10 @@ final class ReviewViewController: BaseViewController { } @objc private func handleAddReviewButtonTap() { - // type이 VARIABLE(식단)일 때만 mealId를 전달 (menuID가 mealId 역할을 함) if type == "VARIABLE" { - let reviewVC = SetRateViewController(mealId: menuID) // ✨ 수정: mealId 전달 + let reviewVC = SetRateViewController(mealId: menuID) navigationController?.pushViewController(reviewVC, animated: true) } else { - // FIXED 메뉴는 기존 로직을 따라 `userTapReviewButton()` 내에서 처리되거나 - // 이전 로직에서 dataBind를 통해 메뉴 ID를 전달해야 함 let reviewVC = SetRateViewController() reviewVC.dataBind(list: menuNameList, idList: menuIDList ?? [], @@ -188,7 +188,7 @@ final class ReviewViewController: BaseViewController { @objc func refreshTable(refresh: UIRefreshControl) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.getReviewRate() + self.getStatistics() self.getReviewList(type: self.type, menuId: self.menuID) refresh.endRefreshing() } @@ -251,14 +251,11 @@ final class ReviewViewController: BaseViewController { // MARK: - Action Method - // @objc func userTapReviewButton() { if RealmService.shared.isAccessTokenPresent() { activityIndicatorView.isHidden = false - DispatchQueue.global().async { // 백그라운드 스레드에서 작업을 수행 - // 작업 완료 후 UI 업데이트를 메인 스레드에서 수행 + DispatchQueue.global().async { DispatchQueue.main.async { [self] in - // 고정메뉴인지 판별(메뉴 ID List에 nil값 들어옴) if menuIDList == nil { // FIXED let setRateViewController = SetRateViewController() @@ -270,11 +267,9 @@ final class ReviewViewController: BaseViewController { activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) } else { - // VARIABLE (meal review) - - // 고정메뉴이고, 메뉴가 1개일때 선택창으로 안가고 바로 작성창으로 가도록 + // VARIABLE if menuIDList?.count == 1 { - let setRateViewController = SetRateViewController(mealId: menuID) // ✨ 수정: mealId 전달 + let setRateViewController = SetRateViewController(mealId: menuID) setRateViewController.dataBind(list: menuNameList, idList: menuIDList ?? [], reviewList: nil, @@ -282,8 +277,7 @@ final class ReviewViewController: BaseViewController { activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) } else { - // 메뉴가 여러 개일 때 (VARIABLE) - let setRateViewController = SetRateViewController(mealId: menuID) // ✨ 수정: mealId 전달 + let setRateViewController = SetRateViewController(mealId: menuID) setRateViewController.dataBind(list: menuNameList, idList: menuIDList ?? [], reviewList: nil, @@ -316,12 +310,6 @@ final class ReviewViewController: BaseViewController { } } -//extension ReviewViewController: AddReviewButtonDelegate { -// func didTapAddReviewButton() { -// userTapReviewButton() -// } -//} - // MARK: - UITableView Delegate, DataSource extension ReviewViewController: UITableViewDelegate { @@ -332,12 +320,10 @@ extension ReviewViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { switch section { case 0: - return 0 // RateViewCell 위쪽 여백 제거 + return 0 case 1: - // RateViewCell 아래 여백 (기존 16 → 6으로 줄임) return 6 case 2: - // Divider 아래, 리뷰 리스트 위쪽 여백 return 8 default: return 0 @@ -352,7 +338,6 @@ extension ReviewViewController: UITableViewDelegate { extension ReviewViewController: UITableViewDataSource { func numberOfSections(in _: UITableView) -> Int { -// 2 3 } @@ -363,7 +348,6 @@ extension ReviewViewController: UITableViewDataSource { case 1: 1 case 2: - // 두 번째 섹션에서 리뷰 개수가 하나도 없을 때 셀 변경 if reviewList.count == 0 { 1 } else { @@ -379,29 +363,18 @@ extension ReviewViewController: UITableViewDataSource { case 0: let cell = tableView.dequeueReusableCell(withIdentifier: ReviewRateViewCell.identifier, for: indexPath) as? ReviewRateViewCell ?? ReviewRateViewCell() cell.selectionStyle = .none + + // ✨ V2 API 데이터로 바인딩 if type == "FIXED" { - cell.fixMenuDataBind(data: fixedResponseData ?? FixedReviewRateResponse(menuName: "", - totalReviewCount: 0, - mainRating: 0, - amountRating: 0, - tasteRating: 0, - reviewRatingCount: StarCount(fiveStarCount: 0, - fourStarCount: 0, - threeStarCount: 0, - twoStarCount: 0, - oneStarCount: 0))) + if let statistics = menuStatistics { + cell.configureWithMenuStatistics(statistics) + } } else { - cell.dataBind(data: responseData ?? ReviewRateResponse(menuNames: [""], - totalReviewCount: 0, - mainRating: 0, - amountRating: 0, - tasteRating: 0, - reviewRatingCount: StarCount(fiveStarCount: 0, - fourStarCount: 0, - threeStarCount: 0, - twoStarCount: 0, - oneStarCount: 0))) + if let statistics = mealStatistics { + cell.configureWithMealStatistics(statistics) + } } + cell.handler = { [weak self] in guard let self else { return } userTapReviewButton() @@ -410,11 +383,11 @@ extension ReviewViewController: UITableViewDataSource { return cell case 1: - // Divider cell let cell = tableView.dequeueReusableCell(withIdentifier: ReviewDividerCell.identifier, for: indexPath) as? ReviewDividerCell ?? ReviewDividerCell() - cell.configure(reviewCount: reviewList.count) + cell.configure(reviewCount: totalReviewCount) cell.selectionStyle = .none return cell + case 2: if reviewList.count == 0 { let cell = tableView.dequeueReusableCell(withIdentifier: ReviewEmptyViewCell.identifier, for: indexPath) as? ReviewEmptyViewCell ?? ReviewEmptyViewCell() @@ -449,9 +422,7 @@ extension ReviewViewController: UITableViewDataSource { switch indexPath.section { case 0: 251.adjusted -// UITableView.automaticDimension case 1: - // Divider cell UITableView.automaticDimension case 2: if reviewList.count == 0 { @@ -468,31 +439,54 @@ extension ReviewViewController: UITableViewDataSource { // MARK: - Server Setting extension ReviewViewController { - // 상단 메뉴 별점 불러오는 API - func getReviewRate() { - reviewProvider.request(.reviewRate(type, menuID)) { response in - switch response { - case let .success(moyaResponse): - do { - if self.type == "FIXED" { - let responseData = try moyaResponse.map(BaseResponse.self) - guard let data = responseData.result else { return } - self.fixedResponseData = data - self.reviewTableView.reloadData() - self.menuNameList = [data.menuName] - } else { - let responseData = try moyaResponse.map(BaseResponse.self) - guard let data = responseData.result else { return } - self.responseData = data - self.reviewTableView.reloadData() - self.menuNameList = data.menuNames - } - self.makeDictionary() - } catch let err { - print(err.localizedDescription) - } - case let .failure(err): - print(err.localizedDescription) + // ✨ V2 API: 통계 데이터 가져오기 + func getStatistics() { + if type == "FIXED" { + getFixedMenuStatistics() + } else { + getMealStatistics() + } + } + + // ✨ V2 API: 고정 메뉴 통계 + func getFixedMenuStatistics() { + NetworkService.shared.request( + ReviewRouter.getFixedMenuStatistics(menuID), + responseType: ReviewMeuStatisticsResponse.self, + useAuth: false + ) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let data): + self.menuStatistics = data + self.totalReviewCount = data.totalReviewCount + self.menuNameList = [data.menuName] + self.makeDictionary() + self.reviewTableView.reloadData() + case .failure(let error): + print("Fixed Menu Statistics Error: \(error.localizedDescription)") + } + } + } + + // ✨ V2 API: 식단 통계 + func getMealStatistics() { + NetworkService.shared.request( + ReviewRouter.getMealStatistics(menuID), + responseType: ReviewMealStatisticsResponse.self, + useAuth: false + ) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let data): + self.mealStatistics = data + self.totalReviewCount = data.totalReviewCount + self.menuNameList = data.menuList.map { $0.name } + self.menuIDList = data.menuList.map { $0.id } + self.makeDictionary() + self.reviewTableView.reloadData() + case .failure(let error): + print("Meal Statistics Error: \(error.localizedDescription)") } } } @@ -521,10 +515,9 @@ extension ReviewViewController { reviewProvider.request(.deleteReview(reviewID)) { response in switch response { case .success: - self.getReviewRate() + self.getStatistics() self.updateViewConstraints() self.getReviewList(type: self.type, menuId: self.menuID) -// self.reviewTableView.showToast(message: "삭제되었어요 !") case let .failure(err): print(err.localizedDescription) } @@ -542,3 +535,76 @@ extension ReviewViewController: ReviewMenuTypeInfoDelegate { menuIDList = reviewMenuTypeInfo.changeMenuIDList } } + +// MARK: - ReviewRateViewCell Extension for V2 API + +extension ReviewRateViewCell { + // ✨ Meal 통계 데이터 바인딩 + func configureWithMealStatistics(_ data: ReviewMealStatisticsResponse) { + // 메뉴명 설정 + let menuNames = data.menuList.map { $0.name } + menuLabel.text = menuNames.joined(separator: " + ") + + // 평균 별점 설정 + let ratingValue = data.rating + if ratingValue == 0.0 { + rateNumLabel.text = "-" + } else { + let total = String(format: "%.1f", ratingValue) + rateNumLabel.text = "\(total)" + } + totalRate = ratingValue + + // 별점 차트 업데이트 + let totalCount = max(data.totalReviewCount, 1) + fiveForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / totalCount) + } + fourForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / totalCount) + } + threeForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / totalCount) + } + twoForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / totalCount) + } + oneForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / totalCount) + } + } + + // ✨ Menu 통계 데이터 바인딩 + func configureWithMenuStatistics(_ data: ReviewMeuStatisticsResponse) { + // 메뉴명 설정 + menuLabel.text = data.menuName + + // 평균 별점 설정 + let ratingValue = data.rating + if ratingValue == 0.0 { + rateNumLabel.text = "-" + } else { + let total = String(format: "%.1f", ratingValue) + rateNumLabel.text = "\(total)" + } + totalRate = ratingValue + + // 별점 차트 업데이트 + let totalCount = max(data.totalReviewCount, 1) + fiveForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / totalCount) + } + fourForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / totalCount) + } + threeForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / totalCount) + } + twoForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / totalCount) + } + oneForeground.snp.updateConstraints { + $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / totalCount) + } + } +} From 2495cd62ae2efc88963cea2fd238fbcf5192a345 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 23 Nov 2025 16:12:17 +0900 Subject: [PATCH 14/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20api=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/NewMealListResponse.swift | 24 -- .../DTO/Review/NewMenuListResponse.swift | 32 --- .../DTO/Review/NewReviewListResponse.swift | 42 +++ .../DTO/Review/ReviewListResponse.swift | 5 - .../Review/ReviewMeuStatisticsResponse.swift | 1 - .../DTO/Review/ReviewRateResponse.swift | 2 - .../DTO/Review/TotalReviewResponse.swift | 23 -- .../Review/View/RateReview/RateView.swift | 8 +- .../View/SeeReview/ReviewTableCell.swift | 118 ++------ .../ViewController/ReviewViewController.swift | 84 +++++- .../SetRateViewController.swift | 256 +++++++++++------- 11 files changed, 306 insertions(+), 289 deletions(-) delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/TotalReviewResponse.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift deleted file mode 100644 index aa8c6c69..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewMealListResponse.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// NewMealListResponse.swift -// EATSSU -// -// Created by 한금준 on 11/16/25. -// - -struct ReviewMealResponse: Encodable { - let reviewId: Int - let writerId: Int - let isWriter: Bool - let writerNickname: String - let rating: Int - let writtenAt: String - let content: String? - let imageUrls: [String]? - let menuList: [ReviewMealInfo]? -} - -struct ReviewMealInfo: Encodable { - let menuId: Int - let name: String - let isLike: Bool -} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift deleted file mode 100644 index b5598503..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewMenuListResponse.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// NewReviewListResponse.swift -// EATSSU -// -// Created by 한금준 on 11/16/25. -// - -/// 리뷰 리스트 조회 API의 result 내부 DTO (공통) -struct NewMenuListResponse: Encodable { - let numberOfElements: Int? // menu API 응답 예시에 있음 - let hasNext: Bool - let dataList: [ReviewListItem] -} - -struct ReviewListItem: Encodable { - let reviewId: Int - let menuList: [ReviewMenuInfo]? - let writerId: Int - let isWriter: Bool // 리뷰 작성자인지 여부 - let writerNickname: String - let rating: Int - let writtenAt: String - let content: String? - let imageUrls: [String]? -} - -struct ReviewMenuInfo: Encodable { - let menuId: Int - let name: String - let isLike: Bool -} - diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift new file mode 100644 index 00000000..d2f311b9 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -0,0 +1,42 @@ +// +// NewReviewListResponse.swift +// EATSSU +// +// Created by 한금준 on 11/16/25. +// + +struct NewReviewListResponse: Codable { + let hasNext: Bool + let dataList: [ReviewListItem] +} + +/// 리뷰 리스트 조회 API의 result 내부 DTO (Menu용 - 페이지 기반) +struct NewMenuListResponse: Codable { + let numberOfElements: Int? + let hasNext: Bool + let dataList: [ReviewListItem] +} + +struct ReviewListItem: Codable { + let reviewId: Int + let menuList: [ReviewMenuInfo]? + let writerId: Int + let isWriter: Bool + let writerNickname: String +// let rating: Int + let rating: Double + let writtenAt: String + let content: String? + let imageUrls: [String]? +} + +struct ReviewMenuInfo: Codable { + let menuId: Int + let name: String + let isLike: Bool +} + +struct Tag: Codable { + let name: String + let isLiked: Bool +} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift index 7d46fde6..3c06baed 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift @@ -36,8 +36,3 @@ struct MenuDataList: Codable { case tags } } - -struct Tag: Codable { - let name: String - let isLiked: Bool -} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift index e18224ab..63b5d70b 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift @@ -10,7 +10,6 @@ struct ReviewMeuStatisticsResponse: Codable { let totalReviewCount: Int let rating: Double let likeCount: Int? - let dislikeCount: Int? let reviewRatingCount: ReviewRatingCount } diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift index d5a306ab..237213f7 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift @@ -11,8 +11,6 @@ struct ReviewRateResponse: Codable { let menuNames: [String] let totalReviewCount: Int let mainRating: Double? - let amountRating: Double? - let tasteRating: Double? let reviewRatingCount: StarCount } diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/TotalReviewResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/TotalReviewResponse.swift deleted file mode 100644 index e3a80265..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/TotalReviewResponse.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TotalReviewResponse.swift -// EatSSU-iOS -// -// Created by 박윤빈 on 2023/05/22. -// - -// import Foundation -// -// struct TotalReviewResponse: Codable { -// let menuName: String -// let totalReviewCount: Int -// let grade: Double -// let reviewGradeCnt: StarCount -// } -// -// struct StarCount: Codable { -// let fiveCnt: Int -// let fourCnt: Int -// let threeCnt: Int -// let twoCnt: Int -// let oneCnt: Int -// } diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift index 011a7a02..7eb30f74 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift @@ -82,9 +82,13 @@ final class RateView: BaseUIView { } func settingStarForFix(currentStar: Int) { - for i in 0 ... currentStar - 1 { - buttons[i].setImage(starFillImage, for: .normal) + // ✨ 수정: currentStar가 0일 때 0...-1 범위 오류를 방지 + if currentStar > 0 { + for i in 0 ... currentStar - 1 { + buttons[i].setImage(starFillImage, for: .normal) + } } + // 빈 별은 currentStar부터 시작 for i in currentStar ..< starNumber { buttons[i].setImage(starEmptyImage, for: .normal) } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 14e44a9f..30967b29 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -23,24 +23,6 @@ final class ReviewTableCell: UITableViewCell { // MARK: - UI Components lazy var totalRateView = RateNumberView() - // lazy var tasteRateView = RateNumberView() - // lazy var quantityRateView = RateNumberView() - - // private let tasteLabel: UILabel = { - // let label = UILabel() - // label.text = "맛" - // label.font = .body3 - // label.textColor = .black - // return label - // }() - - // private let quantityLabel: UILabel = { - // let label = UILabel() - // label.text = "양" - // label.font = .body3 - // label.textColor = .black - // return label - // }() // 태그 표시용 컬렉션 뷰 private lazy var tagCollectionView: UICollectionView = { @@ -80,19 +62,10 @@ final class ReviewTableCell: UITableViewCell { let label = UILabel() label.text = "hellosoongsil1234" label.font = .caption1 - // label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color label.textColor = .black return label }() - // private var menuNameLabel: UILabel = { - // let label = UILabel() - // label.text = "계란국" - // label.font = .caption3 - // label.textColor = .black - // return label - // }() - private let userProfileImageView: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.profile.image @@ -125,27 +98,9 @@ final class ReviewTableCell: UITableViewCell { return imageView }() - /// 맛 별점 - // lazy var tasteStackView: UIStackView = { - // let stackView = UIStackView(arrangedSubviews: [tasteLabel, tasteRateView]) - // stackView.axis = .horizontal - // stackView.spacing = 4.adjusted - // stackView.alignment = .center - // return stackView - // }() - - /// 양 별점 - // lazy var quantityStackView: UIStackView = { - // let stackView = UIStackView(arrangedSubviews: [quantityLabel, quantityRateView]) - // stackView.axis = .horizontal - // stackView.spacing = 4.adjusted - // stackView.alignment = .center - // return stackView - // }() - /// 별점 lazy var rateStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [totalRateView/*, tasteStackView, quantityStackView*/]) + let stackView = UIStackView(arrangedSubviews: [totalRateView]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center @@ -154,7 +109,7 @@ final class ReviewTableCell: UITableViewCell { /// 이름 + 메뉴 lazy var nameMenuStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [userNameLabel,/* menuNameLabel*/]) + let stackView = UIStackView(arrangedSubviews: [userNameLabel]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center @@ -187,14 +142,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - // lazy var contentStackView: UIStackView = { - // let stackView = UIStackView(arrangedSubviews: [reviewTextView, foodImageView]) - // stackView.axis = .vertical - // stackView.spacing = 8.adjusted - // stackView.alignment = .leading - // return stackView - // }() - // MARK: - Functions override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -219,6 +166,9 @@ final class ReviewTableCell: UITableViewCell { sideButton.setImage(UIImage(), for: .normal) foodImageView.image = UIImage() foodImageView.isHidden = true + reviewTextView.text = "" + dateLabel.text = "" + userNameLabel.text = "" } func setLayout() { @@ -286,66 +236,46 @@ extension ReviewTableCell: UICollectionViewDataSource { // MARK: - Data Bind extension ReviewTableCell { + // ✨ V2 API 데이터 바인딩 func dataBind(response: MenuDataList) { - // menuNameLabel.text = response.menu menuName = response.menu userNameLabel.text = response.writerNickname totalRateView.setRating(response.mainRating) - // totalRateView.rateNumberLabel.text = "\(response.mainRating)" - - // if response.tasteRating == nil { - // tasteStackView.isHidden = true - // } else { - // tasteStackView.isHidden = false - // tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" - // } - - // if response.amountRating == nil { - // quantityStackView.isHidden = true - // } else { - // quantityStackView.isHidden = false - // quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" - // } dateLabel.text = response.writedAt reviewTextView.text = response.content reviewId = response.reviewID + + // 이미지 처리 if let firstImageUrl = response.imgURLList.compactMap({ $0 }).first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) } else { foodImageView.isHidden = true } + + // 버튼 설정 sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) - - // tags = (response.tags ?? []).map { ($0.name, $0.isLiked) } - tags = (response.tags ?? [Tag(name: "기본태그", isLiked: true), - Tag(name: "추천", isLiked: false)]) - .map { ($0.name, $0.isLiked) } + // ✨ 태그 처리 (V2 API에서는 menuList가 태그 역할) + if let menuTags = response.tags, !menuTags.isEmpty { + tags = menuTags.map { ($0.name, $0.isLiked) } + } else { + tags = [] + } tagCollectionView.reloadData() + // 태그가 없으면 컬렉션뷰 숨기기 + tagCollectionView.isHidden = tags.isEmpty } func myPageDataBind(response: MyDataList, nickname: String) { userNameLabel.text = "\(nickname)" - // menuNameLabel.text = response.menuName -// totalRateView.rateNumberLabel.text = "\(response.mainRating)" - // if response.tasteRating == nil { - // tasteStackView.isHidden = true - // } else { - // tasteStackView.isHidden = false - // tasteRateView.rateNumberLabel.text = "\(response.tasteRating ?? 0)" - // } - - // if response.amountRating == nil { - // quantityStackView.isHidden = true - // } else { - // quantityStackView.isHidden = false - // quantityRateView.rateNumberLabel.text = "\(response.amountRating ?? 0)" - // } + totalRateView.setRating(response.mainRating) dateLabel.text = response.writeDate reviewTextView.text = response.content + + // 이미지 처리 if response.imgURLList.count != 0 { if response.imgURLList[0] != "" { foodImageView.isHidden = false @@ -354,9 +284,15 @@ extension ReviewTableCell { } else { foodImageView.isHidden = true } + + // 버튼 설정 sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.setTitle("", for: .normal) reviewId = response.reviewID + + // 마이페이지에서는 태그 숨김 + tags = [] + tagCollectionView.isHidden = true } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index f1d10f68..f1b0da4c 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -491,22 +491,78 @@ extension ReviewViewController { } } - // 하단 리뷰 리스트 불러오는 API + // ✨ V2 API: 리뷰 리스트 불러오기 func getReviewList(type: String, menuId _: Int) { - reviewProvider.request(.reviewList(type, menuID)) { response in - switch response { - case let .success(moyaResponse): - do { - let responseData = try moyaResponse.map(BaseResponse.self) - guard let data = responseData.result else { return } - self.reviewList = data.dataList - self.reviewTableView.reloadData() - - } catch let err { - print(err.localizedDescription) + if type == "FIXED" { + getFixedMenuReviewList() + } else { + getMealReviewList() + } + } + + // ✨ V2 API: 고정 메뉴 리뷰 리스트 + func getFixedMenuReviewList() { + NetworkService.shared.request( + ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: 0, size: 20), + responseType: NewMenuListResponse.self, + useAuth: false + ) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let data): + self.reviewList = data.dataList.map { item in + MenuDataList( + reviewID: item.reviewId, + menu: item.menuList?.first?.name ?? "", + writerID: item.writerId, + isWriter: item.isWriter, + writerNickname: item.writerNickname, + mainRating: item.rating, + amountRating: nil, + tasteRating: nil, + writedAt: item.writtenAt, + content: item.content ?? "", + imgURLList: item.imageUrls ?? [], + + tags: item.menuList?.map { Tag(name: $0.name, isLiked: $0.isLike) } + ) } - case let .failure(err): - print(err.localizedDescription) + self.reviewTableView.reloadData() + case .failure(let error): + print("Fixed Menu Review List Error: \(error.localizedDescription)") + } + } + } + + // ✨ V2 API: 식단 리뷰 리스트 + func getMealReviewList() { + NetworkService.shared.request( + ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: nil, size: 20), + responseType: NewMenuListResponse.self, + useAuth: true + ) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let data): + self.reviewList = data.dataList.map { item in + MenuDataList( + reviewID: item.reviewId, + menu: item.menuList?.map { $0.name }.joined(separator: " + ") ?? "", + writerID: item.writerId, + isWriter: item.isWriter, + writerNickname: item.writerNickname, + mainRating: item.rating, + amountRating: nil, + tasteRating: nil, + writedAt: item.writtenAt, + content: item.content ?? "", + imgURLList: item.imageUrls ?? [], + tags: item.menuList?.map { Tag(name: $0.name, isLiked: $0.isLike) } + ) + } + self.reviewTableView.reloadData() + case .failure(let error): + print("Meal Review List Error: \(error.localizedDescription)") } } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index e655a67b..b2db45a4 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -16,13 +16,12 @@ final class SetRateViewController: BaseViewController { // MARK: - Properties private var currentPage: Int = 0 { - didSet { -// menuLabel.text = "\(selectedList[currentPage]) 을/를 추천하시겠어요?" - if currentPage == selectedList.count - 1 { - nextButton.setTitle("리뷰 남기기", for: .normal) + didSet { + // V2 Meal Review는 단일 페이지이므로 항상 '리뷰 남기기'로 표시되어야 함 + // V1의 페이지 넘김 로직을 제거하고, 항상 리뷰 남기기 버튼을 보여줍니다. + nextButton.setTitle("리뷰 남기기", for: .normal) // ✨ 수정: 항상 "리뷰 남기기"로 설정 } } - } private var userPickedImage: UIImage? private var reviewList: [(BeforeSelectedImageDTO, UIImage?)] = [] @@ -285,18 +284,23 @@ final class SetRateViewController: BaseViewController { self.likedStates = Array(repeating: false, count: self.selectedList.count) // reviewList 초기화 (API 결과에 따라 갯수 맞춤) - self.reviewList = Array(repeating: (BeforeSelectedImageDTO(mainRating: 0, - amountRating: nil, - tasteRating: nil, - content: ""), - nil), count: self.validMenuIDList.count) +// self.reviewList = Array(repeating: (BeforeSelectedImageDTO(mainRating: 0, +// amountRating: nil, +// tasteRating: nil, +// content: ""), +// nil), count: self.validMenuIDList.count) + self.reviewList = [(BeforeSelectedImageDTO(mainRating: 0, + amountRating: nil, + tasteRating: nil, + content: ""), + nil)] // ✨ 수정: 1개만 초기화 // 테이블 뷰 리로드 self.menuTableView.reloadData() // viewDidLayoutSubviews를 호출하여 높이 제약조건 업데이트 self.view.setNeedsLayout() - // currentPage 초기화 + // currentPage 초기화 및 버튼 텍스트 업데이트 self.currentPage = 0 case .failure(let error): @@ -528,64 +532,90 @@ final class SetRateViewController: BaseViewController { } @objc - func tappedNextButton() { - if userReviewTextView.text == "3글자 이상 작성해주세요!" || userReviewTextView.text.count < 3 { - showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) - } else { - // 별점 검사: rateView는 메인 별점, quantity/tasteRateView는 사용되지 않으므로 rateView만 확인 - if rateView.currentStar != 0 /*, quantityRateView.currentStar != 0, tasteRateView.currentStar != 0*/ { - // 리뷰 작성하기 버튼이 isEnabled = true일 때의 area - let param = BeforeSelectedImageDTO(mainRating: rateView.currentStar, - amountRating: quantityRateView.currentStar, - tasteRating: tasteRateView.currentStar, - content: userReviewTextView.text) - - switch reviewId { - case .none: - /// 현재 이미지를 별도 변수에 저장 - let currentImage = userPickedImage - reviewList[currentPage] = (param, currentImage) - - /// 현재 페이지가 마지막 메뉴에 대한 리뷰페이지일 때의 액션 - if currentPage == selectedList.count - 1 { + func tappedNextButton() { + // ✨ 수정: V1의 페이지 로직을 제거하고, 바로 데이터 전송 로직 호출 + if userReviewTextView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" || userReviewTextView.text.count < 3 { + showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) + } else { + // 별점 검사: rateView는 메인 별점 + if rateView.currentStar != 0 { + // 리뷰 작성하기 버튼이 isEnabled = true일 때의 area + let param = BeforeSelectedImageDTO(mainRating: rateView.currentStar, + amountRating: quantityRateView.currentStar, + tasteRating: tasteRateView.currentStar, + content: userReviewTextView.text) + + switch reviewId { + case .none: + /// 현재 이미지를 별도 변수에 저장 + let currentImage = userPickedImage + // 단일 페이지이므로 reviewList의 0번 인덱스에 저장 + reviewList[0] = (param, currentImage) // ✨ 수정: currentPage 대신 0번 인덱스 사용 + navigationController?.isNavigationBarHidden = false - sendDataIfCurrentPageIsLast() - } else { - // 다음 리뷰를 위해 현재 화면의 이미지 초기화 - userPickedImage = nil - userReviewImageView.image = nil - imageCountLabel.text = "사진 0/1" - prepareForNextReview() + sendDataIfCurrentPageIsLast() // ✨ 수정: 바로 API 전송 + + case let .some(reviewID): + // 단일 리뷰 수정 로직 (기존 로직 유지) + patchFixedReview(reviewId: reviewID, param: param) } - case let .some(reviewID): - patchFixedReview(reviewId: reviewID, param: param) + } else { + showToast(message: "별점을 모두 입력해주세요!", type: .info) } - - } else { - showToast(message: "별점을 모두 입력해주세요!", type: .info) } } - } - + + // ✨ 수정: V2 Meal Review API를 사용하도록 변경 private func sendDataIfCurrentPageIsLast() { + guard let mealId = mealID else { return } // mealId가 없으면 전송 불가 + _Concurrency.Task { do { - for (index, review) in reviewList.enumerated() { - let (reviewDTO, image) = review - - // Firebase 이벤트 로그 - let photoAttached = (image != nil) ? 1 : 0 - let rating = reviewDTO.mainRating - let selection = self.selectedList.count - ReviewAnalyticsManager.shared.logCompleteReviewV1(photoAttached: photoAttached, rating: rating, selection: selection) - - // 순차적으로 업로드 - try await uploadReview(reviewDTO: reviewDTO, image: image, menuId: validMenuIDList[index]) // ✨ 수정: selectedIDList -> validMenuIDList + // 1. 이미지 업로드 (만약 이미지가 있다면) + var imageUrl: String? +// if let image = reviewList.last?.1 { // 마지막 리뷰의 이미지만 사용 (Meal Review는 이미지 1개) +// imageUrl = try await uploadImage(image: image) +// } +// + if let image = reviewList.first?.1 { // ✨ 수정: .last? -> .first? 로 변경 (단일 리뷰이므로) + imageUrl = try await uploadImage(image: image) + } + + // 2. Meal Review 요청 객체 생성 + let menuLikes: [MenuLike] = validMenuIDList.enumerated().map { (index, menuId) in + MenuLike(menuId: menuId, isLike: likedStates[index]) } + // Meal Review는 하나의 평점/내용/이미지를 사용하므로, 마지막 메뉴의 리뷰 데이터를 사용 +// guard let lastReview = reviewList.last else { +// throw NSError(domain: "ReviewError", code: -1, userInfo: [NSLocalizedDescriptionKey: "리뷰 데이터가 없습니다."]) +// } + + guard let lastReview = reviewList.first else { // ✨ 수정: .last? -> .first? 로 변경 + throw NSError(domain: "ReviewError", code: -1, userInfo: [NSLocalizedDescriptionKey: "리뷰 데이터가 없습니다."]) + } + + let request = WriteReviewMealRequest( + mealId: mealId, + rating: lastReview.0.mainRating, // 메인 평점 + menuLikes: menuLikes, // 메뉴별 좋아요 상태 + content: lastReview.0.content, // 리뷰 내용 + imageUrls: imageUrl != nil ? [imageUrl!] : nil // 이미지 URL + ) + + // Firebase 이벤트 로그 (V1 로그 제거 또는 V2 로직에 맞게 수정 필요) + // 현재는 단일 요청으로 통합되었으므로, 마지막 리뷰 기준으로만 로그를 남깁니다. + let photoAttached = (imageUrl != nil) ? 1 : 0 + let rating = lastReview.0.mainRating + let selection = self.selectedList.count + ReviewAnalyticsManager.shared.logCompleteReviewV1(photoAttached: photoAttached, rating: rating, selection: selection) + + // 3. Meal Review 전송 + try await postMealReview(request: request) + await MainActor.run { - self.moveToReviewVC() + self.moveToReviewVC() // ✨ 수정: 성공 시 화면 이동 } } catch { @@ -597,18 +627,38 @@ final class SetRateViewController: BaseViewController { } } - private func uploadReview(reviewDTO: BeforeSelectedImageDTO, image: UIImage?, menuId: Int) async throws { - if let image = image { - // 이미지 업로드 후 리뷰 작성 - let imageUrl = try await uploadImage(image: image) - let request = WriteReviewRequest(content: reviewDTO, imageURL: imageUrl) - try await postReview(request: request, menuId: menuId) - } else { - // 이미지 없이 리뷰만 작성 - let request = WriteReviewRequest(content: reviewDTO, imageURL: "") - try await postReview(request: request, menuId: menuId) + // ✨ 수정: 기존 uploadReview를 제거하고, postMealReview를 추가 + private func postMealReview(request: WriteReviewMealRequest) async throws { + try await withCheckedThrowingContinuation { continuation in + NetworkService.shared.request( + WriteReviewRouter.writeMealReview(param: request), + responseType: Bool.self, // 응답 타입이 Bool이라고 가정 + useAuth: true + ) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } } } + + // 이 메서드는 단일 메뉴 리뷰를 위한 것이었으나, 현재 Meal Review 로직에서는 사용되지 않습니다. + // Menu Review (V2) 사용 시 재활용 가능성을 위해 주석 처리 없이 남겨둡니다. +// private func uploadReview(reviewDTO: BeforeSelectedImageDTO, image: UIImage?, menuId: Int) async throws { +// if let image = image { +// // 이미지 업로드 후 리뷰 작성 +// let imageUrl = try await uploadImage(image: image) +// let request = WriteReviewRequest(content: reviewDTO, imageURL: imageUrl) +// try await postReview(request: request, menuId: menuId) +// } else { +// // 이미지 없이 리뷰만 작성 +// let request = WriteReviewRequest(content: reviewDTO, imageURL: "") +// try await postReview(request: request, menuId: menuId) +// } +// } private func uploadImage(image: UIImage) async throws -> String { try await withCheckedThrowingContinuation { continuation in @@ -640,25 +690,25 @@ final class SetRateViewController: BaseViewController { closeButton.isHidden = true // Show close button when image is selected } - private func prepareForNextReview() { - let setRateVC: SetRateViewController - - // ✨ 수정: 다음 페이지로 이동할 때 현재 mealId를 전달 - if let mealId = self.mealID { - setRateVC = SetRateViewController(mealId: mealId) - } else { - // mealId가 없으면 기존처럼 인자 없이 초기화 (예: 고정 메뉴 리뷰 수정 후 다음 단계) - setRateVC = SetRateViewController() - } - - setRateVC.dataBind(list: selectedList, - idList: validMenuIDList, // ✨ 수정: selectedIDList -> validMenuIDList - reviewList: reviewList, - currentPage: currentPage + 1) - navigationController?.pushViewController(setRateVC, animated: true) - } - - // 리뷰 리스트 보는 화면으로 넘어가도록 하는 함수 +// private func prepareForNextReview() { +// let setRateVC: SetRateViewController +// +// // ✨ 수정: 다음 페이지로 이동할 때 현재 mealId를 전달 +// if let mealId = self.mealID { +// setRateVC = SetRateViewController(mealId: mealId) +// } else { +// // mealId가 없으면 기존처럼 인자 없이 초기화 (예: 고정 메뉴 리뷰 수정 후 다음 단계) +// setRateVC = SetRateViewController() +// } +// +// setRateVC.dataBind(list: selectedList, +// idList: validMenuIDList, // ✨ 수정: selectedIDList -> validMenuIDList +// reviewList: reviewList, +// currentPage: currentPage + 1) +// navigationController?.pushViewController(setRateVC, animated: true) +// } + + // ✨ 수정: ReviewViewController로 돌아가는 로직 적용 private func moveToReviewVC() { if let reviewViewController = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) { navigationController?.popToViewController(reviewViewController, animated: true) @@ -690,13 +740,13 @@ final class SetRateViewController: BaseViewController { // MARK: - Server extension SetRateViewController { - /// 이미지 O -> URL 받고, URL을 넣어서 리뷰 작성 요청 - /// 이미지 X -> URL 없이 리뷰 작성 요청 - /// 이미지가 아예 없을 때 어떤 경우로 빠지는지 보고, 거기에서 호출하도록 하기 - private func postReview(request: WriteReviewRequest, menuId: Int) async throws { + + // ✨ 수정: V1 postReview 제거 (V2 Meal Review 사용) + // V2 Menu Review API (단일 메뉴 리뷰) + private func postMenuReview(request: WriteReviewMenuRequest) async throws { try await withCheckedThrowingContinuation { continuation in NetworkService.shared.request( - WriteReviewRouter.writeNewReview(param: request, menuID: menuId), + WriteReviewRouter.writeMenuReview(param: request), responseType: Bool.self, useAuth: true ) { result in @@ -710,6 +760,9 @@ extension SetRateViewController { } } + // V2 Meal Review API (식사 리뷰) - `sendDataIfCurrentPageIsLast()`에서 사용 + // private func postMealReview(request: WriteReviewMealRequest) async throws { ... } // 위에 정의됨 + private func patchFixedReview(reviewId: Int, param: BeforeSelectedImageDTO) { NetworkService.shared.request( ReviewRouter.fixReview(reviewId, param), @@ -762,16 +815,25 @@ extension SetRateViewController: UIImagePickerControllerDelegate { extension SetRateViewController: UITextViewDelegate { func textView(_: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - let newLength = userReviewTextView.text.count - range.length + text.count - maximumWordLabel.text = "\(userReviewTextView.text.count) / 300" + let currentText = userReviewTextView.text ?? "" + guard let stringRange = Range(range, in: currentText) else { return false } + let newLength = currentText.count + text.count - range.length + + // 최대 글자수 제한 if newLength > 300 { return false } + + // 글자수 레이블 업데이트는 항상 허용 + // **주의**: `newLength`는 아직 적용되지 않은 새로운 문자열의 길이 + let textToDisplay = currentText.replacingCharacters(in: stringRange, with: text) + maximumWordLabel.text = "\(textToDisplay.count) / 300" + return true } func textViewDidBeginEditing(_ textView: UITextView) { - if textView.text == "3글자 이상 작성해주세요!" { + if textView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" { textView.text = "" textView.textColor = .black } @@ -779,8 +841,12 @@ extension SetRateViewController: UITextViewDelegate { func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { - textView.text = "3글자 이상 작성해주세요!" + textView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" textView.textColor = .gray500 + maximumWordLabel.text = "0 / 300" + } else { + // 끝났을 때 현재 글자 수 업데이트 + maximumWordLabel.text = "\(textView.text.count) / 300" } } } From 3596814083a3d2554587305861e8eab00e0498d0 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 23 Nov 2025 16:38:44 +0900 Subject: [PATCH 15/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20DTO=20?= =?UTF-8?q?=EB=B0=8F=20Router=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/BeforeSelectedImageDTO.swift | 38 +++--- .../DTO/Review/FixedReviewRateResponse.swift | 28 ++--- .../Network/DTO/Review/MenuInfoResponse.swift | 16 +-- .../DTO/Review/MenuReviewResponse.swift | 40 +++---- .../DTO/Review/NewReviewListResponse.swift | 17 ++- .../DTO/Review/ReviewListResponse.swift | 70 ++++++------ .../Review/ReviewMealStatisticsResponse.swift | 9 +- ...ift => ReviewMenuStatisticsResponse.swift} | 5 +- .../DTO/Review/ReviewRateResponse.swift | 40 +++---- .../DTO/Review/ReviewValidMenuResponse.swift | 3 +- .../DTO/Review/WriteReviewMealRequest.swift | 1 + .../DTO/Review/WriteReviewMenuRequest.swift | 2 +- .../DTO/Review/WriteReviewRequest.swift | 58 +++++----- .../Data/Network/Router/ReviewRouter.swift | 108 +++++++++--------- .../Network/Router/WriteReviewRouter.swift | 68 +++++------ 15 files changed, 252 insertions(+), 251 deletions(-) rename EATSSU/App/Sources/Data/Network/DTO/Review/{ReviewMeuStatisticsResponse.swift => ReviewMenuStatisticsResponse.swift} (80%) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift index 9fb4a2d0..1143bf29 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift @@ -1,22 +1,22 @@ +//// +//// BeforeSelectedImageDTO.swift +//// EAT-SSU +//// +//// Created by 박윤빈 on 3/7/24. +//// // -// BeforeSelectedImageDTO.swift -// EAT-SSU +//import Foundation // -// Created by 박윤빈 on 3/7/24. +//struct BeforeSelectedImageDTO: Codable { +// let mainRating: Int +// let amountRating: Int? +// let tasteRating: Int? +// let content: String // - -import Foundation - -struct BeforeSelectedImageDTO: Codable { - let mainRating: Int - let amountRating: Int? - let tasteRating: Int? - let content: String - - init(mainRating: Int, amountRating: Int?, tasteRating: Int?, content: String) { - self.mainRating = mainRating - self.amountRating = amountRating - self.tasteRating = tasteRating - self.content = content - } -} +// init(mainRating: Int, amountRating: Int?, tasteRating: Int?, content: String) { +// self.mainRating = mainRating +// self.amountRating = amountRating +// self.tasteRating = tasteRating +// self.content = content +// } +//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift index 439a3ca7..4c954359 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift @@ -1,17 +1,17 @@ +//// +//// FixedReviewRateResponse.swift +//// EAT-SSU +//// +//// Created by 박윤빈 on 3/18/24. +//// // -// FixedReviewRateResponse.swift -// EAT-SSU +//import Foundation // -// Created by 박윤빈 on 3/18/24. +//// MARK: - FixedReviewRateResponse // - -import Foundation - -// MARK: - FixedReviewRateResponse - -struct FixedReviewRateResponse: Codable { - let menuName: String - let totalReviewCount: Int - let mainRating, amountRating, tasteRating: Double? - let reviewRatingCount: StarCount -} +//struct FixedReviewRateResponse: Codable { +// let menuName: String +// let totalReviewCount: Int +// let mainRating, amountRating, tasteRating: Double? +// let reviewRatingCount: StarCount +//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift index 840c536e..56d87968 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift @@ -1,10 +1,10 @@ +//// +//// MenuInfoResponse.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/05/18. +//// // -// MenuInfoResponse.swift -// EatSSU-iOS +//import Foundation // -// Created by 박윤빈 on 2023/05/18. -// - -import Foundation - -struct MenuInfoResponse: Codable {} +//struct MenuInfoResponse: Codable {} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift index 871bd6bc..5fa5b907 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift @@ -1,23 +1,23 @@ +//// +//// MenuReviewResponse.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/05/18. +//// // -// MenuReviewResponse.swift -// EatSSU-iOS +//import Foundation // -// Created by 박윤빈 on 2023/05/18. +//struct MenuReviewResponse: Codable { +// let numberOfElements: Int +// let hasNext: Bool +// let dataList: [DataList]? +//} // - -import Foundation - -struct MenuReviewResponse: Codable { - let numberOfElements: Int - let hasNext: Bool - let dataList: [DataList]? -} - -struct DataList: Codable { - let writerId: Int - let writerNickname: String - let grade: Int - let writeDate, content: String - let tagList: [String] - let imgUrlList: [String] -} +//struct DataList: Codable { +// let writerId: Int +// let writerNickname: String +// let grade: Int +// let writeDate, content: String +// let tagList: [String] +// let imgUrlList: [String] +//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift index d2f311b9..138cd863 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -5,13 +5,13 @@ // Created by 한금준 on 11/16/25. // -struct NewReviewListResponse: Codable { - let hasNext: Bool - let dataList: [ReviewListItem] -} +//struct NewReviewListResponse: Codable { +// let hasNext: Bool +// let dataList: [ReviewListItem] +//} -/// 리뷰 리스트 조회 API의 result 내부 DTO (Menu용 - 페이지 기반) -struct NewMenuListResponse: Codable { +/// 리뷰 V2 리스트 조회 API의 result 내부 DTO (Menu용 - 페이지 기반) +struct NewReviewListResponse: Codable { let numberOfElements: Int? let hasNext: Bool let dataList: [ReviewListItem] @@ -19,12 +19,11 @@ struct NewMenuListResponse: Codable { struct ReviewListItem: Codable { let reviewId: Int - let menuList: [ReviewMenuInfo]? + let menu: [ReviewMenuInfo]? let writerId: Int let isWriter: Bool let writerNickname: String -// let rating: Int - let rating: Double + let rating: Int let writtenAt: String let content: String? let imageUrls: [String]? diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift index 3c06baed..c2612dd6 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift @@ -1,38 +1,38 @@ +//// +//// ReviewListResponse.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/08/01. +//// // -// ReviewListResponse.swift -// EatSSU-iOS +//import Foundation // -// Created by 박윤빈 on 2023/08/01. +//struct ReviewListResponse: Codable { +// let numberOfElements: Int +// let hasNext: Bool +// let dataList: [MenuDataList] +//} // - -import Foundation - -struct ReviewListResponse: Codable { - let numberOfElements: Int - let hasNext: Bool - let dataList: [MenuDataList] -} - -// MARK: - DataList - -struct MenuDataList: Codable { - let reviewID: Int - let menu: String - let writerID: Int? - let isWriter: Bool - let writerNickname: String - let mainRating: Int - let amountRating, tasteRating: Int? - let writedAt, content: String - let imgURLList: [String?] - let tags: [Tag]? - - enum CodingKeys: String, CodingKey { - case reviewID = "reviewId" - case menu - case writerID = "writerId" - case isWriter, writerNickname, mainRating, amountRating, tasteRating, writedAt, content - case imgURLList = "imageUrls" - case tags - } -} +//// MARK: - DataList +// +//struct MenuDataList: Codable { +// let reviewID: Int +// let menu: String +// let writerID: Int? +// let isWriter: Bool +// let writerNickname: String +// let mainRating: Int +// let amountRating, tasteRating: Int? +// let writedAt, content: String +// let imgURLList: [String?] +// let tags: [Tag]? +// +// enum CodingKeys: String, CodingKey { +// case reviewID = "reviewId" +// case menu +// case writerID = "writerId" +// case isWriter, writerNickname, mainRating, amountRating, tasteRating, writedAt, content +// case imgURLList = "imageUrls" +// case tags +// } +//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift index e41eb8f1..eb381c84 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift @@ -5,14 +5,13 @@ // Created by 한금준 on 11/16/25. // +// 리뷰V2 api struct ReviewMealStatisticsResponse: Codable { - // 식단에 포함된 메뉴 리스트 - let menuList: [MenuInfo] // Meal API 고유 필드 - + let menuList: [MenuInfo] let totalReviewCount: Int - let rating: Double // 메인 평균 별점 + let rating: Double let likeCount: Int? - let reviewRatingCount: ReviewRatingCount // 별점별 카운트 + let reviewRatingCount: ReviewRatingCount } struct MenuInfo: Codable { diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift similarity index 80% rename from EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift rename to EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift index 63b5d70b..6cf3ff67 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMeuStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift @@ -1,11 +1,12 @@ // -// ReviewMeuStatistics.swift +// ReviewMenuStatistics.swift // EATSSU // // Created by 한금준 on 11/16/25. // -struct ReviewMeuStatisticsResponse: Codable { +// 리뷰V2 api +struct ReviewMenuStatisticsResponse: Codable { let menuName: String let totalReviewCount: Int let rating: Double diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift index 237213f7..ef14b939 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift @@ -1,23 +1,23 @@ +//// +//// ReviewRateResponse.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/07/29. +//// // -// ReviewRateResponse.swift -// EatSSU-iOS +//import Foundation // -// Created by 박윤빈 on 2023/07/29. +//struct ReviewRateResponse: Codable { +// let menuNames: [String] +// let totalReviewCount: Int +// let mainRating: Double? +// let reviewRatingCount: StarCount +//} // - -import Foundation - -struct ReviewRateResponse: Codable { - let menuNames: [String] - let totalReviewCount: Int - let mainRating: Double? - let reviewRatingCount: StarCount -} - -struct StarCount: Codable { - let fiveStarCount: Int - let fourStarCount: Int - let threeStarCount: Int - let twoStarCount: Int - let oneStarCount: Int -} +//struct StarCount: Codable { +// let fiveStarCount: Int +// let fourStarCount: Int +// let threeStarCount: Int +// let twoStarCount: Int +// let oneStarCount: Int +//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift index 101f65c2..4b194f37 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewValidMenuResponse.swift @@ -5,8 +5,9 @@ // Created by 한금준 on 11/16/25. // +// 리뷰V2 식단 id를 통해 리뷰 작성할 수 있는 메뉴들 조회 struct ReviewValidMenusResponse: Codable { - let menuList: [ReviewValidMenu] // 메뉴 목록 배열 + let menuList: [ReviewValidMenu] } struct ReviewValidMenu: Codable { diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift index e734bad5..5bd64261 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift @@ -5,6 +5,7 @@ // Created by 한금준 on 11/16/25. // +// 리뷰v2 api struct WriteReviewMealRequest: Encodable { let mealId: Int let rating: Int diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift index 1a78044c..056ae88e 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift @@ -5,7 +5,7 @@ // Created by 한금준 on 11/16/25. // - +// 리뷰v2 api struct WriteReviewMenuRequest: Encodable { let rating: Int let menuLike: MenuLikeItem diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift index c6760a41..6d62999e 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift @@ -1,32 +1,32 @@ +//// +//// WriteReviewRequest.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/05/24. +//// // -// WriteReviewRequest.swift -// EatSSU-iOS +//import UIKit // -// Created by 박윤빈 on 2023/05/24. +//struct WriteReviewRequest: Codable { +// let mainRating: Int +// let amountRating: Int? +// let tasteRating: Int? +// let content: String +// let imageUrl: String // - -import UIKit - -struct WriteReviewRequest: Codable { - let mainRating: Int - let amountRating: Int? - let tasteRating: Int? - let content: String - let imageUrl: String - - init(mainRating: Int, amountRating: Int, tasteRating: Int, content: String, imageURL: String?) { - self.mainRating = mainRating - self.amountRating = amountRating - self.tasteRating = tasteRating - self.content = content - imageUrl = imageURL ?? "" - } - - init(content: BeforeSelectedImageDTO, imageURL: String?) { - mainRating = content.mainRating - amountRating = content.amountRating - tasteRating = content.tasteRating - self.content = content.content - imageUrl = imageURL ?? "" - } -} +// init(mainRating: Int, amountRating: Int, tasteRating: Int, content: String, imageURL: String?) { +// self.mainRating = mainRating +// self.amountRating = amountRating +// self.tasteRating = tasteRating +// self.content = content +// imageUrl = imageURL ?? "" +// } +// +// init(content: BeforeSelectedImageDTO, imageURL: String?) { +// mainRating = content.mainRating +// amountRating = content.amountRating +// tasteRating = content.tasteRating +// self.content = content.content +// imageUrl = imageURL ?? "" +// } +//} diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index e27d9624..144222c4 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -9,14 +9,14 @@ import Foundation import Moya enum ReviewRouter { - // 상단 메뉴 별점 불러오는 API -> 두개로 쪼개짐. 고정, 변동 분기처리는 아래에서! - case reviewRate(_ type: String, _ id: Int) - - // 하단 리뷰 리스트 불러오는 API - case reviewList(_ type: String, _ id: Int) +// // 상단 메뉴 별점 불러오는 API -> 두개로 쪼개짐. 고정, 변동 분기처리는 아래에서! +// case reviewRate(_ type: String, _ id: Int) +// +// // 하단 리뷰 리스트 불러오는 API +// case reviewList(_ type: String, _ id: Int) case report(param: ReportRequest) case deleteReview(_ reviewId: Int) - case fixReview(_ reviewId: Int, _ param: BeforeSelectedImageDTO) +// case fixReview(_ reviewId: Int, _ param: BeforeSelectedImageDTO) // MARK: - New V2 API: 리뷰 작성이 가능한 메뉴 목록 조회 case getValidMenusForReview(_ mealId: Int) @@ -36,23 +36,23 @@ extension ReviewRouter: TargetType { var path: String { switch self { - case let .reviewRate(type, id): - switch type { - case "VARIABLE": - "/reviews/meals/\(id)" - case "FIXED": - "/reviews/menus/\(id)" - default: - "" - } - case .reviewList: - "/reviews" +// case let .reviewRate(type, id): +// switch type { +// case "VARIABLE": +// "/reviews/meals/\(id)" +// case "FIXED": +// "/reviews/menus/\(id)" +// default: +// "" +// } +// case .reviewList: +// "/reviews" case .report: "/reports" case let .deleteReview(reviewId): "/reviews/\(reviewId)" - case let .fixReview(reviewId, _): - "/reviews/\(reviewId)" +// case let .fixReview(reviewId, _): +// "/reviews/\(reviewId)" // MARK: - New V2 Path case let .getValidMenusForReview(mealId): "/v2/reviews/meal/valid-for-review/\(mealId)" // Path Parameter 사용 @@ -74,56 +74,56 @@ extension ReviewRouter: TargetType { var method: Moya.Method { switch self { - case .reviewRate, .reviewList, .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: + case /*.reviewRate,*/ /*.reviewList, */.getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: .get case .report: .post case .deleteReview: .delete - case .fixReview: - .patch +// case .fixReview: +// .patch } } var task: Moya.Task { switch self { - case let .reviewRate(type, id): - switch type { - case "VARIABLE": - .requestParameters(parameters: ["mealId": id], - encoding: URLEncoding.queryString) - case "FIXED": - .requestParameters(parameters: ["menuId": id], - encoding: URLEncoding.queryString) - default: - .requestPlain - } +// case let .reviewRate(type, id): +// switch type { +// case "VARIABLE": +// .requestParameters(parameters: ["mealId": id], +// encoding: URLEncoding.queryString) +// case "FIXED": +// .requestParameters(parameters: ["menuId": id], +// encoding: URLEncoding.queryString) +// default: +// .requestPlain +// } /// 이후 정렬 순서, 리뷰 로드 개수 등 수정 필요하면 고치기 - case let .reviewList(type, id): - switch type { - case "VARIABLE": - .requestParameters(parameters: ["menuType": type, - "mealId": id, - "page": 0, - "size": 20, - "sort": "date,DESC"], - encoding: URLEncoding.queryString) - case "FIXED": - .requestParameters(parameters: ["menuType": type, - "menuId": id, - "page": 0, - "size": 20, - "sort": "date,DESC"], - encoding: URLEncoding.queryString) - default: - .requestPlain - } +// case let .reviewList(type, id): +// switch type { +// case "VARIABLE": +// .requestParameters(parameters: ["menuType": type, +// "mealId": id, +// "page": 0, +// "size": 20, +// "sort": "date,DESC"], +// encoding: URLEncoding.queryString) +// case "FIXED": +// .requestParameters(parameters: ["menuType": type, +// "menuId": id, +// "page": 0, +// "size": 20, +// "sort": "date,DESC"], +// encoding: URLEncoding.queryString) +// default: +// .requestPlain +// } case let .report(param: param): .requestJSONEncodable(param) case .deleteReview: .requestPlain - case let .fixReview(_, param): - .requestJSONEncodable(param) +// case let .fixReview(_, param): +// .requestJSONEncodable(param) // MARK: - New V2 Task case .getValidMenusForReview: // Path에 ID가 포함되므로 Body나 QueryString 없음 diff --git a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift index 023361e9..889cdfb8 100644 --- a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift @@ -11,8 +11,8 @@ import Moya enum WriteReviewRouter { case uploadImage(image: UIImage?) - case writeNewReview(param: WriteReviewRequest, menuID: Int) - case writeReview(param: WriteReviewRequest, image: [UIImage?], menuId: Int) +// case writeNewReview(param: WriteReviewRequest, menuID: Int) +// case writeReview(param: WriteReviewRequest, image: [UIImage?], menuId: Int) // MARK: - New V2 APIs case writeMenuReview(param: WriteReviewMenuRequest) @@ -26,12 +26,12 @@ extension WriteReviewRouter: TargetType { var path: String { switch self { - case .writeReview(param: _, image: _, menuId: let menuId): - "/reviews/\(menuId)" +// case .writeReview(param: _, image: _, menuId: let menuId): +// "/reviews/\(menuId)" case .uploadImage: "/reviews/upload/image" - case .writeNewReview(param: _, menuID: let menuId): - "/reviews/write/\(menuId)" +// case .writeNewReview(param: _, menuID: let menuId): +// "/reviews/write/\(menuId)" // MARK: - New V2 Paths case .writeMenuReview: @@ -43,36 +43,36 @@ extension WriteReviewRouter: TargetType { var method: Moya.Method { switch self { - case .writeReview, .uploadImage, .writeNewReview, .writeMenuReview, .writeMealReview: + case /*.writeReview,*/ .uploadImage, /*.writeNewReview,*/ .writeMenuReview, .writeMealReview: .post } } var task: Moya.Task { switch self { - case .writeReview(param: let param, image: let imageList, menuId: _): - var multipartData = [MultipartFormData]() - do { - let jsonData = try JSONEncoder().encode(param) - multipartData.append(MultipartFormData(provider: .data(jsonData), - name: "createReviewRequest", - mimeType: "application/json")) - } catch { - print("Error encoding ReviewRequest: \(error)") - return .requestPlain - } - - for fileData in imageList { - if let unwrappedImage = fileData { - if let imageData = unwrappedImage.resize(newWidth: 300.adjusted).jpegData(compressionQuality: 0.3) { - multipartData.append(MultipartFormData(provider: .data(imageData), - name: "multipartFileList", - fileName: "image.jpg", - mimeType: "image/jpeg")) - } - } - } - return .uploadMultipart(multipartData) +// case .writeReview(param: let param, image: let imageList, menuId: _): +// var multipartData = [MultipartFormData]() +// do { +// let jsonData = try JSONEncoder().encode(param) +// multipartData.append(MultipartFormData(provider: .data(jsonData), +// name: "createReviewRequest", +// mimeType: "application/json")) +// } catch { +// print("Error encoding ReviewRequest: \(error)") +// return .requestPlain +// } +// +// for fileData in imageList { +// if let unwrappedImage = fileData { +// if let imageData = unwrappedImage.resize(newWidth: 300.adjusted).jpegData(compressionQuality: 0.3) { +// multipartData.append(MultipartFormData(provider: .data(imageData), +// name: "multipartFileList", +// fileName: "image.jpg", +// mimeType: "image/jpeg")) +// } +// } +// } +// return .uploadMultipart(multipartData) case let .uploadImage(image: image): var multipartData = [MultipartFormData]() @@ -85,8 +85,8 @@ extension WriteReviewRouter: TargetType { } return .uploadMultipart(multipartData) - case let .writeNewReview(param: param, _): - return .requestJSONEncodable(param) +// case let .writeNewReview(param: param, _): +// return .requestJSONEncodable(param) // MARK: - New V2 Tasks (JSON Encoded) case let .writeMenuReview(param: param): @@ -98,9 +98,9 @@ extension WriteReviewRouter: TargetType { var headers: [String: String]? { switch self { - case .writeNewReview, .writeMenuReview, .writeMealReview: + case /*.writeNewReview,*/ .writeMenuReview, .writeMealReview: return ["Content-Type": "application/json"] - case .uploadImage, .writeReview: + case .uploadImage/*, .writeReview*/: return ["Content-Type": "multipart/form-data"] } } From 6a59602809dd30ed1e0573a09bcbde9558e6fe97 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 23 Nov 2025 22:55:34 +0900 Subject: [PATCH 16/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=EC=9E=91=EC=84=B1=20api=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/BeforeSelectedImageDTO.swift | 31 +- .../DTO/Review/NewReviewListResponse.swift | 22 +- .../Review/ReviewMealStatisticsResponse.swift | 2 +- .../Review/ReviewMenuStatisticsResponse.swift | 2 +- .../MyReviewViewController.swift | 2 +- .../ChoiceMenuTableViewCell.swift | 176 ++-- .../View/SeeReview/RateNumberView.swift | 22 +- .../View/SeeReview/ReviewDividerCell.swift | 1 - .../View/SeeReview/ReviewEmptyViewCell.swift | 4 - .../View/SeeReview/ReviewRateViewCell.swift | 96 +-- .../View/SeeReview/ReviewTableCell.swift | 30 +- .../ReviewTagCollectionViewCell.swift | 12 +- .../ChoiceMenuViewController.swift | 348 ++++---- .../ViewController/ReviewViewController.swift | 309 +++---- .../SetRateViewController.swift | 780 ++++++------------ 15 files changed, 767 insertions(+), 1070 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift index 1143bf29..2f3f23f4 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift @@ -1,22 +1,15 @@ -//// -//// BeforeSelectedImageDTO.swift -//// EAT-SSU -//// -//// Created by 박윤빈 on 3/7/24. -//// // -//import Foundation +// BeforeSelectedImageDTO.swift +// EAT-SSU // -//struct BeforeSelectedImageDTO: Codable { -// let mainRating: Int -// let amountRating: Int? -// let tasteRating: Int? -// let content: String +// Created by 박윤빈 on 3/7/24. // -// init(mainRating: Int, amountRating: Int?, tasteRating: Int?, content: String) { -// self.mainRating = mainRating -// self.amountRating = amountRating -// self.tasteRating = tasteRating -// self.content = content -// } -//} + +import Foundation + +struct BeforeSelectedImageDTO: Codable { + let mainRating: Int + let amountRating: Int? + let tasteRating: Int? + let content: String +} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift index 138cd863..0da958d9 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -19,20 +19,38 @@ struct NewReviewListResponse: Codable { struct ReviewListItem: Codable { let reviewId: Int - let menu: [ReviewMenuInfo]? + var menu: [ReviewMenuInfo]? let writerId: Int let isWriter: Bool let writerNickname: String - let rating: Int + let rating: Double let writtenAt: String let content: String? let imageUrls: [String]? + + enum CodingKeys: String, CodingKey { + case reviewId + case menu = "menuList" + case writerId + case isWriter + case writerNickname + case rating + case writtenAt + case content + case imageUrls + } } struct ReviewMenuInfo: Codable { let menuId: Int let name: String let isLike: Bool + + enum CodingKeys: String, CodingKey { + case menuId = "id" + case name + case isLike + } } struct Tag: Codable { diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift index eb381c84..9e329a0a 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMealStatisticsResponse.swift @@ -9,7 +9,7 @@ struct ReviewMealStatisticsResponse: Codable { let menuList: [MenuInfo] let totalReviewCount: Int - let rating: Double + let rating: Double? let likeCount: Int? let reviewRatingCount: ReviewRatingCount } diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift index 6cf3ff67..9a136ca1 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewMenuStatisticsResponse.swift @@ -9,7 +9,7 @@ struct ReviewMenuStatisticsResponse: Codable { let menuName: String let totalReviewCount: Int - let rating: Double + let rating: Double? let likeCount: Int? let reviewRatingCount: ReviewRatingCount } diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index bf9d81be..90fd6e77 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -96,7 +96,7 @@ final class MyReviewViewController: BaseViewController { style: .default, handler: { _ in let setRateViewController = SetRateViewController() - setRateViewController.dataBindForFix(list: [menuName], reivewId: reviewID) + setRateViewController.dataBindForFix(list: [menuName], reviewId: reviewID) self.navigationController?.pushViewController(setRateViewController, animated: true) }) diff --git a/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift index 884913c3..bc2e6bf8 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift @@ -1,89 +1,89 @@ +//// +//// ChoiceMenuTableViewCell.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/06/29. +//// // -// ChoiceMenuTableViewCell.swift -// EatSSU-iOS -// -// Created by 박윤빈 on 2023/06/29. -// - -import UIKit - -import SnapKit - -import EATSSUDesign - -final class ChoiceMenuTableViewCell: UITableViewCell { - // MARK: - Properties - - static let identifier = "ChoiceMenuTableViewCell" - var isChecked: Bool = false { - didSet { - tapped() - } - } - - var handler: (() -> Void)? - - // MARK: - UI Components - - lazy var checkButton = UIButton() - private lazy var menuLabel = UILabel() - - // MARK: - init - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, - reuseIdentifier: reuseIdentifier) - backgroundColor = .white - setUI() - setLayout() - } - - // MARK: - Functions - - private func setUI() { - checkButton.addTarget(self, action: #selector(checkButtonIsTapped), for: .touchUpInside) - - menuLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) - menuLabel.textColor = .black - menuLabel.text = "고구마치즈돈까스" - - contentView.addSubviews( - checkButton, - menuLabel - ) - } - - private func setLayout() { - checkButton.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalToSuperview().inset(27) - $0.height.width.equalTo(24) - } - - menuLabel.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.equalTo(checkButton.snp.trailing).offset(15) - } - } - - @objc - func checkButtonIsTapped() { - handler?() - } -} - -extension ChoiceMenuTableViewCell { - func dataBind(menu: String, isTapped: Bool) { - menuLabel.text = menu - isChecked = isTapped - } - - func tapped() { - let image = isChecked ? EATSSUDesignAsset.Images.icCheck.image : EATSSUDesignAsset.Images.icUncheck.image - checkButton.setImage(image, for: .normal) - } -} +//import UIKit +// +//import SnapKit +// +//import EATSSUDesign +// +//final class ChoiceMenuTableViewCell: UITableViewCell { +// // MARK: - Properties +// +// static let identifier = "ChoiceMenuTableViewCell" +// var isChecked: Bool = false { +// didSet { +// tapped() +// } +// } +// +// var handler: (() -> Void)? +// +// // MARK: - UI Components +// +// lazy var checkButton = UIButton() +// private lazy var menuLabel = UILabel() +// +// // MARK: - init +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// } +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, +// reuseIdentifier: reuseIdentifier) +// backgroundColor = .white +// setUI() +// setLayout() +// } +// +// // MARK: - Functions +// +// private func setUI() { +// checkButton.addTarget(self, action: #selector(checkButtonIsTapped), for: .touchUpInside) +// +// menuLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) +// menuLabel.textColor = .black +// menuLabel.text = "고구마치즈돈까스" +// +// contentView.addSubviews( +// checkButton, +// menuLabel +// ) +// } +// +// private func setLayout() { +// checkButton.snp.makeConstraints { +// $0.centerY.equalToSuperview() +// $0.leading.equalToSuperview().inset(27) +// $0.height.width.equalTo(24) +// } +// +// menuLabel.snp.makeConstraints { +// $0.centerY.equalToSuperview() +// $0.leading.equalTo(checkButton.snp.trailing).offset(15) +// } +// } +// +// @objc +// func checkButtonIsTapped() { +// handler?() +// } +//} +// +//extension ChoiceMenuTableViewCell { +// func dataBind(menu: String, isTapped: Bool) { +// menuLabel.text = menu +// isChecked = isTapped +// } +// +// func tapped() { +// let image = isChecked ? EATSSUDesignAsset.Images.icCheck.image : EATSSUDesignAsset.Images.icUncheck.image +// checkButton.setImage(image, for: .normal) +// } +//} diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift index ca704542..ef054990 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift @@ -13,14 +13,9 @@ import EATSSUDesign final class RateNumberView: BaseUIView { // MARK: - UI Components - -// let starImageView = UIImageView() private var starImageViews: [UIImageView] = [] private lazy var starsStackView = UIStackView() -// lazy var rateNumberLabel = UILabel() -// private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starImageView, - private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView, - /*rateNumberLabel*/]) + private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView]) var filledStarImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image var emptyStarImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image @@ -44,12 +39,8 @@ final class RateNumberView: BaseUIView { override func configureUI() { addSubviews(rateNumberStackView) - -// starImageView.image = EATSSUDesignAsset.Images.icStarYellow.image starImageViews = (0..<5).map { _ in let imageView = UIImageView() -// imageView.image = EATSSUDesignAsset.Images.icStarYellow.image.withRenderingMode(.alwaysTemplate) -// imageView.tintColor = EATSSUDesignAsset.Color.gray3.color imageView.image = emptyStarImage return imageView } @@ -58,22 +49,12 @@ final class RateNumberView: BaseUIView { starsStackView.spacing = 3 starsStackView.alignment = .bottom starImageViews.forEach { starsStackView.addArrangedSubview($0) } - - -// rateNumberLabel.text = "5" -// rateNumberLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 14) -// rateNumberLabel.textColor = EATSSUDesignAsset.Color.Main.primary.color - rateNumberStackView.axis = .horizontal -// rateNumberStackView.spacing = 3 rateNumberStackView.spacing = 6 rateNumberStackView.alignment = .bottom } override func setLayout() { -// starImageView.snp.makeConstraints { -// $0.height.equalTo(12.adjusted) -// $0.width.equalTo(12.adjusted) starImageViews.forEach { $0.snp.makeConstraints { $0.height.equalTo(12.adjusted) @@ -95,6 +76,5 @@ final class RateNumberView: BaseUIView { star.image = emptyStarImage } } -// rateNumberLabel.text = "\(rating)" } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift index 0c38ec0e..5657503f 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift @@ -46,7 +46,6 @@ final class ReviewDividerCell: UITableViewCell { required init?(coder: NSCoder) { fatalError() } func configure(reviewCount: Int) { -// label.text = "리뷰 \(reviewCount)" let text = "리뷰 \(reviewCount)" let attributed = NSMutableAttributedString(string: text) let range = (text as NSString).range(of: "\(reviewCount)") diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index c5526095..36a12730 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -18,7 +18,6 @@ final class ReviewEmptyViewCell: UITableViewCell { // MARK: - UI Components private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() -// imageView.image = EATSSUDesignAsset.Images.noReview.image imageView.tintColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return imageView }() @@ -74,14 +73,11 @@ final class ReviewEmptyViewCell: UITableViewCell { // MARK: - Configure func configure(isTokenExist: Bool) { if isTokenExist { -// noReviewImageView.image = ImageLiteral.noReview noReviewImageView.image = EATSSUDesignAsset.Images.noReview.image titleLabel.text = "아직 작성된 리뷰가 없어요" descriptionLabel.text = "메뉴에 가장 먼저 리뷰를 남겨주세요!" } else { noReviewImageView.image = ImageLiteral.pleaseLogin -// titleLabel.text = "로그인이 필요합니다" -// descriptionLabel.text = "리뷰 작성을 위해 먼저 로그인 해주세요" } } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index f8b6681f..3a54759d 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -15,10 +15,8 @@ final class ReviewRateViewCell: UITableViewCell { static let identifier = "ReviewRateViewCell" var handler: (() -> Void)? var totalRate: Double = 0 - var reviewData: ReviewRateResponse? // MARK: - UI Components - private let menuContainer: UIView = { let view = UIView() view.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color @@ -203,7 +201,6 @@ final class ReviewRateViewCell: UITableViewCell { backgroundColor = .white menuContainer.snp.makeConstraints { make in -// make.top.equalTo(safeAreaLayoutGuide.snp.topMargin).offset(10) make.top.equalTo(contentView.snp.top).offset(0) make.centerX.equalToSuperview() make.width.equalTo(320.adjusted) @@ -282,66 +279,69 @@ final class ReviewRateViewCell: UITableViewCell { } } +// MARK: - V2 API Data Binding Extensions + extension ReviewRateViewCell { - func fixMenuDataBind(data: FixedReviewRateResponse) { -// let total = String(format: "%.1f", data.mainRating ?? 0) - let ratingValue = data.mainRating ?? 0 - if ratingValue == 0.0 { - rateNumLabel.text = "-" - } else { - let total = String(format: "%.1f", ratingValue) - rateNumLabel.text = "\(total)" - } + // ✨ Meal 통계 데이터 바인딩 + func configureWithMealStatistics(_ data: ReviewMealStatisticsResponse) { + // 메뉴명 설정 (여러 메뉴를 " + "로 연결) + let menuNames = data.menuList.map { $0.name } + menuLabel.text = menuNames.joined(separator: " + ") + + // 평균 별점 설정 + setRating(data.rating ?? 0) + + // 별점 차트 업데이트 + updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) + } + + // ✨ Menu 통계 데이터 바인딩 + func configureWithMenuStatistics(_ data: ReviewMenuStatisticsResponse) { + // 메뉴명 설정 menuLabel.text = data.menuName -// rateNumLabel.text = "\(total)" -// totalRate = data.mainRating ?? 0 - totalRate = ratingValue - - fiveForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) - } - fourForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / max(data.totalReviewCount, 1)) - } - threeForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / max(data.totalReviewCount, 1)) - } - twoForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / max(data.totalReviewCount, 1)) - } - oneForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / max(data.totalReviewCount, 1)) - } + + // 평균 별점 설정 + setRating(data.rating ?? 0) + + // 별점 차트 업데이트 + updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) } - - func dataBind(data: ReviewRateResponse) { -// let total = String(format: "%.1f", data.mainRating ?? 0) - let ratingValue = data.mainRating ?? 0 - if ratingValue == 0.0 { + + // MARK: - Private Helper Methods + + /// 평균 별점 표시 + private func setRating(_ rating: Double) { + totalRate = rating + + if rating == 0.0 { rateNumLabel.text = "-" } else { - let total = String(format: "%.1f", ratingValue) - rateNumLabel.text = "\(total)" + let formattedRating = String(format: "%.1f", rating) + rateNumLabel.text = formattedRating } - menuLabel.text = data.menuNames.joined(separator: " + ") -// rateNumLabel.text = "\(total)" -// totalRate = data.mainRating ?? 0 - totalRate = ratingValue - + } + + /// 별점 차트 업데이트 + private func updateRatingChart(with ratingCount: ReviewRatingCount, totalCount: Int) { + let safeTotal = max(totalCount, 1) // 0으로 나누기 방지 + fiveForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / max(data.totalReviewCount, 1)) + $0.width.equalTo(126 * ratingCount.fiveStarCount / safeTotal) } fourForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / max(data.totalReviewCount, 1)) + $0.width.equalTo(126 * ratingCount.fourStarCount / safeTotal) } threeForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / max(data.totalReviewCount, 1)) + $0.width.equalTo(126 * ratingCount.threeStarCount / safeTotal) } twoForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / max(data.totalReviewCount, 1)) + $0.width.equalTo(126 * ratingCount.twoStarCount / safeTotal) } oneForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / max(data.totalReviewCount, 1)) + $0.width.equalTo(126 * ratingCount.oneStarCount / safeTotal) } + + // 레이아웃 즉시 업데이트 + layoutIfNeeded() } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 30967b29..aedf6201 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -2,13 +2,11 @@ // ReviewTableCell.swift // EatSSU-iOS // -// Created by 박윤빈 on 2023/03/23. +// Updated to use ReviewListItem instead of MenuDataList // import UIKit - import SnapKit - import EATSSUDesign final class ReviewTableCell: UITableViewCell { @@ -236,17 +234,20 @@ extension ReviewTableCell: UICollectionViewDataSource { // MARK: - Data Bind extension ReviewTableCell { - // ✨ V2 API 데이터 바인딩 - func dataBind(response: MenuDataList) { - menuName = response.menu + // ✨ V2 API: ReviewListItem 직접 바인딩 + func dataBind(response: ReviewListItem) { + // 메뉴명 설정 (여러 메뉴인 경우 " + "로 연결) + menuName = response.menu?.map { $0.name }.joined(separator: " + ") ?? "" + + // 기본 정보 userNameLabel.text = response.writerNickname - totalRateView.setRating(response.mainRating) - dateLabel.text = response.writedAt - reviewTextView.text = response.content - reviewId = response.reviewID + totalRateView.setRating(Int(response.rating)) + dateLabel.text = response.writtenAt + reviewTextView.text = response.content ?? "" + reviewId = response.reviewId // 이미지 처리 - if let firstImageUrl = response.imgURLList.compactMap({ $0 }).first(where: { !$0.isEmpty }) { + if let firstImageUrl = response.imageUrls?.first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) } else { @@ -257,9 +258,9 @@ extension ReviewTableCell { sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) - // ✨ 태그 처리 (V2 API에서는 menuList가 태그 역할) - if let menuTags = response.tags, !menuTags.isEmpty { - tags = menuTags.map { ($0.name, $0.isLiked) } + // ✨ 태그 처리 (V2 API에서는 menu가 태그 역할) + if let menuTags = response.menu, !menuTags.isEmpty { + tags = menuTags.map { ($0.name, $0.isLike) } } else { tags = [] } @@ -269,6 +270,7 @@ extension ReviewTableCell { tagCollectionView.isHidden = tags.isEmpty } + // 마이페이지용 바인딩 (기존 호환성 유지) func myPageDataBind(response: MyDataList, nickname: String) { userNameLabel.text = "\(nickname)" totalRateView.setRating(response.mainRating) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift index 6ca3b8f8..0a4967ee 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -13,7 +13,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { private let iconImageView: UIImageView = { let iv = UIImageView() - iv.image = UIImage(systemName: "hand.thumbsup") // 기본 좋아요 아이콘 + iv.image = UIImage(systemName: "hand.thumbsup") iv.tintColor = .systemTeal iv.isHidden = true iv.contentMode = .scaleAspectFit @@ -52,7 +52,6 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { private func setupViews() { contentView.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.1) -// contentView.layer.cornerRadius = 12 contentView.layer.borderColor = UIColor.systemTeal.cgColor contentView.layer.borderWidth = 1 @@ -61,15 +60,6 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { contentView.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false - -// NSLayoutConstraint.activate([ -// iconImageView.widthAnchor.constraint(equalToConstant: 10), -// iconImageView.heightAnchor.constraint(equalToConstant: 10), -// stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), -// stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), -// stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 2), -// stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -2) -// ]) iconImageView.snp.makeConstraints { make in make.width.height.equalTo(10) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift index b55121cf..1b4dcc97 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift @@ -1,175 +1,175 @@ +//// +//// ChoiceMenuViewController.swift +//// EatSSU-iOS +//// +//// Created by 박윤빈 on 2023/06/29. +//// // -// ChoiceMenuViewController.swift -// EatSSU-iOS -// -// Created by 박윤빈 on 2023/06/29. -// - -import UIKit - -import SnapKit -import FirebaseAnalytics - -import EATSSUDesign - -final class ChoiceMenuViewController: BaseViewController { - // MARK: - Properties - - var menuNameList: [String] = [] - var menuIDList: [Int] = [] - var isMenuSelected: [Bool] = [] { - didSet { - choiceMenuTabelView.reloadData() - print(isMenuSelected) - } - } - - private lazy var selectedList: [String] = [] - private var selectedIDList: [Int] = [] - - // MARK: - UI Component - - private let enjoyLabel = UILabel() - private let whichFoodLabel = UILabel() - private lazy var choiceMenuTabelView = UITableView(frame: .zero, style: .plain) - private lazy var nextButton = MainButton() - - // MARK: - Life Cycles - - override func viewDidLoad() { - super.viewDidLoad() - setTableViewConfig() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - /// pop한 후, 다시 메뉴를 선택할 경우를 방지하기 위하여 선택한 리스트를 초기화합니다 - selectedList = [] - selectedIDList = [] - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - logScreenView(screenID: FirebaseScreenID.Review.V1.review_v1_2) - } - - // MARK: - Functions - - override func configureUI() { - whichFoodLabel.text = "어떤 음식에 대한 리뷰인가요?" - whichFoodLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) - whichFoodLabel.textColor = .black - - enjoyLabel.text = "식사는 맛있게 하셨나요?" - enjoyLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) - enjoyLabel.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - - choiceMenuTabelView.separatorStyle = .none - - nextButton.setTitle("다음 단계로", for: .normal) - - view.addSubviews( - enjoyLabel, - whichFoodLabel, - choiceMenuTabelView, - nextButton - ) - } - - override func setLayout() { - whichFoodLabel.snp.makeConstraints { - $0.top.equalTo(view.safeAreaLayoutGuide.snp.topMargin).inset(25) - $0.centerX.equalToSuperview() - } - - enjoyLabel.snp.makeConstraints { - $0.top.equalTo(whichFoodLabel.snp.bottom).offset(15) - $0.centerX.equalToSuperview() - } - - choiceMenuTabelView.snp.makeConstraints { - $0.top.equalTo(enjoyLabel.snp.bottom).offset(40) - $0.leading.trailing.bottom.equalToSuperview() - } - - nextButton.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(17) - } - } - - override func setButtonEvent() { - nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) - } - - override func setCustomNavigationBar() { - super.setCustomNavigationBar() - navigationItem.title = "리뷰 남기기" - } - - private func setTableViewConfig() { - choiceMenuTabelView.delegate = self - choiceMenuTabelView.dataSource = self - choiceMenuTabelView.register(ChoiceMenuTableViewCell.self, - forCellReuseIdentifier: ChoiceMenuTableViewCell.identifier) - } - - private func makeList(menuList: [String], selectedList: [Bool]) { - for i in 0 ..< menuList.count { - if selectedList[i] { - self.selectedList.append(menuList[i]) - selectedIDList.append(menuIDList[i]) - } - } - } - - @objc - func nextButtonTapped() { - makeList(menuList: menuNameList, selectedList: isMenuSelected) - if selectedList.count == 0 { - showToast(message: "리뷰를 작성할 메뉴를 선택해주세요!", type: .info) - } else { - let setRateVC = SetRateViewController() - setRateVC.dataBind(list: selectedList, - idList: selectedIDList, - reviewList: nil, - currentPage: 0) - navigationController?.pushViewController(setRateVC, animated: true) - } - } - - func menuDataBind(menuList: [String], idList: [Int]) { - menuNameList = menuList - menuIDList = idList - for _ in 0 ..< menuNameList.count { - isMenuSelected.append(false) - } - } -} - -extension ChoiceMenuViewController: UITableViewDelegate {} - -extension ChoiceMenuViewController: UITableViewDataSource { - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - menuNameList.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: ChoiceMenuTableViewCell.identifier) as? ChoiceMenuTableViewCell else { return UITableViewCell() } - cell.selectionStyle = .none - cell.dataBind(menu: menuNameList[indexPath.row], isTapped: isMenuSelected[indexPath.row]) - cell.handler = { [weak self] in - guard let self else { return } - cell.isChecked.toggle() - isMenuSelected[indexPath.row].toggle() - } - - return cell - } - - func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { - 50 - } -} +//import UIKit +// +//import SnapKit +//import FirebaseAnalytics +// +//import EATSSUDesign +// +//final class ChoiceMenuViewController: BaseViewController { +// // MARK: - Properties +// +// var menuNameList: [String] = [] +// var menuIDList: [Int] = [] +// var isMenuSelected: [Bool] = [] { +// didSet { +// choiceMenuTabelView.reloadData() +// print(isMenuSelected) +// } +// } +// +// private lazy var selectedList: [String] = [] +// private var selectedIDList: [Int] = [] +// +// // MARK: - UI Component +// +// private let enjoyLabel = UILabel() +// private let whichFoodLabel = UILabel() +// private lazy var choiceMenuTabelView = UITableView(frame: .zero, style: .plain) +// private lazy var nextButton = MainButton() +// +// // MARK: - Life Cycles +// +// override func viewDidLoad() { +// super.viewDidLoad() +// setTableViewConfig() +// } +// +// override func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// +// /// pop한 후, 다시 메뉴를 선택할 경우를 방지하기 위하여 선택한 리스트를 초기화합니다 +// selectedList = [] +// selectedIDList = [] +// } +// +// override func viewDidAppear(_ animated: Bool) { +// super.viewDidAppear(animated) +// +// logScreenView(screenID: FirebaseScreenID.Review.V1.review_v1_2) +// } +// +// // MARK: - Functions +// +// override func configureUI() { +// whichFoodLabel.text = "어떤 음식에 대한 리뷰인가요?" +// whichFoodLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) +// whichFoodLabel.textColor = .black +// +// enjoyLabel.text = "식사는 맛있게 하셨나요?" +// enjoyLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) +// enjoyLabel.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color +// +// choiceMenuTabelView.separatorStyle = .none +// +// nextButton.setTitle("다음 단계로", for: .normal) +// +// view.addSubviews( +// enjoyLabel, +// whichFoodLabel, +// choiceMenuTabelView, +// nextButton +// ) +// } +// +// override func setLayout() { +// whichFoodLabel.snp.makeConstraints { +// $0.top.equalTo(view.safeAreaLayoutGuide.snp.topMargin).inset(25) +// $0.centerX.equalToSuperview() +// } +// +// enjoyLabel.snp.makeConstraints { +// $0.top.equalTo(whichFoodLabel.snp.bottom).offset(15) +// $0.centerX.equalToSuperview() +// } +// +// choiceMenuTabelView.snp.makeConstraints { +// $0.top.equalTo(enjoyLabel.snp.bottom).offset(40) +// $0.leading.trailing.bottom.equalToSuperview() +// } +// +// nextButton.snp.makeConstraints { +// $0.horizontalEdges.equalToSuperview().inset(16) +// $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(17) +// } +// } +// +// override func setButtonEvent() { +// nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) +// } +// +// override func setCustomNavigationBar() { +// super.setCustomNavigationBar() +// navigationItem.title = "리뷰 남기기" +// } +// +// private func setTableViewConfig() { +// choiceMenuTabelView.delegate = self +// choiceMenuTabelView.dataSource = self +// choiceMenuTabelView.register(ChoiceMenuTableViewCell.self, +// forCellReuseIdentifier: ChoiceMenuTableViewCell.identifier) +// } +// +// private func makeList(menuList: [String], selectedList: [Bool]) { +// for i in 0 ..< menuList.count { +// if selectedList[i] { +// self.selectedList.append(menuList[i]) +// selectedIDList.append(menuIDList[i]) +// } +// } +// } +// +// @objc +// func nextButtonTapped() { +// makeList(menuList: menuNameList, selectedList: isMenuSelected) +// if selectedList.count == 0 { +// showToast(message: "리뷰를 작성할 메뉴를 선택해주세요!", type: .info) +// } else { +// let setRateVC = SetRateViewController() +// setRateVC.dataBind(list: selectedList, +// idList: selectedIDList, +// reviewList: nil, +// currentPage: 0) +// navigationController?.pushViewController(setRateVC, animated: true) +// } +// } +// +// func menuDataBind(menuList: [String], idList: [Int]) { +// menuNameList = menuList +// menuIDList = idList +// for _ in 0 ..< menuNameList.count { +// isMenuSelected.append(false) +// } +// } +//} +// +//extension ChoiceMenuViewController: UITableViewDelegate {} +// +//extension ChoiceMenuViewController: UITableViewDataSource { +// func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { +// menuNameList.count +// } +// +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// guard let cell = tableView.dequeueReusableCell(withIdentifier: ChoiceMenuTableViewCell.identifier) as? ChoiceMenuTableViewCell else { return UITableViewCell() } +// cell.selectionStyle = .none +// cell.dataBind(menu: menuNameList[indexPath.row], isTapped: isMenuSelected[indexPath.row]) +// cell.handler = { [weak self] in +// guard let self else { return } +// cell.isChecked.toggle() +// isMenuSelected[indexPath.row].toggle() +// } +// +// return cell +// } +// +// func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { +// 50 +// } +//} diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index f1b0da4c..e3aff3a3 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -2,13 +2,17 @@ // ReviewViewController.swift // EatSSU-iOS // -// Created by 최지우 on 2023/04/07. +// Updated with full V2 API integration // import UIKit - import FirebaseAnalytics import Moya +import SnapKit + +// MARK: - Properties (모델 변경 반영) + +// MenuInfo는 삭제하고 ReviewValidMenu로 통일 final class ReviewViewController: BaseViewController { // MARK: - Properties @@ -19,12 +23,18 @@ final class ReviewViewController: BaseViewController { private var menuNameList: [String] = [] private var menuIDList: [Int]? = [Int]() private var menuDictionary: [String: Int] = [:] - private var reviewList = [MenuDataList]() + + // ✨ V2 API로 변경: MenuDataList → ReviewListItem + private var reviewList = [ReviewListItem]() // ✨ V2 API 응답 데이터 private var mealStatistics: ReviewMealStatisticsResponse? - private var menuStatistics: ReviewMeuStatisticsResponse? + private var menuStatistics: ReviewMenuStatisticsResponse? private var totalReviewCount: Int = 0 + + // ✨ 리뷰 작성 가능한 메뉴 목록 (getValidMenusForReview) + // 이 프로퍼티는 이제 typealias 덕분에 [ReviewValidMenu]와 동일합니다. + private var validMenusForReview: [ReviewValidMenu] = [] // MARK: - UI Component @@ -59,12 +69,11 @@ final class ReviewViewController: BaseViewController { return view }() - private let reviewTabBarView: MainButton = { - let button = MainButton() - button.title = "리뷰 작성하기" - return button - }() + let button = MainButton() + button.title = "리뷰 작성하기" + return button + }() // MARK: - Life Cycles @@ -78,8 +87,11 @@ final class ReviewViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - // ✨ V2 API 호출로 변경 + // ✨ V2 API 호출 순서: 통계 → 유효 메뉴 → 리뷰 리스트 getStatistics() + if type == "VARIABLE" { + getValidMenusForReview() // VARIABLE 타입일 때만 호출 + } getReviewList(type: type, menuId: menuID) } @@ -130,8 +142,11 @@ final class ReviewViewController: BaseViewController { $0.height.equalTo(80) } + // 🛠️ Auto Layout 충돌 수정: .bottom 제약을 제거하여 MainButton 내부 높이 제약이 우선되도록 함 reviewTabBarView.snp.makeConstraints { - $0.edges.equalToSuperview().inset(12) + $0.horizontalEdges.equalToSuperview().inset(12) + $0.top.equalToSuperview().offset(12) + // $0.bottom.equalToSuperview().offset(-12) // 제거 } } @@ -145,15 +160,25 @@ final class ReviewViewController: BaseViewController { } @objc private func handleAddReviewButtonTap() { + // MARK: - 로직 수정 + if type == "VARIABLE" { let reviewVC = SetRateViewController(mealId: menuID) + + // 🛠️ 수정: .menuId 속성 사용 + reviewVC.dataBind( + list: validMenusForReview.map { $0.name }, + idList: validMenusForReview.map { $0.menuId } + ) navigationController?.pushViewController(reviewVC, animated: true) - } else { - let reviewVC = SetRateViewController() - reviewVC.dataBind(list: menuNameList, - idList: menuIDList ?? [], - reviewList: nil, - currentPage: 0) + + } else { // FIXED + let reviewVC = SetRateViewController(menuId: menuID) + + reviewVC.dataBind( + list: menuNameList, + idList: menuIDList ?? [] + ) navigationController?.pushViewController(reviewVC, animated: true) } } @@ -189,6 +214,9 @@ final class ReviewViewController: BaseViewController { func refreshTable(refresh: UIRefreshControl) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.getStatistics() + if self.type == "VARIABLE" { + self.getValidMenusForReview() + } self.getReviewList(type: self.type, menuId: self.menuID) refresh.endRefreshing() } @@ -198,7 +226,7 @@ final class ReviewViewController: BaseViewController { menuID = id } - private func showFixOrDeleteAlert(data: MenuDataList) { + private func showFixOrDeleteAlert(data: ReviewListItem) { let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", preferredStyle: UIAlertController.Style.actionSheet) @@ -206,8 +234,12 @@ final class ReviewViewController: BaseViewController { let fixAction = UIAlertAction(title: "수정하기", style: .default, handler: { _ in - let setRateViewController = SetRateViewController() - setRateViewController.dataBindForFix(list: [data.menu], reivewId: data.reviewID) + + let menuNames = data.menu?.map { $0.name } ?? [] + + let setRateViewController = SetRateViewController(menuId: self.menuID) + + setRateViewController.dataBindForFix(list: menuNames, reviewId: data.reviewId) setRateViewController.settingForReviewFix(data: data) self.navigationController?.pushViewController(setRateViewController, animated: true) }) @@ -221,7 +253,7 @@ final class ReviewViewController: BaseViewController { cancelButtonTitle: "취소하기", confirmButtonTitle: "삭제하기" ) { [weak self] in - self?.deleteReview(reviewID: data.reviewID) + self?.deleteReview(reviewID: data.reviewId) } }) @@ -248,43 +280,31 @@ final class ReviewViewController: BaseViewController { } } - - // MARK: - Action Method - func userTapReviewButton() { if RealmService.shared.isAccessTokenPresent() { activityIndicatorView.isHidden = false DispatchQueue.global().async { DispatchQueue.main.async { [self] in - if menuIDList == nil { - // FIXED - let setRateViewController = SetRateViewController() - menuIDList = [menuID] - setRateViewController.dataBind(list: menuNameList, - idList: menuIDList ?? [], - reviewList: nil, - currentPage: 0) + + if type == "FIXED" { + let setRateViewController = SetRateViewController(menuId: menuID) + + setRateViewController.dataBind( + list: menuNameList, + idList: menuIDList ?? [] + ) + activityIndicatorView.stopAnimating() + navigationController?.pushViewController(setRateViewController, animated: true) + } else { // VARIABLE + let setRateViewController = SetRateViewController(mealId: menuID) + + // 🛠️ 수정: .menuId 속성 사용 + setRateViewController.dataBind( + list: validMenusForReview.map { $0.name }, + idList: validMenusForReview.map { $0.menuId } + ) activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) - } else { - // VARIABLE - if menuIDList?.count == 1 { - let setRateViewController = SetRateViewController(mealId: menuID) - setRateViewController.dataBind(list: menuNameList, - idList: menuIDList ?? [], - reviewList: nil, - currentPage: 0) - activityIndicatorView.stopAnimating() - navigationController?.pushViewController(setRateViewController, animated: true) - } else { - let setRateViewController = SetRateViewController(mealId: menuID) - setRateViewController.dataBind(list: menuNameList, - idList: menuIDList ?? [], - reviewList: nil, - currentPage: 0) - activityIndicatorView.stopAnimating() - navigationController?.pushViewController(setRateViewController, animated: true) - } } } } @@ -329,6 +349,7 @@ extension ReviewViewController: UITableViewDelegate { return 0 } } + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let spacerView = UIView() spacerView.backgroundColor = .clear @@ -377,7 +398,7 @@ extension ReviewViewController: UITableViewDataSource { cell.handler = { [weak self] in guard let self else { return } - userTapReviewButton() + self.userTapReviewButton() } cell.reloadInputViews() return cell @@ -401,12 +422,19 @@ extension ReviewViewController: UITableViewDataSource { } else { let cell = tableView.dequeueReusableCell(withIdentifier: ReviewTableCell.identifier, for: indexPath) as? ReviewTableCell ?? ReviewTableCell() - cell.dataBind(response: reviewList[indexPath.row]) + // ✨ ReviewListItem 직접 바인딩 + // 🛠️ 요청사항 반영: isLike가 true인 메뉴만 필터링하여 바인딩 + var filteredReviewItem = reviewList[indexPath.row] + let likedMenus = filteredReviewItem.menu?.filter { $0.isLike } + filteredReviewItem.menu = likedMenus + + cell.dataBind(response: filteredReviewItem) + cell.handler = { [weak self] in guard let self else { return } - reviewList[indexPath.row].isWriter ? showFixOrDeleteAlert(data: reviewList[indexPath.row]) - : showReportAlert(reviewID: cell.reviewId) + reviewList[indexPath.row].isWriter ? self.showFixOrDeleteAlert(data: reviewList[indexPath.row]) + : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) } cell.selectionStyle = .none cell.reloadInputViews() @@ -436,7 +464,7 @@ extension ReviewViewController: UITableViewDataSource { } } -// MARK: - Server Setting +// MARK: - V2 API Network Calls extension ReviewViewController { // ✨ V2 API: 통계 데이터 가져오기 @@ -452,7 +480,7 @@ extension ReviewViewController { func getFixedMenuStatistics() { NetworkService.shared.request( ReviewRouter.getFixedMenuStatistics(menuID), - responseType: ReviewMeuStatisticsResponse.self, + responseType: ReviewMenuStatisticsResponse.self, useAuth: false ) { [weak self] result in guard let self = self else { return } @@ -461,10 +489,11 @@ extension ReviewViewController { self.menuStatistics = data self.totalReviewCount = data.totalReviewCount self.menuNameList = [data.menuName] + self.menuIDList = [self.menuID] // FIXED 메뉴는 menuIDList도 menuID로 설정 self.makeDictionary() self.reviewTableView.reloadData() case .failure(let error): - print("Fixed Menu Statistics Error: \(error.localizedDescription)") + print("❌ Fixed Menu Statistics Error: \(error.localizedDescription)") } } } @@ -486,7 +515,33 @@ extension ReviewViewController { self.makeDictionary() self.reviewTableView.reloadData() case .failure(let error): - print("Meal Statistics Error: \(error.localizedDescription)") + // 🛠️ Meal Statistics Error 처리 개선 (rating: null 디코딩 오류 가정) + print("❌ Meal Statistics Error: \(error.localizedDescription)") + // 디코딩 실패해도 UI 갱신을 위해 reloadData 호출 + self.reviewTableView.reloadData() + } + } + } + + // MARK: 🛠️ JSON Decoding 수정: responseType을 ReviewValidMenusResponse.self로 변경 + // ✨ V2 API: 리뷰 작성 가능한 메뉴 목록 조회 (VARIABLE 타입 전용) + func getValidMenusForReview() { + NetworkService.shared.request( + ReviewRouter.getValidMenusForReview(menuID), + responseType: ReviewValidMenusResponse.self, // 🛠️ Wrapper DTO 타입 사용 + useAuth: true + ) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let data): + // 🛠️ 수정: result 내부의 menuList 배열을 사용 (타입이 [MenuInfo]로 일치) + self.validMenusForReview = data.menuList + print("✅ Valid Menus for Review: \(data.menuList.map { $0.name })") + case .failure(let error): + print("❌ Valid Menus Error: \(error.localizedDescription)") + // 에러 발생 시 처리 (Meal Statistics에서 가져온 데이터가 타입이 다를 수 있으므로 임시 주석) + // self.validMenusForReview = (self.mealStatistics?.menuList ?? []) + break } } } @@ -504,32 +559,18 @@ extension ReviewViewController { func getFixedMenuReviewList() { NetworkService.shared.request( ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: 0, size: 20), - responseType: NewMenuListResponse.self, - useAuth: false + responseType: NewReviewListResponse.self, + useAuth: true ) { [weak self] result in guard let self = self else { return } switch result { case .success(let data): - self.reviewList = data.dataList.map { item in - MenuDataList( - reviewID: item.reviewId, - menu: item.menuList?.first?.name ?? "", - writerID: item.writerId, - isWriter: item.isWriter, - writerNickname: item.writerNickname, - mainRating: item.rating, - amountRating: nil, - tasteRating: nil, - writedAt: item.writtenAt, - content: item.content ?? "", - imgURLList: item.imageUrls ?? [], - - tags: item.menuList?.map { Tag(name: $0.name, isLiked: $0.isLike) } - ) - } + // ✨ ReviewListItem을 그대로 사용 + self.reviewList = data.dataList self.reviewTableView.reloadData() + print("✅ Fixed Menu Reviews loaded: \(self.reviewList.count) items") case .failure(let error): - print("Fixed Menu Review List Error: \(error.localizedDescription)") + print("❌ Fixed Menu Review List Error: \(error.localizedDescription)") } } } @@ -538,31 +579,18 @@ extension ReviewViewController { func getMealReviewList() { NetworkService.shared.request( ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: nil, size: 20), - responseType: NewMenuListResponse.self, + responseType: NewReviewListResponse.self, useAuth: true ) { [weak self] result in guard let self = self else { return } switch result { case .success(let data): - self.reviewList = data.dataList.map { item in - MenuDataList( - reviewID: item.reviewId, - menu: item.menuList?.map { $0.name }.joined(separator: " + ") ?? "", - writerID: item.writerId, - isWriter: item.isWriter, - writerNickname: item.writerNickname, - mainRating: item.rating, - amountRating: nil, - tasteRating: nil, - writedAt: item.writtenAt, - content: item.content ?? "", - imgURLList: item.imageUrls ?? [], - tags: item.menuList?.map { Tag(name: $0.name, isLiked: $0.isLike) } - ) - } + // ✨ ReviewListItem을 그대로 사용 + self.reviewList = data.dataList self.reviewTableView.reloadData() + print("✅ Meal Reviews loaded: \(self.reviewList.count) items") case .failure(let error): - print("Meal Review List Error: \(error.localizedDescription)") + print("❌ Meal Review List Error: \(error.localizedDescription)") } } } @@ -572,10 +600,12 @@ extension ReviewViewController { switch response { case .success: self.getStatistics() - self.updateViewConstraints() + if self.type == "VARIABLE" { + self.getValidMenusForReview() + } self.getReviewList(type: self.type, menuId: self.menuID) case let .failure(err): - print(err.localizedDescription) + print("❌ Delete Review Error: \(err.localizedDescription)") } } } @@ -583,84 +613,13 @@ extension ReviewViewController { extension ReviewViewController: ReviewMenuTypeInfoDelegate { func didDelegateReviewMenuTypeInfo(for menuTypeData: ReviewMenuTypeInfo) { - let reviewMenuTypeInfo = ReviewMenuTypeInfo(menuType: menuTypeData.menuType, - menuID: menuTypeData.menuID, - changeMenuIDList: menuTypeData.changeMenuIDList) + let reviewMenuTypeInfo = ReviewMenuTypeInfo( + menuType: menuTypeData.menuType, + menuID: menuTypeData.menuID, + changeMenuIDList: menuTypeData.changeMenuIDList + ) type = reviewMenuTypeInfo.menuType menuID = reviewMenuTypeInfo.menuID menuIDList = reviewMenuTypeInfo.changeMenuIDList } } - -// MARK: - ReviewRateViewCell Extension for V2 API - -extension ReviewRateViewCell { - // ✨ Meal 통계 데이터 바인딩 - func configureWithMealStatistics(_ data: ReviewMealStatisticsResponse) { - // 메뉴명 설정 - let menuNames = data.menuList.map { $0.name } - menuLabel.text = menuNames.joined(separator: " + ") - - // 평균 별점 설정 - let ratingValue = data.rating - if ratingValue == 0.0 { - rateNumLabel.text = "-" - } else { - let total = String(format: "%.1f", ratingValue) - rateNumLabel.text = "\(total)" - } - totalRate = ratingValue - - // 별점 차트 업데이트 - let totalCount = max(data.totalReviewCount, 1) - fiveForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / totalCount) - } - fourForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / totalCount) - } - threeForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / totalCount) - } - twoForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / totalCount) - } - oneForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / totalCount) - } - } - - // ✨ Menu 통계 데이터 바인딩 - func configureWithMenuStatistics(_ data: ReviewMeuStatisticsResponse) { - // 메뉴명 설정 - menuLabel.text = data.menuName - - // 평균 별점 설정 - let ratingValue = data.rating - if ratingValue == 0.0 { - rateNumLabel.text = "-" - } else { - let total = String(format: "%.1f", ratingValue) - rateNumLabel.text = "\(total)" - } - totalRate = ratingValue - - // 별점 차트 업데이트 - let totalCount = max(data.totalReviewCount, 1) - fiveForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fiveStarCount / totalCount) - } - fourForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.fourStarCount / totalCount) - } - threeForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.threeStarCount / totalCount) - } - twoForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.twoStarCount / totalCount) - } - oneForeground.snp.updateConstraints { - $0.width.equalTo(126 * data.reviewRatingCount.oneStarCount / totalCount) - } - } -} diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index b2db45a4..197004f8 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -6,53 +6,49 @@ // import UIKit - import SnapKit import Moya - import EATSSUDesign final class SetRateViewController: BaseViewController { // MARK: - Properties - private var currentPage: Int = 0 { - didSet { - // V2 Meal Review는 단일 페이지이므로 항상 '리뷰 남기기'로 표시되어야 함 - // V1의 페이지 넘김 로직을 제거하고, 항상 리뷰 남기기 버튼을 보여줍니다. - nextButton.setTitle("리뷰 남기기", for: .normal) // ✨ 수정: 항상 "리뷰 남기기"로 설정 - } - } - private var userPickedImage: UIImage? - private var reviewList: [(BeforeSelectedImageDTO, UIImage?)] = [] - // ✨ 수정: selectedIDList를 validMenuIDList로 변경 private var validMenuIDList: [Int] = [] private var selectedList: [String] = [] private var reviewId: Int? - // 좋아요 상태를 보관 (selectedList와 같은 인덱스) private var likedStates: [Bool] = [] private var menuTableViewHeightConstraint: Constraint? - // ✨ 추가: mealId를 저장할 프로퍼티 + // ✨ 타입 구분: FIXED(고정 메뉴) vs VARIABLE(식단) + private var reviewType: ReviewType = .variable private var mealID: Int? + private var menuID: Int? + + enum ReviewType { + case fixed // writeMenuReview 사용 + case variable // writeMealReview 사용 + } // MARK: - Initializer - // ✨ 추가: mealId를 받는 이니셜라이저 convenience init(mealId: Int) { self.init(nibName: nil, bundle: nil) self.mealID = mealId + self.reviewType = .variable + } + + convenience init(menuId: Int) { + self.init(nibName: nil, bundle: nil) + self.menuID = menuId + self.reviewType = .fixed } - // MARK: - UI Components - // ... (기존 UI Component 코드 유지) private var rateView = RateView() - private var tasteRateView = RateView() - private var quantityRateView = RateView() private let imagePickerController = UIImagePickerController() private var contentView: UIView = { @@ -67,15 +63,8 @@ final class SetRateViewController: BaseViewController { return scrollView }() - private let progressView: UIView = { - let view = UIView() - view.backgroundColor = .primary - return view - }() - private var menuLabel: UILabel = { let label = UILabel() -// label.text = "김치볶음밥 & 계란국을 추천하시겠어요?" label.text = "오늘의 식사는 어떠셨나요?" label.font = .subtitle1 label.textColor = .black @@ -98,38 +87,6 @@ final class SetRateViewController: BaseViewController { return tableView }() -// private var tasteLabel: UILabel = { -// let label = UILabel() -// label.text = "맛" -// label.font = .subtitle1 -// label.textColor = .black -// return label -// }() -// -// private var quantityLabel: UILabel = { -// let label = UILabel() -// label.text = "양" -// label.font = .subtitle1 -// label.textColor = .black -// return label -// }() - -// private lazy var tasteStackView: UIStackView = { -// let stackView = UIStackView() -// stackView.axis = .horizontal -// stackView.spacing = 16.adjusted -// stackView.alignment = .center -// return stackView -// }() -// -// private lazy var quantityStackView: UIStackView = { -// let stackView = UIStackView() -// stackView.axis = .horizontal -// stackView.spacing = 16.adjusted -// stackView.alignment = .center -// return stackView -// }() - private let userReviewTextView: UITextView = { let textView = UITextView() textView.font = .body1 @@ -138,7 +95,6 @@ final class SetRateViewController: BaseViewController { textView.layer.borderWidth = 1.adjusted textView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor textView.textContainerInset = UIEdgeInsets(top: 16.0.adjusted, left: 16.0.adjusted, bottom: 16.0.adjusted, right: 16.0.adjusted) -// textView.text = "3글자 이상 작성해주세요!" textView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" textView.textColor = .gray500 return textView @@ -149,7 +105,7 @@ final class SetRateViewController: BaseViewController { imageView.layer.cornerRadius = 10 imageView.clipsToBounds = true imageView.isUserInteractionEnabled = true - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTappedimageView)) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTappedImageView)) imageView.addGestureRecognizer(tapGesture) return imageView }() @@ -158,24 +114,10 @@ final class SetRateViewController: BaseViewController { let button = UIButton() button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) button.tintColor = .lightGray - button.addTarget(self, action: #selector(didTappedimageView), for: .touchUpInside) - button.isHidden = true // Hide close button initially + button.addTarget(self, action: #selector(didTappedImageView), for: .touchUpInside) + button.isHidden = true return button }() - -// private lazy var imageContainerView: UIView = { -// let view = UIView() -// view.layer.cornerRadius = 10 -// view.clipsToBounds = true -// return view -// }() - - private lazy var imageContainer: UIView = { - let view = UIView() - view.addSubview(selectImageButton) - view.addSubview(imageCountLabel) - return view - }() private lazy var selectImageButton: UIButton = { let button = UIButton() @@ -188,8 +130,6 @@ final class SetRateViewController: BaseViewController { button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor button.layer.cornerRadius = 8 button.clipsToBounds = true - button.contentVerticalAlignment = .center - button.contentHorizontalAlignment = .center return button }() @@ -220,7 +160,7 @@ final class SetRateViewController: BaseViewController { private var nextButton: MainButton = { let button = MainButton() - button.title = "다음 단계로" + button.title = "리뷰 남기기" return button }() @@ -230,19 +170,14 @@ final class SetRateViewController: BaseViewController { super.viewDidLoad() setDelegate() - // ✨ 수정: mealId가 있으면 API 호출, 없으면 기존 로직 유지 - if let mealId = mealID { + // ✨ 타입에 따라 적절한 초기화 + if reviewType == .variable, let mealId = mealID { fetchValidMenus(mealId: mealId) - } else { - // 기존 dataBind로 넘어온 데이터가 있거나, 리뷰 수정인 경우 - // selectedList가 비어있지 않다면 테이블 뷰 갱신 (리뷰 수정 등 기존 로직 유지) - if !selectedList.isEmpty { - // 좋아요 상태 배열도 맞춰서 초기화 - likedStates = Array(repeating: false, count: selectedList.count) - // 테이블 갱신 - menuTableView.reloadData() - // 높이 업데이트 (viewDidLayoutSubviews에서 실행됨) - } + } else if reviewType == .fixed { + setupFixedMenuReview() + } else if !selectedList.isEmpty { + likedStates = Array(repeating: false, count: selectedList.count) + menuTableView.reloadData() } } @@ -256,19 +191,16 @@ final class SetRateViewController: BaseViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - // API 호출 후 데이터가 로드되면 높이가 업데이트되도록 처리 menuTableViewHeightConstraint?.update(offset: menuTableView.contentSize.height) } - - // MARK: - API Call + // MARK: - API Calls - // ✨ 추가: 리뷰 가능한 메뉴 목록을 조회하는 메서드 + // ✨ VARIABLE: 리뷰 가능한 메뉴 목록 조회 private func fetchValidMenus(mealId: Int) { - // NetworkService의 request 메서드를 사용하여 ReviewRouter 호출 NetworkService.shared.request( ReviewRouter.getValidMenusForReview(mealId), - responseType: ReviewValidMenusResponse.self, // DTO 타입 사용 + responseType: [MenuInfo].self, useAuth: true ) { [weak self] result in guard let self = self else { return } @@ -276,73 +208,47 @@ final class SetRateViewController: BaseViewController { DispatchQueue.main.async { switch result { case .success(let data): - // 메뉴 이름과 ID를 각각 selectedList와 validMenuIDList에 저장 - self.selectedList = data.menuList.map { $0.name } - self.validMenuIDList = data.menuList.map { $0.menuId } - - // 메뉴 목록 수에 맞춰 좋아요 상태 초기화 (초기값: false) + self.selectedList = data.map { $0.name } + self.validMenuIDList = data.map { $0.id } self.likedStates = Array(repeating: false, count: self.selectedList.count) - - // reviewList 초기화 (API 결과에 따라 갯수 맞춤) -// self.reviewList = Array(repeating: (BeforeSelectedImageDTO(mainRating: 0, -// amountRating: nil, -// tasteRating: nil, -// content: ""), -// nil), count: self.validMenuIDList.count) - self.reviewList = [(BeforeSelectedImageDTO(mainRating: 0, - amountRating: nil, - tasteRating: nil, - content: ""), - nil)] // ✨ 수정: 1개만 초기화 - - // 테이블 뷰 리로드 self.menuTableView.reloadData() - // viewDidLayoutSubviews를 호출하여 높이 제약조건 업데이트 self.view.setNeedsLayout() - // currentPage 초기화 및 버튼 텍스트 업데이트 - self.currentPage = 0 - case .failure(let error): - print("Error fetching valid menus: \(error)") + print("❌ Error fetching valid menus: \(error)") self.showToast(message: "메뉴 목록 조회에 실패했습니다.") } } } } + + // ✨ FIXED: 단일 메뉴 리뷰 설정 + private func setupFixedMenuReview() { + likedStates = [false] + menuTableView.reloadData() + view.setNeedsLayout() + } - // MARK: - Functions + // MARK: - UI Configuration override func configureUI() { dismissKeyboard() view.addSubview(scrollView) scrollView.addSubview(contentView) - contentView.addSubviews(rateView, - menuLabel, -// tasteLabel, -// quantityLabel, - detailLabel, - - menuTableView, -// tasteStackView, -// quantityStackView, - userReviewTextView, - maximumWordLabel, - selectImageButton, - imageCountLabel, - userReviewImageView, - closeButton, -// imageContainerView, - deleteMethodLabel, - nextButton) - -// tasteStackView.addArrangedSubviews([tasteLabel, -// tasteRateView]) - -// quantityStackView.addArrangedSubviews([quantityLabel, -// quantityRateView]) -// imageContainerView.addSubview(userReviewImageView) -// imageContainerView.addSubview(closeButton) + contentView.addSubviews( + rateView, + menuLabel, + detailLabel, + menuTableView, + userReviewTextView, + maximumWordLabel, + selectImageButton, + imageCountLabel, + userReviewImageView, + closeButton, + deleteMethodLabel, + nextButton + ) } override func setLayout() { @@ -372,51 +278,15 @@ final class SetRateViewController: BaseViewController { } menuTableView.snp.makeConstraints { - $0.top.equalTo(detailLabel.snp.bottom).offset(20) + $0.top.equalTo(detailLabel.snp.bottom).offset(20) $0.leading.equalToSuperview().offset(32) $0.trailing.equalToSuperview().offset(-32) -// $0.bottom.equalToSuperview() -// $0.height.equalTo(200) - menuTableViewHeightConstraint = $0.height.equalTo(0).constraint // 처음엔 0으로 - } - -// tasteStackView.snp.makeConstraints { make in -// make.top.equalTo(detailLabel.snp.bottom).offset(30) -// make.centerX.equalToSuperview() -// } -// -// quantityStackView.snp.makeConstraints { make in -// make.top.equalTo(tasteStackView.snp.bottom).offset(30) -// make.centerX.equalToSuperview() -// } - - nextButton.snp.makeConstraints { make in - make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) - make.horizontalEdges.equalToSuperview().inset(16) - make.bottom.equalToSuperview().offset(-15) - } - - for i in 0 ... 4 { - rateView.buttons[i].snp.makeConstraints { make in // rateView로 통합 - make.height.equalTo(28) - make.width.equalTo(29.3) - } -// tasteRateView.buttons[i].snp.makeConstraints { make in -// make.height.equalTo(28) -// make.width.equalTo(29.3) -// } -// -// quantityRateView.buttons[i].snp.makeConstraints { make in -// make.height.equalTo(28) -// make.width.equalTo(29.3) -// } + menuTableViewHeightConstraint = $0.height.equalTo(0).constraint } userReviewTextView.snp.makeConstraints { make in -// make.top.equalTo(quantityStackView.snp.bottom).offset(40) make.top.equalTo(menuTableView.snp.bottom).offset(40) - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) + make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(181) } @@ -428,34 +298,19 @@ final class SetRateViewController: BaseViewController { selectImageButton.snp.makeConstraints { $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) $0.leading.equalToSuperview().offset(15) - $0.width.equalTo(60) - $0.height.equalTo(60) + $0.width.height.equalTo(60) } imageCountLabel.snp.makeConstraints { $0.top.equalTo(selectImageButton.snp.bottom).offset(-19) $0.centerX.equalTo(selectImageButton) - $0.width.equalTo(selectImageButton) } userReviewImageView.snp.makeConstraints { $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) - $0.width.equalTo(60) - $0.height.equalTo(60) - } -// imageContainerView.snp.makeConstraints { -// $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) -// $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) -// $0.width.height.equalTo(70) // 원하는 크기로 조절 -// } - -// userReviewImageView.snp.makeConstraints { -//// $0.edges.equalToSuperview() -// $0.size.equalTo(60) -// -// } - + $0.width.height.equalTo(60) + } closeButton.snp.makeConstraints { $0.top.equalTo(userReviewImageView.snp.top).offset(-6) @@ -467,6 +322,19 @@ final class SetRateViewController: BaseViewController { $0.top.equalTo(selectImageButton.snp.bottom).offset(7) $0.leading.equalTo(selectImageButton) } + + nextButton.snp.makeConstraints { make in + make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) + make.horizontalEdges.equalToSuperview().inset(16) + make.bottom.equalToSuperview().offset(-15) + } + + for i in 0...4 { + rateView.buttons[i].snp.makeConstraints { make in + make.height.equalTo(28) + make.width.equalTo(29.3) + } + } } override func setButtonEvent() { @@ -475,49 +343,51 @@ final class SetRateViewController: BaseViewController { override func setCustomNavigationBar() { super.setCustomNavigationBar() - if reviewId != nil { - navigationItem.title = "리뷰 수정하기" - } else { - navigationItem.title = "리뷰 남기기" - } + navigationItem.title = reviewId != nil ? "리뷰 수정하기" : "리뷰 남기기" } - // ✨ 수정: selectedIDList -> validMenuIDList로 이름 변경 반영 - func dataBind(list: [String], idList: [Int], reviewList: [(BeforeSelectedImageDTO, UIImage?)]?, currentPage: Int) { - selectedList = list - validMenuIDList = idList // ✨ 수정: selectedIDList -> validMenuIDList - if let reviewList { - self.reviewList = reviewList - } else { - self.reviewList = Array(repeating: (BeforeSelectedImageDTO(mainRating: 0, - amountRating: nil, - tasteRating: nil, - content: ""), - nil), count: idList.count) - } - self.currentPage = currentPage - } - - // 좋아요 토글 메서드 (여기가 없으면 'Cannot find toggleLike' 에러) - private func toggleLike(for index: Int) { - likedStates[index].toggle() - let idx = IndexPath(row: index, section: 0) + // MARK: - Data Binding - if let cell = menuTableView.cellForRow(at: idx) as? MenuLikeCell { - cell.dataBind(menu: selectedList[index], isLiked: likedStates[index]) + func dataBind(list: [String], idList: [Int]) { + selectedList = list + validMenuIDList = idList + likedStates = Array(repeating: false, count: list.count) + + // ✨ 타입 추정 + if idList.count == 1 { + reviewType = .fixed + menuID = idList.first } else { - menuTableView.reloadRows(at: [idx], with: .none) + reviewType = .variable } + + menuTableView.reloadData() } - - func dataBindForFix(list: [String], reivewId: Int) { - selectedList = list - reviewId = reivewId - menuLabel.text = "\(selectedList[0]) 을/를 추천하시겠어요?" + + func dataBindForFix(list: [String], reviewId: Int) { + self.selectedList = list + self.reviewId = reviewId + self.likedStates = Array(repeating: false, count: list.count) + + menuLabel.text = "\(list[0]) 을/를 추천하시겠어요?" selectImageButton.isHidden = true deleteMethodLabel.isHidden = true nextButton.setTitle("리뷰 수정 완료하기", for: .normal) } + + func settingForReviewFix(data: ReviewListItem) { + rateView.currentStar = Int(data.rating) + rateView.settingStarForFix(currentStar: Int(data.rating)) + userReviewTextView.text = data.content ?? "" + userReviewTextView.textColor = .black + maximumWordLabel.text = "\(data.content?.count ?? 0) / 300" + + if let imageUrl = data.imageUrls?.first, !imageUrl.isEmpty { + userReviewImageView.kfSetImage(url: imageUrl) + imageCountLabel.text = "사진 1/1" + closeButton.isHidden = false + } + } func setDelegate() { menuTableView.register(MenuLikeCell.self, forCellReuseIdentifier: MenuLikeCell.identifier) @@ -527,222 +397,159 @@ final class SetRateViewController: BaseViewController { imagePickerController.delegate = self imagePickerController.sourceType = .photoLibrary imagePickerController.allowsEditing = false - userReviewTextView.delegate = self } + private func toggleLike(for index: Int) { + likedStates[index].toggle() + let idx = IndexPath(row: index, section: 0) + + if let cell = menuTableView.cellForRow(at: idx) as? MenuLikeCell { + cell.dataBind(menu: selectedList[index], isLiked: likedStates[index]) + } else { + menuTableView.reloadRows(at: [idx], with: .none) + } + } + + // MARK: - Actions + @objc - func tappedNextButton() { - // ✨ 수정: V1의 페이지 로직을 제거하고, 바로 데이터 전송 로직 호출 - if userReviewTextView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" || userReviewTextView.text.count < 3 { - showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) - } else { - // 별점 검사: rateView는 메인 별점 - if rateView.currentStar != 0 { - // 리뷰 작성하기 버튼이 isEnabled = true일 때의 area - let param = BeforeSelectedImageDTO(mainRating: rateView.currentStar, - amountRating: quantityRateView.currentStar, - tasteRating: tasteRateView.currentStar, - content: userReviewTextView.text) - - switch reviewId { - case .none: - /// 현재 이미지를 별도 변수에 저장 - let currentImage = userPickedImage - // 단일 페이지이므로 reviewList의 0번 인덱스에 저장 - reviewList[0] = (param, currentImage) // ✨ 수정: currentPage 대신 0번 인덱스 사용 - - navigationController?.isNavigationBarHidden = false - sendDataIfCurrentPageIsLast() // ✨ 수정: 바로 API 전송 - - case let .some(reviewID): - // 단일 리뷰 수정 로직 (기존 로직 유지) - patchFixedReview(reviewId: reviewID, param: param) - } - - } else { - showToast(message: "별점을 모두 입력해주세요!", type: .info) - } - } + func tappedNextButton() { + // 유효성 검증 + let reviewText = userReviewTextView.text ?? "" + if reviewText == "메뉴에 대한 상세한 리뷰를 작성해주세요" || reviewText.count < 3 { + showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) + return + } + + guard rateView.currentStar != 0 else { + showToast(message: "별점을 입력해주세요!", type: .info) + return } + + // ✨ 타입에 따라 적절한 API 호출 + switch reviewType { + case .variable: + sendMealReview() + case .fixed: + sendMenuReview() + } + } - // ✨ 수정: V2 Meal Review API를 사용하도록 변경 - private func sendDataIfCurrentPageIsLast() { - guard let mealId = mealID else { return } // mealId가 없으면 전송 불가 + // ✨ V2 API: Meal Review (VARIABLE) + private func sendMealReview() { + guard let mealId = mealID else { + showToast(message: "식단 정보가 없습니다.") + return + } _Concurrency.Task { do { - // 1. 이미지 업로드 (만약 이미지가 있다면) + // 1. 이미지 업로드 var imageUrl: String? -// if let image = reviewList.last?.1 { // 마지막 리뷰의 이미지만 사용 (Meal Review는 이미지 1개) -// imageUrl = try await uploadImage(image: image) -// } -// - if let image = reviewList.first?.1 { // ✨ 수정: .last? -> .first? 로 변경 (단일 리뷰이므로) - imageUrl = try await uploadImage(image: image) - } + if let image = userPickedImage { + imageUrl = try await uploadImage(image: image) + } - // 2. Meal Review 요청 객체 생성 - let menuLikes: [MenuLike] = validMenuIDList.enumerated().map { (index, menuId) in + // 2. Meal Review 요청 생성 + let menuLikes = validMenuIDList.enumerated().map { (index, menuId) in MenuLike(menuId: menuId, isLike: likedStates[index]) } - // Meal Review는 하나의 평점/내용/이미지를 사용하므로, 마지막 메뉴의 리뷰 데이터를 사용 -// guard let lastReview = reviewList.last else { -// throw NSError(domain: "ReviewError", code: -1, userInfo: [NSLocalizedDescriptionKey: "리뷰 데이터가 없습니다."]) -// } - - guard let lastReview = reviewList.first else { // ✨ 수정: .last? -> .first? 로 변경 - throw NSError(domain: "ReviewError", code: -1, userInfo: [NSLocalizedDescriptionKey: "리뷰 데이터가 없습니다."]) - } - let request = WriteReviewMealRequest( mealId: mealId, - rating: lastReview.0.mainRating, // 메인 평점 - menuLikes: menuLikes, // 메뉴별 좋아요 상태 - content: lastReview.0.content, // 리뷰 내용 - imageUrls: imageUrl != nil ? [imageUrl!] : nil // 이미지 URL + rating: rateView.currentStar, + menuLikes: menuLikes, + content: userReviewTextView.text, + imageUrls: imageUrl != nil ? [imageUrl!] : nil ) - // Firebase 이벤트 로그 (V1 로그 제거 또는 V2 로직에 맞게 수정 필요) - // 현재는 단일 요청으로 통합되었으므로, 마지막 리뷰 기준으로만 로그를 남깁니다. - let photoAttached = (imageUrl != nil) ? 1 : 0 - let rating = lastReview.0.mainRating - let selection = self.selectedList.count - ReviewAnalyticsManager.shared.logCompleteReviewV1(photoAttached: photoAttached, rating: rating, selection: selection) - - // 3. Meal Review 전송 + // 3. API 전송 try await postMealReview(request: request) await MainActor.run { - self.moveToReviewVC() // ✨ 수정: 성공 시 화면 이동 + self.moveToReviewVC() } } catch { await MainActor.run { - print("리뷰 업로드 실패: \(error)") + print("❌ Meal 리뷰 업로드 실패: \(error)") self.showToast(message: "리뷰 업로드에 실패했습니다.") } } } } - // ✨ 수정: 기존 uploadReview를 제거하고, postMealReview를 추가 - private func postMealReview(request: WriteReviewMealRequest) async throws { - try await withCheckedThrowingContinuation { continuation in - NetworkService.shared.request( - WriteReviewRouter.writeMealReview(param: request), - responseType: Bool.self, // 응답 타입이 Bool이라고 가정 - useAuth: true - ) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } + // ✨ V2 API: Menu Review (FIXED) + private func sendMenuReview() { + guard let menuId = menuID ?? validMenuIDList.first else { + showToast(message: "메뉴 정보가 없습니다.") + return } - } - - // 이 메서드는 단일 메뉴 리뷰를 위한 것이었으나, 현재 Meal Review 로직에서는 사용되지 않습니다. - // Menu Review (V2) 사용 시 재활용 가능성을 위해 주석 처리 없이 남겨둡니다. -// private func uploadReview(reviewDTO: BeforeSelectedImageDTO, image: UIImage?, menuId: Int) async throws { -// if let image = image { -// // 이미지 업로드 후 리뷰 작성 -// let imageUrl = try await uploadImage(image: image) -// let request = WriteReviewRequest(content: reviewDTO, imageURL: imageUrl) -// try await postReview(request: request, menuId: menuId) -// } else { -// // 이미지 없이 리뷰만 작성 -// let request = WriteReviewRequest(content: reviewDTO, imageURL: "") -// try await postReview(request: request, menuId: menuId) -// } -// } - private func uploadImage(image: UIImage) async throws -> String { - try await withCheckedThrowingContinuation { continuation in - NetworkService.shared.request( - WriteReviewRouter.uploadImage(image: image), - responseType: UploadImageResponse.self, - useAuth: true - ) { result in - switch result { - case .success(let data): - continuation.resume(returning: data.url) - case .failure(let error): - continuation.resume(throwing: error) + _Concurrency.Task { + do { + // 1. 이미지 업로드 + var imageUrl: String? + if let image = userPickedImage { + imageUrl = try await uploadImage(image: image) + } + + // 2. Menu Review 요청 생성 + let menuLike = MenuLikeItem( + menuId: menuId, + isLike: likedStates.first ?? false + ) + + let request = WriteReviewMenuRequest( + rating: rateView.currentStar, + menuLike: menuLike, + content: userReviewTextView.text, + imageUrls: imageUrl != nil ? [imageUrl!] : nil + ) + + // 3. API 전송 + try await postMenuReview(request: request) + + await MainActor.run { + self.moveToReviewVC() + } + + } catch { + await MainActor.run { + print("❌ Menu 리뷰 업로드 실패: \(error)") + self.showToast(message: "리뷰 업로드에 실패했습니다.") } } } } - - @objc - func didSelectedImage() { - present(imagePickerController, animated: true, completion: nil) - } - - @objc - func didTappedimageView() { - userReviewImageView.image = nil // 이미지 삭제 - userPickedImage = nil - imageCountLabel.text = "사진 0/1" - closeButton.isHidden = true // Show close button when image is selected - } - -// private func prepareForNextReview() { -// let setRateVC: SetRateViewController -// -// // ✨ 수정: 다음 페이지로 이동할 때 현재 mealId를 전달 -// if let mealId = self.mealID { -// setRateVC = SetRateViewController(mealId: mealId) -// } else { -// // mealId가 없으면 기존처럼 인자 없이 초기화 (예: 고정 메뉴 리뷰 수정 후 다음 단계) -// setRateVC = SetRateViewController() -// } -// -// setRateVC.dataBind(list: selectedList, -// idList: validMenuIDList, // ✨ 수정: selectedIDList -> validMenuIDList -// reviewList: reviewList, -// currentPage: currentPage + 1) -// navigationController?.pushViewController(setRateVC, animated: true) -// } - - // ✨ 수정: ReviewViewController로 돌아가는 로직 적용 + private func moveToReviewVC() { - if let reviewViewController = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) { - navigationController?.popToViewController(reviewViewController, animated: true) + if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) { + navigationController?.popToViewController(reviewVC, animated: true) - // 네비게이션 스택에서 HomeViewController 찾아서 새로고침 if let homeVC = navigationController?.viewControllers.first as? HomeViewController { homeVC.refreshAfterReview() } } } - func settingForReviewFix(data: MenuDataList) { - rateView.currentStar = data.mainRating - rateView.settingStarForFix(currentStar: data.mainRating) - - quantityRateView.currentStar = data.amountRating ?? 0 - quantityRateView.settingStarForFix(currentStar: data.amountRating ?? 0) - - tasteRateView.currentStar = data.tasteRating ?? 0 - tasteRateView.settingStarForFix(currentStar: data.tasteRating ?? 0) + @objc func didSelectedImage() { + present(imagePickerController, animated: true) + } - userReviewTextView.text = data.content - userReviewTextView.textColor = .black + @objc func didTappedImageView() { + userReviewImageView.image = nil + userPickedImage = nil + imageCountLabel.text = "사진 0/1" + closeButton.isHidden = true } - - } -// MARK: - Server +// MARK: - Network extension SetRateViewController { - // ✨ 수정: V1 postReview 제거 (V2 Meal Review 사용) - // V2 Menu Review API (단일 메뉴 리뷰) private func postMenuReview(request: WriteReviewMenuRequest) async throws { try await withCheckedThrowingContinuation { continuation in NetworkService.shared.request( @@ -752,46 +559,48 @@ extension SetRateViewController { ) { result in switch result { case .success: + print("✅ Menu Review 작성 성공") continuation.resume() case .failure(let error): + print("❌ Menu Review 작성 실패: \(error)") continuation.resume(throwing: error) } } } } - // V2 Meal Review API (식사 리뷰) - `sendDataIfCurrentPageIsLast()`에서 사용 - // private func postMealReview(request: WriteReviewMealRequest) async throws { ... } // 위에 정의됨 - - private func patchFixedReview(reviewId: Int, param: BeforeSelectedImageDTO) { - NetworkService.shared.request( - ReviewRouter.fixReview(reviewId, param), - responseType: Bool.self, - useAuth: true - ) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success: - self.navigationController?.popViewController(animated: true) - - case .failure(let error): - print("리뷰 수정 실패: \(error.localizedDescription)") - RealmService.shared.resetDB() - self.navigateToLogin() + private func postMealReview(request: WriteReviewMealRequest) async throws { + try await withCheckedThrowingContinuation { continuation in + NetworkService.shared.request( + WriteReviewRouter.writeMealReview(param: request), + responseType: Bool.self, + useAuth: true + ) { result in + switch result { + case .success: + print("✅ Meal Review 작성 성공") + continuation.resume() + case .failure(let error): + print("❌ Meal Review 작성 실패: \(error)") + continuation.resume(throwing: error) + } } } } - - private func navigateToLogin() { - let loginVC = LoginViewController() - loginVC.toastMessage = "세션이 만료되었습니다. 다시 로그인해주세요." - loginVC.toastType = .info - - DispatchQueue.main.async { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginVC)) + + private func uploadImage(image: UIImage) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + NetworkService.shared.request( + WriteReviewRouter.uploadImage(image: image), + responseType: UploadImageResponse.self, + useAuth: true + ) { result in + switch result { + case .success(let data): + continuation.resume(returning: data.url) + case .failure(let error): + continuation.resume(throwing: error) + } } } } @@ -799,15 +608,15 @@ extension SetRateViewController { // MARK: - UIImagePickerControllerDelegate -extension SetRateViewController: UIImagePickerControllerDelegate { +extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + if let image = info[.originalImage] as? UIImage { userReviewImageView.image = image userPickedImage = image imageCountLabel.text = "사진 1/1" - closeButton.isHidden = false // Show close button when image is selected + closeButton.isHidden = false } - picker.dismiss(animated: true, completion: nil) + picker.dismiss(animated: true) } } @@ -819,16 +628,10 @@ extension SetRateViewController: UITextViewDelegate { guard let stringRange = Range(range, in: currentText) else { return false } let newLength = currentText.count + text.count - range.length - // 최대 글자수 제한 - if newLength > 300 { - return false - } + if newLength > 300 { return false } - // 글자수 레이블 업데이트는 항상 허용 - // **주의**: `newLength`는 아직 적용되지 않은 새로운 문자열의 길이 let textToDisplay = currentText.replacingCharacters(in: stringRange, with: text) maximumWordLabel.text = "\(textToDisplay.count) / 300" - return true } @@ -845,74 +648,45 @@ extension SetRateViewController: UITextViewDelegate { textView.textColor = .gray500 maximumWordLabel.text = "0 / 300" } else { - // 끝났을 때 현재 글자 수 업데이트 - maximumWordLabel.text = "\(textView.text.count) / 300" + maximumWordLabel.text = "\(textView.text.count) / 300" } } } -// MARK: - UINavigationControllerDelegate +// MARK: - Keyboard Handling -extension SetRateViewController: UINavigationControllerDelegate { - func navigationController(_: UINavigationController, willShow viewController: UIViewController, animated _: Bool) { - if viewController == self { - // Pop 되기 직전의 로직을 여기서 실행 - print("Back button pressed, will pop the current view controller") - } - } - - // 키보드가 나타났다는 알림을 받으면 실행할 메서드 - @objc - func keyboardWillShow(_ noti: NSNotification) { - // 키보드의 높이만큼 화면을 올려준다. +extension SetRateViewController { + @objc func keyboardWillShow(_ noti: NSNotification) { if let keyboardFrame: NSValue = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardRectangle = keyboardFrame.cgRectValue - UIView.animate( - withDuration: 0.3, - animations: { - self.view.transform = CGAffineTransform(translationX: 0, y: -keyboardRectangle.height) - self.navigationController?.isNavigationBarHidden = true - } - ) + UIView.animate(withDuration: 0.3) { + self.view.transform = CGAffineTransform(translationX: 0, y: -keyboardRectangle.height) + self.navigationController?.isNavigationBarHidden = true + } } } - // 키보드가 사라졌다는 알림을 받으면 실행할 메서드 - @objc - func keyboardWillHide(_: NSNotification) { + @objc func keyboardWillHide(_: NSNotification) { view.transform = .identity navigationController?.isNavigationBarHidden = false } - // 노티피케이션을 추가하는 메서드 func addKeyboardNotifications() { - // 키보드가 나타날 때 앱에게 알리는 메서드 추가 - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - // 키보드가 사라질 때 앱에게 알리는 메서드 추가 - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) - } - - // 노티피케이션을 제거하는 메서드 + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + func removeKeyboardNotifications() { - // 키보드가 나타날 때 앱에게 알리는 메서드 제거 - NotificationCenter.default.removeObserver(self, - name: UIResponder.keyboardWillShowNotification, - object: nil) - // 키보드가 사라질 때 앱에게 알리는 메서드 제거 - NotificationCenter.default.removeObserver(self, - name: UIResponder.keyboardWillHideNotification, - object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) } } +// MARK: - UITableViewDataSource & Delegate + extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return selectedList.count // 리뷰 대상 메뉴 리스트 + selectedList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -920,31 +694,17 @@ extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { return UITableViewCell() } - let menuName = selectedList[indexPath.row] - let isLiked = likedStates[indexPath.row] - cell.dataBind(menu: menuName, isLiked: isLiked) - - // 인덱스 캡처 대신, 셀로부터 현재 indexPath를 찾아서 토글 (재사용 안전) - cell.onLikeTapped = { [weak self, weak cell, weak tableView] in - guard - let self = self, - let tableView = tableView, - let cell = cell, - let tappedIndexPath = tableView.indexPath(for: cell) - else { return } - self.toggleLike(for: tappedIndexPath.row) - } + cell.dataBind(menu: selectedList[indexPath.row], isLiked: likedStates[indexPath.row]) + cell.onLikeTapped = { [weak self, weak cell, weak tableView] in + guard let self, let tableView, let cell, let idx = tableView.indexPath(for: cell) else { return } + self.toggleLike(for: idx.row) + } return cell } - // 선택 이벤트 (좋아요 버튼 눌렀을 때 등) func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print("\(selectedList[indexPath.row]) 선택됨") - // 선택 효과 제거 (회색 하이라이트 방지) - tableView.deselectRow(at: indexPath, animated: false) - - // 행을 눌렀을 때도 토글 실행 - toggleLike(for: indexPath.row) + tableView.deselectRow(at: indexPath, animated: false) + toggleLike(for: indexPath.row) } } From de1e34d6bab48db1dad41f907d9a57aab03e42e3 Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 24 Nov 2025 22:57:44 +0900 Subject: [PATCH 17/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20api=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/BeforeSelectedImageDTO.swift | 2 - .../DTO/Review/FixedReviewRequestDTO.swift | 14 ++ .../Data/Network/Router/ReviewRouter.swift | 2 +- .../Network/Router/WriteReviewRouter.swift | 9 +- .../ViewController/ReviewViewController.swift | 131 +++++++++++++----- .../SetRateViewController.swift | 77 ++++++++++ 6 files changed, 193 insertions(+), 42 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRequestDTO.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift index 2f3f23f4..062e6469 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift @@ -9,7 +9,5 @@ import Foundation struct BeforeSelectedImageDTO: Codable { let mainRating: Int - let amountRating: Int? - let tasteRating: Int? let content: String } diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRequestDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRequestDTO.swift new file mode 100644 index 00000000..aede13a6 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRequestDTO.swift @@ -0,0 +1,14 @@ +// +// FixedReviewRequestDTO.swift +// EATSSU +// +// Created by 한금준 on 11/24/25. +// + +import Foundation + +struct FixedReviewRequestDTO: Encodable { + let rating: Int + let menuLikes: [MenuLike] + let content: String +} diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 144222c4..8bb6acc5 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -50,7 +50,7 @@ extension ReviewRouter: TargetType { case .report: "/reports" case let .deleteReview(reviewId): - "/reviews/\(reviewId)" + "/v2/reviews/\(reviewId)" // case let .fixReview(reviewId, _): // "/reviews/\(reviewId)" // MARK: - New V2 Path diff --git a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift index 889cdfb8..097de257 100644 --- a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift @@ -17,6 +17,7 @@ enum WriteReviewRouter { // MARK: - New V2 APIs case writeMenuReview(param: WriteReviewMenuRequest) case writeMealReview(param: WriteReviewMealRequest) + case fixReview(reviewId: Int, param: FixedReviewRequestDTO) } extension WriteReviewRouter: TargetType { @@ -38,6 +39,8 @@ extension WriteReviewRouter: TargetType { "/v2/reviews/menu" case .writeMealReview: "/v2/reviews/meal" + case .fixReview(reviewId: let reviewId, param: _): + "/v2/reviews/\(reviewId)" } } @@ -45,6 +48,8 @@ extension WriteReviewRouter: TargetType { switch self { case /*.writeReview,*/ .uploadImage, /*.writeNewReview,*/ .writeMenuReview, .writeMealReview: .post + case .fixReview: + .patch } } @@ -93,12 +98,14 @@ extension WriteReviewRouter: TargetType { return .requestJSONEncodable(param) case let .writeMealReview(param: param): return .requestJSONEncodable(param) + case let .fixReview(reviewId: _, param: param): + return .requestJSONEncodable(param) } } var headers: [String: String]? { switch self { - case /*.writeNewReview,*/ .writeMenuReview, .writeMealReview: + case /*.writeNewReview,*/ .writeMenuReview, .writeMealReview, .fixReview: return ["Content-Type": "application/json"] case .uploadImage/*, .writeReview*/: return ["Content-Type": "multipart/form-data"] diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index e3aff3a3..406168a5 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -226,46 +226,90 @@ final class ReviewViewController: BaseViewController { menuID = id } - private func showFixOrDeleteAlert(data: ReviewListItem) { - let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", - message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", - preferredStyle: UIAlertController.Style.actionSheet) - - let fixAction = UIAlertAction(title: "수정하기", - style: .default, - handler: { _ in + private func showDeleteAlert(data: ReviewListItem) { + + // ✨ 리뷰 작성자가 아니면 바로 신고 다이얼로그를 띄웁니다. + if !data.isWriter { + self.showReportAlert(reviewID: data.reviewId) + return + } - let menuNames = data.menu?.map { $0.name } ?? [] + // ✨ 리뷰 작성자인 경우: 삭제 시 Custom Dialog를 사용합니다. - let setRateViewController = SetRateViewController(menuId: self.menuID) + // Custom Dialog를 위한 데이터 + let title = "리뷰 삭제" + let message = "해당 리뷰를 삭제할까요?" + let confirmButtonTitle = "삭제하기" + let cancelButtonTitle = "취소하기" - setRateViewController.dataBindForFix(list: menuNames, reviewId: data.reviewId) - setRateViewController.settingForReviewFix(data: data) - self.navigationController?.pushViewController(setRateViewController, animated: true) - }) - - let deleteAction = UIAlertAction(title: "삭제하기", - style: .default, - handler: { _ in self.showCustomDialog( - title: "리뷰 삭제하기", - message: "해당 리뷰를 삭제할까요?", - cancelButtonTitle: "취소하기", - confirmButtonTitle: "삭제하기" + title: title, + message: message, + cancelButtonTitle: cancelButtonTitle, + confirmButtonTitle: confirmButtonTitle ) { [weak self] in - self?.deleteReview(reviewID: data.reviewId) + guard let self = self else { return } + + // 삭제 확인 시, deleteReview 함수 호출 + self.deleteReview(reviewID: data.reviewId) } - }) - - let cancelAction = UIAlertAction(title: "취소하기", - style: .cancel, - handler: nil) + } - alert.addAction(fixAction) - alert.addAction(deleteAction) - alert.addAction(cancelAction) - present(alert, animated: true, completion: nil) - } + private func showFixOrDeleteAlert_OLD(data: ReviewListItem) { + let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", + message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", + preferredStyle: UIAlertController.Style.actionSheet) + + let fixAction = UIAlertAction(title: "수정하기", + style: .default, + handler: { _ in + + let menuNames = data.menu?.map { $0.name } ?? [] + // ✨ MenuLike 배열에서 menuId만 추출 (수정 요청 DTO의 menuLikes를 구성하기 위함) + let menuIds = data.menu?.map { $0.menuId } ?? [] + + // 🛠️ 수정: data.type에 따라 SetRateViewController 생성자 변경 필요 + // ReviewViewController는 dataBindForFix를 사용할 것이므로 menuId 생성자를 사용하는 것이 적절 + let setRateViewController = SetRateViewController(menuId: self.menuID) + + // 1. 리뷰 ID와 메뉴 이름을 바인딩 (UI 설정 및 reviewId 저장) + setRateViewController.dataBindForFix(list: menuNames, reviewId: data.reviewId) + + // 2. 리뷰 상세 정보 (별점, 내용, 이미지) 바인딩 + setRateViewController.settingForReviewFix(data: data) + + // 3. 리뷰 수정 API 호출을 위한 추가 정보 바인딩 (menuId, isLike) + // SetRateViewController의 validMenuIDList와 likedStates에 원본 정보를 설정 + let likedStates = data.menu?.map { $0.isLike } ?? [] + setRateViewController.dataBindForFix( + menuNames: menuNames, + menuIds: menuIds, + likedStates: likedStates + ) + + self.navigationController?.pushViewController(setRateViewController, animated: true) + }) + + let deleteAction = UIAlertAction(title: "삭제하기", + style: .destructive, + handler: { [weak self] _ in + guard let self = self else { return } + + // ✨ V2 API를 사용하는 deleteReview 함수 호출 (reviewId 전달) + // ReviewRouter.deleteReview에 V2 Path와 Method가 적용되었으므로 + // 이 함수 내부의 호출 로직은 변경 없이 V2 API를 사용하게 됩니다. + self.deleteReview(reviewID: data.reviewId) + }) + + let cancelAction = UIAlertAction(title: "취소하기", + style: .cancel, + handler: nil) + + alert.addAction(fixAction) + alert.addAction(deleteAction) + alert.addAction(cancelAction) + present(alert, animated: true, completion: nil) + } private func showReportAlert(reviewID: Int) { showCustomDialog( @@ -433,7 +477,7 @@ extension ReviewViewController: UITableViewDataSource { cell.handler = { [weak self] in guard let self else { return } - reviewList[indexPath.row].isWriter ? self.showFixOrDeleteAlert(data: reviewList[indexPath.row]) + reviewList[indexPath.row].isWriter ? self.showDeleteAlert(data: reviewList[indexPath.row]) : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) } cell.selectionStyle = .none @@ -596,16 +640,27 @@ extension ReviewViewController { } func deleteReview(reviewID: Int) { - reviewProvider.request(.deleteReview(reviewID)) { response in - switch response { + NetworkService.shared.request( + ReviewRouter.deleteReview(reviewID), // 1. ReviewRouter를 타겟으로 지정 + responseType: Bool.self, // 2. 응답 타입은 Bool로 가정 + useAuth: true // 3. ✨ 인증 필요! + ) { [weak self] result in + guard let self = self else { return } + + switch result { case .success: + print("✅ Review 삭제 성공") + // 삭제 성공 시, 통계 및 리뷰 목록을 새로고침 self.getStatistics() if self.type == "VARIABLE" { self.getValidMenusForReview() } self.getReviewList(type: self.type, menuId: self.menuID) - case let .failure(err): - print("❌ Delete Review Error: \(err.localizedDescription)") + self.showToast(message: "리뷰가 성공적으로 삭제되었습니다.") // 사용자에게 피드백 제공 + + case let .failure(error): + print("❌ Delete Review Error: \(error.localizedDescription)") + self.showToast(message: "리뷰 삭제에 실패했습니다.") } } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 197004f8..ac2a8385 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -364,6 +364,20 @@ final class SetRateViewController: BaseViewController { menuTableView.reloadData() } + // ✨ 리뷰 수정 시 메뉴 ID와 isLike 상태를 함께 바인딩하는 오버로드 + func dataBindForFix(menuNames: [String], menuIds: [Int], likedStates: [Bool]) { + self.selectedList = menuNames + self.validMenuIDList = menuIds + self.likedStates = likedStates + self.reviewType = .fixed // 리뷰 수정은 일반적으로 단일 메뉴 (fixed) 처럼 동작 + + menuLabel.text = "\(menuNames.first ?? "") 을/를 추천하시겠어요?" + + // 테이블 뷰 다시 로드 및 높이 업데이트 + menuTableView.reloadData() + view.setNeedsLayout() + } + func dataBindForFix(list: [String], reviewId: Int) { self.selectedList = list self.reviewId = reviewId @@ -427,6 +441,12 @@ final class SetRateViewController: BaseViewController { return } + // ✨ 리뷰 ID가 있으면 수정, 없으면 작성 + if reviewId != nil { + sendFixReview() + return + } + // ✨ 타입에 따라 적절한 API 호출 switch reviewType { case .variable: @@ -436,6 +456,44 @@ final class SetRateViewController: BaseViewController { } } + // ✨ V2 API: Review Fix + private func sendFixReview() { + guard let reviewId = reviewId else { + showToast(message: "수정할 리뷰 정보가 없습니다.") + return + } + + _Concurrency.Task { + do { + // 1. MenuLike 배열 생성 (현재는 FIXED 리뷰만 수정 가능하다고 가정) + // FIXED 리뷰는 likedStates에 하나의 Bool 값만 가집니다. + let menuLikes: [MenuLike] = validMenuIDList.enumerated().map { (index, menuId) in + MenuLike(menuId: menuId, isLike: likedStates[index]) + } + + // 2. Fixed Review 요청 생성 + let request = FixedReviewRequestDTO( + rating: rateView.currentStar, + menuLikes: menuLikes, + content: userReviewTextView.text + ) + + // 3. API 전송 + try await postFixReview(reviewId: reviewId, request: request) + + await MainActor.run { + self.moveToReviewVC() + } + + } catch { + await MainActor.run { + print("❌ Review 수정 업로드 실패: \(error)") + self.showToast(message: "리뷰 수정에 실패했습니다.") + } + } + } + } + // ✨ V2 API: Meal Review (VARIABLE) private func sendMealReview() { guard let mealId = mealID else { @@ -604,6 +662,25 @@ extension SetRateViewController { } } } + + private func postFixReview(reviewId: Int, request: FixedReviewRequestDTO) async throws { + try await withCheckedThrowingContinuation { continuation in + NetworkService.shared.request( + WriteReviewRouter.fixReview(reviewId: reviewId, param: request), + responseType: Bool.self, // 수정 성공 시 Bool (또는 BaseResponse의 result가 nil인 경우) + useAuth: true + ) { result in + switch result { + case .success: + print("✅ Review 수정 성공") + continuation.resume() + case .failure(let error): + print("❌ Review 수정 실패: \(error)") + continuation.resume(throwing: error) + } + } + } + } } // MARK: - UIImagePickerControllerDelegate From 11ef6f4ed20d1d77fc22ac07f50872732c5646b4 Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 25 Nov 2025 00:28:57 +0900 Subject: [PATCH 18/69] =?UTF-8?q?[#321]=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=ED=83=AD=EB=B0=94=20=EC=88=A8=EA=B9=80?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../ViewController/ReviewViewController.swift | 4 +- .../SetRateViewController.swift | 167 +++++++++++++++--- 2 files changed, 145 insertions(+), 26 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 406168a5..38106816 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -16,7 +16,9 @@ import SnapKit final class ReviewViewController: BaseViewController { // MARK: - Properties - + override var shouldHideTabBar: Bool { + return true + } let reviewProvider = MoyaProvider(plugins: [ESMoyaLoggingPlugin()]) var menuID: Int = .init() var type = "VARIABLE" diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index ac2a8385..246c313f 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -12,6 +12,9 @@ import EATSSUDesign final class SetRateViewController: BaseViewController { // MARK: - Properties + override var shouldHideTabBar: Bool { + return true + } private var userPickedImage: UIImage? @@ -157,6 +160,14 @@ final class SetRateViewController: BaseViewController { label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return label }() + + private let buttonContainer: UIView = { + let view = UIView() + view.backgroundColor = .white // 버튼 배경색 (TabBarContainer와 유사) + view.layer.cornerRadius = 0 + view.clipsToBounds = true + return view + }() private var nextButton: MainButton = { let button = MainButton() @@ -233,7 +244,10 @@ final class SetRateViewController: BaseViewController { override func configureUI() { dismissKeyboard() - view.addSubview(scrollView) + view.addSubviews(scrollView, buttonContainer) + + // buttonContainer에 nextButton을 추가 + buttonContainer.addSubview(nextButton) scrollView.addSubview(contentView) contentView.addSubviews( rateView, @@ -247,7 +261,7 @@ final class SetRateViewController: BaseViewController { userReviewImageView, closeButton, deleteMethodLabel, - nextButton +// nextButton ) } @@ -255,6 +269,21 @@ final class SetRateViewController: BaseViewController { scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } + + // ✨ 2. buttonContainer 레이아웃 (화면 하단에 고정) + buttonContainer.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + // safeAreaLayoutGuide.bottom을 기준으로 높이 80인 버튼 영역의 top을 잡습니다. + $0.top.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-80) + // 그리고 view의 맨 아래까지 확장하여 버튼 영역 아래를 채웁니다. + $0.bottom.equalToSuperview() + } + + // ✨ 3. nextButton 레이아웃 (buttonContainer 내부에) + nextButton.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.top.equalToSuperview().offset(12) + } contentView.snp.makeConstraints { make in make.top.bottom.equalToSuperview() @@ -296,21 +325,28 @@ final class SetRateViewController: BaseViewController { } selectImageButton.snp.makeConstraints { - $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) - $0.leading.equalToSuperview().offset(15) - $0.width.height.equalTo(60) - } + $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) + $0.leading.equalToSuperview().offset(16) // 인셋 16으로 통일 + $0.width.height.equalTo(60) + } - imageCountLabel.snp.makeConstraints { - $0.top.equalTo(selectImageButton.snp.bottom).offset(-19) - $0.centerX.equalTo(selectImageButton) - } + imageCountLabel.snp.makeConstraints { + $0.top.equalTo(selectImageButton.snp.bottom).offset(5) + $0.centerX.equalTo(selectImageButton) + } - userReviewImageView.snp.makeConstraints { - $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) - $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) - $0.width.height.equalTo(60) - } + userReviewImageView.snp.makeConstraints { + $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) + $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) + $0.width.height.equalTo(60) + } + + deleteMethodLabel.snp.makeConstraints { + $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) + $0.leading.equalTo(selectImageButton) + // ✨ contentView bottom 제약을 deleteMethodLabel 아래로 연결 (여유 공간 100pt 확보) + $0.bottom.equalTo(contentView.snp.bottom).offset(-100) + } closeButton.snp.makeConstraints { $0.top.equalTo(userReviewImageView.snp.top).offset(-6) @@ -318,16 +354,16 @@ final class SetRateViewController: BaseViewController { $0.size.equalTo(24) } - deleteMethodLabel.snp.makeConstraints { - $0.top.equalTo(selectImageButton.snp.bottom).offset(7) - $0.leading.equalTo(selectImageButton) - } +// deleteMethodLabel.snp.makeConstraints { +// $0.top.equalTo(selectImageButton.snp.bottom).offset(7) +// $0.leading.equalTo(selectImageButton) +// } - nextButton.snp.makeConstraints { make in - make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) - make.horizontalEdges.equalToSuperview().inset(16) - make.bottom.equalToSuperview().offset(-15) - } +// nextButton.snp.makeConstraints { make in +// make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) +// make.horizontalEdges.equalToSuperview().inset(16) +// make.bottom.equalToSuperview().offset(-15) +// } for i in 0...4 { rateView.buttons[i].snp.makeConstraints { make in @@ -412,6 +448,12 @@ final class SetRateViewController: BaseViewController { imagePickerController.sourceType = .photoLibrary imagePickerController.allowsEditing = false userReviewTextView.delegate = self + + // ✨ 네비게이션 델리게이트 설정 + self.navigationController?.delegate = self + + // ✨ 스와이프 제스처 델리게이트 설정 + self.navigationController?.interactivePopGestureRecognizer?.delegate = self } private func toggleLike(for index: Int) { @@ -685,7 +727,7 @@ extension SetRateViewController { // MARK: - UIImagePickerControllerDelegate -extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { +extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let image = info[.originalImage] as? UIImage { userReviewImageView.image = image @@ -695,6 +737,81 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo } picker.dismiss(animated: true) } + + // 💡 네비게이션 컨트롤러의 뷰 컨트롤러가 pop되기 직전에 호출됩니다. + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + + // 현재 뷰 컨트롤러(SetRateViewController)가 pop되는지 확인합니다. + let isPopping = !navigationController.viewControllers.contains(self) + + // 만약 pop이 발생하고, 리뷰 작성 중이라면 + if isPopping { + + // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) + let textHasContent = userReviewTextView.text != "메뉴에 대한 상세한 리뷰를 작성해주세요" && !userReviewTextView.text.isEmpty + let isReviewStarted: Bool = rateView.currentStar > 0 || textHasContent + + // 2. 리뷰를 새로 작성 중이며 (reviewId == nil) 내용이 있을 경우에만 다이얼로그 표시 + if reviewId == nil, isReviewStarted { + + // pop을 즉시 취소 (다이얼로그 결과를 기다림) + navigationController.viewControllers.append(self) + + let title = "작성 취소" + let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" + let confirmButtonTitle = "나가기" + let cancelButtonTitle = "계속 작성" + + showCustomDialog( + title: title, + message: message, + cancelButtonTitle: cancelButtonTitle, + confirmButtonTitle: confirmButtonTitle + ) { [weak self] in + guard let self = self else { return } + // "나가기" 확인 시, delegate를 nil로 설정하여 재귀 방지 + self.navigationController?.delegate = nil + self.navigationController?.popViewController(animated: true) + // pop 완료 후 다시 delegate 설정 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.navigationController?.delegate = self + } + } + } + } + } + + // 💡 스와이프 제스처가 시작될 때 호출됩니다. + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + + // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) + let isReviewStarted: Bool = rateView.currentStar > 0 || !userReviewTextView.text.isEmpty + + // 2. 리뷰를 새로 작성 중이며 내용이 있을 경우 + if reviewId == nil, isReviewStarted { + + let title = "작성 취소" + let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" + let confirmButtonTitle = "나가기" + let cancelButtonTitle = "계속 작성" + + // pop을 바로 막고 다이얼로그 표시 + showCustomDialog( + title: title, + message: message, + cancelButtonTitle: cancelButtonTitle, + confirmButtonTitle: confirmButtonTitle + ) { [weak self] in + // "나가기" 확인 시, 명시적으로 pop + self?.navigationController?.popViewController(animated: true) + } + // 스와이프 동작을 취소합니다. + return false + } + + // 내용이 없으면 기본 동작 (스와이프 가능) + return true + } } // MARK: - UITextViewDelegate From b9d96c85d16629862c75b1e41c4e0493b2b386d1 Mon Sep 17 00:00:00 2001 From: Funital Date: Fri, 28 Nov 2025 21:44:47 +0900 Subject: [PATCH 19/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=A9=94=EB=89=B4=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/WriteReviewMealRequest.swift | 5 + .../View/SeeReview/ReviewTableCell.swift | 6 +- .../SetRateViewController.swift | 380 ++++++++++-------- 3 files changed, 214 insertions(+), 177 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift index 5bd64261..40f2224b 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMealRequest.swift @@ -17,4 +17,9 @@ struct WriteReviewMealRequest: Encodable { struct MenuLike: Encodable { let menuId: Int let isLike: Bool + + private enum CodingKeys: String, CodingKey { + case menuId + case isLike + } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index aedf6201..3f45397e 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -147,6 +147,7 @@ final class ReviewTableCell: UITableViewCell { contentView.addSubview(profileStackView) contentView.addSubview(dateReportStackView) contentView.addSubview(contentStackView) + contentStackView.setCustomSpacing(8, after: reviewTextView) setLayout() } @@ -193,7 +194,10 @@ final class ReviewTableCell: UITableViewCell { } foodImageView.snp.makeConstraints { make in - make.height.width.equalTo(358) +// make.height.width.equalTo(358) + make.top.equalTo(reviewTextView.snp.bottom).offset(8) + make.leading.trailing.equalToSuperview() + make.height.equalTo(foodImageView.snp.width).multipliedBy(0.75) } sideButton.snp.makeConstraints { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 246c313f..48fb4146 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -13,9 +13,9 @@ import EATSSUDesign final class SetRateViewController: BaseViewController { // MARK: - Properties override var shouldHideTabBar: Bool { - return true - } - + return true + } + private var userPickedImage: UIImage? private var validMenuIDList: [Int] = [] @@ -30,11 +30,14 @@ final class SetRateViewController: BaseViewController { private var mealID: Int? private var menuID: Int? + private var isReviewSubmitting = false // 서버로 전송 중 여부 + private var isReviewSubmitted = false // 리뷰가 정상 제출된 상태 + enum ReviewType { case fixed // writeMenuReview 사용 case variable // writeMealReview 사용 } - + // MARK: - Initializer convenience init(mealId: Int) { @@ -48,24 +51,24 @@ final class SetRateViewController: BaseViewController { self.menuID = menuId self.reviewType = .fixed } - + // MARK: - UI Components - + private var rateView = RateView() private let imagePickerController = UIImagePickerController() - + private var contentView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() - + private let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false return scrollView }() - + private var menuLabel: UILabel = { let label = UILabel() label.text = "오늘의 식사는 어떠셨나요?" @@ -73,7 +76,7 @@ final class SetRateViewController: BaseViewController { label.textColor = .black return label }() - + private var detailLabel: UILabel = { let label = UILabel() label.text = "추천하고 싶은 메뉴가 있나요?" @@ -89,7 +92,7 @@ final class SetRateViewController: BaseViewController { tableView.isScrollEnabled = false return tableView }() - + private let userReviewTextView: UITextView = { let textView = UITextView() textView.font = .body1 @@ -102,7 +105,7 @@ final class SetRateViewController: BaseViewController { textView.textColor = .gray500 return textView }() - + private lazy var userReviewImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 10 @@ -121,7 +124,7 @@ final class SetRateViewController: BaseViewController { button.isHidden = true return button }() - + private lazy var selectImageButton: UIButton = { let button = UIButton() var config = UIButton.Configuration.plain() @@ -135,7 +138,7 @@ final class SetRateViewController: BaseViewController { button.clipsToBounds = true return button }() - + private let imageCountLabel: UILabel = { let label = UILabel() label.text = "사진 0/1" @@ -144,7 +147,7 @@ final class SetRateViewController: BaseViewController { label.textAlignment = .center return label }() - + private let deleteMethodLabel: UILabel = { let label = UILabel() label.text = "사진 클릭 시, 삭제됩니다" @@ -152,7 +155,7 @@ final class SetRateViewController: BaseViewController { label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color return label }() - + private let maximumWordLabel: UILabel = { let label = UILabel() label.text = "0 / 300" @@ -162,21 +165,21 @@ final class SetRateViewController: BaseViewController { }() private let buttonContainer: UIView = { - let view = UIView() - view.backgroundColor = .white // 버튼 배경색 (TabBarContainer와 유사) - view.layer.cornerRadius = 0 - view.clipsToBounds = true - return view - }() - + let view = UIView() + view.backgroundColor = .white // 버튼 배경색 (TabBarContainer와 유사) + view.layer.cornerRadius = 0 + view.clipsToBounds = true + return view + }() + private var nextButton: MainButton = { let button = MainButton() button.title = "리뷰 남기기" return button }() - + // MARK: - Life Cycles - + override func viewDidLoad() { super.viewDidLoad() setDelegate() @@ -191,11 +194,11 @@ final class SetRateViewController: BaseViewController { menuTableView.reloadData() } } - + override func viewWillAppear(_: Bool) { addKeyboardNotifications() } - + override func viewWillDisappear(_: Bool) { removeKeyboardNotifications() } @@ -204,14 +207,14 @@ final class SetRateViewController: BaseViewController { super.viewDidLayoutSubviews() menuTableViewHeightConstraint?.update(offset: menuTableView.contentSize.height) } - + // MARK: - API Calls // ✨ VARIABLE: 리뷰 가능한 메뉴 목록 조회 private func fetchValidMenus(mealId: Int) { NetworkService.shared.request( ReviewRouter.getValidMenusForReview(mealId), - responseType: [MenuInfo].self, + responseType: ReviewValidMenusResponse.self, useAuth: true ) { [weak self] result in guard let self = self else { return } @@ -219,8 +222,8 @@ final class SetRateViewController: BaseViewController { DispatchQueue.main.async { switch result { case .success(let data): - self.selectedList = data.map { $0.name } - self.validMenuIDList = data.map { $0.id } + self.selectedList = data.menuList.map { $0.name } + self.validMenuIDList = data.menuList.map { $0.menuId } self.likedStates = Array(repeating: false, count: self.selectedList.count) self.menuTableView.reloadData() self.view.setNeedsLayout() @@ -239,14 +242,14 @@ final class SetRateViewController: BaseViewController { menuTableView.reloadData() view.setNeedsLayout() } - + // MARK: - UI Configuration - + override func configureUI() { dismissKeyboard() view.addSubviews(scrollView, buttonContainer) - - // buttonContainer에 nextButton을 추가 + + // buttonContainer에 nextButton을 추가 buttonContainer.addSubview(nextButton) scrollView.addSubview(contentView) contentView.addSubviews( @@ -261,10 +264,10 @@ final class SetRateViewController: BaseViewController { userReviewImageView, closeButton, deleteMethodLabel, -// nextButton + // nextButton ) } - + override func setLayout() { scrollView.snp.makeConstraints { $0.edges.equalToSuperview() @@ -278,29 +281,29 @@ final class SetRateViewController: BaseViewController { // 그리고 view의 맨 아래까지 확장하여 버튼 영역 아래를 채웁니다. $0.bottom.equalToSuperview() } - - // ✨ 3. nextButton 레이아웃 (buttonContainer 내부에) - nextButton.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.top.equalToSuperview().offset(12) - } - + + // ✨ 3. nextButton 레이아웃 (buttonContainer 내부에) + nextButton.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.top.equalToSuperview().offset(12) + } + contentView.snp.makeConstraints { make in make.top.bottom.equalToSuperview() make.width.equalTo(scrollView) } - + menuLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(20) make.centerX.equalToSuperview() } - + rateView.snp.makeConstraints { make in make.top.equalTo(menuLabel.snp.bottom).offset(17) make.centerX.equalToSuperview() make.height.equalTo(36.12) } - + detailLabel.snp.makeConstraints { make in make.top.equalTo(rateView.snp.bottom).offset(35) make.centerX.equalToSuperview() @@ -312,78 +315,78 @@ final class SetRateViewController: BaseViewController { $0.trailing.equalToSuperview().offset(-32) menuTableViewHeightConstraint = $0.height.equalTo(0).constraint } - + userReviewTextView.snp.makeConstraints { make in make.top.equalTo(menuTableView.snp.bottom).offset(40) make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(181) } - + maximumWordLabel.snp.makeConstraints { make in make.top.equalTo(userReviewTextView.snp.bottom).offset(7) make.trailing.equalTo(userReviewTextView) } - + selectImageButton.snp.makeConstraints { - $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) - $0.leading.equalToSuperview().offset(16) // 인셋 16으로 통일 - $0.width.height.equalTo(60) - } - - imageCountLabel.snp.makeConstraints { - $0.top.equalTo(selectImageButton.snp.bottom).offset(5) - $0.centerX.equalTo(selectImageButton) - } - - userReviewImageView.snp.makeConstraints { - $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) - $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) - $0.width.height.equalTo(60) - } - - deleteMethodLabel.snp.makeConstraints { - $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) - $0.leading.equalTo(selectImageButton) - // ✨ contentView bottom 제약을 deleteMethodLabel 아래로 연결 (여유 공간 100pt 확보) - $0.bottom.equalTo(contentView.snp.bottom).offset(-100) - } + $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) + $0.leading.equalToSuperview().offset(16) // 인셋 16으로 통일 + $0.width.height.equalTo(60) + } + + imageCountLabel.snp.makeConstraints { + $0.top.equalTo(selectImageButton.snp.bottom).offset(5) + $0.centerX.equalTo(selectImageButton) + } + + userReviewImageView.snp.makeConstraints { + $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) + $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) + $0.width.height.equalTo(60) + } + + deleteMethodLabel.snp.makeConstraints { + $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) + $0.leading.equalTo(selectImageButton) + // ✨ contentView bottom 제약을 deleteMethodLabel 아래로 연결 (여유 공간 100pt 확보) + $0.bottom.equalTo(contentView.snp.bottom).offset(-100) + } closeButton.snp.makeConstraints { $0.top.equalTo(userReviewImageView.snp.top).offset(-6) $0.trailing.equalTo(userReviewImageView.snp.trailing).offset(6) $0.size.equalTo(24) } - -// deleteMethodLabel.snp.makeConstraints { -// $0.top.equalTo(selectImageButton.snp.bottom).offset(7) -// $0.leading.equalTo(selectImageButton) -// } - -// nextButton.snp.makeConstraints { make in -// make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) -// make.horizontalEdges.equalToSuperview().inset(16) -// make.bottom.equalToSuperview().offset(-15) -// } - + + // deleteMethodLabel.snp.makeConstraints { + // $0.top.equalTo(selectImageButton.snp.bottom).offset(7) + // $0.leading.equalTo(selectImageButton) + // } + + // nextButton.snp.makeConstraints { make in + // make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) + // make.horizontalEdges.equalToSuperview().inset(16) + // make.bottom.equalToSuperview().offset(-15) + // } + for i in 0...4 { rateView.buttons[i].snp.makeConstraints { make in - make.height.equalTo(28) + // make.height.equalTo(28) make.width.equalTo(29.3) } } } - + override func setButtonEvent() { nextButton.addTarget(self, action: #selector(tappedNextButton), for: .touchUpInside) } - + override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = reviewId != nil ? "리뷰 수정하기" : "리뷰 남기기" } - + // MARK: - Data Binding - + func dataBind(list: [String], idList: [Int]) { selectedList = list validMenuIDList = idList @@ -438,7 +441,7 @@ final class SetRateViewController: BaseViewController { closeButton.isHidden = false } } - + func setDelegate() { menuTableView.register(MenuLikeCell.self, forCellReuseIdentifier: MenuLikeCell.identifier) menuTableView.dataSource = self @@ -451,11 +454,11 @@ final class SetRateViewController: BaseViewController { // ✨ 네비게이션 델리게이트 설정 self.navigationController?.delegate = self - + // ✨ 스와이프 제스처 델리게이트 설정 self.navigationController?.interactivePopGestureRecognizer?.delegate = self } - + private func toggleLike(for index: Int) { likedStates[index].toggle() let idx = IndexPath(row: index, section: 0) @@ -466,9 +469,9 @@ final class SetRateViewController: BaseViewController { menuTableView.reloadRows(at: [idx], with: .none) } } - + // MARK: - Actions - + @objc func tappedNextButton() { // 유효성 검증 @@ -483,11 +486,14 @@ final class SetRateViewController: BaseViewController { return } + // 리뷰 전송 시작 → 인터셉트 차단 + isReviewSubmitting = true + // ✨ 리뷰 ID가 있으면 수정, 없으면 작성 - if reviewId != nil { - sendFixReview() - return - } + if reviewId != nil { + sendFixReview() + return + } // ✨ 타입에 따라 적절한 API 호출 switch reviewType { @@ -504,7 +510,7 @@ final class SetRateViewController: BaseViewController { showToast(message: "수정할 리뷰 정보가 없습니다.") return } - + _Concurrency.Task { do { // 1. MenuLike 배열 생성 (현재는 FIXED 리뷰만 수정 가능하다고 가정) @@ -524,6 +530,7 @@ final class SetRateViewController: BaseViewController { try await postFixReview(reviewId: reviewId, request: request) await MainActor.run { + self.isReviewSubmitted = true self.moveToReviewVC() } @@ -542,7 +549,7 @@ final class SetRateViewController: BaseViewController { showToast(message: "식단 정보가 없습니다.") return } - + _Concurrency.Task { do { // 1. 이미지 업로드 @@ -568,6 +575,7 @@ final class SetRateViewController: BaseViewController { try await postMealReview(request: request) await MainActor.run { + self.isReviewSubmitted = true self.moveToReviewVC() } @@ -586,7 +594,7 @@ final class SetRateViewController: BaseViewController { showToast(message: "메뉴 정보가 없습니다.") return } - + _Concurrency.Task { do { // 1. 이미지 업로드 @@ -612,6 +620,7 @@ final class SetRateViewController: BaseViewController { try await postMenuReview(request: request) await MainActor.run { + self.isReviewSubmitted = true self.moveToReviewVC() } @@ -633,11 +642,23 @@ final class SetRateViewController: BaseViewController { } } } - + +// @objc func didSelectedImage() { +// present(imagePickerController, animated: true) +// } @objc func didSelectedImage() { - present(imagePickerController, animated: true) + // ✨ navigationController delegate 잠시 비활성화 + let originalDelegate = self.navigationController?.delegate + self.navigationController?.delegate = nil + + present(imagePickerController, animated: true) { [weak self] in + // ✨ picker가 올라온 후 delegate 다시 복원 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self?.navigationController?.delegate = originalDelegate + } + } } - + @objc func didTappedImageView() { userReviewImageView.image = nil userPickedImage = nil @@ -687,7 +708,7 @@ extension SetRateViewController { } } } - + private func uploadImage(image: UIImage) async throws -> String { try await withCheckedThrowingContinuation { continuation in NetworkService.shared.request( @@ -706,23 +727,23 @@ extension SetRateViewController { } private func postFixReview(reviewId: Int, request: FixedReviewRequestDTO) async throws { - try await withCheckedThrowingContinuation { continuation in - NetworkService.shared.request( - WriteReviewRouter.fixReview(reviewId: reviewId, param: request), - responseType: Bool.self, // 수정 성공 시 Bool (또는 BaseResponse의 result가 nil인 경우) - useAuth: true - ) { result in - switch result { - case .success: - print("✅ Review 수정 성공") - continuation.resume() - case .failure(let error): - print("❌ Review 수정 실패: \(error)") - continuation.resume(throwing: error) - } + try await withCheckedThrowingContinuation { continuation in + NetworkService.shared.request( + WriteReviewRouter.fixReview(reviewId: reviewId, param: request), + responseType: Bool.self, // 수정 성공 시 Bool (또는 BaseResponse의 result가 nil인 경우) + useAuth: true + ) { result in + switch result { + case .success: + print("✅ Review 수정 성공") + continuation.resume() + case .failure(let error): + print("❌ Review 수정 실패: \(error)") + continuation.resume(throwing: error) } } } + } } // MARK: - UIImagePickerControllerDelegate @@ -739,79 +760,86 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo } // 💡 네비게이션 컨트롤러의 뷰 컨트롤러가 pop되기 직전에 호출됩니다. - func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - - // 현재 뷰 컨트롤러(SetRateViewController)가 pop되는지 확인합니다. - let isPopping = !navigationController.viewControllers.contains(self) - - // 만약 pop이 발생하고, 리뷰 작성 중이라면 - if isPopping { - - // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) - let textHasContent = userReviewTextView.text != "메뉴에 대한 상세한 리뷰를 작성해주세요" && !userReviewTextView.text.isEmpty - let isReviewStarted: Bool = rateView.currentStar > 0 || textHasContent - - // 2. 리뷰를 새로 작성 중이며 (reviewId == nil) 내용이 있을 경우에만 다이얼로그 표시 - if reviewId == nil, isReviewStarted { - - // pop을 즉시 취소 (다이얼로그 결과를 기다림) - navigationController.viewControllers.append(self) - - let title = "작성 취소" - let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" - let confirmButtonTitle = "나가기" - let cancelButtonTitle = "계속 작성" - - showCustomDialog( - title: title, - message: message, - cancelButtonTitle: cancelButtonTitle, - confirmButtonTitle: confirmButtonTitle - ) { [weak self] in - guard let self = self else { return } - // "나가기" 확인 시, delegate를 nil로 설정하여 재귀 방지 - self.navigationController?.delegate = nil - self.navigationController?.popViewController(animated: true) - // pop 완료 후 다시 delegate 설정 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.navigationController?.delegate = self - } - } - } + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + if isReviewSubmitted { + return } - } - - // 💡 스와이프 제스처가 시작될 때 호출됩니다. - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + + if navigationController is UIImagePickerController { + return + } + + // 현재 뷰 컨트롤러(SetRateViewController)가 pop되는지 확인합니다. + let isPopping = !navigationController.viewControllers.contains(self) + + // 만약 pop이 발생하고, 리뷰 작성 중이라면 + if isPopping { // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) - let isReviewStarted: Bool = rateView.currentStar > 0 || !userReviewTextView.text.isEmpty + let textHasContent = userReviewTextView.text != "메뉴에 대한 상세한 리뷰를 작성해주세요" && !userReviewTextView.text.isEmpty + let isReviewStarted: Bool = rateView.currentStar > 0 || textHasContent - // 2. 리뷰를 새로 작성 중이며 내용이 있을 경우 + // 2. 리뷰를 새로 작성 중이며 (reviewId == nil) 내용이 있을 경우에만 다이얼로그 표시 if reviewId == nil, isReviewStarted { + // pop을 즉시 취소 (다이얼로그 결과를 기다림) + navigationController.viewControllers.append(self) + let title = "작성 취소" let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" let confirmButtonTitle = "나가기" let cancelButtonTitle = "계속 작성" - // pop을 바로 막고 다이얼로그 표시 showCustomDialog( title: title, message: message, cancelButtonTitle: cancelButtonTitle, confirmButtonTitle: confirmButtonTitle ) { [weak self] in - // "나가기" 확인 시, 명시적으로 pop - self?.navigationController?.popViewController(animated: true) + guard let self = self else { return } + // "나가기" 확인 시, delegate를 nil로 설정하여 재귀 방지 + self.navigationController?.delegate = nil + self.navigationController?.popViewController(animated: true) + // pop 완료 후 다시 delegate 설정 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.navigationController?.delegate = self + } } - // 스와이프 동작을 취소합니다. - return false } + } + } + + // 💡 스와이프 제스처가 시작될 때 호출됩니다. + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + + // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) + let isReviewStarted: Bool = rateView.currentStar > 0 || !userReviewTextView.text.isEmpty + + // 2. 리뷰를 새로 작성 중이며 내용이 있을 경우 + if reviewId == nil, isReviewStarted { + + let title = "작성 취소" + let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" + let confirmButtonTitle = "나가기" + let cancelButtonTitle = "계속 작성" - // 내용이 없으면 기본 동작 (스와이프 가능) - return true + // pop을 바로 막고 다이얼로그 표시 + showCustomDialog( + title: title, + message: message, + cancelButtonTitle: cancelButtonTitle, + confirmButtonTitle: confirmButtonTitle + ) { [weak self] in + // "나가기" 확인 시, 명시적으로 pop + self?.navigationController?.popViewController(animated: true) + } + // 스와이프 동작을 취소합니다. + return false } + + // 내용이 없으면 기본 동작 (스와이프 가능) + return true + } } // MARK: - UITextViewDelegate @@ -828,14 +856,14 @@ extension SetRateViewController: UITextViewDelegate { maximumWordLabel.text = "\(textToDisplay.count) / 300" return true } - + func textViewDidBeginEditing(_ textView: UITextView) { if textView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" { textView.text = "" textView.textColor = .black } } - + func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { textView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" @@ -859,17 +887,17 @@ extension SetRateViewController { } } } - + @objc func keyboardWillHide(_: NSNotification) { view.transform = .identity navigationController?.isNavigationBarHidden = false } - + func addKeyboardNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) } - + func removeKeyboardNotifications() { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) From 6010e79231efab86fbf7fafb8da1824c46a438d8 Mon Sep 17 00:00:00 2001 From: Funital Date: Fri, 28 Nov 2025 21:58:10 +0900 Subject: [PATCH 20/69] =?UTF-8?q?[#321]=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/BeforeSelectedImageDTO.swift | 2 +- .../DTO/Review/FixedReviewRateResponse.swift | 17 -- .../Network/DTO/Review/MenuInfoResponse.swift | 10 - .../DTO/Review/MenuReviewResponse.swift | 23 -- .../DTO/Review/NewReviewListResponse.swift | 7 +- .../DTO/Review/ReviewListResponse.swift | 38 --- .../DTO/Review/ReviewRateResponse.swift | 23 -- .../DTO/Review/WriteReviewRequest.swift | 32 -- .../Data/Network/Router/ReviewRouter.swift | 138 +++------ .../Network/Router/WriteReviewRouter.swift | 61 +--- .../ChoiceMenuTableViewCell.swift | 89 ------ .../Review/View/RateReview/MenuLikeCell.swift | 27 +- .../Review/View/RateReview/RateView.swift | 32 +- .../View/SeeReview/RateNumberView.swift | 18 +- .../View/SeeReview/ReviewEmptyViewCell.swift | 16 +- .../View/SeeReview/ReviewRateViewCell.swift | 91 +++--- .../View/SeeReview/ReviewTableCell.swift | 17 -- .../ReviewTagCollectionViewCell.swift | 20 +- .../ChoiceMenuViewController.swift | 175 ----------- .../ViewController/ReportViewController.swift | 4 +- .../ViewController/ReviewViewController.swift | 276 +++++++----------- .../SetRateViewController.swift | 96 +----- 22 files changed, 270 insertions(+), 942 deletions(-) delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift delete mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift delete mode 100644 EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift delete mode 100644 EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift index 062e6469..604c58e6 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/BeforeSelectedImageDTO.swift @@ -2,7 +2,7 @@ // BeforeSelectedImageDTO.swift // EAT-SSU // -// Created by 박윤빈 on 3/7/24. +// Created by 한금준 on 28/11/25. // import Foundation diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift deleted file mode 100644 index 4c954359..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/FixedReviewRateResponse.swift +++ /dev/null @@ -1,17 +0,0 @@ -//// -//// FixedReviewRateResponse.swift -//// EAT-SSU -//// -//// Created by 박윤빈 on 3/18/24. -//// -// -//import Foundation -// -//// MARK: - FixedReviewRateResponse -// -//struct FixedReviewRateResponse: Codable { -// let menuName: String -// let totalReviewCount: Int -// let mainRating, amountRating, tasteRating: Double? -// let reviewRatingCount: StarCount -//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift deleted file mode 100644 index 56d87968..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuInfoResponse.swift +++ /dev/null @@ -1,10 +0,0 @@ -//// -//// MenuInfoResponse.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/05/18. -//// -// -//import Foundation -// -//struct MenuInfoResponse: Codable {} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift deleted file mode 100644 index 5fa5b907..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/MenuReviewResponse.swift +++ /dev/null @@ -1,23 +0,0 @@ -//// -//// MenuReviewResponse.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/05/18. -//// -// -//import Foundation -// -//struct MenuReviewResponse: Codable { -// let numberOfElements: Int -// let hasNext: Bool -// let dataList: [DataList]? -//} -// -//struct DataList: Codable { -// let writerId: Int -// let writerNickname: String -// let grade: Int -// let writeDate, content: String -// let tagList: [String] -// let imgUrlList: [String] -//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift index 0da958d9..f5b39119 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -5,12 +5,7 @@ // Created by 한금준 on 11/16/25. // -//struct NewReviewListResponse: Codable { -// let hasNext: Bool -// let dataList: [ReviewListItem] -//} - -/// 리뷰 V2 리스트 조회 API의 result 내부 DTO (Menu용 - 페이지 기반) +/// 리뷰 V2 리스트 조회 API struct NewReviewListResponse: Codable { let numberOfElements: Int? let hasNext: Bool diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift deleted file mode 100644 index c2612dd6..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewListResponse.swift +++ /dev/null @@ -1,38 +0,0 @@ -//// -//// ReviewListResponse.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/08/01. -//// -// -//import Foundation -// -//struct ReviewListResponse: Codable { -// let numberOfElements: Int -// let hasNext: Bool -// let dataList: [MenuDataList] -//} -// -//// MARK: - DataList -// -//struct MenuDataList: Codable { -// let reviewID: Int -// let menu: String -// let writerID: Int? -// let isWriter: Bool -// let writerNickname: String -// let mainRating: Int -// let amountRating, tasteRating: Int? -// let writedAt, content: String -// let imgURLList: [String?] -// let tags: [Tag]? -// -// enum CodingKeys: String, CodingKey { -// case reviewID = "reviewId" -// case menu -// case writerID = "writerId" -// case isWriter, writerNickname, mainRating, amountRating, tasteRating, writedAt, content -// case imgURLList = "imageUrls" -// case tags -// } -//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift deleted file mode 100644 index ef14b939..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/ReviewRateResponse.swift +++ /dev/null @@ -1,23 +0,0 @@ -//// -//// ReviewRateResponse.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/07/29. -//// -// -//import Foundation -// -//struct ReviewRateResponse: Codable { -// let menuNames: [String] -// let totalReviewCount: Int -// let mainRating: Double? -// let reviewRatingCount: StarCount -//} -// -//struct StarCount: Codable { -// let fiveStarCount: Int -// let fourStarCount: Int -// let threeStarCount: Int -// let twoStarCount: Int -// let oneStarCount: Int -//} diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift deleted file mode 100644 index 6d62999e..00000000 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewRequest.swift +++ /dev/null @@ -1,32 +0,0 @@ -//// -//// WriteReviewRequest.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/05/24. -//// -// -//import UIKit -// -//struct WriteReviewRequest: Codable { -// let mainRating: Int -// let amountRating: Int? -// let tasteRating: Int? -// let content: String -// let imageUrl: String -// -// init(mainRating: Int, amountRating: Int, tasteRating: Int, content: String, imageURL: String?) { -// self.mainRating = mainRating -// self.amountRating = amountRating -// self.tasteRating = tasteRating -// self.content = content -// imageUrl = imageURL ?? "" -// } -// -// init(content: BeforeSelectedImageDTO, imageURL: String?) { -// mainRating = content.mainRating -// amountRating = content.amountRating -// tasteRating = content.tasteRating -// self.content = content.content -// imageUrl = imageURL ?? "" -// } -//} diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 8bb6acc5..83776923 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -9,22 +9,16 @@ import Foundation import Moya enum ReviewRouter { -// // 상단 메뉴 별점 불러오는 API -> 두개로 쪼개짐. 고정, 변동 분기처리는 아래에서! -// case reviewRate(_ type: String, _ id: Int) -// -// // 하단 리뷰 리스트 불러오는 API -// case reviewList(_ type: String, _ id: Int) case report(param: ReportRequest) case deleteReview(_ reviewId: Int) -// case fixReview(_ reviewId: Int, _ param: BeforeSelectedImageDTO) - // MARK: - New V2 API: 리뷰 작성이 가능한 메뉴 목록 조회 + // MARK: - New V2 API case getValidMenusForReview(_ mealId: Int) case newReviewList(_ type: String, - _ id: Int, - lastReviewId: Int?, // ✨ 추가: lastReviewId 파라미터 - page: Int? = 0, // menu API용 (옵션) - size: Int? = 20) + _ id: Int, + lastReviewId: Int?, + page: Int? = 0, + size: Int? = 20) case getFixedMenuStatistics(_ menuId: Int) case getMealStatistics(_ mealId: Int) } @@ -33,125 +27,77 @@ extension ReviewRouter: TargetType { var baseURL: URL { URL(string: Config.baseURL)! } - + var path: String { switch self { -// case let .reviewRate(type, id): -// switch type { -// case "VARIABLE": -// "/reviews/meals/\(id)" -// case "FIXED": -// "/reviews/menus/\(id)" -// default: -// "" -// } -// case .reviewList: -// "/reviews" case .report: "/reports" case let .deleteReview(reviewId): "/v2/reviews/\(reviewId)" -// case let .fixReview(reviewId, _): -// "/reviews/\(reviewId)" - // MARK: - New V2 Path + // MARK: - New V2 Path case let .getValidMenusForReview(mealId): - "/v2/reviews/meal/valid-for-review/\(mealId)" // Path Parameter 사용 + "/v2/reviews/meal/valid-for-review/\(mealId)" case .newReviewList(let type, _, _, _, _): - switch type { - case "VARIABLE": - "/v2/reviews/list/meal" // ✨ 수정: V2 Meal List API 경로 - case "FIXED": - "/v2/reviews/list/menu" // ✨ 수정: V2 Menu List API 경로 - default: - "" // 기존 경로 유지 (혹시 모를 에러 방지) - } + switch type { + case "VARIABLE": + "/v2/reviews/list/meal" + case "FIXED": + "/v2/reviews/list/menu" + default: + "" + } case let .getFixedMenuStatistics(menuId): - "/v2/reviews/statistics/menus/\(menuId)" + "/v2/reviews/statistics/menus/\(menuId)" case let .getMealStatistics(mealId): - "/v2/reviews/statistics/meals/\(mealId)" + "/v2/reviews/statistics/meals/\(mealId)" } } - + var method: Moya.Method { switch self { - case /*.reviewRate,*/ /*.reviewList, */.getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: - .get + case .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: + .get case .report: - .post + .post case .deleteReview: - .delete -// case .fixReview: -// .patch + .delete } } - + var task: Moya.Task { switch self { -// case let .reviewRate(type, id): -// switch type { -// case "VARIABLE": -// .requestParameters(parameters: ["mealId": id], -// encoding: URLEncoding.queryString) -// case "FIXED": -// .requestParameters(parameters: ["menuId": id], -// encoding: URLEncoding.queryString) -// default: -// .requestPlain -// } - /// 이후 정렬 순서, 리뷰 로드 개수 등 수정 필요하면 고치기 -// case let .reviewList(type, id): -// switch type { -// case "VARIABLE": -// .requestParameters(parameters: ["menuType": type, -// "mealId": id, -// "page": 0, -// "size": 20, -// "sort": "date,DESC"], -// encoding: URLEncoding.queryString) -// case "FIXED": -// .requestParameters(parameters: ["menuType": type, -// "menuId": id, -// "page": 0, -// "size": 20, -// "sort": "date,DESC"], -// encoding: URLEncoding.queryString) -// default: -// .requestPlain -// } case let .report(param: param): - .requestJSONEncodable(param) + .requestJSONEncodable(param) case .deleteReview: - .requestPlain -// case let .fixReview(_, param): -// .requestJSONEncodable(param) - - // MARK: - New V2 Task - case .getValidMenusForReview: // Path에 ID가 포함되므로 Body나 QueryString 없음 - .requestPlain + .requestPlain + + // MARK: - New V2 Task + case .getValidMenusForReview: + .requestPlain case let .newReviewList(type, id, lastReviewId, page, size): switch type { case "VARIABLE": - .requestParameters( - parameters: (lastReviewId != nil) + .requestParameters( + parameters: (lastReviewId != nil) ? ["mealId": id, "size": size ?? 20, "lastReviewId": lastReviewId!] : ["mealId": id, "size": size ?? 20], - encoding: URLEncoding.queryString - ) + encoding: URLEncoding.queryString + ) case "FIXED": - .requestParameters( - parameters: ["menuId": id, "page": page ?? 0, "size": size ?? 20], - encoding: URLEncoding.queryString - ) + .requestParameters( + parameters: ["menuId": id, "page": page ?? 0, "size": size ?? 20], + encoding: URLEncoding.queryString + ) default: - .requestPlain - + .requestPlain + } case .getFixedMenuStatistics: - .requestPlain + .requestPlain case .getMealStatistics: - .requestPlain + .requestPlain } } diff --git a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift index 097de257..4b8114a8 100644 --- a/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/WriteReviewRouter.swift @@ -11,9 +11,6 @@ import Moya enum WriteReviewRouter { case uploadImage(image: UIImage?) -// case writeNewReview(param: WriteReviewRequest, menuID: Int) -// case writeReview(param: WriteReviewRequest, image: [UIImage?], menuId: Int) - // MARK: - New V2 APIs case writeMenuReview(param: WriteReviewMenuRequest) case writeMealReview(param: WriteReviewMealRequest) @@ -24,61 +21,32 @@ extension WriteReviewRouter: TargetType { var baseURL: URL { URL(string: Config.baseURL)! } - + var path: String { switch self { -// case .writeReview(param: _, image: _, menuId: let menuId): -// "/reviews/\(menuId)" case .uploadImage: "/reviews/upload/image" -// case .writeNewReview(param: _, menuID: let menuId): -// "/reviews/write/\(menuId)" - - // MARK: - New V2 Paths + // MARK: - New V2 Paths case .writeMenuReview: "/v2/reviews/menu" case .writeMealReview: "/v2/reviews/meal" case .fixReview(reviewId: let reviewId, param: _): - "/v2/reviews/\(reviewId)" + "/v2/reviews/\(reviewId)" } } - + var method: Moya.Method { switch self { - case /*.writeReview,*/ .uploadImage, /*.writeNewReview,*/ .writeMenuReview, .writeMealReview: - .post + case .uploadImage, .writeMenuReview, .writeMealReview: + .post case .fixReview: - .patch + .patch } } - + var task: Moya.Task { switch self { -// case .writeReview(param: let param, image: let imageList, menuId: _): -// var multipartData = [MultipartFormData]() -// do { -// let jsonData = try JSONEncoder().encode(param) -// multipartData.append(MultipartFormData(provider: .data(jsonData), -// name: "createReviewRequest", -// mimeType: "application/json")) -// } catch { -// print("Error encoding ReviewRequest: \(error)") -// return .requestPlain -// } -// -// for fileData in imageList { -// if let unwrappedImage = fileData { -// if let imageData = unwrappedImage.resize(newWidth: 300.adjusted).jpegData(compressionQuality: 0.3) { -// multipartData.append(MultipartFormData(provider: .data(imageData), -// name: "multipartFileList", -// fileName: "image.jpg", -// mimeType: "image/jpeg")) -// } -// } -// } -// return .uploadMultipart(multipartData) - case let .uploadImage(image: image): var multipartData = [MultipartFormData]() guard let unwrappedImage = image else { return .requestPlain } @@ -89,25 +57,22 @@ extension WriteReviewRouter: TargetType { mimeType: "image/jpeg")) } return .uploadMultipart(multipartData) - -// case let .writeNewReview(param: param, _): -// return .requestJSONEncodable(param) - // MARK: - New V2 Tasks (JSON Encoded) + // MARK: - New V2 Tasks (JSON Encoded) case let .writeMenuReview(param: param): return .requestJSONEncodable(param) case let .writeMealReview(param: param): return .requestJSONEncodable(param) case let .fixReview(reviewId: _, param: param): - return .requestJSONEncodable(param) + return .requestJSONEncodable(param) } } - + var headers: [String: String]? { switch self { - case /*.writeNewReview,*/ .writeMenuReview, .writeMealReview, .fixReview: + case .writeMenuReview, .writeMealReview, .fixReview: return ["Content-Type": "application/json"] - case .uploadImage/*, .writeReview*/: + case .uploadImage: return ["Content-Type": "multipart/form-data"] } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift deleted file mode 100644 index bc2e6bf8..00000000 --- a/EATSSU/App/Sources/Presentation/Review/View/ChoiceMenuView/ChoiceMenuTableViewCell.swift +++ /dev/null @@ -1,89 +0,0 @@ -//// -//// ChoiceMenuTableViewCell.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/06/29. -//// -// -//import UIKit -// -//import SnapKit -// -//import EATSSUDesign -// -//final class ChoiceMenuTableViewCell: UITableViewCell { -// // MARK: - Properties -// -// static let identifier = "ChoiceMenuTableViewCell" -// var isChecked: Bool = false { -// didSet { -// tapped() -// } -// } -// -// var handler: (() -> Void)? -// -// // MARK: - UI Components -// -// lazy var checkButton = UIButton() -// private lazy var menuLabel = UILabel() -// -// // MARK: - init -// -// required init?(coder: NSCoder) { -// super.init(coder: coder) -// } -// -// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { -// super.init(style: style, -// reuseIdentifier: reuseIdentifier) -// backgroundColor = .white -// setUI() -// setLayout() -// } -// -// // MARK: - Functions -// -// private func setUI() { -// checkButton.addTarget(self, action: #selector(checkButtonIsTapped), for: .touchUpInside) -// -// menuLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) -// menuLabel.textColor = .black -// menuLabel.text = "고구마치즈돈까스" -// -// contentView.addSubviews( -// checkButton, -// menuLabel -// ) -// } -// -// private func setLayout() { -// checkButton.snp.makeConstraints { -// $0.centerY.equalToSuperview() -// $0.leading.equalToSuperview().inset(27) -// $0.height.width.equalTo(24) -// } -// -// menuLabel.snp.makeConstraints { -// $0.centerY.equalToSuperview() -// $0.leading.equalTo(checkButton.snp.trailing).offset(15) -// } -// } -// -// @objc -// func checkButtonIsTapped() { -// handler?() -// } -//} -// -//extension ChoiceMenuTableViewCell { -// func dataBind(menu: String, isTapped: Bool) { -// menuLabel.text = menu -// isChecked = isTapped -// } -// -// func tapped() { -// let image = isChecked ? EATSSUDesignAsset.Images.icCheck.image : EATSSUDesignAsset.Images.icUncheck.image -// checkButton.setImage(image, for: .normal) -// } -//} diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift index 796b5134..dadabca3 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -17,7 +17,7 @@ final class MenuLikeCell: UITableViewCell { var onLikeTapped: (() -> Void)? var isLiked: Bool = false { didSet { - tapped() // 상태값 변경 시 UI 갱신 + tapped() } } @@ -36,7 +36,7 @@ final class MenuLikeCell: UITableViewCell { button.isUserInteractionEnabled = false return button }() - + private let likeContainer: UIView = { let view = UIView() view.layer.cornerRadius = 14 @@ -60,11 +60,11 @@ final class MenuLikeCell: UITableViewCell { contentView.addSubview(hStack) likeContainer.addSubview(likeButton) - + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(likeTapped)) likeContainer.isUserInteractionEnabled = true likeContainer.addGestureRecognizer(tapGesture) - + hStack.snp.makeConstraints { $0.edges.equalToSuperview().inset(12) } @@ -99,17 +99,16 @@ final class MenuLikeCell: UITableViewCell { print("tapped 실행됨 → isLiked:", isLiked) let image = isLiked ? EATSSUDesignAsset.Images.thumbUp.image : EATSSUDesignAsset.Images.thumbUpGray.image DispatchQueue.main.async { - self.likeButton.setImage(image.withRenderingMode(.alwaysOriginal), for: .normal) - - // Container 스타일 업데이트 - if self.isLiked { - self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor - } else { - self.likeContainer.backgroundColor = .clear - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor - } + self.likeButton.setImage(image.withRenderingMode(.alwaysOriginal), for: .normal) + + if self.isLiked { + self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor + } else { + self.likeContainer.backgroundColor = .clear + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor } + } } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift index 7eb30f74..e8a1c73d 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift @@ -13,15 +13,15 @@ import EATSSUDesign final class RateView: BaseUIView { // MARK: - Properties - + var buttons: [UIButton] = [] var currentStar: Int = 0 var starNumber: Int = 5 { - didSet { bind() } /// 초기화할 별의 개수 (button의 개수) + didSet { bind() } } - + // MARK: - UI Component - + lazy var starStackView: UIStackView = { let view = UIStackView() view.axis = .horizontal @@ -29,34 +29,33 @@ final class RateView: BaseUIView { view.backgroundColor = .white return view }() - + lazy var starFillImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image - + lazy var starEmptyImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image - + override init(frame: CGRect) { super.init(frame: frame) bind() } - + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Functions - + override func configureUI() { addSubview(starStackView) } - + override func setLayout() { starStackView.snp.makeConstraints { make in make.top.leading.bottom.trailing.equalToSuperview() } } - - /// 별점 버튼 초기화. tag 생성이 핵심 + func bind() { for i in 0 ..< 5 { let button = UIButton() @@ -67,8 +66,7 @@ final class RateView: BaseUIView { button.addTarget(self, action: #selector(didTappedTag(sender:)), for: .touchUpInside) } } - - /// tag를 이용한 선택처리 + @objc private func didTappedTag(sender: UIButton) { let end = sender.tag @@ -80,15 +78,13 @@ final class RateView: BaseUIView { } currentStar = end + 1 } - + func settingStarForFix(currentStar: Int) { - // ✨ 수정: currentStar가 0일 때 0...-1 범위 오류를 방지 if currentStar > 0 { for i in 0 ... currentStar - 1 { buttons[i].setImage(starFillImage, for: .normal) } } - // 빈 별은 currentStar부터 시작 for i in currentStar ..< starNumber { buttons[i].setImage(starEmptyImage, for: .normal) } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift index ef054990..f9e8294a 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift @@ -16,27 +16,27 @@ final class RateNumberView: BaseUIView { private var starImageViews: [UIImageView] = [] private lazy var starsStackView = UIStackView() private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView]) - + var filledStarImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image var emptyStarImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image - + // MARK: - init - + override init(frame: CGRect) { super.init(frame: frame) } - + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() } - + // MARK: - Functions - + override func configureUI() { addSubviews(rateNumberStackView) starImageViews = (0..<5).map { _ in @@ -53,7 +53,7 @@ final class RateNumberView: BaseUIView { rateNumberStackView.spacing = 6 rateNumberStackView.alignment = .bottom } - + override func setLayout() { starImageViews.forEach { $0.snp.makeConstraints { @@ -61,7 +61,7 @@ final class RateNumberView: BaseUIView { $0.width.equalTo(12.adjusted) } } - + rateNumberStackView.snp.makeConstraints { $0.edges.equalToSuperview() } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index 36a12730..baf5e071 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -14,14 +14,14 @@ import EATSSUDesign final class ReviewEmptyViewCell: UITableViewCell { // MARK: - Properties static let identifier = "ReviewEmptyViewCell" - + // MARK: - UI Components private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() imageView.tintColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return imageView }() - + private lazy var titleLabel: UILabel = { let label = UILabel() label.text = "아직 작성된 리뷰가 없어요" @@ -30,7 +30,7 @@ final class ReviewEmptyViewCell: UITableViewCell { label.textAlignment = .center return label }() - + private lazy var descriptionLabel: UILabel = { let label = UILabel() label.text = "메뉴에 가장 먼저 리뷰를 남겨주세요!" @@ -39,7 +39,7 @@ final class ReviewEmptyViewCell: UITableViewCell { label.textAlignment = .center return label }() - + private lazy var stackView: UIStackView = { let stack = UIStackView(arrangedSubviews: [noReviewImageView, titleLabel, descriptionLabel]) stack.axis = .vertical @@ -47,19 +47,19 @@ final class ReviewEmptyViewCell: UITableViewCell { stack.spacing = 16 return stack }() - + // MARK: - Init override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(stackView) setLayout() } - + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Layout private func setLayout() { stackView.snp.makeConstraints { @@ -69,7 +69,7 @@ final class ReviewEmptyViewCell: UITableViewCell { $0.size.equalTo(48) } } - + // MARK: - Configure func configure(isTokenExist: Bool) { if isTokenExist { diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 3a54759d..26351392 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -11,11 +11,11 @@ import EATSSUDesign final class ReviewRateViewCell: UITableViewCell { // MARK: - Properties - + static let identifier = "ReviewRateViewCell" var handler: (() -> Void)? var totalRate: Double = 0 - + // MARK: - UI Components private let menuContainer: UIView = { let view = UIView() @@ -24,7 +24,7 @@ final class ReviewRateViewCell: UITableViewCell { view.layer.masksToBounds = true return view }() - + var menuLabel: UILabel = { let label = UILabel() label.text = "김치볶음밥 & 계란국" @@ -40,7 +40,7 @@ final class ReviewRateViewCell: UITableViewCell { imageView.image = EATSSUDesignAsset.Images.icRestaurant.image return imageView }() - + private let menuTitleLabel: UILabel = { let label = UILabel() label.text = "오늘의 메뉴" @@ -48,7 +48,7 @@ final class ReviewRateViewCell: UITableViewCell { label.textColor = .black return label }() - + private lazy var menuTitleStackView: UIStackView = { let stack = UIStackView(arrangedSubviews: [menuIcon, menuTitleLabel]) stack.axis = .horizontal @@ -56,7 +56,7 @@ final class ReviewRateViewCell: UITableViewCell { stack.spacing = 6 return stack }() - + private let rateSectionContainer: UIView = { let view = UIView() return view @@ -67,7 +67,7 @@ final class ReviewRateViewCell: UITableViewCell { imageView.image = EATSSUDesignAsset.Images.icStarYellow.image return imageView }() - + var rateNumLabel: UILabel = { let label = UILabel() label.text = "4.3" @@ -75,13 +75,13 @@ final class ReviewRateViewCell: UITableViewCell { label.textColor = .black return label }() - + private let fivePointLabel = ReviewRateViewCell.makePointLabel("5점") private let fourPointLabel = ReviewRateViewCell.makePointLabel("4점") private let threePointLabel = ReviewRateViewCell.makePointLabel("3점") private let twoPointLabel = ReviewRateViewCell.makePointLabel("2점") private let onePointLabel = ReviewRateViewCell.makePointLabel("1점") - + // Chart bar containers and foregrounds var oneChartBar: UIView! var twoChartBar: UIView! @@ -94,7 +94,7 @@ final class ReviewRateViewCell: UITableViewCell { var threeForeground: UIView! var fourForeground: UIView! var fiveForeground: UIView! - + lazy var yAxisStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [fivePointLabel, fourPointLabel, @@ -106,7 +106,7 @@ final class ReviewRateViewCell: UITableViewCell { stackView.alignment = .trailing return stackView }() - + lazy var totalRateStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [bigStarImageView, rateNumLabel]) @@ -115,22 +115,22 @@ final class ReviewRateViewCell: UITableViewCell { stackView.alignment = .center return stackView }() - + // MARK: - Init - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) configureUI() setLayout() } - + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Helper - + private static func makePointLabel(_ text: String) -> UILabel { let label = UILabel() label.text = text @@ -138,11 +138,10 @@ final class ReviewRateViewCell: UITableViewCell { label.textColor = .black return label } - + // MARK: - UI Setup - + func configureUI() { - // Helper to create chart bar with background and foreground func makeChartBar() -> (container: UIView, foreground: UIView) { let container = UIView() container.backgroundColor = .gray200 @@ -159,7 +158,7 @@ final class ReviewRateViewCell: UITableViewCell { } return (container, foreground) } - + let oneBar = makeChartBar() oneChartBar = oneBar.container oneForeground = oneBar.foreground @@ -175,31 +174,31 @@ final class ReviewRateViewCell: UITableViewCell { let fiveBar = makeChartBar() fiveChartBar = fiveBar.container fiveForeground = fiveBar.foreground - + contentView.addSubviews( menuContainer, rateSectionContainer ) - + menuContainer.addSubviews(menuTitleStackView, menuLabel) - + rateSectionContainer.addSubviews(totalRateStackView, yAxisStackView, oneChartBar, twoChartBar, threeChartBar, fourChartBar, fiveChartBar) - + totalRateStackView.snp.makeConstraints { make in make.top.bottom.equalToSuperview().offset(35.5) make.leading.equalToSuperview().offset(36) } - + yAxisStackView.snp.makeConstraints { make in make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) make.centerY.equalTo(totalRateStackView) } } - + func setLayout() { backgroundColor = .white - + menuContainer.snp.makeConstraints { make in make.top.equalTo(contentView.snp.top).offset(0) make.centerX.equalToSuperview() @@ -215,7 +214,7 @@ final class ReviewRateViewCell: UITableViewCell { menuIcon.snp.makeConstraints { make in make.width.height.equalTo(20) } - + menuLabel.snp.makeConstraints { make in make.top.equalTo(menuTitleStackView.snp.bottom).offset(12) make.leading.trailing.equalToSuperview().inset(28) @@ -226,90 +225,74 @@ final class ReviewRateViewCell: UITableViewCell { make.top.equalTo(menuLabel.snp.bottom).offset(40) make.leading.trailing.equalToSuperview().inset(60) } - + oneChartBar.snp.makeConstraints { make in make.centerY.equalTo(onePointLabel) make.leading.equalTo(onePointLabel.snp.trailing).offset(7) make.height.equalTo(10) make.width.equalTo(126) } - + twoChartBar.snp.makeConstraints { make in make.centerY.equalTo(twoPointLabel) make.leading.equalTo(twoPointLabel.snp.trailing).offset(7) make.height.equalTo(10) make.width.equalTo(126) } - + threeChartBar.snp.makeConstraints { make in make.centerY.equalTo(threePointLabel) make.leading.equalTo(threePointLabel.snp.trailing).offset(7) make.height.equalTo(10) make.width.equalTo(126) } - + fourChartBar.snp.makeConstraints { make in make.centerY.equalTo(fourPointLabel) make.leading.equalTo(fourPointLabel.snp.trailing).offset(7) make.height.equalTo(10) make.width.equalTo(126) } - + fiveChartBar.snp.makeConstraints { make in make.centerY.equalTo(fivePointLabel) make.leading.equalTo(fivePointLabel.snp.trailing).offset(7) make.height.equalTo(10) make.width.equalTo(126) } - + for item in [onePointLabel, twoPointLabel, threePointLabel, fourPointLabel, fivePointLabel] { item.snp.makeConstraints { $0.height.equalTo(18.adjusted) } } - + bigStarImageView.snp.makeConstraints { $0.height.width.equalTo(24.adjusted) } } - + @objc func touchAddReviewButton() { handler?() } } -// MARK: - V2 API Data Binding Extensions - extension ReviewRateViewCell { - // ✨ Meal 통계 데이터 바인딩 func configureWithMealStatistics(_ data: ReviewMealStatisticsResponse) { - // 메뉴명 설정 (여러 메뉴를 " + "로 연결) let menuNames = data.menuList.map { $0.name } menuLabel.text = menuNames.joined(separator: " + ") - - // 평균 별점 설정 setRating(data.rating ?? 0) - - // 별점 차트 업데이트 updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) } - // ✨ Menu 통계 데이터 바인딩 func configureWithMenuStatistics(_ data: ReviewMenuStatisticsResponse) { - // 메뉴명 설정 menuLabel.text = data.menuName - - // 평균 별점 설정 setRating(data.rating ?? 0) - - // 별점 차트 업데이트 updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) } // MARK: - Private Helper Methods - - /// 평균 별점 표시 private func setRating(_ rating: Double) { totalRate = rating @@ -321,9 +304,8 @@ extension ReviewRateViewCell { } } - /// 별점 차트 업데이트 private func updateRatingChart(with ratingCount: ReviewRatingCount, totalCount: Int) { - let safeTotal = max(totalCount, 1) // 0으로 나누기 방지 + let safeTotal = max(totalCount, 1) fiveForeground.snp.updateConstraints { $0.width.equalTo(126 * ratingCount.fiveStarCount / safeTotal) @@ -341,7 +323,6 @@ extension ReviewRateViewCell { $0.width.equalTo(126 * ratingCount.oneStarCount / safeTotal) } - // 레이아웃 즉시 업데이트 layoutIfNeeded() } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 3f45397e..c24fc26c 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -22,7 +22,6 @@ final class ReviewTableCell: UITableViewCell { lazy var totalRateView = RateNumberView() - // 태그 표시용 컬렉션 뷰 private lazy var tagCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -96,7 +95,6 @@ final class ReviewTableCell: UITableViewCell { return imageView }() - /// 별점 lazy var rateStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [totalRateView]) stackView.axis = .horizontal @@ -105,7 +103,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - /// 이름 + 메뉴 lazy var nameMenuStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [userNameLabel]) stackView.axis = .horizontal @@ -114,7 +111,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - /// 이름 + 메뉴 + 별점 lazy var infoStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [nameMenuStackView, rateStackView]) stackView.axis = .vertical @@ -123,7 +119,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - /// 프로필 + 이름 + 메뉴 + 별점 lazy var profileStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [userProfileImageView, infoStackView]) stackView.axis = .horizontal @@ -194,7 +189,6 @@ final class ReviewTableCell: UITableViewCell { } foodImageView.snp.makeConstraints { make in -// make.height.width.equalTo(358) make.top.equalTo(reviewTextView.snp.bottom).offset(8) make.leading.trailing.equalToSuperview() make.height.equalTo(foodImageView.snp.width).multipliedBy(0.75) @@ -238,19 +232,15 @@ extension ReviewTableCell: UICollectionViewDataSource { // MARK: - Data Bind extension ReviewTableCell { - // ✨ V2 API: ReviewListItem 직접 바인딩 func dataBind(response: ReviewListItem) { - // 메뉴명 설정 (여러 메뉴인 경우 " + "로 연결) menuName = response.menu?.map { $0.name }.joined(separator: " + ") ?? "" - // 기본 정보 userNameLabel.text = response.writerNickname totalRateView.setRating(Int(response.rating)) dateLabel.text = response.writtenAt reviewTextView.text = response.content ?? "" reviewId = response.reviewId - // 이미지 처리 if let firstImageUrl = response.imageUrls?.first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) @@ -258,11 +248,9 @@ extension ReviewTableCell { foodImageView.isHidden = true } - // 버튼 설정 sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) - // ✨ 태그 처리 (V2 API에서는 menu가 태그 역할) if let menuTags = response.menu, !menuTags.isEmpty { tags = menuTags.map { ($0.name, $0.isLike) } } else { @@ -270,18 +258,15 @@ extension ReviewTableCell { } tagCollectionView.reloadData() - // 태그가 없으면 컬렉션뷰 숨기기 tagCollectionView.isHidden = tags.isEmpty } - // 마이페이지용 바인딩 (기존 호환성 유지) func myPageDataBind(response: MyDataList, nickname: String) { userNameLabel.text = "\(nickname)" totalRateView.setRating(response.mainRating) dateLabel.text = response.writeDate reviewTextView.text = response.content - // 이미지 처리 if response.imgURLList.count != 0 { if response.imgURLList[0] != "" { foodImageView.isHidden = false @@ -291,13 +276,11 @@ extension ReviewTableCell { foodImageView.isHidden = true } - // 버튼 설정 sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.setTitle("", for: .normal) reviewId = response.reviewID - // 마이페이지에서는 태그 숨김 tags = [] tagCollectionView.isHidden = true } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift index 0a4967ee..480f6ccc 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -10,7 +10,7 @@ import SnapKit final class ReviewTagCollectionViewCell: UICollectionViewCell { static let identifier = "ReviewTagCollectionViewCell" - + private let iconImageView: UIImageView = { let iv = UIImageView() iv.image = UIImage(systemName: "hand.thumbsup") @@ -19,14 +19,14 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { iv.contentMode = .scaleAspectFit return iv }() - + private let titleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = .systemTeal return label }() - + private let stackView: UIStackView = { let sv = UIStackView() sv.axis = .horizontal @@ -34,12 +34,12 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { sv.alignment = .center return sv }() - + override init(frame: CGRect) { super.init(frame: frame) setupViews() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -48,16 +48,16 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { super.layoutSubviews() contentView.layer.cornerRadius = contentView.bounds.height / 2 } - - + + private func setupViews() { contentView.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.1) contentView.layer.borderColor = UIColor.systemTeal.cgColor contentView.layer.borderWidth = 1 - + stackView.addArrangedSubview(iconImageView) stackView.addArrangedSubview(titleLabel) - + contentView.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false @@ -71,7 +71,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { make.bottom.equalToSuperview().inset(2) } } - + func configure(tagName: String, isLiked: Bool) { titleLabel.text = tagName if isLiked { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift deleted file mode 100644 index 1b4dcc97..00000000 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ChoiceMenuViewController.swift +++ /dev/null @@ -1,175 +0,0 @@ -//// -//// ChoiceMenuViewController.swift -//// EatSSU-iOS -//// -//// Created by 박윤빈 on 2023/06/29. -//// -// -//import UIKit -// -//import SnapKit -//import FirebaseAnalytics -// -//import EATSSUDesign -// -//final class ChoiceMenuViewController: BaseViewController { -// // MARK: - Properties -// -// var menuNameList: [String] = [] -// var menuIDList: [Int] = [] -// var isMenuSelected: [Bool] = [] { -// didSet { -// choiceMenuTabelView.reloadData() -// print(isMenuSelected) -// } -// } -// -// private lazy var selectedList: [String] = [] -// private var selectedIDList: [Int] = [] -// -// // MARK: - UI Component -// -// private let enjoyLabel = UILabel() -// private let whichFoodLabel = UILabel() -// private lazy var choiceMenuTabelView = UITableView(frame: .zero, style: .plain) -// private lazy var nextButton = MainButton() -// -// // MARK: - Life Cycles -// -// override func viewDidLoad() { -// super.viewDidLoad() -// setTableViewConfig() -// } -// -// override func viewWillAppear(_ animated: Bool) { -// super.viewWillAppear(animated) -// -// /// pop한 후, 다시 메뉴를 선택할 경우를 방지하기 위하여 선택한 리스트를 초기화합니다 -// selectedList = [] -// selectedIDList = [] -// } -// -// override func viewDidAppear(_ animated: Bool) { -// super.viewDidAppear(animated) -// -// logScreenView(screenID: FirebaseScreenID.Review.V1.review_v1_2) -// } -// -// // MARK: - Functions -// -// override func configureUI() { -// whichFoodLabel.text = "어떤 음식에 대한 리뷰인가요?" -// whichFoodLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) -// whichFoodLabel.textColor = .black -// -// enjoyLabel.text = "식사는 맛있게 하셨나요?" -// enjoyLabel.font = EATSSUDesignFontFamily.Pretendard.medium.font(size: 16) -// enjoyLabel.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color -// -// choiceMenuTabelView.separatorStyle = .none -// -// nextButton.setTitle("다음 단계로", for: .normal) -// -// view.addSubviews( -// enjoyLabel, -// whichFoodLabel, -// choiceMenuTabelView, -// nextButton -// ) -// } -// -// override func setLayout() { -// whichFoodLabel.snp.makeConstraints { -// $0.top.equalTo(view.safeAreaLayoutGuide.snp.topMargin).inset(25) -// $0.centerX.equalToSuperview() -// } -// -// enjoyLabel.snp.makeConstraints { -// $0.top.equalTo(whichFoodLabel.snp.bottom).offset(15) -// $0.centerX.equalToSuperview() -// } -// -// choiceMenuTabelView.snp.makeConstraints { -// $0.top.equalTo(enjoyLabel.snp.bottom).offset(40) -// $0.leading.trailing.bottom.equalToSuperview() -// } -// -// nextButton.snp.makeConstraints { -// $0.horizontalEdges.equalToSuperview().inset(16) -// $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(17) -// } -// } -// -// override func setButtonEvent() { -// nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) -// } -// -// override func setCustomNavigationBar() { -// super.setCustomNavigationBar() -// navigationItem.title = "리뷰 남기기" -// } -// -// private func setTableViewConfig() { -// choiceMenuTabelView.delegate = self -// choiceMenuTabelView.dataSource = self -// choiceMenuTabelView.register(ChoiceMenuTableViewCell.self, -// forCellReuseIdentifier: ChoiceMenuTableViewCell.identifier) -// } -// -// private func makeList(menuList: [String], selectedList: [Bool]) { -// for i in 0 ..< menuList.count { -// if selectedList[i] { -// self.selectedList.append(menuList[i]) -// selectedIDList.append(menuIDList[i]) -// } -// } -// } -// -// @objc -// func nextButtonTapped() { -// makeList(menuList: menuNameList, selectedList: isMenuSelected) -// if selectedList.count == 0 { -// showToast(message: "리뷰를 작성할 메뉴를 선택해주세요!", type: .info) -// } else { -// let setRateVC = SetRateViewController() -// setRateVC.dataBind(list: selectedList, -// idList: selectedIDList, -// reviewList: nil, -// currentPage: 0) -// navigationController?.pushViewController(setRateVC, animated: true) -// } -// } -// -// func menuDataBind(menuList: [String], idList: [Int]) { -// menuNameList = menuList -// menuIDList = idList -// for _ in 0 ..< menuNameList.count { -// isMenuSelected.append(false) -// } -// } -//} -// -//extension ChoiceMenuViewController: UITableViewDelegate {} -// -//extension ChoiceMenuViewController: UITableViewDataSource { -// func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { -// menuNameList.count -// } -// -// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { -// guard let cell = tableView.dequeueReusableCell(withIdentifier: ChoiceMenuTableViewCell.identifier) as? ChoiceMenuTableViewCell else { return UITableViewCell() } -// cell.selectionStyle = .none -// cell.dataBind(menu: menuNameList[indexPath.row], isTapped: isMenuSelected[indexPath.row]) -// cell.handler = { [weak self] in -// guard let self else { return } -// cell.isChecked.toggle() -// isMenuSelected[indexPath.row].toggle() -// } -// -// return cell -// } -// -// func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { -// 50 -// } -//} diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift index ebc4101e..7e24dc6d 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift @@ -27,7 +27,7 @@ final class ReportViewController: BaseViewController { private var buttonArray: [UIButton] = [] private var contentArray: [String?] = [] private var reviewID: Int = .init() - + // MARK: - View Life Cycle override func viewWillAppear(_: Bool) { @@ -261,7 +261,7 @@ extension ReportViewController: UITextViewDelegate { func textView(_: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { let newLength = reportView.reviewReportReasonTextView.text.count - range.length + text.count reportView.characterCountLabel.text = "\(reportView.reviewReportReasonTextView.text.count) / 300" - + if newLength > 300 { return false } else { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 38106816..325348e4 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -10,52 +10,41 @@ import FirebaseAnalytics import Moya import SnapKit -// MARK: - Properties (모델 변경 반영) - -// MenuInfo는 삭제하고 ReviewValidMenu로 통일 - final class ReviewViewController: BaseViewController { // MARK: - Properties override var shouldHideTabBar: Bool { - return true - } + return true + } let reviewProvider = MoyaProvider(plugins: [ESMoyaLoggingPlugin()]) var menuID: Int = .init() var type = "VARIABLE" private var menuNameList: [String] = [] private var menuIDList: [Int]? = [Int]() private var menuDictionary: [String: Int] = [:] - - // ✨ V2 API로 변경: MenuDataList → ReviewListItem private var reviewList = [ReviewListItem]() - - // ✨ V2 API 응답 데이터 private var mealStatistics: ReviewMealStatisticsResponse? private var menuStatistics: ReviewMenuStatisticsResponse? private var totalReviewCount: Int = 0 - - // ✨ 리뷰 작성 가능한 메뉴 목록 (getValidMenusForReview) - // 이 프로퍼티는 이제 typealias 덕분에 [ReviewValidMenu]와 동일합니다. private var validMenusForReview: [ReviewValidMenu] = [] - + // MARK: - UI Component - + let refreshControl = UIRefreshControl() - + let reviewTableView: UITableView = { let tableView = UITableView() tableView.separatorStyle = .none tableView.showsVerticalScrollIndicator = false return tableView }() - + private var activityIndicatorView: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .large) indicator.startAnimating() indicator.isHidden = true return indicator }() - + private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() imageView.image = ImageLiteral.noReview @@ -76,30 +65,29 @@ final class ReviewViewController: BaseViewController { button.title = "리뷰 작성하기" return button }() - + // MARK: - Life Cycles - + override func viewDidLoad() { super.viewDidLoad() - + setTableView() initRefresh() setFirebaseTask() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - // ✨ V2 API 호출 순서: 통계 → 유효 메뉴 → 리뷰 리스트 getStatistics() if type == "VARIABLE" { - getValidMenusForReview() // VARIABLE 타입일 때만 호출 + getValidMenusForReview() } getReviewList(type: type, menuId: menuID) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + if self.isMovingFromParent { var parentVC = self.parent while parentVC != nil { @@ -111,9 +99,9 @@ final class ReviewViewController: BaseViewController { } } } - + // MARK: - Functions - + override func configureUI() { reviewTableView.backgroundColor = .white view.addSubviews(reviewTableView, @@ -122,7 +110,7 @@ final class ReviewViewController: BaseViewController { reviewTabBarContainer) reviewTabBarContainer.addSubview(reviewTabBarView) } - + override func setLayout() { reviewTableView.snp.makeConstraints { make in make.top.equalToSuperview().offset(24) @@ -144,14 +132,12 @@ final class ReviewViewController: BaseViewController { $0.height.equalTo(80) } - // 🛠️ Auto Layout 충돌 수정: .bottom 제약을 제거하여 MainButton 내부 높이 제약이 우선되도록 함 reviewTabBarView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(12) $0.top.equalToSuperview().offset(12) - // $0.bottom.equalToSuperview().offset(-12) // 제거 } } - + override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = "리뷰" @@ -162,19 +148,16 @@ final class ReviewViewController: BaseViewController { } @objc private func handleAddReviewButtonTap() { - // MARK: - 로직 수정 - if type == "VARIABLE" { let reviewVC = SetRateViewController(mealId: menuID) - // 🛠️ 수정: .menuId 속성 사용 reviewVC.dataBind( list: validMenusForReview.map { $0.name }, idList: validMenusForReview.map { $0.menuId } ) navigationController?.pushViewController(reviewVC, animated: true) - } else { // FIXED + } else { let reviewVC = SetRateViewController(menuId: menuID) reviewVC.dataBind( @@ -184,34 +167,34 @@ final class ReviewViewController: BaseViewController { navigationController?.pushViewController(reviewVC, animated: true) } } - + private func setFirebaseTask() { FirebaseRemoteConfig.shared.fetchRestaurantInfo() - - #if DEBUG - #else - Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) - #endif + +#if DEBUG +#else + Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) +#endif } - + func setTableView() { reviewTableView.register(ReviewTableCell.self, forCellReuseIdentifier: ReviewTableCell.identifier) reviewTableView.register(ReviewRateViewCell.self, forCellReuseIdentifier: ReviewRateViewCell.identifier) reviewTableView.register(ReviewEmptyViewCell.self, forCellReuseIdentifier: ReviewEmptyViewCell.identifier) reviewTableView.register(ReviewDividerCell.self, forCellReuseIdentifier: ReviewDividerCell.identifier) - + reviewTableView.delegate = self reviewTableView.dataSource = self } - + func initRefresh() { refreshControl.addTarget(self, action: #selector(refreshTable(refresh:)), for: .valueChanged) - + reviewTableView.refreshControl = refreshControl } - + @objc func refreshTable(refresh: UIRefreshControl) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { @@ -223,95 +206,77 @@ final class ReviewViewController: BaseViewController { refresh.endRefreshing() } } - + func bindMenuID(id: Int) { menuID = id } - + private func showDeleteAlert(data: ReviewListItem) { + if !data.isWriter { + self.showReportAlert(reviewID: data.reviewId) + return + } + + let title = "리뷰 삭제" + let message = "해당 리뷰를 삭제할까요?" + let confirmButtonTitle = "삭제하기" + let cancelButtonTitle = "취소하기" + + self.showCustomDialog( + title: title, + message: message, + cancelButtonTitle: cancelButtonTitle, + confirmButtonTitle: confirmButtonTitle + ) { [weak self] in + guard let self = self else { return } - // ✨ 리뷰 작성자가 아니면 바로 신고 다이얼로그를 띄웁니다. - if !data.isWriter { - self.showReportAlert(reviewID: data.reviewId) - return - } - - // ✨ 리뷰 작성자인 경우: 삭제 시 Custom Dialog를 사용합니다. - - // Custom Dialog를 위한 데이터 - let title = "리뷰 삭제" - let message = "해당 리뷰를 삭제할까요?" - let confirmButtonTitle = "삭제하기" - let cancelButtonTitle = "취소하기" - - self.showCustomDialog( - title: title, - message: message, - cancelButtonTitle: cancelButtonTitle, - confirmButtonTitle: confirmButtonTitle - ) { [weak self] in - guard let self = self else { return } - - // 삭제 확인 시, deleteReview 함수 호출 - self.deleteReview(reviewID: data.reviewId) - } + self.deleteReview(reviewID: data.reviewId) } + } + + private func showFixOrDeleteAlert_OLD(data: ReviewListItem) { + let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", + message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", + preferredStyle: UIAlertController.Style.actionSheet) - private func showFixOrDeleteAlert_OLD(data: ReviewListItem) { - let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", - message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", - preferredStyle: UIAlertController.Style.actionSheet) - - let fixAction = UIAlertAction(title: "수정하기", - style: .default, - handler: { _ in - - let menuNames = data.menu?.map { $0.name } ?? [] - // ✨ MenuLike 배열에서 menuId만 추출 (수정 요청 DTO의 menuLikes를 구성하기 위함) - let menuIds = data.menu?.map { $0.menuId } ?? [] - - // 🛠️ 수정: data.type에 따라 SetRateViewController 생성자 변경 필요 - // ReviewViewController는 dataBindForFix를 사용할 것이므로 menuId 생성자를 사용하는 것이 적절 - let setRateViewController = SetRateViewController(menuId: self.menuID) - - // 1. 리뷰 ID와 메뉴 이름을 바인딩 (UI 설정 및 reviewId 저장) - setRateViewController.dataBindForFix(list: menuNames, reviewId: data.reviewId) - - // 2. 리뷰 상세 정보 (별점, 내용, 이미지) 바인딩 - setRateViewController.settingForReviewFix(data: data) - - // 3. 리뷰 수정 API 호출을 위한 추가 정보 바인딩 (menuId, isLike) - // SetRateViewController의 validMenuIDList와 likedStates에 원본 정보를 설정 - let likedStates = data.menu?.map { $0.isLike } ?? [] - setRateViewController.dataBindForFix( - menuNames: menuNames, - menuIds: menuIds, - likedStates: likedStates - ) - - self.navigationController?.pushViewController(setRateViewController, animated: true) - }) + let fixAction = UIAlertAction(title: "수정하기", + style: .default, + handler: { _ in - let deleteAction = UIAlertAction(title: "삭제하기", - style: .destructive, - handler: { [weak self] _ in - guard let self = self else { return } - - // ✨ V2 API를 사용하는 deleteReview 함수 호출 (reviewId 전달) - // ReviewRouter.deleteReview에 V2 Path와 Method가 적용되었으므로 - // 이 함수 내부의 호출 로직은 변경 없이 V2 API를 사용하게 됩니다. - self.deleteReview(reviewID: data.reviewId) - }) + let menuNames = data.menu?.map { $0.name } ?? [] + let menuIds = data.menu?.map { $0.menuId } ?? [] + let setRateViewController = SetRateViewController(menuId: self.menuID) - let cancelAction = UIAlertAction(title: "취소하기", - style: .cancel, - handler: nil) + setRateViewController.dataBindForFix(list: menuNames, reviewId: data.reviewId) + setRateViewController.settingForReviewFix(data: data) - alert.addAction(fixAction) - alert.addAction(deleteAction) - alert.addAction(cancelAction) - present(alert, animated: true, completion: nil) - } + let likedStates = data.menu?.map { $0.isLike } ?? [] + setRateViewController.dataBindForFix( + menuNames: menuNames, + menuIds: menuIds, + likedStates: likedStates + ) + + self.navigationController?.pushViewController(setRateViewController, animated: true) + }) + + let deleteAction = UIAlertAction(title: "삭제하기", + style: .destructive, + handler: { [weak self] _ in + guard let self = self else { return } + + self.deleteReview(reviewID: data.reviewId) + }) + + let cancelAction = UIAlertAction(title: "취소하기", + style: .cancel, + handler: nil) + + alert.addAction(fixAction) + alert.addAction(deleteAction) + alert.addAction(cancelAction) + present(alert, animated: true, completion: nil) + } private func showReportAlert(reviewID: Int) { showCustomDialog( @@ -325,7 +290,7 @@ final class ReviewViewController: BaseViewController { self?.navigationController?.pushViewController(reportViewController, animated: true) } } - + func userTapReviewButton() { if RealmService.shared.isAccessTokenPresent() { activityIndicatorView.isHidden = false @@ -341,10 +306,8 @@ final class ReviewViewController: BaseViewController { ) activityIndicatorView.stopAnimating() navigationController?.pushViewController(setRateViewController, animated: true) - } else { // VARIABLE + } else { let setRateViewController = SetRateViewController(mealId: menuID) - - // 🛠️ 수정: .menuId 속성 사용 setRateViewController.dataBind( list: validMenusForReview.map { $0.name }, idList: validMenusForReview.map { $0.menuId } @@ -360,12 +323,12 @@ final class ReviewViewController: BaseViewController { } } } - + private func pushToLoginVC() { let loginVC = LoginViewController() navigationController?.pushViewController(loginVC, animated: true) } - + func makeDictionary() { if menuIDList != [] { for (index, string) in menuNameList.enumerated() { @@ -407,7 +370,7 @@ extension ReviewViewController: UITableViewDataSource { func numberOfSections(in _: UITableView) -> Int { 3 } - + func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: @@ -424,14 +387,13 @@ extension ReviewViewController: UITableViewDataSource { 0 } } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch indexPath.section { case 0: let cell = tableView.dequeueReusableCell(withIdentifier: ReviewRateViewCell.identifier, for: indexPath) as? ReviewRateViewCell ?? ReviewRateViewCell() cell.selectionStyle = .none - // ✨ V2 API 데이터로 바인딩 if type == "FIXED" { if let statistics = menuStatistics { cell.configureWithMenuStatistics(statistics) @@ -448,7 +410,7 @@ extension ReviewViewController: UITableViewDataSource { } cell.reloadInputViews() return cell - + case 1: let cell = tableView.dequeueReusableCell(withIdentifier: ReviewDividerCell.identifier, for: indexPath) as? ReviewDividerCell ?? ReviewDividerCell() cell.configure(reviewCount: totalReviewCount) @@ -467,9 +429,7 @@ extension ReviewViewController: UITableViewDataSource { return cell } else { let cell = tableView.dequeueReusableCell(withIdentifier: ReviewTableCell.identifier, for: indexPath) as? ReviewTableCell ?? ReviewTableCell() - - // ✨ ReviewListItem 직접 바인딩 - // 🛠️ 요청사항 반영: isLike가 true인 메뉴만 필터링하여 바인딩 + var filteredReviewItem = reviewList[indexPath.row] let likedMenus = filteredReviewItem.menu?.filter { $0.isLike } filteredReviewItem.menu = likedMenus @@ -478,20 +438,20 @@ extension ReviewViewController: UITableViewDataSource { cell.handler = { [weak self] in guard let self else { return } - + reviewList[indexPath.row].isWriter ? self.showDeleteAlert(data: reviewList[indexPath.row]) - : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) + : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) } cell.selectionStyle = .none cell.reloadInputViews() return cell } - + default: return UITableViewCell() } } - + func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { switch indexPath.section { case 0: @@ -513,7 +473,6 @@ extension ReviewViewController: UITableViewDataSource { // MARK: - V2 API Network Calls extension ReviewViewController { - // ✨ V2 API: 통계 데이터 가져오기 func getStatistics() { if type == "FIXED" { getFixedMenuStatistics() @@ -522,7 +481,6 @@ extension ReviewViewController { } } - // ✨ V2 API: 고정 메뉴 통계 func getFixedMenuStatistics() { NetworkService.shared.request( ReviewRouter.getFixedMenuStatistics(menuID), @@ -535,7 +493,7 @@ extension ReviewViewController { self.menuStatistics = data self.totalReviewCount = data.totalReviewCount self.menuNameList = [data.menuName] - self.menuIDList = [self.menuID] // FIXED 메뉴는 menuIDList도 menuID로 설정 + self.menuIDList = [self.menuID] self.makeDictionary() self.reviewTableView.reloadData() case .failure(let error): @@ -544,7 +502,6 @@ extension ReviewViewController { } } - // ✨ V2 API: 식단 통계 func getMealStatistics() { NetworkService.shared.request( ReviewRouter.getMealStatistics(menuID), @@ -561,38 +518,30 @@ extension ReviewViewController { self.makeDictionary() self.reviewTableView.reloadData() case .failure(let error): - // 🛠️ Meal Statistics Error 처리 개선 (rating: null 디코딩 오류 가정) print("❌ Meal Statistics Error: \(error.localizedDescription)") - // 디코딩 실패해도 UI 갱신을 위해 reloadData 호출 self.reviewTableView.reloadData() } } } - // MARK: 🛠️ JSON Decoding 수정: responseType을 ReviewValidMenusResponse.self로 변경 - // ✨ V2 API: 리뷰 작성 가능한 메뉴 목록 조회 (VARIABLE 타입 전용) func getValidMenusForReview() { NetworkService.shared.request( ReviewRouter.getValidMenusForReview(menuID), - responseType: ReviewValidMenusResponse.self, // 🛠️ Wrapper DTO 타입 사용 + responseType: ReviewValidMenusResponse.self, useAuth: true ) { [weak self] result in guard let self = self else { return } switch result { case .success(let data): - // 🛠️ 수정: result 내부의 menuList 배열을 사용 (타입이 [MenuInfo]로 일치) self.validMenusForReview = data.menuList print("✅ Valid Menus for Review: \(data.menuList.map { $0.name })") case .failure(let error): print("❌ Valid Menus Error: \(error.localizedDescription)") - // 에러 발생 시 처리 (Meal Statistics에서 가져온 데이터가 타입이 다를 수 있으므로 임시 주석) - // self.validMenusForReview = (self.mealStatistics?.menuList ?? []) break } } } - - // ✨ V2 API: 리뷰 리스트 불러오기 + func getReviewList(type: String, menuId _: Int) { if type == "FIXED" { getFixedMenuReviewList() @@ -601,7 +550,6 @@ extension ReviewViewController { } } - // ✨ V2 API: 고정 메뉴 리뷰 리스트 func getFixedMenuReviewList() { NetworkService.shared.request( ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: 0, size: 20), @@ -611,7 +559,6 @@ extension ReviewViewController { guard let self = self else { return } switch result { case .success(let data): - // ✨ ReviewListItem을 그대로 사용 self.reviewList = data.dataList self.reviewTableView.reloadData() print("✅ Fixed Menu Reviews loaded: \(self.reviewList.count) items") @@ -621,7 +568,6 @@ extension ReviewViewController { } } - // ✨ V2 API: 식단 리뷰 리스트 func getMealReviewList() { NetworkService.shared.request( ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: nil, size: 20), @@ -631,7 +577,6 @@ extension ReviewViewController { guard let self = self else { return } switch result { case .success(let data): - // ✨ ReviewListItem을 그대로 사용 self.reviewList = data.dataList self.reviewTableView.reloadData() print("✅ Meal Reviews loaded: \(self.reviewList.count) items") @@ -640,25 +585,24 @@ extension ReviewViewController { } } } - + func deleteReview(reviewID: Int) { NetworkService.shared.request( - ReviewRouter.deleteReview(reviewID), // 1. ReviewRouter를 타겟으로 지정 - responseType: Bool.self, // 2. 응답 타입은 Bool로 가정 - useAuth: true // 3. ✨ 인증 필요! + ReviewRouter.deleteReview(reviewID), + responseType: Bool.self, + useAuth: true ) { [weak self] result in guard let self = self else { return } switch result { case .success: print("✅ Review 삭제 성공") - // 삭제 성공 시, 통계 및 리뷰 목록을 새로고침 self.getStatistics() if self.type == "VARIABLE" { self.getValidMenusForReview() } self.getReviewList(type: self.type, menuId: self.menuID) - self.showToast(message: "리뷰가 성공적으로 삭제되었습니다.") // 사용자에게 피드백 제공 + self.showToast(message: "리뷰가 성공적으로 삭제되었습니다.") case let .failure(error): print("❌ Delete Review Error: \(error.localizedDescription)") diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 48fb4146..bcefd14e 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -25,17 +25,16 @@ final class SetRateViewController: BaseViewController { private var likedStates: [Bool] = [] private var menuTableViewHeightConstraint: Constraint? - // ✨ 타입 구분: FIXED(고정 메뉴) vs VARIABLE(식단) private var reviewType: ReviewType = .variable private var mealID: Int? private var menuID: Int? - private var isReviewSubmitting = false // 서버로 전송 중 여부 - private var isReviewSubmitted = false // 리뷰가 정상 제출된 상태 + private var isReviewSubmitting = false + private var isReviewSubmitted = false enum ReviewType { - case fixed // writeMenuReview 사용 - case variable // writeMealReview 사용 + case fixed + case variable } // MARK: - Initializer @@ -166,7 +165,7 @@ final class SetRateViewController: BaseViewController { private let buttonContainer: UIView = { let view = UIView() - view.backgroundColor = .white // 버튼 배경색 (TabBarContainer와 유사) + view.backgroundColor = .white view.layer.cornerRadius = 0 view.clipsToBounds = true return view @@ -184,7 +183,6 @@ final class SetRateViewController: BaseViewController { super.viewDidLoad() setDelegate() - // ✨ 타입에 따라 적절한 초기화 if reviewType == .variable, let mealId = mealID { fetchValidMenus(mealId: mealId) } else if reviewType == .fixed { @@ -209,8 +207,6 @@ final class SetRateViewController: BaseViewController { } // MARK: - API Calls - - // ✨ VARIABLE: 리뷰 가능한 메뉴 목록 조회 private func fetchValidMenus(mealId: Int) { NetworkService.shared.request( ReviewRouter.getValidMenusForReview(mealId), @@ -236,7 +232,6 @@ final class SetRateViewController: BaseViewController { } } - // ✨ FIXED: 단일 메뉴 리뷰 설정 private func setupFixedMenuReview() { likedStates = [false] menuTableView.reloadData() @@ -249,7 +244,6 @@ final class SetRateViewController: BaseViewController { dismissKeyboard() view.addSubviews(scrollView, buttonContainer) - // buttonContainer에 nextButton을 추가 buttonContainer.addSubview(nextButton) scrollView.addSubview(contentView) contentView.addSubviews( @@ -263,8 +257,7 @@ final class SetRateViewController: BaseViewController { imageCountLabel, userReviewImageView, closeButton, - deleteMethodLabel, - // nextButton + deleteMethodLabel ) } @@ -273,16 +266,12 @@ final class SetRateViewController: BaseViewController { $0.edges.equalToSuperview() } - // ✨ 2. buttonContainer 레이아웃 (화면 하단에 고정) buttonContainer.snp.makeConstraints { $0.leading.trailing.equalToSuperview() - // safeAreaLayoutGuide.bottom을 기준으로 높이 80인 버튼 영역의 top을 잡습니다. $0.top.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-80) - // 그리고 view의 맨 아래까지 확장하여 버튼 영역 아래를 채웁니다. $0.bottom.equalToSuperview() } - // ✨ 3. nextButton 레이아웃 (buttonContainer 내부에) nextButton.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(16) $0.top.equalToSuperview().offset(12) @@ -347,7 +336,6 @@ final class SetRateViewController: BaseViewController { deleteMethodLabel.snp.makeConstraints { $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) $0.leading.equalTo(selectImageButton) - // ✨ contentView bottom 제약을 deleteMethodLabel 아래로 연결 (여유 공간 100pt 확보) $0.bottom.equalTo(contentView.snp.bottom).offset(-100) } @@ -357,20 +345,8 @@ final class SetRateViewController: BaseViewController { $0.size.equalTo(24) } - // deleteMethodLabel.snp.makeConstraints { - // $0.top.equalTo(selectImageButton.snp.bottom).offset(7) - // $0.leading.equalTo(selectImageButton) - // } - - // nextButton.snp.makeConstraints { make in - // make.top.equalTo(maximumWordLabel.snp.bottom).offset(132) - // make.horizontalEdges.equalToSuperview().inset(16) - // make.bottom.equalToSuperview().offset(-15) - // } - for i in 0...4 { rateView.buttons[i].snp.makeConstraints { make in - // make.height.equalTo(28) make.width.equalTo(29.3) } } @@ -392,7 +368,6 @@ final class SetRateViewController: BaseViewController { validMenuIDList = idList likedStates = Array(repeating: false, count: list.count) - // ✨ 타입 추정 if idList.count == 1 { reviewType = .fixed menuID = idList.first @@ -403,16 +378,13 @@ final class SetRateViewController: BaseViewController { menuTableView.reloadData() } - // ✨ 리뷰 수정 시 메뉴 ID와 isLike 상태를 함께 바인딩하는 오버로드 func dataBindForFix(menuNames: [String], menuIds: [Int], likedStates: [Bool]) { self.selectedList = menuNames self.validMenuIDList = menuIds self.likedStates = likedStates - self.reviewType = .fixed // 리뷰 수정은 일반적으로 단일 메뉴 (fixed) 처럼 동작 + self.reviewType = .fixed menuLabel.text = "\(menuNames.first ?? "") 을/를 추천하시겠어요?" - - // 테이블 뷰 다시 로드 및 높이 업데이트 menuTableView.reloadData() view.setNeedsLayout() } @@ -452,10 +424,7 @@ final class SetRateViewController: BaseViewController { imagePickerController.allowsEditing = false userReviewTextView.delegate = self - // ✨ 네비게이션 델리게이트 설정 self.navigationController?.delegate = self - - // ✨ 스와이프 제스처 델리게이트 설정 self.navigationController?.interactivePopGestureRecognizer?.delegate = self } @@ -486,16 +455,13 @@ final class SetRateViewController: BaseViewController { return } - // 리뷰 전송 시작 → 인터셉트 차단 isReviewSubmitting = true - // ✨ 리뷰 ID가 있으면 수정, 없으면 작성 if reviewId != nil { sendFixReview() return } - // ✨ 타입에 따라 적절한 API 호출 switch reviewType { case .variable: sendMealReview() @@ -504,7 +470,6 @@ final class SetRateViewController: BaseViewController { } } - // ✨ V2 API: Review Fix private func sendFixReview() { guard let reviewId = reviewId else { showToast(message: "수정할 리뷰 정보가 없습니다.") @@ -513,20 +478,16 @@ final class SetRateViewController: BaseViewController { _Concurrency.Task { do { - // 1. MenuLike 배열 생성 (현재는 FIXED 리뷰만 수정 가능하다고 가정) - // FIXED 리뷰는 likedStates에 하나의 Bool 값만 가집니다. let menuLikes: [MenuLike] = validMenuIDList.enumerated().map { (index, menuId) in MenuLike(menuId: menuId, isLike: likedStates[index]) } - // 2. Fixed Review 요청 생성 let request = FixedReviewRequestDTO( rating: rateView.currentStar, menuLikes: menuLikes, content: userReviewTextView.text ) - // 3. API 전송 try await postFixReview(reviewId: reviewId, request: request) await MainActor.run { @@ -543,7 +504,6 @@ final class SetRateViewController: BaseViewController { } } - // ✨ V2 API: Meal Review (VARIABLE) private func sendMealReview() { guard let mealId = mealID else { showToast(message: "식단 정보가 없습니다.") @@ -552,13 +512,10 @@ final class SetRateViewController: BaseViewController { _Concurrency.Task { do { - // 1. 이미지 업로드 var imageUrl: String? if let image = userPickedImage { imageUrl = try await uploadImage(image: image) } - - // 2. Meal Review 요청 생성 let menuLikes = validMenuIDList.enumerated().map { (index, menuId) in MenuLike(menuId: menuId, isLike: likedStates[index]) } @@ -570,8 +527,6 @@ final class SetRateViewController: BaseViewController { content: userReviewTextView.text, imageUrls: imageUrl != nil ? [imageUrl!] : nil ) - - // 3. API 전송 try await postMealReview(request: request) await MainActor.run { @@ -588,7 +543,6 @@ final class SetRateViewController: BaseViewController { } } - // ✨ V2 API: Menu Review (FIXED) private func sendMenuReview() { guard let menuId = menuID ?? validMenuIDList.first else { showToast(message: "메뉴 정보가 없습니다.") @@ -597,13 +551,11 @@ final class SetRateViewController: BaseViewController { _Concurrency.Task { do { - // 1. 이미지 업로드 var imageUrl: String? if let image = userPickedImage { imageUrl = try await uploadImage(image: image) } - // 2. Menu Review 요청 생성 let menuLike = MenuLikeItem( menuId: menuId, isLike: likedStates.first ?? false @@ -616,7 +568,6 @@ final class SetRateViewController: BaseViewController { imageUrls: imageUrl != nil ? [imageUrl!] : nil ) - // 3. API 전송 try await postMenuReview(request: request) await MainActor.run { @@ -643,16 +594,11 @@ final class SetRateViewController: BaseViewController { } } -// @objc func didSelectedImage() { -// present(imagePickerController, animated: true) -// } @objc func didSelectedImage() { - // ✨ navigationController delegate 잠시 비활성화 let originalDelegate = self.navigationController?.delegate self.navigationController?.delegate = nil present(imagePickerController, animated: true) { [weak self] in - // ✨ picker가 올라온 후 delegate 다시 복원 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self?.navigationController?.delegate = originalDelegate } @@ -759,30 +705,22 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo picker.dismiss(animated: true) } - // 💡 네비게이션 컨트롤러의 뷰 컨트롤러가 pop되기 직전에 호출됩니다. func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { if isReviewSubmitted { - return - } + return + } if navigationController is UIImagePickerController { - return - } + return + } - // 현재 뷰 컨트롤러(SetRateViewController)가 pop되는지 확인합니다. let isPopping = !navigationController.viewControllers.contains(self) - // 만약 pop이 발생하고, 리뷰 작성 중이라면 if isPopping { - - // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) let textHasContent = userReviewTextView.text != "메뉴에 대한 상세한 리뷰를 작성해주세요" && !userReviewTextView.text.isEmpty let isReviewStarted: Bool = rateView.currentStar > 0 || textHasContent - // 2. 리뷰를 새로 작성 중이며 (reviewId == nil) 내용이 있을 경우에만 다이얼로그 표시 if reviewId == nil, isReviewStarted { - - // pop을 즉시 취소 (다이얼로그 결과를 기다림) navigationController.viewControllers.append(self) let title = "작성 취소" @@ -797,10 +735,8 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo confirmButtonTitle: confirmButtonTitle ) { [weak self] in guard let self = self else { return } - // "나가기" 확인 시, delegate를 nil로 설정하여 재귀 방지 self.navigationController?.delegate = nil self.navigationController?.popViewController(animated: true) - // pop 완료 후 다시 delegate 설정 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.navigationController?.delegate = self } @@ -809,13 +745,8 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo } } - // 💡 스와이프 제스처가 시작될 때 호출됩니다. func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - - // 1. 리뷰 내용이 있는지 확인 (별점, 텍스트 등) let isReviewStarted: Bool = rateView.currentStar > 0 || !userReviewTextView.text.isEmpty - - // 2. 리뷰를 새로 작성 중이며 내용이 있을 경우 if reviewId == nil, isReviewStarted { let title = "작성 취소" @@ -823,21 +754,16 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo let confirmButtonTitle = "나가기" let cancelButtonTitle = "계속 작성" - // pop을 바로 막고 다이얼로그 표시 showCustomDialog( title: title, message: message, cancelButtonTitle: cancelButtonTitle, confirmButtonTitle: confirmButtonTitle ) { [weak self] in - // "나가기" 확인 시, 명시적으로 pop self?.navigationController?.popViewController(animated: true) } - // 스와이프 동작을 취소합니다. return false } - - // 내용이 없으면 기본 동작 (스와이프 가능) return true } } From 700218a6ba7535408d41d07bf7aedaba2340688f Mon Sep 17 00:00:00 2001 From: Funital Date: Fri, 28 Nov 2025 23:43:39 +0900 Subject: [PATCH 21/69] =?UTF-8?q?[#321]=20=EC=8B=A0=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20view=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/RerportView/ReportView.swift | 57 +++++++++++++++---- .../ViewController/ReportViewController.swift | 56 ++++++++---------- 2 files changed, 72 insertions(+), 41 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift b/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift index fa53a882..ae08548a 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift @@ -59,11 +59,12 @@ final class ReportView: BaseUIView { textView.layer.borderWidth = 1 textView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray200.color.cgColor textView.textContainerInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) + textView.text = "리뷰 신고 사유를 작성해 주세요" + textView.textColor = EATSSUDesignAsset.Color.GrayScale.gray400.color textView.isEditable = false return textView }() - /// 0 / 300 와 같이 현재 작성된 글자 수 상태 레이블 var characterCountLabel: UILabel = { let label = UILabel() label.text = "0 / 300" @@ -72,12 +73,23 @@ final class ReportView: BaseUIView { return label }() - /// "EAT SSU 팀에게 보내기" 버튼 - let sendToEATSSUButton = ESButton(size: .big, title: "EAT SSU 팀에게 보내기") - // MARK: - Functions + func enableTextView() { + reviewReportReasonTextView.isEditable = true + } + + func disableTextView() { + reviewReportReasonTextView.isEditable = false + reviewReportReasonTextView.text = "리뷰 신고 사유를 작성해 주세요" + reviewReportReasonTextView.textColor = EATSSUDesignAsset.Color.GrayScale.gray400.color + reviewReportReasonTextView.resignFirstResponder() + characterCountLabel.text = "0 / 300" + } + override func configureUI() { + reviewReportReasonTextView.delegate = self + addSubviews( reviewReportReasonLabel, singleReportPerDayLabel, @@ -88,8 +100,7 @@ final class ReportView: BaseUIView { copyrightInfringementButton, otherReasonButton, reviewReportReasonTextView, - characterCountLabel, - sendToEATSSUButton + characterCountLabel ) } @@ -149,12 +160,38 @@ final class ReportView: BaseUIView { characterCountLabel.snp.makeConstraints { make in make.trailing.equalTo(reviewReportReasonTextView) make.top.equalTo(reviewReportReasonTextView.snp.bottom).offset(6) + make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).inset(24) } + } +} - sendToEATSSUButton.snp.makeConstraints { make in - make.leading.trailing.equalTo(self).inset(24) - make.height.equalTo(52) - make.top.equalTo(reviewReportReasonTextView.snp.bottom).offset(56) +extension ReportView: UITextViewDelegate { + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + let currentText = textView.text ?? "" + guard let stringRange = Range(range, in: currentText) else { return false } + let newLength = currentText.count + text.count - range.length + + if newLength > 300 { return false } + + let textToDisplay = currentText.replacingCharacters(in: stringRange, with: text) + characterCountLabel.text = "\(textToDisplay.count) / 300" + return true + } + + func textViewDidBeginEditing(_ textView: UITextView) { + if textView.textColor == EATSSUDesignAsset.Color.GrayScale.gray400.color { + textView.text = "" + textView.textColor = .black + } + } + + func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + textView.text = "리뷰 신고 사유를 작성해 주세요" + textView.textColor = EATSSUDesignAsset.Color.GrayScale.gray400.color + characterCountLabel.text = "0 / 300" + } else { + characterCountLabel.text = "\(textView.text.count) / 300" } } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift index 7e24dc6d..c467494b 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift @@ -20,6 +20,8 @@ final class ReportViewController: BaseViewController { private let reportView = ReportView() private let scrollView = UIScrollView() + private let sendToEATSSUButton = ESButton(size: .big, title: "EAT SSU 팀에게 보내기") + // Variable Properties private var isChecked = false private var isReasonSelected = false @@ -28,6 +30,10 @@ final class ReportViewController: BaseViewController { private var contentArray: [String?] = [] private var reviewID: Int = .init() + override var shouldHideTabBar: Bool { + return true + } + // MARK: - View Life Cycle override func viewWillAppear(_: Bool) { @@ -46,12 +52,11 @@ final class ReportViewController: BaseViewController { configureUI() setLayout() setScrollViewSetting() - setDelegate() addArray() setButtonEvent() setCustomNavigationBar() - reportView.sendToEATSSUButton.isEnabled = false + sendToEATSSUButton.isEnabled = false } override func viewWillDisappear(_: Bool) { @@ -61,17 +66,25 @@ final class ReportViewController: BaseViewController { // MARK: - Methods override func configureUI() { + view.addSubview(sendToEATSSUButton) view.addSubview(scrollView) scrollView.snp.makeConstraints { make in - make.edges.equalTo(view.safeAreaLayoutGuide) + make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide) + make.bottom.equalTo(sendToEATSSUButton.snp.top) + } + + + sendToEATSSUButton.snp.makeConstraints { make in + make.leading.trailing.equalTo(view).inset(24) + make.height.equalTo(52) + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).inset(24) } + scrollView.addSubview(reportView) reportView.snp.makeConstraints { make in make.edges.equalTo(scrollView.contentLayoutGuide) make.width.equalTo(scrollView.frameLayoutGuide) - - make.height.equalTo(800) } } @@ -99,7 +112,8 @@ final class ReportViewController: BaseViewController { reportView.otherReasonButton].forEach { $0.addTarget(self, action: #selector(checkButtonIsTapped(_:)), for: .touchUpInside) } - reportView.sendToEATSSUButton.addTarget(self, action: #selector(sendButtonIsTapped), for: .touchUpInside) + + sendToEATSSUButton.addTarget(self, action: #selector(sendButtonIsTapped), for: .touchUpInside) } func bindData(reviewID: Int) { @@ -169,21 +183,14 @@ final class ReportViewController: BaseViewController { sender.isSelected = true isChecked = true status = sender.tag - canTextViewUsed(status: status) - - reportView.sendToEATSSUButton.isEnabled = true - } - - private func canTextViewUsed(status: Int) { + if status == 5 { - reportView.reviewReportReasonTextView.isEditable = true + reportView.enableTextView() } else { - reportView.reviewReportReasonTextView.isEditable = false + reportView.disableTextView() } - } - - private func setDelegate() { - reportView.reviewReportReasonTextView.delegate = self + + sendToEATSSUButton.isEnabled = true } @objc @@ -257,19 +264,6 @@ final class ReportViewController: BaseViewController { } } -extension ReportViewController: UITextViewDelegate { - func textView(_: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - let newLength = reportView.reviewReportReasonTextView.text.count - range.length + text.count - reportView.characterCountLabel.text = "\(reportView.reviewReportReasonTextView.text.count) / 300" - - if newLength > 300 { - return false - } else { - return true - } - } -} - // MARK: - Server extension ReportViewController { From 63730ef98f0a4c11025e20bfee0761d73f47292d Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 29 Nov 2025 13:33:57 +0900 Subject: [PATCH 22/69] =?UTF-8?q?[#321]=20=EC=B6=A9=EB=8F=8C=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/SeeReview/ReviewEmptyViewCell.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index 7fd81087..bdd71aa7 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -24,10 +24,8 @@ final class ReviewEmptyViewCell: UITableViewCell { private lazy var titleLabel: UILabel = { let label = UILabel() - label.text = "아직 작성된 리뷰가 없어요" label.font = .subtitle2 label.text = "아직 작성된 리뷰가 없어요!" - label.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 16) label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color label.textAlignment = .center return label @@ -84,7 +82,7 @@ final class ReviewEmptyViewCell: UITableViewCell { } func configureForMyReview() { - mainLabel.text = "아직 작성한 리뷰가 없어요" - subLabel.text = "첫 리뷰를 남겨 주세요!" + titleLabel.text = "아직 작성한 리뷰가 없어요" + descriptionLabel.text = "첫 리뷰를 남겨 주세요!" } } From 4a9814f250b857ae6b2a3debb60e709a40567c20 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 29 Nov 2025 16:05:56 +0900 Subject: [PATCH 23/69] =?UTF-8?q?[#321]=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/MyReviewResponseDTO.swift | 41 + .../Data/Network/Router/ReviewRouter.swift | 25 +- .../Review/View/RateReview/MenuLikeCell.swift | 85 +- .../Review/View/RateReview/RateView.swift | 104 ++- .../Review/View/RateReview/SetRateView.swift | 288 +++++++ .../Review/View/RerportView/ReportView.swift | 2 +- .../View/SeeReview/RateNumberView.swift | 40 +- .../View/SeeReview/ReviewDividerCell.swift | 42 +- .../View/SeeReview/ReviewEmptyViewCell.swift | 38 +- .../View/SeeReview/ReviewRateViewCell.swift | 178 ++-- .../View/SeeReview/ReviewTableCell.swift | 289 ++++--- .../ReviewTagCollectionViewCell.swift | 30 +- .../ViewController/ReviewViewController.swift | 509 +++++++---- .../SetRateViewController.swift | 788 +++++++----------- 14 files changed, 1554 insertions(+), 905 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift create mode 100644 EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift new file mode 100644 index 00000000..91d17aeb --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift @@ -0,0 +1,41 @@ +// +// MyReviewResponseDTO.swift +// EATSSU +// +// Created by 한금준 on 11/29/25. +// + +import Foundation + +// MARK: - API Response DTO + +/// 내가 쓴 리뷰 리스트 전체 응답 구조 +struct MyReviewResponseDTO: Codable { + let result: MyReviewList +} + +/// 리뷰 리스트 데이터 컨테이너 +struct MyReviewList: Codable { + let numberOfElements: Int // 총 요소 개수 + let hasNext: Bool // 다음 페이지 존재 여부 + let dataList: [MyReviewListItem] // 리뷰 목록 +} + +// MARK: - Review Item DTO + +/// 개별 리뷰 아이템 구조 +struct MyReviewListItem: Codable { + let reviewId: Int // 리뷰 ID + let rating: Double // 별점 (4) + let writtenAt: String // 작성일 ("2023-04-07") + let content: String? // 리뷰 내용 ("맛있당") + let imageUrls: [String]? // 이미지 URL 리스트 ("imgurl1", "imgurl2") + let menuList: [ReviewMenu] // 리뷰에 포함된 메뉴 리스트 +} + +/// 리뷰에 포함된 개별 메뉴 구조 +struct ReviewMenu: Codable { + let menuId: Int // 메뉴 ID (3143) + let name: String // 메뉴 이름 ("생고기제육볶음") + let isLike: Bool // 좋아요 여부 (true) +} diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 83776923..1657a1c6 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -21,6 +21,10 @@ enum ReviewRouter { size: Int? = 20) case getFixedMenuStatistics(_ menuId: Int) case getMealStatistics(_ mealId: Int) + case getMyReviewList(lastReviewId: Int?, + page: Int? = 0, + size: Int? = 20, + sort: String? = "date,DESC") // 기본값: date,DESC } extension ReviewRouter: TargetType { @@ -50,12 +54,14 @@ extension ReviewRouter: TargetType { "/v2/reviews/statistics/menus/\(menuId)" case let .getMealStatistics(mealId): "/v2/reviews/statistics/meals/\(mealId)" + case .getMyReviewList: + "/v2/reviews/my" } } var method: Moya.Method { switch self { - case .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: + case .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics, .getMyReviewList: .get case .report: .post @@ -98,6 +104,23 @@ extension ReviewRouter: TargetType { .requestPlain case .getMealStatistics: .requestPlain + case let .getMyReviewList(lastReviewId, page, size, sort): + .requestParameters( + parameters: { + var dict: [String: Any] = [:] + + if let lastId = lastReviewId { + dict["lastReviewId"] = lastId + } else { + dict["page"] = page ?? 0 + } + + dict["size"] = size ?? 20 + dict["sort"] = sort ?? "date,DESC" + return dict + }(), + encoding: URLEncoding.queryString + ) } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift index dadabca3..9e4a2767 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -1,6 +1,6 @@ // // MenuLikeCell.swift -// EATSSU +// EatSSU-iOS // // Created by 한금준 on 9/28/25. // @@ -11,17 +11,24 @@ import SnapKit import EATSSUDesign final class MenuLikeCell: UITableViewCell { - static let identifier = "MenuLikeCell" // MARK: - Properties + + static let identifier = "MenuLikeCell" + + /// 좋아요 버튼 탭 핸들러 (Controller에게 이벤트 전달) var onLikeTapped: (() -> Void)? + + /// 좋아요 상태 var isLiked: Bool = false { didSet { - tapped() + updateLikeState() } } // MARK: - UI Components + + /// 메뉴 이름 레이블 private let menuLabel: UILabel = { let label = UILabel() label.font = .body3 @@ -29,14 +36,16 @@ final class MenuLikeCell: UITableViewCell { return label }() + /// 좋아요 버튼 이미지 private let likeButton: UIButton = { let button = UIButton(type: .system) button.tintColor = .gray button.backgroundColor = .clear - button.isUserInteractionEnabled = false + button.isUserInteractionEnabled = false // Container가 이벤트를 받도록 설정 return button }() + /// 좋아요 버튼 컨테이너 (탭 제스처 인식 영역) private let likeContainer: UIView = { let view = UIView() view.layer.cornerRadius = 14 @@ -45,6 +54,7 @@ final class MenuLikeCell: UITableViewCell { return view }() + /// 가로 스택뷰 (메뉴 레이블 + 좋아요 컨테이너) private lazy var hStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [menuLabel, likeContainer]) stack.axis = .horizontal @@ -53,20 +63,34 @@ final class MenuLikeCell: UITableViewCell { return stack }() - // MARK: - Init + // MARK: - Initialization + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + setLayout() + setupGesture() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Configuration + + /// UI 컴포넌트 설정 + private func setupUI() { selectionStyle = .none contentView.addSubview(hStack) likeContainer.addSubview(likeButton) - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(likeTapped)) - likeContainer.isUserInteractionEnabled = true - likeContainer.addGestureRecognizer(tapGesture) - + } + + /// 레이아웃 제약조건 설정 + private func setLayout() { hStack.snp.makeConstraints { - $0.edges.equalToSuperview().inset(12) + $0.verticalEdges.equalToSuperview().inset(12) + $0.horizontalEdges.equalToSuperview() // TableView에서 inset 처리 } likeContainer.snp.makeConstraints { @@ -80,35 +104,52 @@ final class MenuLikeCell: UITableViewCell { } } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + /// 제스처 설정 + private func setupGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(likeTapped)) + likeContainer.isUserInteractionEnabled = true + likeContainer.addGestureRecognizer(tapGesture) } // MARK: - Actions + + /// 좋아요 버튼 탭 처리 (Controller에게 이벤트 전달) @objc private func likeTapped() { onLikeTapped?() } - // MARK: - Public Functions + // MARK: - Public Methods + + /// 셀 데이터 바인딩 + /// - Parameters: + /// - menu: 메뉴 이름 + /// - isLiked: 좋아요 상태 func dataBind(menu: String, isLiked: Bool) { menuLabel.text = menu self.isLiked = isLiked } - private func tapped() { - print("tapped 실행됨 → isLiked:", isLiked) - let image = isLiked ? EATSSUDesignAsset.Images.thumbUp.image : EATSSUDesignAsset.Images.thumbUpGray.image + // MARK: - Private Methods + + /// 좋아요 상태에 따라 UI 업데이트 + private func updateLikeState() { + + let image = isLiked + ? EATSSUDesignAsset.Images.thumbUp.image // 채워진 좋아요 + : EATSSUDesignAsset.Images.thumbUpGray.image // 빈 좋아요 + DispatchQueue.main.async { + // 버튼 이미지 업데이트 self.likeButton.setImage(image.withRenderingMode(.alwaysOriginal), for: .normal) + // 컨테이너 스타일 업데이트 if self.isLiked { - self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor + self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color // 배경색 + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor // 테두리 색 } else { - self.likeContainer.backgroundColor = .clear - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor + self.likeContainer.backgroundColor = .clear // 배경색 제거 + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor // 테두리 색 } } } } - diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift index e8a1c73d..debdff38 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift @@ -6,22 +6,32 @@ // import UIKit - import SnapKit import EATSSUDesign -final class RateView: BaseUIView { +final class RateView: UIView { // BaseUIView 대신 UIView 상속 + // MARK: - Properties + /// 별 버튼 배열 var buttons: [UIButton] = [] + + /// 현재 선택된 별점 (1~5) var currentStar: Int = 0 - var starNumber: Int = 5 { - didSet { bind() } - } - // MARK: - UI Component + /// 별의 개수 (기본값: 5) + private var starNumber: Int = 5 // 내부 프로퍼티로 변경 + + /// 채워진 별 이미지 + private lazy var starFillImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image + /// 빈 별 이미지 + private lazy var starEmptyImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image + + // MARK: - UI Components + + /// 별들을 가로로 배치하는 스택뷰 lazy var starStackView: UIStackView = { let view = UIStackView() view.axis = .horizontal @@ -30,13 +40,13 @@ final class RateView: BaseUIView { return view }() - lazy var starFillImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image - - lazy var starEmptyImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image + // MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) - bind() + setupStars() + configureUI() + setLayout() } @available(*, unavailable) @@ -44,49 +54,79 @@ final class RateView: BaseUIView { fatalError("init(coder:) has not been implemented") } - // MARK: - Functions + // MARK: - UI Configuration - override func configureUI() { + /// UI 컴포넌트 설정 + private func configureUI() { addSubview(starStackView) } - override func setLayout() { + /// 레이아웃 제약조건 설정 + private func setLayout() { starStackView.snp.makeConstraints { make in - make.top.leading.bottom.trailing.equalToSuperview() + make.edges.equalToSuperview() } } - func bind() { - for i in 0 ..< 5 { + // MARK: - Private Methods + + /// 별 버튼들 생성 및 설정 + private func setupStars() { + // 기존 버튼 제거 + buttons.forEach { $0.removeFromSuperview() } + buttons.removeAll() + + // 5개의 별 버튼 생성 및 스택뷰에 추가 + for i in 0.. 0 { - for i in 0 ... currentStar - 1 { - buttons[i].setImage(starFillImage, for: .normal) - } - } - for i in currentStar ..< starNumber { - buttons[i].setImage(starEmptyImage, for: .normal) - } + self.currentStar = currentStar + updateStars() } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift new file mode 100644 index 00000000..cd448126 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift @@ -0,0 +1,288 @@ +// +// SetRateView.swift +// EATSSU +// +// Created by 한금준 on 11/29/25. +// + +import UIKit +import SnapKit + +import EATSSUDesign + +final class SetRateView: UIView { + + // MARK: - Properties + + /// 메뉴 테이블뷰 높이 제약조건 (Controller에서 content size에 따라 업데이트) + var menuTableViewHeightConstraint: Constraint? + + // MARK: - UI Components + + // ⭐️ Scroll View Container + let scrollView: UIScrollView = { + let scrollView = UIScrollView() + return scrollView + }() + + let contentView: UIView = UIView() + + // ⭐️ Review Rate Section + let menuLabel: UILabel = { + let label = UILabel() + label.text = "오늘의 식사는 어떠셨나요?" + label.font = .subtitle1 + label.textColor = .black + return label + }() + + let rateView = RateView() + + // ⭐️ Menu Like Section + let detailLabel: UILabel = { + let label = UILabel() + label.text = "추천하고 싶은 메뉴가 있나요?" + label.font = .subtitle1 + label.textColor = .black + return label + }() + + let menuTableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.isScrollEnabled = false // 높이를 content size에 맞춤 + tableView.rowHeight = 48.adjusted // Cell height + return tableView + }() + + // ⭐️ Review Text Section + let userReviewTextView: UITextView = { + let textView = UITextView() + textView.font = .body1 + textView.layer.cornerRadius = 10.adjusted + textView.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color + textView.layer.borderWidth = 1.adjusted + textView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + textView.textContainerInset = UIEdgeInsets(top: 16.0.adjusted, left: 16.0.adjusted, bottom: 16.0.adjusted, right: 16.0.adjusted) + return textView + }() + + let maximumWordLabel: UILabel = { + let label = UILabel() + label.text = "0 / 300" + label.font = .caption2 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + return label + }() + + // ⭐️ Image Section + let selectImageButton: UIButton = { + let button = UIButton() + var config = UIButton.Configuration.plain() + config.image = EATSSUDesignAsset.Images.addImageButton.image + config.contentInsets = NSDirectionalEdgeInsets(top: -5, leading: 0, bottom: 5, trailing: 0) + button.configuration = config + button.layer.borderWidth = 1 + button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor + button.layer.cornerRadius = 8 + button.clipsToBounds = true + return button + }() + + let imageCountLabel: UILabel = { + let label = UILabel() + label.text = "사진 0/1" + label.font = .caption3 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color + label.textAlignment = .center + return label + }() + + let userReviewImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 10 + imageView.clipsToBounds = true + imageView.isUserInteractionEnabled = true // Controller에서 제스처 추가 + imageView.contentMode = .scaleAspectFill + imageView.isHidden = true // 초기 상태 숨김 + return imageView + }() + + let closeButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) + button.tintColor = .lightGray + button.isHidden = true + return button + }() + + let deleteMethodLabel: UILabel = { + let label = UILabel() + label.text = "사진 클릭 시, 삭제됩니다" + label.font = .caption3 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color + return label + }() + + // ⭐️ Bottom Button Section + let buttonContainer: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 0 + view.clipsToBounds = true + return view + }() + + let nextButton: MainButton = { + let button = MainButton() + button.title = "리뷰 남기기" + return button + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + + setupUI() + setupLayout() + // 뷰 초기 상태 설정 + setInitialTextViewState() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setupUI() { + self.backgroundColor = .white + } + + private func setupLayout() { + self.addSubviews(scrollView, buttonContainer) + + buttonContainer.addSubview(nextButton) + scrollView.addSubview(contentView) + contentView.addSubviews( + rateView, + menuLabel, + detailLabel, + menuTableView, + userReviewTextView, + maximumWordLabel, + selectImageButton, + imageCountLabel, + userReviewImageView, + closeButton, + deleteMethodLabel + ) + + // 1. ScrollView & ContentView + scrollView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.bottom.equalTo(buttonContainer.snp.top) + } + + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.width.equalTo(scrollView) + } + + // 2. Bottom Button + buttonContainer.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(safeAreaLayoutGuide) + $0.height.equalTo(80) + } + + nextButton.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.top.equalToSuperview().offset(12) + } + + // 3. Main Content + menuLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(20) + make.centerX.equalToSuperview() + } + + rateView.snp.makeConstraints { make in + make.top.equalTo(menuLabel.snp.bottom).offset(17) + make.centerX.equalToSuperview() + make.height.equalTo(36.12) + } + + detailLabel.snp.makeConstraints { make in + make.top.equalTo(rateView.snp.bottom).offset(35) + make.centerX.equalToSuperview() + } + + menuTableView.snp.makeConstraints { + $0.top.equalTo(detailLabel.snp.bottom).offset(20) + $0.leading.equalToSuperview().offset(32) + $0.trailing.equalToSuperview().offset(-32) + // 초기 높이 제약조건 설정 (Controller에서 업데이트) + self.menuTableViewHeightConstraint = $0.height.equalTo(0).constraint + } + + userReviewTextView.snp.makeConstraints { make in + make.top.equalTo(menuTableView.snp.bottom).offset(40) + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(181) + } + + maximumWordLabel.snp.makeConstraints { make in + make.top.equalTo(userReviewTextView.snp.bottom).offset(7) + make.trailing.equalTo(userReviewTextView) + } + + selectImageButton.snp.makeConstraints { + $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) + $0.leading.equalToSuperview().offset(16) + $0.width.height.equalTo(60) + } + + imageCountLabel.snp.makeConstraints { + $0.top.equalTo(selectImageButton.snp.bottom).offset(5) + $0.centerX.equalTo(selectImageButton) + } + + userReviewImageView.snp.makeConstraints { + $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) + $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) + $0.width.height.equalTo(60) + } + + deleteMethodLabel.snp.makeConstraints { + $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) + $0.leading.equalTo(selectImageButton) + // ContentView의 bottom에 연결하여 스크롤 가능하게 함 + $0.bottom.equalTo(contentView.snp.bottom).offset(-100) + } + + closeButton.snp.makeConstraints { + $0.top.equalTo(userReviewImageView.snp.top).offset(-6) + $0.trailing.equalTo(userReviewImageView.snp.trailing).offset(6) + $0.size.equalTo(24) + } + } + + // MARK: - Public Methods + + /// 리뷰 텍스트뷰의 초기 상태를 설정합니다. + func setInitialTextViewState() { + userReviewTextView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" + userReviewTextView.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color + } + + /// 이미지 뷰와 관련 UI를 업데이트합니다. + func updateImageViewState(image: UIImage?, count: Int, isHidden: Bool) { + userReviewImageView.image = image + userReviewImageView.isHidden = isHidden + closeButton.isHidden = isHidden || (image == nil) + imageCountLabel.text = "사진 \(count)/1" + } +} diff --git a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift b/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift index ae08548a..03c1f519 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift @@ -1,6 +1,6 @@ // // ReportView.swift -// EATSSU +// EatSSU-iOS // // Created by Jiwoong CHOI on 8/30/24. // diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift index f9e8294a..1ed3b7a6 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/RateNumberView.swift @@ -6,21 +6,35 @@ // import UIKit - import SnapKit import EATSSUDesign +// MARK: - RateNumberView + +/// 별점을 시각적으로 표시하는 커스텀 뷰 final class RateNumberView: BaseUIView { + + // MARK: - Properties + + /// 채워진 별 이미지 + var filledStarImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image + + /// 빈 별 이미지 + var emptyStarImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image + // MARK: - UI Components + + /// 별 이미지뷰 배열 (5개) private var starImageViews: [UIImageView] = [] + + /// 별들을 가로로 배치하는 스택뷰 private lazy var starsStackView = UIStackView() - private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView]) - var filledStarImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image - var emptyStarImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image + /// 전체 레이팅 컴포넌트를 담는 스택뷰 + private lazy var rateNumberStackView = UIStackView(arrangedSubviews: [starsStackView]) - // MARK: - init + // MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) @@ -31,30 +45,40 @@ final class RateNumberView: BaseUIView { fatalError("init(coder:) has not been implemented") } + // MARK: - Layout + override func layoutSubviews() { super.layoutSubviews() } - // MARK: - Functions + // MARK: - UI Configuration + /// UI 컴포넌트 설정 override func configureUI() { addSubviews(rateNumberStackView) + + // 5개의 별 이미지뷰 생성 starImageViews = (0..<5).map { _ in let imageView = UIImageView() imageView.image = emptyStarImage return imageView } + // 별 스택뷰 설정 starsStackView.axis = .horizontal starsStackView.spacing = 3 starsStackView.alignment = .bottom starImageViews.forEach { starsStackView.addArrangedSubview($0) } + + // 전체 레이팅 스택뷰 설정 rateNumberStackView.axis = .horizontal rateNumberStackView.spacing = 6 rateNumberStackView.alignment = .bottom } + /// 레이아웃 제약조건 설정 override func setLayout() { + // 각 별의 크기 설정 starImageViews.forEach { $0.snp.makeConstraints { $0.height.equalTo(12.adjusted) @@ -62,12 +86,16 @@ final class RateNumberView: BaseUIView { } } + // 전체 스택뷰 제약조건 rateNumberStackView.snp.makeConstraints { $0.edges.equalToSuperview() } } + // MARK: - Public Methods + /// 별점 설정 (1~5점) + /// - Parameter rating: 표시할 별점 (1-5) func setRating(_ rating: Int) { for (index, star) in starImageViews.enumerated() { if index < rating { diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift index 5657503f..5bbc6e80 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift @@ -5,20 +5,30 @@ // Created by 한금준 on 10/4/25. // -import SnapKit import UIKit +import SnapKit import EATSSUDesign +// MARK: - ReviewDividerCell + +/// 리뷰 섹션을 구분하고 리뷰 개수를 표시하는 셀 final class ReviewDividerCell: UITableViewCell { + + // MARK: - Properties + static let identifier = "ReviewDividerCell" + // MARK: - UI Components + + /// 상단 구분선 private let divider: UIView = { let divider = UIView() divider.backgroundColor = .gray100 return divider }() + /// 리뷰 개수 표시 레이블 private let label: UILabel = { let label = UILabel() label.text = "리뷰 15" @@ -27,15 +37,33 @@ final class ReviewDividerCell: UITableViewCell { return label }() + // MARK: - Initialization + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI Configuration + + /// UI 컴포넌트 추가 + private func setupUI() { contentView.addSubview(divider) contentView.addSubview(label) - + } + + /// 레이아웃 제약조건 설정 + private func setLayout() { divider.snp.makeConstraints { $0.top.leading.trailing.equalToSuperview() $0.height.equalTo(12) } + label.snp.makeConstraints { $0.top.equalTo(divider.snp.bottom).offset(16) $0.leading.equalToSuperview().offset(16) @@ -43,13 +71,19 @@ final class ReviewDividerCell: UITableViewCell { } } - required init?(coder: NSCoder) { fatalError() } + // MARK: - Public Methods + /// 리뷰 개수로 셀 구성 + /// - Parameter reviewCount: 표시할 리뷰 개수 func configure(reviewCount: Int) { let text = "리뷰 \(reviewCount)" let attributed = NSMutableAttributedString(string: text) let range = (text as NSString).range(of: "\(reviewCount)") - attributed.addAttribute(.foregroundColor, value: EATSSUDesignAsset.Color.Main.primary.color, range: range) + attributed.addAttribute( + .foregroundColor, + value: EATSSUDesignAsset.Color.Main.primary.color, + range: range + ) label.attributedText = attributed } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index bdd71aa7..9bca4291 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -6,22 +6,29 @@ // import UIKit - import SnapKit import EATSSUDesign +// MARK: - ReviewEmptyViewCell + +/// 리뷰가 없을 때 표시하는 빈 상태 셀 final class ReviewEmptyViewCell: UITableViewCell { + // MARK: - Properties + static let identifier = "ReviewEmptyViewCell" // MARK: - UI Components + + /// 빈 상태 이미지 private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() imageView.tintColor = EATSSUDesignAsset.Color.GrayScale.gray600.color return imageView }() + /// 제목 레이블 private lazy var titleLabel: UILabel = { let label = UILabel() label.font = .subtitle2 @@ -31,6 +38,7 @@ final class ReviewEmptyViewCell: UITableViewCell { return label }() + /// 설명 레이블 private lazy var descriptionLabel: UILabel = { let label = UILabel() label.text = "메뉴에 가장 먼저 리뷰를 남겨주세요!" @@ -40,18 +48,24 @@ final class ReviewEmptyViewCell: UITableViewCell { return label }() + /// 컴포넌트들을 세로로 배치하는 스택뷰 private lazy var stackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: [noReviewImageView, titleLabel, descriptionLabel]) + let stack = UIStackView(arrangedSubviews: [ + noReviewImageView, + titleLabel, + descriptionLabel + ]) stack.axis = .vertical stack.alignment = .center stack.spacing = 16 return stack }() - // MARK: - Init + // MARK: - Initialization + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(stackView) + setupUI() setLayout() } @@ -60,17 +74,28 @@ final class ReviewEmptyViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - // MARK: - Layout + // MARK: - UI Configuration + + /// UI 컴포넌트 추가 + private func setupUI() { + contentView.addSubview(stackView) + } + + /// 레이아웃 제약조건 설정 private func setLayout() { stackView.snp.makeConstraints { $0.center.equalToSuperview() } + noReviewImageView.snp.makeConstraints { $0.size.equalTo(48) } } - // MARK: - Configure + // MARK: - Public Methods + + /// 토큰 존재 여부에 따라 셀 구성 + /// - Parameter isTokenExist: 로그인 토큰 존재 여부 func configure(isTokenExist: Bool) { if isTokenExist { noReviewImageView.image = EATSSUDesignAsset.Images.noReview.image @@ -81,6 +106,7 @@ final class ReviewEmptyViewCell: UITableViewCell { } } + /// 마이페이지용 빈 상태 구성 func configureForMyReview() { titleLabel.text = "아직 작성한 리뷰가 없어요" descriptionLabel.text = "첫 리뷰를 남겨 주세요!" diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 26351392..6790c676 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -7,16 +7,27 @@ import UIKit import SnapKit + import EATSSUDesign +// MARK: - ReviewRateViewCell + +/// 메뉴 정보와 별점 통계를 표시하는 셀 final class ReviewRateViewCell: UITableViewCell { + // MARK: - Properties static let identifier = "ReviewRateViewCell" + + /// 리뷰 작성 버튼 탭 핸들러 var handler: (() -> Void)? + + /// 전체 평균 별점 var totalRate: Double = 0 - // MARK: - UI Components + // MARK: - UI Components - Menu Section + + /// 메뉴 정보 컨테이너 private let menuContainer: UIView = { let view = UIView() view.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color @@ -25,6 +36,7 @@ final class ReviewRateViewCell: UITableViewCell { return view }() + /// 메뉴 이름 레이블 var menuLabel: UILabel = { let label = UILabel() label.text = "김치볶음밥 & 계란국" @@ -35,12 +47,14 @@ final class ReviewRateViewCell: UITableViewCell { return label }() + /// 메뉴 아이콘 private let menuIcon: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.icRestaurant.image return imageView }() + /// "오늘의 메뉴" 타이틀 레이블 private let menuTitleLabel: UILabel = { let label = UILabel() label.text = "오늘의 메뉴" @@ -49,6 +63,7 @@ final class ReviewRateViewCell: UITableViewCell { return label }() + /// 메뉴 타이틀 섹션 스택뷰 private lazy var menuTitleStackView: UIStackView = { let stack = UIStackView(arrangedSubviews: [menuIcon, menuTitleLabel]) stack.axis = .horizontal @@ -57,17 +72,22 @@ final class ReviewRateViewCell: UITableViewCell { return stack }() + // MARK: - UI Components - Rating Section + + /// 별점 섹션 컨테이너 private let rateSectionContainer: UIView = { let view = UIView() return view }() + /// 큰 별 아이콘 private let bigStarImageView: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.icStarYellow.image return imageView }() + /// 평균 별점 숫자 레이블 var rateNumLabel: UILabel = { let label = UILabel() label.text = "4.3" @@ -76,47 +96,57 @@ final class ReviewRateViewCell: UITableViewCell { return label }() + // MARK: - UI Components - Rating Chart + + /// 별점별 레이블들 private let fivePointLabel = ReviewRateViewCell.makePointLabel("5점") private let fourPointLabel = ReviewRateViewCell.makePointLabel("4점") private let threePointLabel = ReviewRateViewCell.makePointLabel("3점") private let twoPointLabel = ReviewRateViewCell.makePointLabel("2점") private let onePointLabel = ReviewRateViewCell.makePointLabel("1점") - // Chart bar containers and foregrounds + /// 차트 바 컨테이너들 var oneChartBar: UIView! var twoChartBar: UIView! var threeChartBar: UIView! var fourChartBar: UIView! var fiveChartBar: UIView! + /// 차트 바 전경(채워지는 부분)들 var oneForeground: UIView! var twoForeground: UIView! var threeForeground: UIView! var fourForeground: UIView! var fiveForeground: UIView! + /// Y축(별점) 레이블 스택뷰 lazy var yAxisStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [fivePointLabel, - fourPointLabel, - threePointLabel, - twoPointLabel, - onePointLabel]) + let stackView = UIStackView(arrangedSubviews: [ + fivePointLabel, + fourPointLabel, + threePointLabel, + twoPointLabel, + onePointLabel + ]) stackView.axis = .vertical stackView.spacing = 0 stackView.alignment = .trailing return stackView }() + /// 전체 별점 표시 스택뷰 lazy var totalRateStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [bigStarImageView, - rateNumLabel]) + let stackView = UIStackView(arrangedSubviews: [ + bigStarImageView, + rateNumLabel + ]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center return stackView }() - // MARK: - Init + // MARK: - Initialization override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -129,8 +159,11 @@ final class ReviewRateViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - // MARK: - Helper + // MARK: - Helper Methods + /// 별점 레이블 생성 헬퍼 + /// - Parameter text: 레이블 텍스트 + /// - Returns: 설정된 UILabel private static func makePointLabel(_ text: String) -> UILabel { let label = UILabel() label.text = text @@ -139,66 +172,72 @@ final class ReviewRateViewCell: UITableViewCell { return label } - // MARK: - UI Setup + /// 차트 바 생성 헬퍼 + /// - Returns: 차트 바 컨테이너와 전경 뷰 튜플 + private func makeChartBar() -> (container: UIView, foreground: UIView) { + let container = UIView() + container.backgroundColor = .gray200 + container.layer.cornerRadius = 5 + container.layer.masksToBounds = true + + let foreground = UIView() + foreground.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color + foreground.layer.cornerRadius = 5 + foreground.layer.masksToBounds = true + + container.addSubview(foreground) + foreground.snp.makeConstraints { make in + make.leading.top.bottom.equalToSuperview() + make.width.equalTo(0) + } + + return (container, foreground) + } + + // MARK: - UI Configuration + /// UI 컴포넌트 설정 func configureUI() { - func makeChartBar() -> (container: UIView, foreground: UIView) { - let container = UIView() - container.backgroundColor = .gray200 - container.layer.cornerRadius = 5 - container.layer.masksToBounds = true - let foreground = UIView() - foreground.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color - foreground.layer.cornerRadius = 5 - foreground.layer.masksToBounds = true - container.addSubview(foreground) - foreground.snp.makeConstraints { make in - make.leading.top.bottom.equalToSuperview() - make.width.equalTo(0) - } - return (container, foreground) - } + backgroundColor = .white + // 차트 바들 생성 let oneBar = makeChartBar() oneChartBar = oneBar.container oneForeground = oneBar.foreground + let twoBar = makeChartBar() twoChartBar = twoBar.container twoForeground = twoBar.foreground + let threeBar = makeChartBar() threeChartBar = threeBar.container threeForeground = threeBar.foreground + let fourBar = makeChartBar() fourChartBar = fourBar.container fourForeground = fourBar.foreground + let fiveBar = makeChartBar() fiveChartBar = fiveBar.container fiveForeground = fiveBar.foreground - contentView.addSubviews( - menuContainer, - rateSectionContainer - ) - + // 서브뷰 추가 + contentView.addSubviews(menuContainer, rateSectionContainer) menuContainer.addSubviews(menuTitleStackView, menuLabel) - - rateSectionContainer.addSubviews(totalRateStackView, yAxisStackView, - oneChartBar, twoChartBar, threeChartBar, fourChartBar, fiveChartBar) - - totalRateStackView.snp.makeConstraints { make in - make.top.bottom.equalToSuperview().offset(35.5) - make.leading.equalToSuperview().offset(36) - } - - yAxisStackView.snp.makeConstraints { make in - make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) - make.centerY.equalTo(totalRateStackView) - } + rateSectionContainer.addSubviews( + totalRateStackView, + yAxisStackView, + oneChartBar, + twoChartBar, + threeChartBar, + fourChartBar, + fiveChartBar + ) } + /// 레이아웃 제약조건 설정 func setLayout() { - backgroundColor = .white - + // 메뉴 컨테이너 menuContainer.snp.makeConstraints { make in make.top.equalTo(contentView.snp.top).offset(0) make.centerX.equalToSuperview() @@ -221,11 +260,27 @@ final class ReviewRateViewCell: UITableViewCell { make.bottom.equalToSuperview().inset(16) } + // 별점 섹션 rateSectionContainer.snp.makeConstraints { make in make.top.equalTo(menuLabel.snp.bottom).offset(40) make.leading.trailing.equalToSuperview().inset(60) } + totalRateStackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview().offset(35.5) + make.leading.equalToSuperview().offset(36) + } + + bigStarImageView.snp.makeConstraints { + $0.height.width.equalTo(24.adjusted) + } + + yAxisStackView.snp.makeConstraints { make in + make.leading.equalTo(totalRateStackView.snp.trailing).offset(36) + make.centerY.equalTo(totalRateStackView) + } + + // 차트 바들 oneChartBar.snp.makeConstraints { make in make.centerY.equalTo(onePointLabel) make.leading.equalTo(onePointLabel.snp.trailing).offset(7) @@ -261,24 +316,18 @@ final class ReviewRateViewCell: UITableViewCell { make.width.equalTo(126) } + // 포인트 레이블 높이 for item in [onePointLabel, twoPointLabel, threePointLabel, fourPointLabel, fivePointLabel] { item.snp.makeConstraints { $0.height.equalTo(18.adjusted) } } - - bigStarImageView.snp.makeConstraints { - $0.height.width.equalTo(24.adjusted) - } } - @objc - func touchAddReviewButton() { - handler?() - } -} - -extension ReviewRateViewCell { + // MARK: - Public Methods + + /// 식사(Meal) 통계 데이터로 셀 구성 + /// - Parameter data: 식사 통계 응답 데이터 func configureWithMealStatistics(_ data: ReviewMealStatisticsResponse) { let menuNames = data.menuList.map { $0.name } menuLabel.text = menuNames.joined(separator: " + ") @@ -286,13 +335,18 @@ extension ReviewRateViewCell { updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) } + /// 메뉴(Menu) 통계 데이터로 셀 구성 + /// - Parameter data: 메뉴 통계 응답 데이터 func configureWithMenuStatistics(_ data: ReviewMenuStatisticsResponse) { menuLabel.text = data.menuName setRating(data.rating ?? 0) updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) } - // MARK: - Private Helper Methods + // MARK: - Private Methods + + /// 평균 별점 설정 + /// - Parameter rating: 별점 값 (0.0 ~ 5.0) private func setRating(_ rating: Double) { totalRate = rating @@ -304,6 +358,10 @@ extension ReviewRateViewCell { } } + /// 별점별 분포 차트 업데이트 + /// - Parameters: + /// - ratingCount: 별점별 개수 데이터 + /// - totalCount: 전체 리뷰 개수 private func updateRatingChart(with ratingCount: ReviewRatingCount, totalCount: Int) { let safeTotal = max(totalCount, 1) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index c24fc26c..11accebc 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -2,59 +2,45 @@ // ReviewTableCell.swift // EatSSU-iOS // -// Updated to use ReviewListItem instead of MenuDataList +// Created by 한금준 on 20/11/25. // import UIKit import SnapKit + import EATSSUDesign +// MARK: - ReviewTableCell + +/// 개별 리뷰를 표시하는 테이블뷰 셀 final class ReviewTableCell: UITableViewCell { // MARK: - Properties static let identifier = "ReviewTableCell" + + /// 더보기 버튼 탭 핸들러 var handler: (() -> Void)? - var reviewId: Int = .init() - var menuName: String = .init() - // MARK: - UI Components + /// 리뷰 ID + var reviewId: Int = 0 - lazy var totalRateView = RateNumberView() - - private lazy var tagCollectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - layout.minimumInteritemSpacing = 8 - layout.minimumLineSpacing = 8 - - let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.isScrollEnabled = false - cv.register(ReviewTagCollectionViewCell.self, forCellWithReuseIdentifier: ReviewTagCollectionViewCell.identifier) - cv.dataSource = self - return cv - }() + /// 메뉴 이름 + var menuName: String = "" + /// 메뉴 태그 데이터 private var tags: [(name: String, isLiked: Bool)] = [] - lazy var contentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [tagCollectionView, reviewTextView, foodImageView]) - stackView.axis = .vertical - stackView.spacing = 8.adjusted - stackView.alignment = .leading - return stackView - }() + // MARK: - UI Components - Profile Section - private var dateLabel: UILabel = { - let label = UILabel() - label.text = "2023.03.03" - label.font = .caption3 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - return label + /// 사용자 프로필 이미지 + private let userProfileImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = EATSSUDesignAsset.Images.profile.image + return imageView }() + /// 사용자 닉네임 레이블 private var userNameLabel: UILabel = { let label = UILabel() label.text = "hellosoongsil1234" @@ -63,54 +49,28 @@ final class ReviewTableCell: UITableViewCell { return label }() - private let userProfileImageView: UIImageView = { - let imageView = UIImageView() - imageView.image = EATSSUDesignAsset.Images.profile.image - return imageView - }() - - private var sideButton: BaseButton = { - let button = BaseButton() - button.setTitleColor(EATSSUDesignAsset.Color.GrayScale.gray400.color, for: .normal) - button.titleLabel?.font = .caption2 - button.configuration?.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 15) - return button - }() - - var reviewTextView: UITextView = { - let textView = UITextView() - textView.textColor = UIColor.black - textView.isEditable = false - textView.isScrollEnabled = false - textView.backgroundColor = .systemBackground - textView.font = .body1 - textView.text = "여기 계란국 맛집임... 김치볶음밥에 계란후라이 없어서 아쉽 다음에 또 먹어야지" - return textView - }() - - var foodImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.isHidden = true - return imageView - }() + /// 별점 표시 뷰 + lazy var totalRateView = RateNumberView() - lazy var rateStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [totalRateView]) + /// 닉네임과 메뉴를 담는 스택뷰 + lazy var nameMenuStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [userNameLabel]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center return stackView }() - lazy var nameMenuStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [userNameLabel]) + /// 별점 스택뷰 + lazy var rateStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [totalRateView]) stackView.axis = .horizontal stackView.spacing = 8.adjusted stackView.alignment = .center return stackView }() + /// 사용자 정보(닉네임, 별점) 스택뷰 lazy var infoStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [nameMenuStackView, rateStackView]) stackView.axis = .vertical @@ -119,6 +79,7 @@ final class ReviewTableCell: UITableViewCell { return stackView }() + /// 프로필 전체 스택뷰 lazy var profileStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [userProfileImageView, infoStackView]) stackView.axis = .horizontal @@ -127,6 +88,27 @@ final class ReviewTableCell: UITableViewCell { return stackView }() + // MARK: - UI Components - Right Section + + /// 작성 날짜 레이블 + private var dateLabel: UILabel = { + let label = UILabel() + label.text = "2023.03.03" + label.font = .caption3 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + return label + }() + + /// 더보기 버튼 (수정/삭제/신고) + private var sideButton: BaseButton = { + let button = BaseButton() + button.setTitleColor(EATSSUDesignAsset.Color.GrayScale.gray400.color, for: .normal) + button.titleLabel?.font = .caption2 + button.configuration?.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 15) + return button + }() + + /// 날짜와 더보기 버튼 스택뷰 lazy var dateReportStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [sideButton, dateLabel]) stackView.axis = .vertical @@ -135,14 +117,65 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - // MARK: - Functions + // MARK: - UI Components - Content Section + + /// 메뉴 태그 컬렉션뷰 + private lazy var tagCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.minimumInteritemSpacing = 8 + layout.minimumLineSpacing = 8 + + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.isScrollEnabled = false + cv.register( + ReviewTagCollectionViewCell.self, + forCellWithReuseIdentifier: ReviewTagCollectionViewCell.identifier + ) + cv.dataSource = self + return cv + }() + + /// 리뷰 내용 텍스트뷰 + var reviewTextView: UITextView = { + let textView = UITextView() + textView.textColor = UIColor.black + textView.isEditable = false + textView.isScrollEnabled = false + textView.backgroundColor = .systemBackground + textView.font = .body1 + textView.text = "여기 계란국 맛집임... 김치볶음밥에 계란후라이 없어서 아쉽 다음에 또 먹어야지" + return textView + }() + + /// 음식 이미지뷰 + var foodImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.isHidden = true + return imageView + }() + + /// 콘텐츠(태그, 텍스트, 이미지) 스택뷰 + lazy var contentStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [ + tagCollectionView, + reviewTextView, + foodImageView + ]) + stackView.axis = .vertical + stackView.spacing = 8.adjusted + stackView.alignment = .leading + return stackView + }() + + // MARK: - Initialization override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(profileStackView) - contentView.addSubview(dateReportStackView) - contentView.addSubview(contentStackView) - contentStackView.setCustomSpacing(8, after: reviewTextView) + setupUI() setLayout() } @@ -151,9 +184,12 @@ final class ReviewTableCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } + // MARK: - Lifecycle + override func prepareForReuse() { super.prepareForReuse() + // 재사용 시 데이터 초기화 tags = [] tagCollectionView.reloadData() sideButton.setTitle("", for: .normal) @@ -165,22 +201,43 @@ final class ReviewTableCell: UITableViewCell { userNameLabel.text = "" } + // MARK: - UI Configuration + + /// UI 컴포넌트 추가 + private func setupUI() { + contentView.addSubview(profileStackView) + contentView.addSubview(dateReportStackView) + contentView.addSubview(contentStackView) + + // 리뷰 텍스트 뒤에 추가 간격 설정 + contentStackView.setCustomSpacing(8, after: reviewTextView) + } + + /// 레이아웃 제약조건 설정 func setLayout() { + // 프로필 이미지 userProfileImageView.snp.makeConstraints { make in make.width.height.equalTo(30) } + // 프로필 섹션 profileStackView.snp.makeConstraints { make in make.top.equalToSuperview().offset(5) make.leading.equalToSuperview().offset(16) make.height.equalTo(50) } + // 날짜/더보기 버튼 섹션 dateReportStackView.snp.makeConstraints { make in make.centerY.equalTo(profileStackView) make.trailing.equalToSuperview().inset(16) } + sideButton.snp.makeConstraints { + $0.height.equalTo(12.adjusted) + } + + // 콘텐츠 섹션 contentStackView.snp.makeConstraints { make in make.top.equalTo(profileStackView.snp.bottom) make.leading.equalToSuperview().offset(16) @@ -188,59 +245,44 @@ final class ReviewTableCell: UITableViewCell { make.trailing.equalToSuperview().offset(-16) } - foodImageView.snp.makeConstraints { make in - make.top.equalTo(reviewTextView.snp.bottom).offset(8) + // 태그 컬렉션뷰 + tagCollectionView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() - make.height.equalTo(foodImageView.snp.width).multipliedBy(0.75) - } - - sideButton.snp.makeConstraints { - $0.height.equalTo(12.adjusted) + make.height.greaterThanOrEqualTo(30) } - tagCollectionView.snp.makeConstraints { make in + // 음식 이미지 + foodImageView.snp.makeConstraints { make in + make.top.equalTo(reviewTextView.snp.bottom).offset(8) make.leading.trailing.equalToSuperview() - make.height.greaterThanOrEqualTo(30) + make.height.equalTo(foodImageView.snp.width).multipliedBy(0.75) } } + // MARK: - Actions + + /// 더보기 버튼 탭 이벤트 @objc func touchedSideButtonEvent() { handler?() } -} - -// MARK: - CollectionView DataSource -extension ReviewTableCell: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return tags.count - } - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: ReviewTagCollectionViewCell.identifier, - for: indexPath - ) as? ReviewTagCollectionViewCell else { - return UICollectionViewCell() - } - let tag = tags[indexPath.item] - cell.configure(tagName: tag.name, isLiked: tag.isLiked) - return cell - } -} - -// MARK: - Data Bind - -extension ReviewTableCell { + // MARK: - Public Methods + + /// 리뷰 데이터로 셀 구성 + /// - Parameter response: 리뷰 리스트 아이템 func dataBind(response: ReviewListItem) { + // 메뉴 이름 설정 menuName = response.menu?.map { $0.name }.joined(separator: " + ") ?? "" + // 기본 정보 설정 userNameLabel.text = response.writerNickname totalRateView.setRating(Int(response.rating)) dateLabel.text = response.writtenAt reviewTextView.text = response.content ?? "" reviewId = response.reviewId + // 이미지 설정 if let firstImageUrl = response.imageUrls?.first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) @@ -248,25 +290,31 @@ extension ReviewTableCell { foodImageView.isHidden = true } + // 더보기 버튼 설정 sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) + // 태그 설정 if let menuTags = response.menu, !menuTags.isEmpty { tags = menuTags.map { ($0.name, $0.isLike) } } else { tags = [] } tagCollectionView.reloadData() - tagCollectionView.isHidden = tags.isEmpty } + /// 마이페이지용 리뷰 데이터 바인딩 + /// - Parameters: + /// - response: 마이페이지 데이터 리스트 + /// - nickname: 사용자 닉네임 func myPageDataBind(response: MyDataList, nickname: String) { userNameLabel.text = "\(nickname)" totalRateView.setRating(response.mainRating) dateLabel.text = response.writeDate reviewTextView.text = response.content + // 이미지 설정 if response.imgURLList.count != 0 { if response.imgURLList[0] != "" { foodImageView.isHidden = false @@ -276,12 +324,41 @@ extension ReviewTableCell { foodImageView.isHidden = true } + // 더보기 버튼 설정 sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.setTitle("", for: .normal) reviewId = response.reviewID + // 마이페이지에서는 태그 숨김 tags = [] tagCollectionView.isHidden = true } } + +// MARK: - UICollectionViewDataSource + +extension ReviewTableCell: UICollectionViewDataSource { + + /// 컬렉션뷰 아이템 개수 + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return tags.count + } + + /// 컬렉션뷰 셀 구성 + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: ReviewTagCollectionViewCell.identifier, + for: indexPath + ) as? ReviewTagCollectionViewCell else { + return UICollectionViewCell() + } + + let tag = tags[indexPath.item] + cell.configure(tagName: tag.name, isLiked: tag.isLiked) + return cell + } +} diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift index 480f6ccc..eb106455 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -8,9 +8,18 @@ import UIKit import SnapKit +// MARK: - ReviewTagCollectionViewCell + +/// 리뷰의 메뉴 태그를 표시하는 컬렉션뷰 셀 final class ReviewTagCollectionViewCell: UICollectionViewCell { + + // MARK: - Properties + static let identifier = "ReviewTagCollectionViewCell" + // MARK: - UI Components + + /// 좋아요 아이콘 private let iconImageView: UIImageView = { let iv = UIImageView() iv.image = UIImage(systemName: "hand.thumbsup") @@ -20,6 +29,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { return iv }() + /// 태그 이름 레이블 private let titleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) @@ -27,6 +37,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { return label }() + /// 아이콘과 레이블을 담는 스택뷰 private let stackView: UIStackView = { let sv = UIStackView() sv.axis = .horizontal @@ -35,6 +46,8 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { return sv }() + // MARK: - Initialization + override init(frame: CGRect) { super.init(frame: frame) setupViews() @@ -44,26 +57,34 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + // MARK: - Lifecycle + override func layoutSubviews() { super.layoutSubviews() contentView.layer.cornerRadius = contentView.bounds.height / 2 } + // MARK: - UI Configuration + /// UI 컴포넌트 설정 private func setupViews() { + // 배경 및 테두리 설정 contentView.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.1) contentView.layer.borderColor = UIColor.systemTeal.cgColor contentView.layer.borderWidth = 1 + // 스택뷰 구성 stackView.addArrangedSubview(iconImageView) stackView.addArrangedSubview(titleLabel) - contentView.addSubview(stackView) + + // 레이아웃 설정 stackView.translatesAutoresizingMaskIntoConstraints = false iconImageView.snp.makeConstraints { make in make.width.height.equalTo(10) } + stackView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(8) make.trailing.equalToSuperview().inset(8) @@ -72,8 +93,15 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { } } + // MARK: - Public Methods + + /// 태그 데이터로 셀 구성 + /// - Parameters: + /// - tagName: 태그 이름 + /// - isLiked: 좋아요 여부 func configure(tagName: String, isLiked: Bool) { titleLabel.text = tagName + if isLiked { iconImageView.isHidden = false iconImageView.image = UIImage(systemName: "hand.thumbsup") diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 325348e4..0d2aab98 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -2,35 +2,68 @@ // ReviewViewController.swift // EatSSU-iOS // -// Updated with full V2 API integration +// Created by 한금준 on 20/11/25. // import UIKit +import SnapKit import FirebaseAnalytics import Moya -import SnapKit +// MARK: - ReviewViewController + +/// 메뉴의 리뷰 목록과 통계를 표시하는 뷰 컨트롤러 final class ReviewViewController: BaseViewController { + // MARK: - Properties + override var shouldHideTabBar: Bool { return true } + + // MARK: - Network + + /// 리뷰 API 프로바이더 let reviewProvider = MoyaProvider(plugins: [ESMoyaLoggingPlugin()]) - var menuID: Int = .init() + + // MARK: - Data Properties + + /// 메뉴 ID (FIXED 타입) 또는 식사 ID (VARIABLE 타입) + var menuID: Int = 0 + + /// 메뉴 타입 ("FIXED" 또는 "VARIABLE") var type = "VARIABLE" + + /// 메뉴 이름 리스트 private var menuNameList: [String] = [] - private var menuIDList: [Int]? = [Int]() + + /// 메뉴 ID 리스트 + private var menuIDList: [Int]? = [] + + /// 메뉴 이름-ID 매핑 딕셔너리 private var menuDictionary: [String: Int] = [:] + + /// 리뷰 목록 데이터 private var reviewList = [ReviewListItem]() + + /// 식사(Meal) 통계 데이터 private var mealStatistics: ReviewMealStatisticsResponse? + + /// 메뉴(Menu) 통계 데이터 private var menuStatistics: ReviewMenuStatisticsResponse? + + /// 전체 리뷰 개수 private var totalReviewCount: Int = 0 + + /// 리뷰 작성 가능한 메뉴 목록 (VARIABLE 타입) private var validMenusForReview: [ReviewValidMenu] = [] - // MARK: - UI Component + // MARK: - UI Components + /// 당겨서 새로고침 컨트롤 let refreshControl = UIRefreshControl() + /// 리뷰 목록 테이블뷰 let reviewTableView: UITableView = { let tableView = UITableView() tableView.separatorStyle = .none @@ -38,6 +71,7 @@ final class ReviewViewController: BaseViewController { return tableView }() + /// 로딩 인디케이터 private var activityIndicatorView: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .large) indicator.startAnimating() @@ -45,6 +79,7 @@ final class ReviewViewController: BaseViewController { return indicator }() + /// 빈 상태 이미지뷰 private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() imageView.image = ImageLiteral.noReview @@ -52,6 +87,7 @@ final class ReviewViewController: BaseViewController { return imageView }() + /// 리뷰 작성 버튼 컨테이너 private let reviewTabBarContainer: UIView = { let view = UIView() view.backgroundColor = .white @@ -60,13 +96,14 @@ final class ReviewViewController: BaseViewController { return view }() + /// 리뷰 작성 버튼 private let reviewTabBarView: MainButton = { let button = MainButton() button.title = "리뷰 작성하기" return button }() - // MARK: - Life Cycles + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() @@ -78,6 +115,8 @@ final class ReviewViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + + // 데이터 로드 getStatistics() if type == "VARIABLE" { getValidMenusForReview() @@ -88,6 +127,7 @@ final class ReviewViewController: BaseViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + // 뒤로 가기 시 탭바 표시 if self.isMovingFromParent { var parentVC = self.parent while parentVC != nil { @@ -100,14 +140,17 @@ final class ReviewViewController: BaseViewController { } } - // MARK: - Functions + // MARK: - UI Configuration override func configureUI() { reviewTableView.backgroundColor = .white - view.addSubviews(reviewTableView, - activityIndicatorView, - noReviewImageView, - reviewTabBarContainer) + + view.addSubviews( + reviewTableView, + activityIndicatorView, + noReviewImageView, + reviewTabBarContainer + ) reviewTabBarContainer.addSubview(reviewTabBarView) } @@ -144,13 +187,57 @@ final class ReviewViewController: BaseViewController { } override func setButtonEvent() { - reviewTabBarView.addTarget(self, action: #selector(handleAddReviewButtonTap), for: .touchUpInside) + reviewTabBarView.addTarget( + self, + action: #selector(handleAddReviewButtonTap), + for: .touchUpInside + ) } + // MARK: - TableView Setup + + /// 테이블뷰 설정 + private func setTableView() { + // 셀 등록 + reviewTableView.register( + ReviewTableCell.self, + forCellReuseIdentifier: ReviewTableCell.identifier + ) + reviewTableView.register( + ReviewRateViewCell.self, + forCellReuseIdentifier: ReviewRateViewCell.identifier + ) + reviewTableView.register( + ReviewEmptyViewCell.self, + forCellReuseIdentifier: ReviewEmptyViewCell.identifier + ) + reviewTableView.register( + ReviewDividerCell.self, + forCellReuseIdentifier: ReviewDividerCell.identifier + ) + + // 델리게이트 설정 + reviewTableView.delegate = self + reviewTableView.dataSource = self + } + + /// 당겨서 새로고침 초기화 + private func initRefresh() { + refreshControl.addTarget( + self, + action: #selector(refreshTable(refresh:)), + for: .valueChanged + ) + reviewTableView.refreshControl = refreshControl + } + + // MARK: - Actions + + /// 리뷰 작성 버튼 탭 처리 @objc private func handleAddReviewButtonTap() { if type == "VARIABLE" { + // 가변 메뉴 (식사) let reviewVC = SetRateViewController(mealId: menuID) - reviewVC.dataBind( list: validMenusForReview.map { $0.name }, idList: validMenusForReview.map { $0.menuId } @@ -158,8 +245,8 @@ final class ReviewViewController: BaseViewController { navigationController?.pushViewController(reviewVC, animated: true) } else { + // 고정 메뉴 let reviewVC = SetRateViewController(menuId: menuID) - reviewVC.dataBind( list: menuNameList, idList: menuIDList ?? [] @@ -168,35 +255,8 @@ final class ReviewViewController: BaseViewController { } } - private func setFirebaseTask() { - FirebaseRemoteConfig.shared.fetchRestaurantInfo() - -#if DEBUG -#else - Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) -#endif - } - - func setTableView() { - reviewTableView.register(ReviewTableCell.self, forCellReuseIdentifier: ReviewTableCell.identifier) - reviewTableView.register(ReviewRateViewCell.self, forCellReuseIdentifier: ReviewRateViewCell.identifier) - reviewTableView.register(ReviewEmptyViewCell.self, forCellReuseIdentifier: ReviewEmptyViewCell.identifier) - reviewTableView.register(ReviewDividerCell.self, forCellReuseIdentifier: ReviewDividerCell.identifier) - - reviewTableView.delegate = self - reviewTableView.dataSource = self - } - - func initRefresh() { - refreshControl.addTarget(self, - action: #selector(refreshTable(refresh:)), - for: .valueChanged) - - reviewTableView.refreshControl = refreshControl - } - - @objc - func refreshTable(refresh: UIRefreshControl) { + /// 테이블 새로고침 + @objc private func refreshTable(refresh: UIRefreshControl) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.getStatistics() if self.type == "VARIABLE" { @@ -207,11 +267,12 @@ final class ReviewViewController: BaseViewController { } } - func bindMenuID(id: Int) { - menuID = id - } + // MARK: - Alert Methods + /// 리뷰 삭제 확인 알림 표시 + /// - Parameter data: 리뷰 데이터 private func showDeleteAlert(data: ReviewListItem) { + // 작성자가 아니면 신고 알림 표시 if !data.isWriter { self.showReportAlert(reviewID: data.reviewId) return @@ -229,55 +290,12 @@ final class ReviewViewController: BaseViewController { confirmButtonTitle: confirmButtonTitle ) { [weak self] in guard let self = self else { return } - self.deleteReview(reviewID: data.reviewId) } } - private func showFixOrDeleteAlert_OLD(data: ReviewListItem) { - let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", - message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", - preferredStyle: UIAlertController.Style.actionSheet) - - let fixAction = UIAlertAction(title: "수정하기", - style: .default, - handler: { _ in - - let menuNames = data.menu?.map { $0.name } ?? [] - let menuIds = data.menu?.map { $0.menuId } ?? [] - let setRateViewController = SetRateViewController(menuId: self.menuID) - - setRateViewController.dataBindForFix(list: menuNames, reviewId: data.reviewId) - setRateViewController.settingForReviewFix(data: data) - - let likedStates = data.menu?.map { $0.isLike } ?? [] - setRateViewController.dataBindForFix( - menuNames: menuNames, - menuIds: menuIds, - likedStates: likedStates - ) - - self.navigationController?.pushViewController(setRateViewController, animated: true) - }) - - let deleteAction = UIAlertAction(title: "삭제하기", - style: .destructive, - handler: { [weak self] _ in - guard let self = self else { return } - - self.deleteReview(reviewID: data.reviewId) - }) - - let cancelAction = UIAlertAction(title: "취소하기", - style: .cancel, - handler: nil) - - alert.addAction(fixAction) - alert.addAction(deleteAction) - alert.addAction(cancelAction) - present(alert, animated: true, completion: nil) - } - + /// 리뷰 신고 알림 표시 + /// - Parameter reviewID: 신고할 리뷰 ID private func showReportAlert(reviewID: Int) { showCustomDialog( title: "리뷰 신고하기", @@ -291,6 +309,21 @@ final class ReviewViewController: BaseViewController { } } + /// 로그인으로 이동 + private func pushToLoginVC() { + let loginVC = LoginViewController() + navigationController?.pushViewController(loginVC, animated: true) + } + + // MARK: - Public Methods + + /// 메뉴 ID 바인딩 + /// - Parameter id: 메뉴 ID + func bindMenuID(id: Int) { + menuID = id + } + + /// 리뷰 작성 버튼 탭 처리 (로그인 체크 포함) func userTapReviewButton() { if RealmService.shared.isAccessTokenPresent() { activityIndicatorView.isHidden = false @@ -299,13 +332,15 @@ final class ReviewViewController: BaseViewController { if type == "FIXED" { let setRateViewController = SetRateViewController(menuId: menuID) - setRateViewController.dataBind( list: menuNameList, idList: menuIDList ?? [] ) activityIndicatorView.stopAnimating() - navigationController?.pushViewController(setRateViewController, animated: true) + navigationController?.pushViewController( + setRateViewController, + animated: true + ) } else { let setRateViewController = SetRateViewController(mealId: menuID) setRateViewController.dataBind( @@ -313,23 +348,28 @@ final class ReviewViewController: BaseViewController { idList: validMenusForReview.map { $0.menuId } ) activityIndicatorView.stopAnimating() - navigationController?.pushViewController(setRateViewController, animated: true) + navigationController?.pushViewController( + setRateViewController, + animated: true + ) } } } } else { - showAlertControllerWithCancel(title: "로그인이 필요한 서비스입니다", message: "로그인 하시겠습니까?", confirmStyle: .default) { + showAlertControllerWithCancel( + title: "로그인이 필요한 서비스입니다", + message: "로그인 하시겠습니까?", + confirmStyle: .default + ) { self.pushToLoginVC() } } } - private func pushToLoginVC() { - let loginVC = LoginViewController() - navigationController?.pushViewController(loginVC, animated: true) - } + // MARK: - Helper Methods - func makeDictionary() { + /// 메뉴 이름-ID 딕셔너리 생성 + private func makeDictionary() { if menuIDList != [] { for (index, string) in menuNameList.enumerated() { let idValue = menuIDList?[index] @@ -337,15 +377,28 @@ final class ReviewViewController: BaseViewController { } } } + + /// Firebase 작업 설정 + private func setFirebaseTask() { + FirebaseRemoteConfig.shared.fetchRestaurantInfo() + +#if DEBUG +#else + Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) +#endif + } } -// MARK: - UITableView Delegate, DataSource +// MARK: - UITableViewDelegate extension ReviewViewController: UITableViewDelegate { + + /// 셀 선택 처리 func tableView(_: UITableView, didSelectRowAt _: IndexPath) { print("cell did touched") } + /// 섹션 헤더 높이 func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { switch section { case 0: @@ -359,120 +412,184 @@ extension ReviewViewController: UITableViewDelegate { } } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + /// 섹션 헤더 뷰 + func tableView( + _ tableView: UITableView, + viewForHeaderInSection section: Int + ) -> UIView? { let spacerView = UIView() spacerView.backgroundColor = .clear return spacerView } + + /// 셀 높이 + func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + switch indexPath.section { + case 0: + return 251.adjusted + case 1: + return UITableView.automaticDimension + case 2: + if reviewList.count == 0 { + return 300.adjusted + } else { + return UITableView.automaticDimension + } + default: + return UITableView.automaticDimension + } + } } +// MARK: - UITableViewDataSource + extension ReviewViewController: UITableViewDataSource { + + /// 섹션 개수 func numberOfSections(in _: UITableView) -> Int { - 3 + return 3 } + /// 섹션별 행 개수 func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: - 1 + return 1 // 통계 셀 case 1: - 1 + return 1 // 구분선 셀 case 2: - if reviewList.count == 0 { - 1 - } else { - reviewList.count - } + return reviewList.count == 0 ? 1 : reviewList.count // 리뷰 목록 또는 빈 상태 default: - 0 + return 0 } } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + /// 셀 구성 + func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { switch indexPath.section { case 0: - let cell = tableView.dequeueReusableCell(withIdentifier: ReviewRateViewCell.identifier, for: indexPath) as? ReviewRateViewCell ?? ReviewRateViewCell() - cell.selectionStyle = .none - - if type == "FIXED" { - if let statistics = menuStatistics { - cell.configureWithMenuStatistics(statistics) - } - } else { - if let statistics = mealStatistics { - cell.configureWithMealStatistics(statistics) - } - } - - cell.handler = { [weak self] in - guard let self else { return } - self.userTapReviewButton() - } - cell.reloadInputViews() - return cell + // 통계 셀 + return configureStatisticsCell(tableView, indexPath: indexPath) case 1: - let cell = tableView.dequeueReusableCell(withIdentifier: ReviewDividerCell.identifier, for: indexPath) as? ReviewDividerCell ?? ReviewDividerCell() - cell.configure(reviewCount: totalReviewCount) - cell.selectionStyle = .none - return cell + // 구분선 셀 + return configureDividerCell(tableView, indexPath: indexPath) case 2: - if reviewList.count == 0 { - let cell = tableView.dequeueReusableCell(withIdentifier: ReviewEmptyViewCell.identifier, for: indexPath) as? ReviewEmptyViewCell ?? ReviewEmptyViewCell() - if RealmService.shared.getToken() == "" { - cell.configure(isTokenExist: false) - } else { - cell.configure(isTokenExist: true) - } - cell.selectionStyle = .none - return cell - } else { - let cell = tableView.dequeueReusableCell(withIdentifier: ReviewTableCell.identifier, for: indexPath) as? ReviewTableCell ?? ReviewTableCell() - - var filteredReviewItem = reviewList[indexPath.row] - let likedMenus = filteredReviewItem.menu?.filter { $0.isLike } - filteredReviewItem.menu = likedMenus - - cell.dataBind(response: filteredReviewItem) - - cell.handler = { [weak self] in - guard let self else { return } - - reviewList[indexPath.row].isWriter ? self.showDeleteAlert(data: reviewList[indexPath.row]) - : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) - } - cell.selectionStyle = .none - cell.reloadInputViews() - return cell - } + // 리뷰 목록 또는 빈 상태 셀 + return configureReviewCell(tableView, indexPath: indexPath) default: return UITableViewCell() } } - func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - switch indexPath.section { - case 0: - 251.adjusted - case 1: - UITableView.automaticDimension - case 2: - if reviewList.count == 0 { - 300.adjusted + // MARK: - Cell Configuration Helpers + + /// 통계 셀 구성 + private func configureStatisticsCell( + _ tableView: UITableView, + indexPath: IndexPath + ) -> UITableViewCell { + let cell = tableView.dequeueReusableCell( + withIdentifier: ReviewRateViewCell.identifier, + for: indexPath + ) as? ReviewRateViewCell ?? ReviewRateViewCell() + + cell.selectionStyle = .none + + if type == "FIXED" { + if let statistics = menuStatistics { + cell.configureWithMenuStatistics(statistics) + } + } else { + if let statistics = mealStatistics { + cell.configureWithMealStatistics(statistics) + } + } + + cell.handler = { [weak self] in + guard let self else { return } + self.userTapReviewButton() + } + + cell.reloadInputViews() + return cell + } + + /// 구분선 셀 구성 + private func configureDividerCell( + _ tableView: UITableView, + indexPath: IndexPath + ) -> UITableViewCell { + let cell = tableView.dequeueReusableCell( + withIdentifier: ReviewDividerCell.identifier, + for: indexPath + ) as? ReviewDividerCell ?? ReviewDividerCell() + + cell.configure(reviewCount: totalReviewCount) + cell.selectionStyle = .none + return cell + } + + /// 리뷰 셀 또는 빈 상태 셀 구성 + private func configureReviewCell( + _ tableView: UITableView, + indexPath: IndexPath + ) -> UITableViewCell { + if reviewList.count == 0 { + // 빈 상태 셀 + let cell = tableView.dequeueReusableCell( + withIdentifier: ReviewEmptyViewCell.identifier, + for: indexPath + ) as? ReviewEmptyViewCell ?? ReviewEmptyViewCell() + + if RealmService.shared.getToken() == "" { + cell.configure(isTokenExist: false) } else { - UITableView.automaticDimension + cell.configure(isTokenExist: true) } - default: - UITableView.automaticDimension + cell.selectionStyle = .none + return cell + + } else { + // 리뷰 셀 + let cell = tableView.dequeueReusableCell( + withIdentifier: ReviewTableCell.identifier, + for: indexPath + ) as? ReviewTableCell ?? ReviewTableCell() + + // 좋아요한 메뉴만 필터링 + var filteredReviewItem = reviewList[indexPath.row] + let likedMenus = filteredReviewItem.menu?.filter { $0.isLike } + filteredReviewItem.menu = likedMenus + + cell.dataBind(response: filteredReviewItem) + + // 더보기 버튼 핸들러 + cell.handler = { [weak self] in + guard let self else { return } + + reviewList[indexPath.row].isWriter + ? self.showDeleteAlert(data: reviewList[indexPath.row]) + : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) + } + + cell.selectionStyle = .none + cell.reloadInputViews() + return cell } } } -// MARK: - V2 API Network Calls +// MARK: - Network Methods extension ReviewViewController { + + /// 통계 데이터 조회 func getStatistics() { if type == "FIXED" { getFixedMenuStatistics() @@ -481,13 +598,15 @@ extension ReviewViewController { } } - func getFixedMenuStatistics() { + /// 고정 메뉴 통계 조회 + private func getFixedMenuStatistics() { NetworkService.shared.request( ReviewRouter.getFixedMenuStatistics(menuID), responseType: ReviewMenuStatisticsResponse.self, useAuth: false ) { [weak self] result in guard let self = self else { return } + switch result { case .success(let data): self.menuStatistics = data @@ -496,19 +615,22 @@ extension ReviewViewController { self.menuIDList = [self.menuID] self.makeDictionary() self.reviewTableView.reloadData() + case .failure(let error): print("❌ Fixed Menu Statistics Error: \(error.localizedDescription)") } } } - func getMealStatistics() { + /// 식사 통계 조회 + private func getMealStatistics() { NetworkService.shared.request( ReviewRouter.getMealStatistics(menuID), responseType: ReviewMealStatisticsResponse.self, useAuth: false ) { [weak self] result in guard let self = self else { return } + switch result { case .success(let data): self.mealStatistics = data @@ -517,6 +639,7 @@ extension ReviewViewController { self.menuIDList = data.menuList.map { $0.id } self.makeDictionary() self.reviewTableView.reloadData() + case .failure(let error): print("❌ Meal Statistics Error: \(error.localizedDescription)") self.reviewTableView.reloadData() @@ -524,6 +647,7 @@ extension ReviewViewController { } } + /// 리뷰 작성 가능한 메뉴 목록 조회 (VARIABLE 타입) func getValidMenusForReview() { NetworkService.shared.request( ReviewRouter.getValidMenusForReview(menuID), @@ -531,17 +655,22 @@ extension ReviewViewController { useAuth: true ) { [weak self] result in guard let self = self else { return } + switch result { case .success(let data): self.validMenusForReview = data.menuList print("✅ Valid Menus for Review: \(data.menuList.map { $0.name })") + case .failure(let error): print("❌ Valid Menus Error: \(error.localizedDescription)") - break } } } + /// 리뷰 목록 조회 + /// - Parameters: + /// - type: 메뉴 타입 ("FIXED" 또는 "VARIABLE") + /// - menuId: 메뉴/식사 ID func getReviewList(type: String, menuId _: Int) { if type == "FIXED" { getFixedMenuReviewList() @@ -550,42 +679,51 @@ extension ReviewViewController { } } - func getFixedMenuReviewList() { + /// 고정 메뉴 리뷰 목록 조회 + private func getFixedMenuReviewList() { NetworkService.shared.request( ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: 0, size: 20), responseType: NewReviewListResponse.self, useAuth: true ) { [weak self] result in guard let self = self else { return } + switch result { case .success(let data): self.reviewList = data.dataList self.reviewTableView.reloadData() print("✅ Fixed Menu Reviews loaded: \(self.reviewList.count) items") + case .failure(let error): print("❌ Fixed Menu Review List Error: \(error.localizedDescription)") } } } - func getMealReviewList() { + /// 식사 리뷰 목록 조회 + private func getMealReviewList() { NetworkService.shared.request( ReviewRouter.newReviewList(type, menuID, lastReviewId: nil, page: nil, size: 20), responseType: NewReviewListResponse.self, useAuth: true ) { [weak self] result in guard let self = self else { return } + switch result { case .success(let data): self.reviewList = data.dataList self.reviewTableView.reloadData() print("✅ Meal Reviews loaded: \(self.reviewList.count) items") + case .failure(let error): print("❌ Meal Review List Error: \(error.localizedDescription)") } } } + + /// 리뷰 삭제 + /// - Parameter reviewID: 삭제할 리뷰 ID func deleteReview(reviewID: Int) { NetworkService.shared.request( ReviewRouter.deleteReview(reviewID), @@ -597,12 +735,14 @@ extension ReviewViewController { switch result { case .success: print("✅ Review 삭제 성공") + + // 데이터 새로고침 self.getStatistics() if self.type == "VARIABLE" { self.getValidMenusForReview() } self.getReviewList(type: self.type, menuId: self.menuID) - self.showToast(message: "리뷰가 성공적으로 삭제되었습니다.") + self.showToast(message: "리뷰가 성공적으로 삭제되었습니다.") case let .failure(error): print("❌ Delete Review Error: \(error.localizedDescription)") @@ -612,13 +752,18 @@ extension ReviewViewController { } } +// MARK: - ReviewMenuTypeInfoDelegate + extension ReviewViewController: ReviewMenuTypeInfoDelegate { + + /// 메뉴 타입 정보 델리게이트 func didDelegateReviewMenuTypeInfo(for menuTypeData: ReviewMenuTypeInfo) { let reviewMenuTypeInfo = ReviewMenuTypeInfo( menuType: menuTypeData.menuType, menuID: menuTypeData.menuID, changeMenuIDList: menuTypeData.changeMenuIDList ) + type = reviewMenuTypeInfo.menuType menuID = reviewMenuTypeInfo.menuID menuIDList = reviewMenuTypeInfo.changeMenuIDList diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index bcefd14e..4850dae8 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -8,193 +8,80 @@ import UIKit import SnapKit import Moya + import EATSSUDesign final class SetRateViewController: BaseViewController { + // MARK: - Properties override var shouldHideTabBar: Bool { return true } - private var userPickedImage: UIImage? + // Data Model + private var reviewType: ReviewType = .variable + private var mealID: Int? + private var menuID: Int? + private var reviewId: Int? // 수정 시 사용되는 리뷰 ID + // Review Data State private var validMenuIDList: [Int] = [] private var selectedList: [String] = [] - private var reviewId: Int? - private var likedStates: [Bool] = [] - private var menuTableViewHeightConstraint: Constraint? - - private var reviewType: ReviewType = .variable - private var mealID: Int? - private var menuID: Int? + private var userPickedImage: UIImage? - private var isReviewSubmitting = false + // State Flags private var isReviewSubmitted = false enum ReviewType { - case fixed - case variable + case fixed // 단일 메뉴 리뷰 + case variable // 식단 리뷰 (여러 메뉴) } + // MARK: - UI Components + + // Root View + private let setRateView = SetRateView() + private let imagePickerController = UIImagePickerController() + // MARK: - Initializer - convenience init(mealId: Int) { - self.init(nibName: nil, bundle: nil) + init() { + super.init(nibName: nil, bundle: nil) + // 리뷰 수정 모드에서는 이 초기화를 사용하며, reviewType 등은 dataBindForFix에서 설정됩니다. + } + + init(mealId: Int) { + super.init(nibName: nil, bundle: nil) self.mealID = mealId self.reviewType = .variable } - convenience init(menuId: Int) { - self.init(nibName: nil, bundle: nil) + init(menuId: Int) { + super.init(nibName: nil, bundle: nil) self.menuID = menuId self.reviewType = .fixed + self.validMenuIDList = [menuId] } - // MARK: - UI Components - - private var rateView = RateView() - private let imagePickerController = UIImagePickerController() - - private var contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.translatesAutoresizingMaskIntoConstraints = false - return scrollView - }() - - private var menuLabel: UILabel = { - let label = UILabel() - label.text = "오늘의 식사는 어떠셨나요?" - label.font = .subtitle1 - label.textColor = .black - return label - }() - - private var detailLabel: UILabel = { - let label = UILabel() - label.text = "추천하고 싶은 메뉴가 있나요?" - label.font = .subtitle1 - label.textColor = .black - return label - }() - - private let menuTableView: UITableView = { - let tableView = UITableView() - tableView.separatorStyle = .none - tableView.showsVerticalScrollIndicator = false - tableView.isScrollEnabled = false - return tableView - }() - - private let userReviewTextView: UITextView = { - let textView = UITextView() - textView.font = .body1 - textView.layer.cornerRadius = 10.adjusted - textView.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color - textView.layer.borderWidth = 1.adjusted - textView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor - textView.textContainerInset = UIEdgeInsets(top: 16.0.adjusted, left: 16.0.adjusted, bottom: 16.0.adjusted, right: 16.0.adjusted) - textView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" - textView.textColor = .gray500 - return textView - }() - - private lazy var userReviewImageView: UIImageView = { - let imageView = UIImageView() - imageView.layer.cornerRadius = 10 - imageView.clipsToBounds = true - imageView.isUserInteractionEnabled = true - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTappedImageView)) - imageView.addGestureRecognizer(tapGesture) - return imageView - }() - - private lazy var closeButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) - button.tintColor = .lightGray - button.addTarget(self, action: #selector(didTappedImageView), for: .touchUpInside) - button.isHidden = true - return button - }() - - private lazy var selectImageButton: UIButton = { - let button = UIButton() - var config = UIButton.Configuration.plain() - config.image = EATSSUDesignAsset.Images.addImageButton.image - config.contentInsets = NSDirectionalEdgeInsets(top: -5, leading: 0, bottom: 5, trailing: 0) - button.configuration = config - button.addTarget(self, action: #selector(didSelectedImage), for: .touchUpInside) - button.layer.borderWidth = 1 - button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor - button.layer.cornerRadius = 8 - button.clipsToBounds = true - return button - }() - - private let imageCountLabel: UILabel = { - let label = UILabel() - label.text = "사진 0/1" - label.font = .caption3 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color - label.textAlignment = .center - return label - }() - - private let deleteMethodLabel: UILabel = { - let label = UILabel() - label.text = "사진 클릭 시, 삭제됩니다" - label.font = .caption3 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color - return label - }() - - private let maximumWordLabel: UILabel = { - let label = UILabel() - label.text = "0 / 300" - label.font = .caption2 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color - return label - }() - - private let buttonContainer: UIView = { - let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 0 - view.clipsToBounds = true - return view - }() - - private var nextButton: MainButton = { - let button = MainButton() - button.title = "리뷰 남기기" - return button - }() + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: - Life Cycles override func viewDidLoad() { super.viewDidLoad() - setDelegate() - if reviewType == .variable, let mealId = mealID { - fetchValidMenus(mealId: mealId) - } else if reviewType == .fixed { - setupFixedMenuReview() - } else if !selectedList.isEmpty { - likedStates = Array(repeating: false, count: selectedList.count) - menuTableView.reloadData() - } + setDelegates() + setupInitialDataFetch() } override func viewWillAppear(_: Bool) { addKeyboardNotifications() + if navigationController?.isNavigationBarHidden == true { + navigationController?.isNavigationBarHidden = false + } } override func viewWillDisappear(_: Bool) { @@ -203,157 +90,31 @@ final class SetRateViewController: BaseViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - menuTableViewHeightConstraint?.update(offset: menuTableView.contentSize.height) + // 테이블뷰 content size에 따라 높이 제약조건 업데이트 + setRateView.menuTableViewHeightConstraint?.update(offset: setRateView.menuTableView.contentSize.height) } - // MARK: - API Calls - private func fetchValidMenus(mealId: Int) { - NetworkService.shared.request( - ReviewRouter.getValidMenusForReview(mealId), - responseType: ReviewValidMenusResponse.self, - useAuth: true - ) { [weak self] result in - guard let self = self else { return } - - DispatchQueue.main.async { - switch result { - case .success(let data): - self.selectedList = data.menuList.map { $0.name } - self.validMenuIDList = data.menuList.map { $0.menuId } - self.likedStates = Array(repeating: false, count: self.selectedList.count) - self.menuTableView.reloadData() - self.view.setNeedsLayout() - - case .failure(let error): - print("❌ Error fetching valid menus: \(error)") - self.showToast(message: "메뉴 목록 조회에 실패했습니다.") - } - } - } - } - - private func setupFixedMenuReview() { - likedStates = [false] - menuTableView.reloadData() - view.setNeedsLayout() - } - - // MARK: - UI Configuration + // MARK: - Configuration override func configureUI() { dismissKeyboard() - view.addSubviews(scrollView, buttonContainer) - - buttonContainer.addSubview(nextButton) - scrollView.addSubview(contentView) - contentView.addSubviews( - rateView, - menuLabel, - detailLabel, - menuTableView, - userReviewTextView, - maximumWordLabel, - selectImageButton, - imageCountLabel, - userReviewImageView, - closeButton, - deleteMethodLabel - ) + view.addSubview(setRateView) } override func setLayout() { - scrollView.snp.makeConstraints { + setRateView.snp.makeConstraints { $0.edges.equalToSuperview() } - - buttonContainer.snp.makeConstraints { - $0.leading.trailing.equalToSuperview() - $0.top.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-80) - $0.bottom.equalToSuperview() - } - - nextButton.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.top.equalToSuperview().offset(12) - } - - contentView.snp.makeConstraints { make in - make.top.bottom.equalToSuperview() - make.width.equalTo(scrollView) - } - - menuLabel.snp.makeConstraints { make in - make.top.equalToSuperview().inset(20) - make.centerX.equalToSuperview() - } - - rateView.snp.makeConstraints { make in - make.top.equalTo(menuLabel.snp.bottom).offset(17) - make.centerX.equalToSuperview() - make.height.equalTo(36.12) - } - - detailLabel.snp.makeConstraints { make in - make.top.equalTo(rateView.snp.bottom).offset(35) - make.centerX.equalToSuperview() - } - - menuTableView.snp.makeConstraints { - $0.top.equalTo(detailLabel.snp.bottom).offset(20) - $0.leading.equalToSuperview().offset(32) - $0.trailing.equalToSuperview().offset(-32) - menuTableViewHeightConstraint = $0.height.equalTo(0).constraint - } - - userReviewTextView.snp.makeConstraints { make in - make.top.equalTo(menuTableView.snp.bottom).offset(40) - make.leading.trailing.equalToSuperview().inset(16) - make.height.equalTo(181) - } - - maximumWordLabel.snp.makeConstraints { make in - make.top.equalTo(userReviewTextView.snp.bottom).offset(7) - make.trailing.equalTo(userReviewTextView) - } - - selectImageButton.snp.makeConstraints { - $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) - $0.leading.equalToSuperview().offset(16) // 인셋 16으로 통일 - $0.width.height.equalTo(60) - } - - imageCountLabel.snp.makeConstraints { - $0.top.equalTo(selectImageButton.snp.bottom).offset(5) - $0.centerX.equalTo(selectImageButton) - } - - userReviewImageView.snp.makeConstraints { - $0.top.equalTo(maximumWordLabel.snp.bottom).offset(15) - $0.leading.equalTo(selectImageButton.snp.trailing).offset(13) - $0.width.height.equalTo(60) - } - - deleteMethodLabel.snp.makeConstraints { - $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) - $0.leading.equalTo(selectImageButton) - $0.bottom.equalTo(contentView.snp.bottom).offset(-100) - } - - closeButton.snp.makeConstraints { - $0.top.equalTo(userReviewImageView.snp.top).offset(-6) - $0.trailing.equalTo(userReviewImageView.snp.trailing).offset(6) - $0.size.equalTo(24) - } - - for i in 0...4 { - rateView.buttons[i].snp.makeConstraints { make in - make.width.equalTo(29.3) - } - } } override func setButtonEvent() { - nextButton.addTarget(self, action: #selector(tappedNextButton), for: .touchUpInside) + setRateView.nextButton.addTarget(self, action: #selector(tappedNextButton), for: .touchUpInside) + setRateView.selectImageButton.addTarget(self, action: #selector(didSelectedImage), for: .touchUpInside) + setRateView.closeButton.addTarget(self, action: #selector(didTappedImageView), for: .touchUpInside) + + // 이미지 뷰 탭 제스처 + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTappedImageView)) + setRateView.userReviewImageView.addGestureRecognizer(tapGesture) } override func setCustomNavigationBar() { @@ -361,112 +122,210 @@ final class SetRateViewController: BaseViewController { navigationItem.title = reviewId != nil ? "리뷰 수정하기" : "리뷰 남기기" } + // MARK: - Setup & Delegate + + /// 초기 데이터 (유효 메뉴 목록)를 가져오거나 기본 설정을 합니다. + private func setupInitialDataFetch() { + if reviewId == nil { + if reviewType == .variable, let mealId = mealID { + fetchValidMenus(mealId: mealId) + } else if reviewType == .fixed { + // Fixed 메뉴는 초기 좋아요 상태만 설정 (메뉴명은 DataBind에서 처리) + likedStates = [false] + setRateView.menuTableView.reloadData() + } + } + } + + /// Delegate 및 DataSource를 설정합니다. + private func setDelegates() { + setRateView.menuTableView.register(MenuLikeCell.self, forCellReuseIdentifier: MenuLikeCell.identifier) + setRateView.menuTableView.dataSource = self + setRateView.menuTableView.delegate = self + + imagePickerController.delegate = self + imagePickerController.sourceType = .photoLibrary + imagePickerController.allowsEditing = false + setRateView.userReviewTextView.delegate = self + + self.navigationController?.delegate = self + self.navigationController?.interactivePopGestureRecognizer?.delegate = self + } + // MARK: - Data Binding + /// 일반 리뷰 작성 시 메뉴 목록 데이터를 바인딩합니다. func dataBind(list: [String], idList: [Int]) { - selectedList = list - validMenuIDList = idList - likedStates = Array(repeating: false, count: list.count) + self.selectedList = list + self.validMenuIDList = idList + self.likedStates = Array(repeating: false, count: list.count) if idList.count == 1 { - reviewType = .fixed - menuID = idList.first + self.reviewType = .fixed + self.menuID = idList.first } else { - reviewType = .variable + self.reviewType = .variable } - menuTableView.reloadData() + setRateView.menuTableView.reloadData() } + /// 리뷰 수정 (Fixed 타입) 시 데이터를 바인딩합니다. (사용되지 않으나 원본 유지) func dataBindForFix(menuNames: [String], menuIds: [Int], likedStates: [Bool]) { self.selectedList = menuNames self.validMenuIDList = menuIds self.likedStates = likedStates self.reviewType = .fixed - menuLabel.text = "\(menuNames.first ?? "") 을/를 추천하시겠어요?" - menuTableView.reloadData() + setRateView.menuLabel.text = "\(menuNames.first ?? "") 을/를 추천하시겠어요?" + setRateView.menuTableView.reloadData() view.setNeedsLayout() } + /// 리뷰 수정 모드 시작 시 설정합니다. (리뷰 ID 바인딩) func dataBindForFix(list: [String], reviewId: Int) { self.selectedList = list self.reviewId = reviewId self.likedStates = Array(repeating: false, count: list.count) - menuLabel.text = "\(list[0]) 을/를 추천하시겠어요?" - selectImageButton.isHidden = true - deleteMethodLabel.isHidden = true - nextButton.setTitle("리뷰 수정 완료하기", for: .normal) + setRateView.menuLabel.text = "\(list[0]) 을/를 추천하시겠어요?" + setRateView.selectImageButton.isHidden = true + setRateView.deleteMethodLabel.isHidden = true + setRateView.nextButton.setTitle("리뷰 수정 완료하기", for: .normal) } + /// 수정할 리뷰의 기존 내용을 화면에 표시합니다. func settingForReviewFix(data: ReviewListItem) { - rateView.currentStar = Int(data.rating) - rateView.settingStarForFix(currentStar: Int(data.rating)) - userReviewTextView.text = data.content ?? "" - userReviewTextView.textColor = .black - maximumWordLabel.text = "\(data.content?.count ?? 0) / 300" + // 별점 설정 + setRateView.rateView.currentStar = Int(data.rating) + setRateView.rateView.settingStarForFix(currentStar: Int(data.rating)) + + // 리뷰 텍스트 설정 + setRateView.userReviewTextView.text = data.content ?? "" + setRateView.userReviewTextView.textColor = .black + setRateView.maximumWordLabel.text = "\(data.content?.count ?? 0) / 300" + // 이미지 설정 (kfSetImage는 Kingfisher 확장 가정) if let imageUrl = data.imageUrls?.first, !imageUrl.isEmpty { - userReviewImageView.kfSetImage(url: imageUrl) - imageCountLabel.text = "사진 1/1" - closeButton.isHidden = false + setRateView.userReviewImageView.kfSetImage(url: imageUrl) + setRateView.updateImageViewState(image: setRateView.userReviewImageView.image, count: 1, isHidden: false) + } else { + setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) } - } - - func setDelegate() { - menuTableView.register(MenuLikeCell.self, forCellReuseIdentifier: MenuLikeCell.identifier) - menuTableView.dataSource = self - menuTableView.delegate = self - - imagePickerController.delegate = self - imagePickerController.sourceType = .photoLibrary - imagePickerController.allowsEditing = false - userReviewTextView.delegate = self - self.navigationController?.delegate = self - self.navigationController?.interactivePopGestureRecognizer?.delegate = self + // 좋아요 상태 복원 + if let menuLikes = data.menu { + self.likedStates = validMenuIDList.map { menuId in + return menuLikes.first(where: { $0.menuId == menuId })?.isLike ?? false + } + } + setRateView.menuTableView.reloadData() } + // MARK: - Menu Like Logic + + /// 리뷰 좋아요/취소 상태를 토글합니다. private func toggleLike(for index: Int) { likedStates[index].toggle() let idx = IndexPath(row: index, section: 0) - if let cell = menuTableView.cellForRow(at: idx) as? MenuLikeCell { + if let cell = setRateView.menuTableView.cellForRow(at: idx) as? MenuLikeCell { cell.dataBind(menu: selectedList[index], isLiked: likedStates[index]) } else { - menuTableView.reloadRows(at: [idx], with: .none) + setRateView.menuTableView.reloadRows(at: [idx], with: .none) + } + } + + // MARK: - Image Handling Actions + + /// 이미지 선택 버튼 탭 시 ImagePicker를 표시합니다. + @objc func didSelectedImage() { + let originalDelegate = self.navigationController?.delegate + self.navigationController?.delegate = nil + + present(imagePickerController, animated: true) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self?.navigationController?.delegate = originalDelegate + } } } - // MARK: - Actions + /// 이미지 뷰 탭 또는 삭제 버튼 탭 시 이미지를 삭제합니다. + @objc func didTappedImageView() { + userPickedImage = nil + setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) + } + + // MARK: - Review Submission Logic + /// 리뷰 작성/수정 버튼 탭 시 호출됩니다. @objc func tappedNextButton() { - // 유효성 검증 - let reviewText = userReviewTextView.text ?? "" - if reviewText == "메뉴에 대한 상세한 리뷰를 작성해주세요" || reviewText.count < 3 { + + // 1. 유효성 검증 + if setRateView.userReviewTextView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" || (setRateView.userReviewTextView.text ?? "").count < 3 { showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) return } - guard rateView.currentStar != 0 else { + guard setRateView.rateView.currentStar != 0 else { showToast(message: "별점을 입력해주세요!", type: .info) return } - isReviewSubmitting = true - + // 2. 리뷰 전송 분기 if reviewId != nil { sendFixReview() - return + } else { + switch reviewType { + case .variable: + sendMealReview() + case .fixed: + sendMenuReview() + } } - - switch reviewType { - case .variable: - sendMealReview() - case .fixed: - sendMenuReview() + } + + /// 리뷰 작성/수정 완료 후 ReviewViewController로 돌아갑니다. + private func moveToReviewVC() { + if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) { + navigationController?.popToViewController(reviewVC, animated: true) + + if let homeVC = navigationController?.viewControllers.first as? HomeViewController { + homeVC.refreshAfterReview() + } + } + } +} + +// MARK: - API Call & Logic + +extension SetRateViewController { + + /// Meal(Variable) 리뷰 작성을 위한 유효 메뉴 목록을 요청합니다. + private func fetchValidMenus(mealId: Int) { + NetworkService.shared.request( + ReviewRouter.getValidMenusForReview(mealId), + responseType: ReviewValidMenusResponse.self, + useAuth: true + ) { [weak self] result in + guard let self = self else { return } + + DispatchQueue.main.async { + switch result { + case .success(let data): + self.selectedList = data.menuList.map { $0.name } + self.validMenuIDList = data.menuList.map { $0.menuId } + self.likedStates = Array(repeating: false, count: self.selectedList.count) + + self.setRateView.menuTableView.reloadData() + self.view.setNeedsLayout() + + case .failure(let error): + print("❌ Error fetching valid menus: \(error)") + self.showToast(message: "메뉴 목록 조회에 실패했습니다.") + } + } } } @@ -483,9 +342,9 @@ final class SetRateViewController: BaseViewController { } let request = FixedReviewRequestDTO( - rating: rateView.currentStar, + rating: setRateView.rateView.currentStar, menuLikes: menuLikes, - content: userReviewTextView.text + content: setRateView.userReviewTextView.text ) try await postFixReview(reviewId: reviewId, request: request) @@ -493,6 +352,7 @@ final class SetRateViewController: BaseViewController { await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() + self.showToast(message: "리뷰가 성공적으로 수정되었습니다.") } } catch { @@ -522,9 +382,9 @@ final class SetRateViewController: BaseViewController { let request = WriteReviewMealRequest( mealId: mealId, - rating: rateView.currentStar, + rating: setRateView.rateView.currentStar, menuLikes: menuLikes, - content: userReviewTextView.text, + content: setRateView.userReviewTextView.text, imageUrls: imageUrl != nil ? [imageUrl!] : nil ) try await postMealReview(request: request) @@ -532,6 +392,7 @@ final class SetRateViewController: BaseViewController { await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() + self.showToast(message: "리뷰가 성공적으로 작성되었습니다.") } } catch { @@ -562,9 +423,9 @@ final class SetRateViewController: BaseViewController { ) let request = WriteReviewMenuRequest( - rating: rateView.currentStar, + rating: setRateView.rateView.currentStar, menuLike: menuLike, - content: userReviewTextView.text, + content: setRateView.userReviewTextView.text, imageUrls: imageUrl != nil ? [imageUrl!] : nil ) @@ -573,6 +434,7 @@ final class SetRateViewController: BaseViewController { await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() + self.showToast(message: "리뷰가 성공적으로 작성되었습니다.") } } catch { @@ -584,38 +446,7 @@ final class SetRateViewController: BaseViewController { } } - private func moveToReviewVC() { - if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) { - navigationController?.popToViewController(reviewVC, animated: true) - - if let homeVC = navigationController?.viewControllers.first as? HomeViewController { - homeVC.refreshAfterReview() - } - } - } - - @objc func didSelectedImage() { - let originalDelegate = self.navigationController?.delegate - self.navigationController?.delegate = nil - - present(imagePickerController, animated: true) { [weak self] in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self?.navigationController?.delegate = originalDelegate - } - } - } - - @objc func didTappedImageView() { - userReviewImageView.image = nil - userPickedImage = nil - imageCountLabel.text = "사진 0/1" - closeButton.isHidden = true - } -} - -// MARK: - Network - -extension SetRateViewController { + // MARK: - Network Utility Methods (Private) private func postMenuReview(request: WriteReviewMenuRequest) async throws { try await withCheckedThrowingContinuation { continuation in @@ -626,10 +457,8 @@ extension SetRateViewController { ) { result in switch result { case .success: - print("✅ Menu Review 작성 성공") continuation.resume() case .failure(let error): - print("❌ Menu Review 작성 실패: \(error)") continuation.resume(throwing: error) } } @@ -645,10 +474,8 @@ extension SetRateViewController { ) { result in switch result { case .success: - print("✅ Meal Review 작성 성공") continuation.resume() case .failure(let error): - print("❌ Meal Review 작성 실패: \(error)") continuation.resume(throwing: error) } } @@ -676,15 +503,13 @@ extension SetRateViewController { try await withCheckedThrowingContinuation { continuation in NetworkService.shared.request( WriteReviewRouter.fixReview(reviewId: reviewId, param: request), - responseType: Bool.self, // 수정 성공 시 Bool (또는 BaseResponse의 result가 nil인 경우) + responseType: Bool.self, useAuth: true ) { result in switch result { case .success: - print("✅ Review 수정 성공") continuation.resume() case .failure(let error): - print("❌ Review 수정 실패: \(error)") continuation.resume(throwing: error) } } @@ -692,99 +517,60 @@ extension SetRateViewController { } } -// MARK: - UIImagePickerControllerDelegate +// MARK: - UITableViewDataSource & Delegate -extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - if let image = info[.originalImage] as? UIImage { - userReviewImageView.image = image - userPickedImage = image - imageCountLabel.text = "사진 1/1" - closeButton.isHidden = false - } - picker.dismiss(animated: true) +extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return selectedList.count } - func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - if isReviewSubmitted { - return - } - - if navigationController is UIImagePickerController { - return + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: MenuLikeCell.identifier, for: indexPath) as? MenuLikeCell else { + return UITableViewCell() } - let isPopping = !navigationController.viewControllers.contains(self) + cell.dataBind(menu: selectedList[indexPath.row], isLiked: likedStates[indexPath.row]) - if isPopping { - let textHasContent = userReviewTextView.text != "메뉴에 대한 상세한 리뷰를 작성해주세요" && !userReviewTextView.text.isEmpty - let isReviewStarted: Bool = rateView.currentStar > 0 || textHasContent - - if reviewId == nil, isReviewStarted { - navigationController.viewControllers.append(self) - - let title = "작성 취소" - let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" - let confirmButtonTitle = "나가기" - let cancelButtonTitle = "계속 작성" - - showCustomDialog( - title: title, - message: message, - cancelButtonTitle: cancelButtonTitle, - confirmButtonTitle: confirmButtonTitle - ) { [weak self] in - guard let self = self else { return } - self.navigationController?.delegate = nil - self.navigationController?.popViewController(animated: true) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.navigationController?.delegate = self - } - } - } + // Controller가 Cell의 좋아요 탭 이벤트를 처리합니다. + cell.onLikeTapped = { [weak self] in + guard let self else { return } + self.toggleLike(for: indexPath.row) } + + return cell } - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let isReviewStarted: Bool = rateView.currentStar > 0 || !userReviewTextView.text.isEmpty - if reviewId == nil, isReviewStarted { - - let title = "작성 취소" - let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" - let confirmButtonTitle = "나가기" - let cancelButtonTitle = "계속 작성" - - showCustomDialog( - title: title, - message: message, - cancelButtonTitle: cancelButtonTitle, - confirmButtonTitle: confirmButtonTitle - ) { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - return false - } - return true + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + toggleLike(for: indexPath.row) } } // MARK: - UITextViewDelegate extension SetRateViewController: UITextViewDelegate { - func textView(_: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - let currentText = userReviewTextView.text ?? "" + + // 플레이스홀더 텍스트 + private var placeholderText: String { + return "메뉴에 대한 상세한 리뷰를 작성해주세요" + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + let currentText = textView.text ?? "" guard let stringRange = Range(range, in: currentText) else { return false } - let newLength = currentText.count + text.count - range.length - if newLength > 300 { return false } + let finalLength = currentText.count + text.count - range.length + + if finalLength > 300 { return false } let textToDisplay = currentText.replacingCharacters(in: stringRange, with: text) - maximumWordLabel.text = "\(textToDisplay.count) / 300" + setRateView.maximumWordLabel.text = "\(textToDisplay.count) / 300" + return true } func textViewDidBeginEditing(_ textView: UITextView) { - if textView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" { + if textView.text == placeholderText { textView.text = "" textView.textColor = .black } @@ -792,28 +578,89 @@ extension SetRateViewController: UITextViewDelegate { func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { - textView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" - textView.textColor = .gray500 - maximumWordLabel.text = "0 / 300" + setRateView.setInitialTextViewState() } else { - maximumWordLabel.text = "\(textView.text.count) / 300" + setRateView.maximumWordLabel.text = "\(textView.text.count) / 300" + } + } +} + +// MARK: - ImagePicker & Navigation Delegate + +extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let image = info[.originalImage] as? UIImage { + userPickedImage = image + setRateView.updateImageViewState(image: image, count: 1, isHidden: false) + } + picker.dismiss(animated: true) + } + + // 뒤로가기 제스처 및 버튼 탭 시 리뷰 작성 여부 확인 로직 + private func checkReviewStatusAndConfirmExit() -> Bool { + let textHasContent = setRateView.userReviewTextView.text != placeholderText && !(setRateView.userReviewTextView.text ?? "").isEmpty + let isReviewStarted: Bool = setRateView.rateView.currentStar > 0 || textHasContent + + if reviewId == nil, isReviewStarted { + + let title = "작성 취소" + let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" + let confirmButtonTitle = "나가기" + let cancelButtonTitle = "계속 작성" + + self.showCustomDialog( + title: title, + message: message, + cancelButtonTitle: cancelButtonTitle, + confirmButtonTitle: confirmButtonTitle + ) { [weak self] in + // 다이얼로그에서 '나가기' 선택 시 실제로 pop + self?.navigationController?.popViewController(animated: true) + } + return false // Pop 방지 + } + return true // Pop 허용 + } + + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + if isReviewSubmitted { return } + if navigationController is UIImagePickerController { return } + + let isPopping = !navigationController.viewControllers.contains(self) + if isPopping { + navigationController.viewControllers.append(self) + + if checkReviewStatusAndConfirmExit() { + navigationController.delegate = nil + navigationController.popViewController(animated: true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.navigationController?.delegate = self + } + } } } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return checkReviewStatusAndConfirmExit() + } } // MARK: - Keyboard Handling extension SetRateViewController { + // 키보드 등장 시 View를 위로 올립니다. @objc func keyboardWillShow(_ noti: NSNotification) { if let keyboardFrame: NSValue = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardRectangle = keyboardFrame.cgRectValue UIView.animate(withDuration: 0.3) { - self.view.transform = CGAffineTransform(translationX: 0, y: -keyboardRectangle.height) + self.view.transform = CGAffineTransform(translationX: 0, y: -keyboardRectangle.height + self.view.safeAreaInsets.bottom) self.navigationController?.isNavigationBarHidden = true } } } + // 키보드 사라질 때 View를 원래 위치로 되돌립니다. @objc func keyboardWillHide(_: NSNotification) { view.transform = .identity navigationController?.isNavigationBarHidden = false @@ -829,30 +676,3 @@ extension SetRateViewController { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) } } - -// MARK: - UITableViewDataSource & Delegate - -extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - selectedList.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: MenuLikeCell.identifier, for: indexPath) as? MenuLikeCell else { - return UITableViewCell() - } - - cell.dataBind(menu: selectedList[indexPath.row], isLiked: likedStates[indexPath.row]) - cell.onLikeTapped = { [weak self, weak cell, weak tableView] in - guard let self, let tableView, let cell, let idx = tableView.indexPath(for: cell) else { return } - self.toggleLike(for: idx.row) - } - - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: false) - toggleLike(for: indexPath.row) - } -} From 044c1facb2c1a20e4c8110e960dfcf54425ae947 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 29 Nov 2025 18:36:51 +0900 Subject: [PATCH 24/69] =?UTF-8?q?[#321]=20=EB=82=B4=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=8F=84=20V2=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/MyReviewResponseDTO.swift | 24 +++++++------- .../Data/Network/Router/MyRouter.swift | 33 ++++++++++++++----- .../Data/Network/Router/ReviewRouter.swift | 2 +- .../MyReviewViewController.swift | 20 +++++++---- .../View/SeeReview/ReviewTableCell.swift | 25 +++++++------- 5 files changed, 63 insertions(+), 41 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift index 91d17aeb..5a3b176b 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift @@ -16,26 +16,26 @@ struct MyReviewResponseDTO: Codable { /// 리뷰 리스트 데이터 컨테이너 struct MyReviewList: Codable { - let numberOfElements: Int // 총 요소 개수 - let hasNext: Bool // 다음 페이지 존재 여부 - let dataList: [MyReviewListItem] // 리뷰 목록 + let numberOfElements: Int + let hasNext: Bool + let dataList: [MyReviewListItem] } // MARK: - Review Item DTO /// 개별 리뷰 아이템 구조 struct MyReviewListItem: Codable { - let reviewId: Int // 리뷰 ID - let rating: Double // 별점 (4) - let writtenAt: String // 작성일 ("2023-04-07") - let content: String? // 리뷰 내용 ("맛있당") - let imageUrls: [String]? // 이미지 URL 리스트 ("imgurl1", "imgurl2") - let menuList: [ReviewMenu] // 리뷰에 포함된 메뉴 리스트 + let reviewId: Int + let rating: Double? + let writtenAt: String + let content: String? + let imageUrls: [String]? + let menuList: [ReviewMenu] } /// 리뷰에 포함된 개별 메뉴 구조 struct ReviewMenu: Codable { - let menuId: Int // 메뉴 ID (3143) - let name: String // 메뉴 이름 ("생고기제육볶음") - let isLike: Bool // 좋아요 여부 (true) + let id: Int + let name: String + let isLike: Bool } diff --git a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift index 646176a4..1acc3b9b 100644 --- a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift @@ -10,7 +10,10 @@ import UIKit import Moya enum MyRouter { - case myReview + case getMyReviewList(lastReviewId: Int?, + page: Int? = 0, + size: Int? = 20, + sort: String? = "date,DESC") case myInfo case signOut case inquiry(param: InquiryRequest) @@ -27,8 +30,8 @@ extension MyRouter: TargetType { var path: String { switch self { - case .myReview: - "/users/reviews" + case .getMyReviewList: + "users/v2/reviews" case .myInfo: "/users/mypage" case .signOut: @@ -48,7 +51,7 @@ extension MyRouter: TargetType { var method: Moya.Method { switch self { - case .myReview, .departments, .colleges: + case .getMyReviewList, .departments, .colleges: .get case .myInfo, .getDepartment, .getMyPartnerships: .get @@ -61,11 +64,23 @@ extension MyRouter: TargetType { var task: Moya.Task { switch self { - case .myReview: - .requestParameters(parameters: ["page": 0, - "size": 20, - "sort": "date,DESC"], - encoding: URLEncoding.queryString) + case let .getMyReviewList(lastReviewId, page, size, sort): + .requestParameters( + parameters: { + var dict: [String: Any] = [:] + + if let lastId = lastReviewId { + dict["lastReviewId"] = lastId + } else { + dict["page"] = page ?? 0 + } + + dict["size"] = size ?? 20 + dict["sort"] = sort ?? "date,DESC" + return dict + }(), + encoding: URLEncoding.queryString + ) case .myInfo: .requestPlain case .signOut: diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 1657a1c6..5e90d801 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -24,7 +24,7 @@ enum ReviewRouter { case getMyReviewList(lastReviewId: Int?, page: Int? = 0, size: Int? = 20, - sort: String? = "date,DESC") // 기본값: date,DESC + sort: String? = "date,DESC") } extension ReviewRouter: TargetType { diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index 423af805..cacdda97 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -15,7 +15,8 @@ final class MyReviewViewController: BaseViewController { override var shouldHideTabBar: Bool { true } // MARK: - Properties - private var reviewList = [MyDataList]() + // DTO 변경에 따라 타입 수정: MyDataList -> MyReviewListItem + private var reviewList = [MyReviewListItem]() var nickname: String = .init() private var menuName: String = .init() @@ -143,10 +144,17 @@ extension MyReviewViewController: UITableViewDataSource { } let cell = tableView.dequeueReusableCell(withIdentifier: ReviewTableCell.identifier, for: indexPath) as? ReviewTableCell ?? ReviewTableCell() - cell.myPageDataBind(response: reviewList[indexPath.row], nickname: nickname) + + // DTO에 맞게 데이터 바인딩 로직 수정 필요 (ReviewTableCell의 myPageDataBind 함수도 수정되었다고 가정) + let reviewItem = reviewList[indexPath.row] + cell.myPageDataBind(response: reviewItem, nickname: nickname) + cell.handler = { [weak self] in guard let self else { return } - menuName = reviewList[indexPath.row].menuName + + // DTO 구조에 맞게 메뉴 이름을 reviewItem.menuList에서 가져옴 + let menuName = reviewItem.menuList.first?.name ?? "알 수 없는 메뉴" + showFixOrDeleteAlert(reviewID: cell.reviewId, menuName: menuName) } @@ -160,15 +168,15 @@ extension MyReviewViewController: UITableViewDataSource { extension MyReviewViewController { private func getMyReview() { NetworkService.shared.request( - MyRouter.myReview, - responseType: MyReviewResponse.self, + MyRouter.getMyReviewList(lastReviewId: nil, page: 0, size: 20), + responseType: MyReviewResponseDTO.self, useAuth: true ) { [weak self] result in guard let self = self else { return } switch result { case .success(let response): - self.reviewList = response.dataList + self.reviewList = response.result.dataList self.myReviewView.myReviewTableView.reloadData() case .failure(let error): diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 11accebc..bac105cf 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -306,31 +306,30 @@ final class ReviewTableCell: UITableViewCell { /// 마이페이지용 리뷰 데이터 바인딩 /// - Parameters: - /// - response: 마이페이지 데이터 리스트 + /// - response: MyReviewListItem DTO /// - nickname: 사용자 닉네임 - func myPageDataBind(response: MyDataList, nickname: String) { + func myPageDataBind(response: MyReviewListItem, nickname: String) { // 인자 타입 변경 (MyDataList -> MyReviewListItem) userNameLabel.text = "\(nickname)" - totalRateView.setRating(response.mainRating) - dateLabel.text = response.writeDate + totalRateView.setRating(Int(response.rating ?? 0)) + dateLabel.text = response.writtenAt + reviewTextView.text = response.content - // 이미지 설정 - if response.imgURLList.count != 0 { - if response.imgURLList[0] != "" { - foodImageView.isHidden = false - foodImageView.kfSetImage(url: response.imgURLList[0]) - } + if let imageUrls = response.imageUrls, + let firstImageUrl = imageUrls.first(where: { !$0.isEmpty }) { + + foodImageView.isHidden = false + foodImageView.kfSetImage(url: firstImageUrl) + } else { foodImageView.isHidden = true } - // 더보기 버튼 설정 sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.setTitle("", for: .normal) - reviewId = response.reviewID - // 마이페이지에서는 태그 숨김 + reviewId = response.reviewId tags = [] tagCollectionView.isHidden = true } From 2921c07b6d501e58b543a839edc4f32b51b1aff0 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 29 Nov 2025 18:53:34 +0900 Subject: [PATCH 25/69] =?UTF-8?q?[#321]=20=ED=83=9C=EA=B7=B8=20=EC=99=BC?= =?UTF-8?q?=EC=AA=BD=20=EC=A0=95=EB=A0=AC=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewTableCell.swift | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index bac105cf..d5179207 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -121,7 +121,7 @@ final class ReviewTableCell: UITableViewCell { /// 메뉴 태그 컬렉션뷰 private lazy var tagCollectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() + let layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize layout.minimumInteritemSpacing = 8 @@ -361,3 +361,26 @@ extension ReviewTableCell: UICollectionViewDataSource { return cell } } + +class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var maxY: CGFloat = -1.0 + + attributes?.forEach { layoutAttribute in + if layoutAttribute.representedElementCategory == .cell { + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + + layoutAttribute.frame.origin.x = leftMargin + + maxY = max(maxY, layoutAttribute.frame.maxY) + leftMargin = layoutAttribute.frame.maxX + minimumInteritemSpacing + } + } + return attributes + } +} From 19d0ac310fb9c8c7af704314f6dbbc9f81c9813a Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 29 Nov 2025 19:17:10 +0900 Subject: [PATCH 26/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=A4=91=EA=B0=84=EC=97=90=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../SetRateViewController.swift | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 4850dae8..84dbaa36 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -2,7 +2,7 @@ // SetRateViewController.swift // EatSSU-iOS // -// Created by 박윤빈 on 2023/03/23. +// Created by 한금준 on 29/11/25. // import UIKit @@ -597,13 +597,11 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo picker.dismiss(animated: true) } - // 뒤로가기 제스처 및 버튼 탭 시 리뷰 작성 여부 확인 로직 - private func checkReviewStatusAndConfirmExit() -> Bool { + private func checkReviewStatusAndConfirmExit(completion: @escaping (Bool) -> Void) { let textHasContent = setRateView.userReviewTextView.text != placeholderText && !(setRateView.userReviewTextView.text ?? "").isEmpty let isReviewStarted: Bool = setRateView.rateView.currentStar > 0 || textHasContent if reviewId == nil, isReviewStarted { - let title = "작성 취소" let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" let confirmButtonTitle = "나가기" @@ -614,13 +612,13 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo message: message, cancelButtonTitle: cancelButtonTitle, confirmButtonTitle: confirmButtonTitle - ) { [weak self] in - // 다이얼로그에서 '나가기' 선택 시 실제로 pop - self?.navigationController?.popViewController(animated: true) + ) { + completion(true) } - return false // Pop 방지 + completion(false) + } else { + completion(true) } - return true // Pop 허용 } func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { @@ -628,21 +626,34 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo if navigationController is UIImagePickerController { return } let isPopping = !navigationController.viewControllers.contains(self) + if isPopping { - navigationController.viewControllers.append(self) + navigationController.delegate = nil + + var viewControllers = navigationController.viewControllers + viewControllers.append(self) + navigationController.setViewControllers(viewControllers, animated: false) - if checkReviewStatusAndConfirmExit() { - navigationController.delegate = nil - navigationController.popViewController(animated: true) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.navigationController?.delegate = self + checkReviewStatusAndConfirmExit { [weak self] shouldPop in + guard let self = self else { return } + + if shouldPop { + var controllers = navigationController.viewControllers + if let index = controllers.firstIndex(of: self) { + controllers.remove(at: index) + navigationController.setViewControllers(controllers, animated: true) + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + navigationController.delegate = self } } } } func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - return checkReviewStatusAndConfirmExit() + return true } } From e3079f8401e060462aa9bdcc8f7265eddd16b384 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 29 Nov 2025 19:30:40 +0900 Subject: [PATCH 27/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20refresh=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../ViewController/ReviewViewController.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 0d2aab98..9e612de9 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -60,9 +60,6 @@ final class ReviewViewController: BaseViewController { // MARK: - UI Components - /// 당겨서 새로고침 컨트롤 - let refreshControl = UIRefreshControl() - /// 리뷰 목록 테이블뷰 let reviewTableView: UITableView = { let tableView = UITableView() @@ -109,7 +106,6 @@ final class ReviewViewController: BaseViewController { super.viewDidLoad() setTableView() - initRefresh() setFirebaseTask() } @@ -221,16 +217,6 @@ final class ReviewViewController: BaseViewController { reviewTableView.dataSource = self } - /// 당겨서 새로고침 초기화 - private func initRefresh() { - refreshControl.addTarget( - self, - action: #selector(refreshTable(refresh:)), - for: .valueChanged - ) - reviewTableView.refreshControl = refreshControl - } - // MARK: - Actions /// 리뷰 작성 버튼 탭 처리 From 0516728fa15c0f373552b9f47f05936fa6cc2454 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 18:47:37 +0900 Subject: [PATCH 28/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EB=86=92=EC=9D=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/SeeReview/ReviewDividerCell.swift | 2 +- .../Presentation/Review/View/SeeReview/ReviewTableCell.swift | 5 +++-- .../Review/ViewController/ReviewViewController.swift | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift index 5bbc6e80..5bbb186d 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewDividerCell.swift @@ -67,7 +67,7 @@ final class ReviewDividerCell: UITableViewCell { label.snp.makeConstraints { $0.top.equalTo(divider.snp.bottom).offset(16) $0.leading.equalToSuperview().offset(16) - $0.bottom.equalToSuperview().inset(8) + $0.bottom.equalToSuperview() } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index d5179207..acd728ec 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -153,8 +153,9 @@ final class ReviewTableCell: UITableViewCell { /// 음식 이미지뷰 var foodImageView: UIImageView = { let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit + imageView.contentMode = .scaleAspectFill imageView.isHidden = true + imageView.clipsToBounds = true return imageView }() @@ -241,7 +242,7 @@ final class ReviewTableCell: UITableViewCell { contentStackView.snp.makeConstraints { make in make.top.equalTo(profileStackView.snp.bottom) make.leading.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-15) + make.bottom.equalToSuperview().offset(-7) make.trailing.equalToSuperview().offset(-16) } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 9e612de9..b33b8d6f 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -392,7 +392,7 @@ extension ReviewViewController: UITableViewDelegate { case 1: return 6 case 2: - return 8 + return 0 default: return 0 } From 49976d3c61cd523bc727abb4610b34628acf3083 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 18:54:38 +0900 Subject: [PATCH 29/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20null=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Data/Network/DTO/Review/NewReviewListResponse.swift | 2 +- .../App/Sources/Data/Network/Service/NetworkService.swift | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift index f5b39119..9add8337 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -15,7 +15,7 @@ struct NewReviewListResponse: Codable { struct ReviewListItem: Codable { let reviewId: Int var menu: [ReviewMenuInfo]? - let writerId: Int + let writerId: Int? let isWriter: Bool let writerNickname: String let rating: Double diff --git a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift index bcd4e636..ae863ee0 100644 --- a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift +++ b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift @@ -48,6 +48,14 @@ final class NetworkService { switch result { case .success(let response): do { + // 🔥 Raw JSON 출력 (디버깅용) + print("------ Raw JSON ------") + if let raw = String(data: response.data, encoding: .utf8) { + print(raw) + } else { + print("❌ Raw JSON 출력 실패: 인코딩 불가") + } + print("-----------------------") let baseResponse = try response.map(BaseResponse.self) if baseResponse.isSuccess { From 1dc5a6b7938f447eb65986afaec3a6a0629d7aab Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 19:42:04 +0900 Subject: [PATCH 30/69] =?UTF-8?q?[#321]=20=ED=95=98=EB=8B=A8=20=ED=83=AD?= =?UTF-8?q?=EB=B0=94=20=EC=88=A8=EA=B9=80=EC=B2=98=EB=A6=AC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../ViewController/HomeViewController.swift | 2 + .../Review/View/RateReview/SetRateView.swift | 15 ++- .../ViewController/ReviewViewController.swift | 16 +-- .../SetRateViewController.swift | 49 ++++++-- .../CustomTabBarContainerController.swift | 119 +++++++----------- 5 files changed, 92 insertions(+), 109 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index c60c9467..305fec3f 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -59,6 +59,8 @@ final class HomeViewController: BaseViewController { navigationController?.setNavigationBarHidden(true, animated: animated) addNewDayObserver() + + self.tabBarController?.tabBar.isHidden = false } override func viewWillDisappear(_ animated: Bool) { diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift index cd448126..c64f99db 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift @@ -19,7 +19,7 @@ final class SetRateView: UIView { // MARK: - UI Components - // ⭐️ Scroll View Container + // Scroll View Container let scrollView: UIScrollView = { let scrollView = UIScrollView() return scrollView @@ -27,7 +27,7 @@ final class SetRateView: UIView { let contentView: UIView = UIView() - // ⭐️ Review Rate Section + // Review Rate Section let menuLabel: UILabel = { let label = UILabel() label.text = "오늘의 식사는 어떠셨나요?" @@ -38,7 +38,7 @@ final class SetRateView: UIView { let rateView = RateView() - // ⭐️ Menu Like Section + // Menu Like Section let detailLabel: UILabel = { let label = UILabel() label.text = "추천하고 싶은 메뉴가 있나요?" @@ -56,7 +56,7 @@ final class SetRateView: UIView { return tableView }() - // ⭐️ Review Text Section + // Review Text Section let userReviewTextView: UITextView = { let textView = UITextView() textView.font = .body1 @@ -76,7 +76,7 @@ final class SetRateView: UIView { return label }() - // ⭐️ Image Section + // Image Section let selectImageButton: UIButton = { let button = UIButton() var config = UIButton.Configuration.plain() @@ -125,7 +125,7 @@ final class SetRateView: UIView { return label }() - // ⭐️ Bottom Button Section + // Bottom Button Section let buttonContainer: UIView = { let view = UIView() view.backgroundColor = .white @@ -259,8 +259,7 @@ final class SetRateView: UIView { deleteMethodLabel.snp.makeConstraints { $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) $0.leading.equalTo(selectImageButton) - // ContentView의 bottom에 연결하여 스크롤 가능하게 함 - $0.bottom.equalTo(contentView.snp.bottom).offset(-100) + $0.bottom.equalTo(contentView.snp.bottom).offset(-50) } closeButton.snp.makeConstraints { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index b33b8d6f..75d81157 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -16,10 +16,7 @@ import Moya final class ReviewViewController: BaseViewController { // MARK: - Properties - - override var shouldHideTabBar: Bool { - return true - } + override var shouldHideTabBar: Bool { true } // MARK: - Network @@ -123,17 +120,6 @@ final class ReviewViewController: BaseViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - // 뒤로 가기 시 탭바 표시 - if self.isMovingFromParent { - var parentVC = self.parent - while parentVC != nil { - if let customTabBar = parentVC as? CustomTabBarContainerController { - customTabBar.setTabBarHidden(false, animated: false) - break - } - parentVC = parentVC?.parent - } - } } // MARK: - UI Configuration diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 84dbaa36..5a193c2d 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -14,9 +14,7 @@ import EATSSUDesign final class SetRateViewController: BaseViewController { // MARK: - Properties - override var shouldHideTabBar: Bool { - return true - } + override var shouldHideTabBar: Bool { true } // Data Model private var reviewType: ReviewType = .variable @@ -33,6 +31,8 @@ final class SetRateViewController: BaseViewController { // State Flags private var isReviewSubmitted = false + private weak var originalNavigationDelegate: UINavigationControllerDelegate? + enum ReviewType { case fixed // 단일 메뉴 리뷰 case variable // 식단 리뷰 (여러 메뉴) @@ -73,6 +73,8 @@ final class SetRateViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() + originalNavigationDelegate = navigationController?.delegate + setDelegates() setupInitialDataFetch() } @@ -82,10 +84,16 @@ final class SetRateViewController: BaseViewController { if navigationController?.isNavigationBarHidden == true { navigationController?.isNavigationBarHidden = false } + + navigationController?.delegate = self } override func viewWillDisappear(_: Bool) { removeKeyboardNotifications() + + if isMovingFromParent { + navigationController?.delegate = originalNavigationDelegate + } } override func viewDidLayoutSubviews() { @@ -148,7 +156,7 @@ final class SetRateViewController: BaseViewController { imagePickerController.allowsEditing = false setRateView.userReviewTextView.delegate = self - self.navigationController?.delegate = self +// self.navigationController?.delegate = self self.navigationController?.interactivePopGestureRecognizer?.delegate = self } @@ -621,15 +629,32 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo } } - func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + func navigationController( + _ navigationController: UINavigationController, + willShow viewController: UIViewController, + animated: Bool + ) { + originalNavigationDelegate?.navigationController?( + navigationController, + willShow: viewController, + animated: animated + ) + if isReviewSubmitted { return } if navigationController is UIImagePickerController { return } let isPopping = !navigationController.viewControllers.contains(self) if isPopping { - navigationController.delegate = nil - + let textHasContent = setRateView.userReviewTextView.text != placeholderText + && !(setRateView.userReviewTextView.text ?? "").isEmpty + let isReviewStarted = setRateView.rateView.currentStar > 0 || textHasContent + + if reviewId != nil || !isReviewStarted { + navigationController.delegate = originalNavigationDelegate + return + } + var viewControllers = navigationController.viewControllers viewControllers.append(self) navigationController.setViewControllers(viewControllers, animated: false) @@ -641,12 +666,14 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo var controllers = navigationController.viewControllers if let index = controllers.firstIndex(of: self) { controllers.remove(at: index) + + navigationController.delegate = self.originalNavigationDelegate navigationController.setViewControllers(controllers, animated: true) } - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in - navigationController.delegate = self + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + navigationController.delegate = self + } } } } diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 45cd5c35..2881696e 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -32,27 +32,21 @@ final class CustomTabBarContainerController: BaseViewController { guard let self = self else { return } if index == 1 { - // firebase - click_map 이벤트 호출 MapAnalyticsManager.shared.logClickMap() } - // 마이페이지와 지도는 로그인 필요 - // TODO: 지도는 서버팀과 함께 나중에 둘러보기 상태에서보 "전체" 카테고리는 볼 수 있게 수정 if (index == 1 || index == 2), RealmService.shared.isAccessTokenPresent() == false { self.presentLoginAlert() return } - // 같은 탭 다시 클릭 시 처리 if index == self.currentIndex { if index == 0 { - // 학식 탭: 오늘이 아니면 오늘로 이동 if let nav = self.viewControllers[index] as? UINavigationController, let homeVC = nav.viewControllers.first as? HomeViewController { homeVC.resetToToday() } } else if index == 1 { - // 지도 탭: 콘텐츠 리로드 if let nav = self.viewControllers[index] as? UINavigationController, let mapVC = nav.viewControllers.first as? MainMapViewController { mapVC.reloadContent() @@ -63,7 +57,6 @@ final class CustomTabBarContainerController: BaseViewController { self.switchToViewController(at: index) } - // 각 네비게이션 컨트롤러의 delegate 설정 viewControllers.forEach { navController in navController.delegate = self navController.setNavigationBarHidden(false, animated: false) @@ -82,15 +75,13 @@ final class CustomTabBarContainerController: BaseViewController { } } - // MARK: - Life Cycle - override func viewDidLoad() { super.viewDidLoad() switchToViewController(at: currentIndex) } + // MARK: - Navigation Control - /// 탭 전환 처리 private func switchToViewController(at index: Int) { contentContainerView.subviews.forEach { $0.removeFromSuperview() } @@ -104,17 +95,14 @@ final class CustomTabBarContainerController: BaseViewController { tabBarView.setSelectedIndex(index) currentIndex = index - // 현재 표시 중인 VC의 shouldHideTabBar 확인 updateTabBarVisibility(for: selectedNav.topViewController) } - /// 탭바 가시성 업데이트 private func updateTabBarVisibility(for viewController: UIViewController?) { guard let vc = viewController as? BaseViewController else { return } - setTabBarHidden(vc.shouldHideTabBar, animated: true) + setTabBarHidden(vc.shouldHideTabBar, animated: false) } - /// 로그인 필요 시 알림창 표시 private func presentLoginAlert() { let alert = UIAlertController( title: "로그인이 필요한 서비스입니다", @@ -135,7 +123,6 @@ final class CustomTabBarContainerController: BaseViewController { present(alert, animated: true) } - /// 로그인 화면으로 전환 private func navigateToLogin() { let loginVC = LoginViewController() @@ -146,7 +133,6 @@ final class CustomTabBarContainerController: BaseViewController { } } - /// 공용 다이얼로그(팝업)를 표시하는 함수 public func showDialog( title: String, message: String, @@ -156,16 +142,13 @@ final class CustomTabBarContainerController: BaseViewController { ) { let dialogView = EATSSUDialogView() - // 다이얼로그 내용 설정 dialogView.configure(title: title, message: message) dialogView.setButtonTitles(cancel: cancelButtonTitle, confirm: confirmButtonTitle) - // '취소' 버튼 액션: 팝업 닫기 dialogView.cancelButton.addAction(UIAction { _ in dialogView.removeFromSuperview() }, for: .touchUpInside) - // '확인' 버튼 액션: 전달받은 클로저 실행 후 팝업 닫기 dialogView.confirmButton.addAction(UIAction { _ in confirmAction() dialogView.removeFromSuperview() @@ -179,82 +162,68 @@ final class CustomTabBarContainerController: BaseViewController { // MARK: - Public Interface - /// 외부에서 탭 전환 요청 시 사용 public func setTab(index: Int) { switchToViewController(at: index) } - /// 특정 인덱스의 네비게이션 컨트롤러를 반환 public func getNavController(at index: Int) -> UINavigationController? { guard index < viewControllers.count else { return nil } return viewControllers[index] } - - /// 탭바를 숨기거나 표시하는 메서드 -// public func setTabBarHidden(_ hidden: Bool, animated: Bool) { -// guard tabBarView.isHidden != hidden else { return } -// -// // 제약 업데이트 -// contentBottomConstraint?.deactivate() -// contentContainerView.snp.makeConstraints { -// if hidden { -// contentBottomConstraint = $0.bottom.equalToSuperview().constraint -// } else { -// contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint -// } -// } -// -// if animated { -// UIView.animate(withDuration: 0.3) { -// self.tabBarView.alpha = hidden ? 0 : 1 -// self.view.layoutIfNeeded() -// } completion: { _ in -// self.tabBarView.isHidden = hidden -// } -// } else { -// self.tabBarView.alpha = hidden ? 0 : 1 -// self.tabBarView.isHidden = hidden -// self.view.layoutIfNeeded() -// } -// } -} - -// MARK: - UINavigationControllerDelegate - -extension CustomTabBarContainerController: UINavigationControllerDelegate { - func navigationController( - _ navigationController: UINavigationController, - willShow viewController: UIViewController, - animated: Bool - ) { - guard let vc = viewController as? BaseViewController else { return } - let shouldHide = vc.shouldHideTabBar + + // MARK: - 탭바 가시성 제어 + + public func setTabBarHidden(_ hidden: Bool, animated: Bool) { + // 중복 호출 방지 + if tabBarView.isHidden == hidden { return } + + print("🎯 TabBar 상태 변경: \(hidden ? "숨김" : "표시")") + // 제약조건 업데이트 contentBottomConstraint?.deactivate() - contentContainerView.snp.makeConstraints { - if shouldHide { + contentContainerView.snp.remakeConstraints { + $0.top.leading.trailing.equalToSuperview() + if hidden { contentBottomConstraint = $0.bottom.equalToSuperview().constraint } else { contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint } } - self.tabBarView.alpha = shouldHide ? 0 : 1 - self.tabBarView.isHidden = shouldHide - self.view.layoutIfNeeded() - } - - public func setTabBarHidden(_ hidden: Bool, animated: Bool) { - tabBarView.snp.updateConstraints { - $0.height.equalTo(hidden ? 0 : 80) + let animations = { + self.tabBarView.alpha = hidden ? 0 : 1 + self.view.layoutIfNeeded() } + if animated { - UIView.animate(withDuration: 0.3) { - self.view.layoutIfNeeded() + UIView.animate(withDuration: 0.3, animations: animations) { _ in + self.tabBarView.isHidden = hidden } } else { - view.layoutIfNeeded() + animations() + self.tabBarView.isHidden = hidden } - tabBarView.isHidden = hidden + } +} + +// MARK: - UINavigationControllerDelegate + +extension CustomTabBarContainerController: UINavigationControllerDelegate { + + /// ✅ 네비게이션 전환 시 탭바 가시성 자동 업데이트 + func navigationController( + _ navigationController: UINavigationController, + willShow viewController: UIViewController, + animated: Bool + ) { + guard let vc = viewController as? BaseViewController else { + print("⚠️ BaseViewController가 아닌 VC: \(viewController)") + return + } + + print("📱 네비게이션 전환: \(type(of: vc)) - shouldHideTabBar: \(vc.shouldHideTabBar)") + + // shouldHideTabBar 값에 따라 자동으로 탭바 가시성 조절 + setTabBarHidden(vc.shouldHideTabBar, animated: animated) } } From 702d570994bfce742c073732fb818207ef0b9d32 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 19:46:59 +0900 Subject: [PATCH 31/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20nil?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EB=B9=88=20=EB=B0=B0=EC=97=B4=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ViewController/SetRateViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 5a193c2d..ff86c308 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -393,7 +393,7 @@ extension SetRateViewController { rating: setRateView.rateView.currentStar, menuLikes: menuLikes, content: setRateView.userReviewTextView.text, - imageUrls: imageUrl != nil ? [imageUrl!] : nil + imageUrls: imageUrl != nil ? [imageUrl!] : [] ) try await postMealReview(request: request) @@ -434,7 +434,7 @@ extension SetRateViewController { rating: setRateView.rateView.currentStar, menuLike: menuLike, content: setRateView.userReviewTextView.text, - imageUrls: imageUrl != nil ? [imageUrl!] : nil + imageUrls: imageUrl != nil ? [imageUrl!] : [] ) try await postMenuReview(request: request) From f3146acf630a215792f54832ce228602aa880209 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 20:08:56 +0900 Subject: [PATCH 32/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=EA=B0=80=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EB=84=98?= =?UTF-8?q?=EC=96=B4=EA=B0=88=EB=95=8C=202=EC=A4=84=EB=A1=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewTableCell.swift | 157 +++++++++++------- .../ReviewTagCollectionViewCell.swift | 48 +++++- 2 files changed, 138 insertions(+), 67 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index acd728ec..6804b122 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -19,28 +19,21 @@ final class ReviewTableCell: UITableViewCell { static let identifier = "ReviewTableCell" - /// 더보기 버튼 탭 핸들러 var handler: (() -> Void)? - - /// 리뷰 ID var reviewId: Int = 0 - - /// 메뉴 이름 var menuName: String = "" - - /// 메뉴 태그 데이터 private var tags: [(name: String, isLiked: Bool)] = [] + + private var tagCollectionViewHeightConstraint: Constraint? // MARK: - UI Components - Profile Section - /// 사용자 프로필 이미지 private let userProfileImageView: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.profile.image return imageView }() - /// 사용자 닉네임 레이블 private var userNameLabel: UILabel = { let label = UILabel() label.text = "hellosoongsil1234" @@ -49,10 +42,8 @@ final class ReviewTableCell: UITableViewCell { return label }() - /// 별점 표시 뷰 lazy var totalRateView = RateNumberView() - /// 닉네임과 메뉴를 담는 스택뷰 lazy var nameMenuStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [userNameLabel]) stackView.axis = .horizontal @@ -61,7 +52,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - /// 별점 스택뷰 lazy var rateStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [totalRateView]) stackView.axis = .horizontal @@ -70,7 +60,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - /// 사용자 정보(닉네임, 별점) 스택뷰 lazy var infoStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [nameMenuStackView, rateStackView]) stackView.axis = .vertical @@ -79,7 +68,6 @@ final class ReviewTableCell: UITableViewCell { return stackView }() - /// 프로필 전체 스택뷰 lazy var profileStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [userProfileImageView, infoStackView]) stackView.axis = .horizontal @@ -90,7 +78,6 @@ final class ReviewTableCell: UITableViewCell { // MARK: - UI Components - Right Section - /// 작성 날짜 레이블 private var dateLabel: UILabel = { let label = UILabel() label.text = "2023.03.03" @@ -99,7 +86,6 @@ final class ReviewTableCell: UITableViewCell { return label }() - /// 더보기 버튼 (수정/삭제/신고) private var sideButton: BaseButton = { let button = BaseButton() button.setTitleColor(EATSSUDesignAsset.Color.GrayScale.gray400.color, for: .normal) @@ -108,7 +94,6 @@ final class ReviewTableCell: UITableViewCell { return button }() - /// 날짜와 더보기 버튼 스택뷰 lazy var dateReportStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [sideButton, dateLabel]) stackView.axis = .vertical @@ -123,7 +108,7 @@ final class ReviewTableCell: UITableViewCell { private lazy var tagCollectionView: UICollectionView = { let layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.estimatedItemSize = CGSize(width: 100, height: 26) layout.minimumInteritemSpacing = 8 layout.minimumLineSpacing = 8 @@ -135,10 +120,10 @@ final class ReviewTableCell: UITableViewCell { forCellWithReuseIdentifier: ReviewTagCollectionViewCell.identifier ) cv.dataSource = self + cv.delegate = self return cv }() - /// 리뷰 내용 텍스트뷰 var reviewTextView: UITextView = { let textView = UITextView() textView.textColor = UIColor.black @@ -150,7 +135,6 @@ final class ReviewTableCell: UITableViewCell { return textView }() - /// 음식 이미지뷰 var foodImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill @@ -159,7 +143,6 @@ final class ReviewTableCell: UITableViewCell { return imageView }() - /// 콘텐츠(태그, 텍스트, 이미지) 스택뷰 lazy var contentStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [ tagCollectionView, @@ -190,7 +173,6 @@ final class ReviewTableCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - // 재사용 시 데이터 초기화 tags = [] tagCollectionView.reloadData() sideButton.setTitle("", for: .normal) @@ -201,34 +183,42 @@ final class ReviewTableCell: UITableViewCell { dateLabel.text = "" userNameLabel.text = "" } + + override func layoutSubviews() { + super.layoutSubviews() + + // 컬렉션뷰의 contentSize가 계산된 후 높이 업데이트 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + let contentHeight = self.tagCollectionView.collectionViewLayout.collectionViewContentSize.height + + if contentHeight > 0 { + self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) + } + } + } // MARK: - UI Configuration - /// UI 컴포넌트 추가 private func setupUI() { contentView.addSubview(profileStackView) contentView.addSubview(dateReportStackView) contentView.addSubview(contentStackView) - // 리뷰 텍스트 뒤에 추가 간격 설정 contentStackView.setCustomSpacing(8, after: reviewTextView) } - /// 레이아웃 제약조건 설정 func setLayout() { - // 프로필 이미지 userProfileImageView.snp.makeConstraints { make in make.width.height.equalTo(30) } - // 프로필 섹션 profileStackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(5) + make.top.equalToSuperview().offset(15) make.leading.equalToSuperview().offset(16) make.height.equalTo(50) } - // 날짜/더보기 버튼 섹션 dateReportStackView.snp.makeConstraints { make in make.centerY.equalTo(profileStackView) make.trailing.equalToSuperview().inset(16) @@ -238,21 +228,19 @@ final class ReviewTableCell: UITableViewCell { $0.height.equalTo(12.adjusted) } - // 콘텐츠 섹션 contentStackView.snp.makeConstraints { make in make.top.equalTo(profileStackView.snp.bottom) make.leading.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-7) + make.bottom.equalToSuperview().offset(-16) make.trailing.equalToSuperview().offset(-16) } // 태그 컬렉션뷰 tagCollectionView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() - make.height.greaterThanOrEqualTo(30) + tagCollectionViewHeightConstraint = make.height.equalTo(26).constraint } - // 음식 이미지 foodImageView.snp.makeConstraints { make in make.top.equalTo(reviewTextView.snp.bottom).offset(8) make.leading.trailing.equalToSuperview() @@ -262,7 +250,6 @@ final class ReviewTableCell: UITableViewCell { // MARK: - Actions - /// 더보기 버튼 탭 이벤트 @objc func touchedSideButtonEvent() { handler?() @@ -270,20 +257,15 @@ final class ReviewTableCell: UITableViewCell { // MARK: - Public Methods - /// 리뷰 데이터로 셀 구성 - /// - Parameter response: 리뷰 리스트 아이템 func dataBind(response: ReviewListItem) { - // 메뉴 이름 설정 menuName = response.menu?.map { $0.name }.joined(separator: " + ") ?? "" - // 기본 정보 설정 userNameLabel.text = response.writerNickname totalRateView.setRating(Int(response.rating)) dateLabel.text = response.writtenAt reviewTextView.text = response.content ?? "" reviewId = response.reviewId - // 이미지 설정 if let firstImageUrl = response.imageUrls?.first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) @@ -291,25 +273,32 @@ final class ReviewTableCell: UITableViewCell { foodImageView.isHidden = true } - // 더보기 버튼 설정 sideButton.setImage(EATSSUDesignAsset.Images.icMenu.image, for: .normal) sideButton.addTarget(self, action: #selector(touchedSideButtonEvent), for: .touchUpInside) - // 태그 설정 if let menuTags = response.menu, !menuTags.isEmpty { tags = menuTags.map { ($0.name, $0.isLike) } } else { tags = [] } - tagCollectionView.reloadData() + tagCollectionView.isHidden = tags.isEmpty + tagCollectionView.reloadData() + + tagCollectionView.layoutIfNeeded() + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + let contentHeight = self.tagCollectionView.collectionViewLayout.collectionViewContentSize.height + + if contentHeight > 0 { + self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) + self.layoutIfNeeded() + } + } } - /// 마이페이지용 리뷰 데이터 바인딩 - /// - Parameters: - /// - response: MyReviewListItem DTO - /// - nickname: 사용자 닉네임 - func myPageDataBind(response: MyReviewListItem, nickname: String) { // 인자 타입 변경 (MyDataList -> MyReviewListItem) + func myPageDataBind(response: MyReviewListItem, nickname: String) { userNameLabel.text = "\(nickname)" totalRateView.setRating(Int(response.rating ?? 0)) dateLabel.text = response.writtenAt @@ -340,12 +329,10 @@ final class ReviewTableCell: UITableViewCell { extension ReviewTableCell: UICollectionViewDataSource { - /// 컬렉션뷰 아이템 개수 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return tags.count } - /// 컬렉션뷰 셀 구성 func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath @@ -363,25 +350,69 @@ extension ReviewTableCell: UICollectionViewDataSource { } } +// MARK: - UICollectionViewDelegateFlowLayout + +extension ReviewTableCell: UICollectionViewDelegateFlowLayout { + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { + let tag = tags[indexPath.item] + let maxWidth = collectionView.bounds.width - 32 + + return ReviewTagCollectionViewCell.estimatedSize( + for: tag.name, + isLiked: tag.isLiked, + maxWidth: maxWidth + ) + } +} + +// MARK: - LeftAlignedCollectionViewFlowLayout + class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - let attributes = super.layoutAttributesForElements(in: rect) - + guard let attributes = super.layoutAttributesForElements(in: rect) else { + return nil + } + var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 - - attributes?.forEach { layoutAttribute in - if layoutAttribute.representedElementCategory == .cell { - if layoutAttribute.frame.origin.y >= maxY { - leftMargin = sectionInset.left - } - - layoutAttribute.frame.origin.x = leftMargin - - maxY = max(maxY, layoutAttribute.frame.maxY) - leftMargin = layoutAttribute.frame.maxX + minimumInteritemSpacing + + let modifiedAttributes = attributes.compactMap { layoutAttribute -> UICollectionViewLayoutAttributes? in + guard layoutAttribute.representedElementCategory == .cell else { + return layoutAttribute } + + let copiedAttribute = layoutAttribute.copy() as! UICollectionViewLayoutAttributes + + if copiedAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + + copiedAttribute.frame.origin.x = leftMargin + + leftMargin = copiedAttribute.frame.maxX + minimumInteritemSpacing + maxY = max(maxY, copiedAttribute.frame.maxY) + + return copiedAttribute } - return attributes + + return modifiedAttributes + } + + override var collectionViewContentSize: CGSize { + guard let collectionView = collectionView else { + return super.collectionViewContentSize + } + + let superSize = super.collectionViewContentSize + let minHeight: CGFloat = 26 + sectionInset.top + sectionInset.bottom + let actualHeight = max(superSize.height, minHeight) + + return CGSize(width: collectionView.bounds.width, height: actualHeight) } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift index eb106455..079d3b07 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -34,6 +34,9 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = .systemTeal + label.numberOfLines = 1 + label.lineBreakMode = .byClipping + label.adjustsFontSizeToFitWidth = false return label }() @@ -43,6 +46,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { sv.axis = .horizontal sv.spacing = 4 sv.alignment = .center + sv.distribution = .fill return sv }() @@ -64,6 +68,19 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { contentView.layer.cornerRadius = contentView.bounds.height / 2 } + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + setNeedsLayout() + layoutIfNeeded() + + let size = contentView.systemLayoutSizeFitting(layoutAttributes.size) + var newFrame = layoutAttributes.frame + newFrame.size.width = ceil(size.width) + newFrame.size.height = ceil(size.height) + layoutAttributes.frame = newFrame + + return layoutAttributes + } + // MARK: - UI Configuration /// UI 컴포넌트 설정 @@ -72,6 +89,12 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { contentView.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.1) contentView.layer.borderColor = UIColor.systemTeal.cgColor contentView.layer.borderWidth = 1 + + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + titleLabel.setContentHuggingPriority(.required, for: .horizontal) + + iconImageView.setContentCompressionResistancePriority(.required, for: .horizontal) + iconImageView.setContentHuggingPriority(.required, for: .horizontal) // 스택뷰 구성 stackView.addArrangedSubview(iconImageView) @@ -79,8 +102,6 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { contentView.addSubview(stackView) // 레이아웃 설정 - stackView.translatesAutoresizingMaskIntoConstraints = false - iconImageView.snp.makeConstraints { make in make.width.height.equalTo(10) } @@ -88,8 +109,8 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { stackView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(8) make.trailing.equalToSuperview().inset(8) - make.top.equalToSuperview().offset(2) - make.bottom.equalToSuperview().inset(2) + make.top.equalToSuperview().offset(6) + make.bottom.equalToSuperview().inset(6) } } @@ -108,5 +129,24 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { } else { iconImageView.isHidden = true } + + setNeedsLayout() + layoutIfNeeded() + } + + static func estimatedSize(for text: String, isLiked: Bool, maxWidth: CGFloat) -> CGSize { + let label = UILabel() + label.font = .systemFont(ofSize: 10, weight: .medium) + label.text = text + label.numberOfLines = 1 + + let labelSize = label.sizeThatFits(CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) + + // 아이콘(10) + spacing(4) + 여백(8+8) = 30 + let iconWidth: CGFloat = isLiked ? 14 : 0 + let totalWidth = labelSize.width + iconWidth + 16 + let height: CGFloat = 26 // 고정 높이 + + return CGSize(width: ceil(totalWidth), height: height) } } From 345a08f642ee662d812dc5792680cac6c1e9dce8 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 20:11:40 +0900 Subject: [PATCH 33/69] =?UTF-8?q?[#321]=20NetworkService=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A1=9C=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../App/Sources/Data/Network/Service/NetworkService.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift index ae863ee0..bcd4e636 100644 --- a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift +++ b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift @@ -48,14 +48,6 @@ final class NetworkService { switch result { case .success(let response): do { - // 🔥 Raw JSON 출력 (디버깅용) - print("------ Raw JSON ------") - if let raw = String(data: response.data, encoding: .utf8) { - print(raw) - } else { - print("❌ Raw JSON 출력 실패: 인코딩 불가") - } - print("-----------------------") let baseResponse = try response.map(BaseResponse.self) if baseResponse.isSuccess { From 0ff54067e1abfe1d018c8781460702aa108e5305 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 30 Nov 2025 20:36:11 +0900 Subject: [PATCH 34/69] =?UTF-8?q?[#321]=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewTableCell.swift | 39 +++++++++++++------ .../ViewController/ReviewViewController.swift | 14 +++++++ .../SetRateViewController.swift | 6 +-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 6804b122..f00c50d8 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -137,7 +137,7 @@ final class ReviewTableCell: UITableViewCell { var foodImageView: UIImageView = { let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill + imageView.contentMode = .scaleAspectFit imageView.isHidden = true imageView.clipsToBounds = true return imageView @@ -187,15 +187,7 @@ final class ReviewTableCell: UITableViewCell { override func layoutSubviews() { super.layoutSubviews() - // 컬렉션뷰의 contentSize가 계산된 후 높이 업데이트 - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - let contentHeight = self.tagCollectionView.collectionViewLayout.collectionViewContentSize.height - - if contentHeight > 0 { - self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) - } - } + // layoutSubviews에서 높이 업데이트 로직 제거 (dataBind로 이동) } // MARK: - UI Configuration @@ -238,6 +230,7 @@ final class ReviewTableCell: UITableViewCell { // 태그 컬렉션뷰 tagCollectionView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() + // 높이 제약을 저장하고 기본 높이를 줍니다. tagCollectionViewHeightConstraint = make.height.equalTo(26).constraint } @@ -258,6 +251,10 @@ final class ReviewTableCell: UITableViewCell { // MARK: - Public Methods func dataBind(response: ReviewListItem) { + + // 💡 1. 텍스트뷰의 너비가 계산되도록 레이아웃 업데이트 (시작) + self.layoutIfNeeded() + menuName = response.menu?.map { $0.name }.joined(separator: " + ") ?? "" userNameLabel.text = response.writerNickname @@ -266,6 +263,11 @@ final class ReviewTableCell: UITableViewCell { reviewTextView.text = response.content ?? "" reviewId = response.reviewId + // 💡 2. 텍스트뷰 높이를 내용물에 맞게 계산하여 프레임 업데이트 + let fixedWidth = reviewTextView.frame.size.width + let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) + reviewTextView.frame.size.height = newSize.height + if let firstImageUrl = response.imageUrls?.first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) @@ -287,24 +289,36 @@ final class ReviewTableCell: UITableViewCell { tagCollectionView.layoutIfNeeded() + // 🚀 3. 컬렉션 뷰 높이 업데이트 및 셀 레이아웃 강제 업데이트 (비동기) DispatchQueue.main.async { [weak self] in guard let self = self else { return } + let contentHeight = self.tagCollectionView.collectionViewLayout.collectionViewContentSize.height if contentHeight > 0 { self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) - self.layoutIfNeeded() + + // 텍스트뷰와 컬렉션뷰의 높이 변경을 스택뷰와 셀이 반영하도록 강제합니다. + self.contentView.layoutIfNeeded() } } } func myPageDataBind(response: MyReviewListItem, nickname: String) { + // 💡 1. 텍스트뷰의 너비가 계산되도록 레이아웃 업데이트 (시작) + self.layoutIfNeeded() + userNameLabel.text = "\(nickname)" totalRateView.setRating(Int(response.rating ?? 0)) dateLabel.text = response.writtenAt reviewTextView.text = response.content + // 💡 2. 텍스트뷰 높이를 내용물에 맞게 계산하여 프레임 업데이트 + let fixedWidth = reviewTextView.frame.size.width + let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) + reviewTextView.frame.size.height = newSize.height + if let imageUrls = response.imageUrls, let firstImageUrl = imageUrls.first(where: { !$0.isEmpty }) { @@ -322,6 +336,9 @@ final class ReviewTableCell: UITableViewCell { reviewId = response.reviewId tags = [] tagCollectionView.isHidden = true + + // 🚀 3. 셀 레이아웃 강제 업데이트 + self.contentView.layoutIfNeeded() } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 75d81157..9fbf1231 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -18,6 +18,8 @@ final class ReviewViewController: BaseViewController { // MARK: - Properties override var shouldHideTabBar: Bool { true } + private var shouldShowSuccessToast: Bool = false + // MARK: - Network /// 리뷰 API 프로바이더 @@ -117,6 +119,15 @@ final class ReviewViewController: BaseViewController { getReviewList(type: type, menuId: menuID) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if shouldShowSuccessToast { + showToast(message: "리뷰가 성공적으로 등록되었습니다.") + shouldShowSuccessToast = false + } + } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -288,6 +299,9 @@ final class ReviewViewController: BaseViewController { } // MARK: - Public Methods + func setReviewSubmittedSuccessfully() { + shouldShowSuccessToast = true + } /// 메뉴 ID 바인딩 /// - Parameter id: 메뉴 ID diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index ff86c308..0dff7f48 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -296,7 +296,9 @@ final class SetRateViewController: BaseViewController { /// 리뷰 작성/수정 완료 후 ReviewViewController로 돌아갑니다. private func moveToReviewVC() { - if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) { + if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) as? ReviewViewController { + + reviewVC.setReviewSubmittedSuccessfully() navigationController?.popToViewController(reviewVC, animated: true) if let homeVC = navigationController?.viewControllers.first as? HomeViewController { @@ -400,7 +402,6 @@ extension SetRateViewController { await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() - self.showToast(message: "리뷰가 성공적으로 작성되었습니다.") } } catch { @@ -442,7 +443,6 @@ extension SetRateViewController { await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() - self.showToast(message: "리뷰가 성공적으로 작성되었습니다.") } } catch { From 67b656fec0a6c75a3e3ac59814925fd4dc9c3ea6 Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 1 Dec 2025 20:26:11 +0900 Subject: [PATCH 35/69] =?UTF-8?q?[#321]=20=EA=B0=95=EC=A0=9C=20=EC=96=B8?= =?UTF-8?q?=EB=9E=98=ED=95=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Data/Network/Router/ReviewRouter.swift | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 5e90d801..6a5851f2 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -83,12 +83,21 @@ extension ReviewRouter: TargetType { case let .newReviewList(type, id, lastReviewId, page, size): switch type { case "VARIABLE": - .requestParameters( - parameters: (lastReviewId != nil) - ? ["mealId": id, "size": size ?? 20, "lastReviewId": lastReviewId!] - : ["mealId": id, "size": size ?? 20], - encoding: URLEncoding.queryString - ) + .requestParameters( + parameters: { + var dict: [String: Any] = [ + "mealId": id, + "size": size ?? 20 + ] + + if let lastId = lastReviewId { + dict["lastReviewId"] = lastId + } + + return dict + }(), + encoding: URLEncoding.queryString + ) case "FIXED": .requestParameters( From cdc8543a9c81498e32988979d435d3463f229715 Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 1 Dec 2025 20:29:34 +0900 Subject: [PATCH 36/69] =?UTF-8?q?[#321]=20=EB=8F=99=EC=9D=BC=ED=95=9C=20Me?= =?UTF-8?q?nuLike=20=EA=B5=AC=EC=A1=B0=EC=B2=B4=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=EC=8B=9C=ED=82=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Data/Network/DTO/Review/WriteReviewMenuRequest.swift | 7 +------ .../Review/ViewController/SetRateViewController.swift | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift index 056ae88e..ba4f75c3 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/WriteReviewMenuRequest.swift @@ -8,12 +8,7 @@ // 리뷰v2 api struct WriteReviewMenuRequest: Encodable { let rating: Int - let menuLike: MenuLikeItem + let menuLike: MenuLike let content: String? let imageUrls: [String]? } - -struct MenuLikeItem: Encodable { - let menuId: Int - let isLike: Bool -} diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 0dff7f48..e0d55397 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -426,7 +426,7 @@ extension SetRateViewController { imageUrl = try await uploadImage(image: image) } - let menuLike = MenuLikeItem( + let menuLike = MenuLike( menuId: menuId, isLike: likedStates.first ?? false ) From 16bd2bb3345fa442ed6f57332ed14c840279f875 Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 1 Dec 2025 20:35:03 +0900 Subject: [PATCH 37/69] =?UTF-8?q?[#321]=20=EC=A4=91=EB=B3=B5=EB=90=9C=20my?= =?UTF-8?q?ReviewList=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index 6a5851f2..aa46d147 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -21,10 +21,6 @@ enum ReviewRouter { size: Int? = 20) case getFixedMenuStatistics(_ menuId: Int) case getMealStatistics(_ mealId: Int) - case getMyReviewList(lastReviewId: Int?, - page: Int? = 0, - size: Int? = 20, - sort: String? = "date,DESC") } extension ReviewRouter: TargetType { @@ -54,14 +50,12 @@ extension ReviewRouter: TargetType { "/v2/reviews/statistics/menus/\(menuId)" case let .getMealStatistics(mealId): "/v2/reviews/statistics/meals/\(mealId)" - case .getMyReviewList: - "/v2/reviews/my" } } var method: Moya.Method { switch self { - case .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics, .getMyReviewList: + case .getValidMenusForReview, .newReviewList, .getFixedMenuStatistics, .getMealStatistics: .get case .report: .post From aa0b6c7071c87f4944c62ee486a3c11575859b09 Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 1 Dec 2025 20:35:42 +0900 Subject: [PATCH 38/69] =?UTF-8?q?[#321]=20=EC=A0=9C=EA=B1=B0=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20myReviewList=20Task=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Data/Network/Router/ReviewRouter.swift | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift index aa46d147..6a72614f 100644 --- a/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/ReviewRouter.swift @@ -107,23 +107,6 @@ extension ReviewRouter: TargetType { .requestPlain case .getMealStatistics: .requestPlain - case let .getMyReviewList(lastReviewId, page, size, sort): - .requestParameters( - parameters: { - var dict: [String: Any] = [:] - - if let lastId = lastReviewId { - dict["lastReviewId"] = lastId - } else { - dict["page"] = page ?? 0 - } - - dict["size"] = size ?? 20 - dict["sort"] = sort ?? "date,DESC" - return dict - }(), - encoding: URLEncoding.queryString - ) } } From cf0ec3f99e6d93b0ce189615b07c4bdb2c5e9465 Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 1 Dec 2025 20:36:44 +0900 Subject: [PATCH 39/69] =?UTF-8?q?[#321]=20Report=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/{RerportView => ReportView}/ReportView.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename EATSSU/App/Sources/Presentation/Review/View/{RerportView => ReportView}/ReportView.swift (100%) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift b/EATSSU/App/Sources/Presentation/Review/View/ReportView/ReportView.swift similarity index 100% rename from EATSSU/App/Sources/Presentation/Review/View/RerportView/ReportView.swift rename to EATSSU/App/Sources/Presentation/Review/View/ReportView/ReportView.swift From f345afdc90d25e7e53439fd8006158678a81059e Mon Sep 17 00:00:00 2001 From: Funital Date: Mon, 1 Dec 2025 20:51:17 +0900 Subject: [PATCH 40/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=A4=91?= =?UTF-8?q?=EA=B0=84=EC=97=90=20=EB=82=98=EA=B0=80=EA=B8=B0=20=EC=8B=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A0=84=ED=99=98=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../SetRateViewController.swift | 98 +++++++------------ 1 file changed, 35 insertions(+), 63 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index e0d55397..a7f0de35 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -11,7 +11,7 @@ import Moya import EATSSUDesign -final class SetRateViewController: BaseViewController { +final class SetRateViewController: BaseViewController, UINavigationControllerDelegate { // MARK: - Properties override var shouldHideTabBar: Bool { true } @@ -31,7 +31,6 @@ final class SetRateViewController: BaseViewController { // State Flags private var isReviewSubmitted = false - private weak var originalNavigationDelegate: UINavigationControllerDelegate? enum ReviewType { case fixed // 단일 메뉴 리뷰 @@ -73,8 +72,6 @@ final class SetRateViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - originalNavigationDelegate = navigationController?.delegate - setDelegates() setupInitialDataFetch() } @@ -84,16 +81,11 @@ final class SetRateViewController: BaseViewController { if navigationController?.isNavigationBarHidden == true { navigationController?.isNavigationBarHidden = false } - - navigationController?.delegate = self } override func viewWillDisappear(_: Bool) { removeKeyboardNotifications() - if isMovingFromParent { - navigationController?.delegate = originalNavigationDelegate - } } override func viewDidLayoutSubviews() { @@ -128,6 +120,17 @@ final class SetRateViewController: BaseViewController { override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = reviewId != nil ? "리뷰 수정하기" : "리뷰 남기기" + + navigationItem.hidesBackButton = true + + let backButton = UIBarButtonItem( + image: UIImage(systemName: "chevron.backward"), + style: .plain, + target: self, + action: #selector(didTapCustomBackButton) + ) + backButton.tintColor = .lightGray + navigationItem.leftBarButtonItem = backButton } // MARK: - Setup & Delegate @@ -156,7 +159,6 @@ final class SetRateViewController: BaseViewController { imagePickerController.allowsEditing = false setRateView.userReviewTextView.delegate = self -// self.navigationController?.delegate = self self.navigationController?.interactivePopGestureRecognizer?.delegate = self } @@ -248,14 +250,7 @@ final class SetRateViewController: BaseViewController { /// 이미지 선택 버튼 탭 시 ImagePicker를 표시합니다. @objc func didSelectedImage() { - let originalDelegate = self.navigationController?.delegate - self.navigationController?.delegate = nil - - present(imagePickerController, animated: true) { [weak self] in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self?.navigationController?.delegate = originalDelegate - } - } + present(imagePickerController, animated: true) } /// 이미지 뷰 탭 또는 삭제 버튼 탭 시 이미지를 삭제합니다. @@ -264,6 +259,17 @@ final class SetRateViewController: BaseViewController { setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) } + // MARK: - Custom Back Button Action + @objc private func didTapCustomBackButton() { + checkReviewStatusAndConfirmExit { [weak self] shouldPop in + guard let self = self else { return } + + if shouldPop { + self.navigationController?.popViewController(animated: true) + } + } + } + // MARK: - Review Submission Logic /// 리뷰 작성/수정 버튼 탭 시 호출됩니다. @@ -595,7 +601,7 @@ extension SetRateViewController: UITextViewDelegate { // MARK: - ImagePicker & Navigation Delegate -extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIGestureRecognizerDelegate { +extension SetRateViewController: UIImagePickerControllerDelegate, UIGestureRecognizerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let image = info[.originalImage] as? UIImage { @@ -623,63 +629,29 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UINavigationCo ) { completion(true) } - completion(false) } else { completion(true) } } - - func navigationController( - _ navigationController: UINavigationController, - willShow viewController: UIViewController, - animated: Bool - ) { - originalNavigationDelegate?.navigationController?( - navigationController, - willShow: viewController, - animated: animated - ) - if isReviewSubmitted { return } - if navigationController is UIImagePickerController { return } + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard gestureRecognizer == navigationController?.interactivePopGestureRecognizer else { + return true + } - let isPopping = !navigationController.viewControllers.contains(self) + let textHasContent = setRateView.userReviewTextView.text != placeholderText + && !(setRateView.userReviewTextView.text ?? "").isEmpty + let isReviewStarted = setRateView.rateView.currentStar > 0 || textHasContent - if isPopping { - let textHasContent = setRateView.userReviewTextView.text != placeholderText - && !(setRateView.userReviewTextView.text ?? "").isEmpty - let isReviewStarted = setRateView.rateView.currentStar > 0 || textHasContent - - if reviewId != nil || !isReviewStarted { - navigationController.delegate = originalNavigationDelegate - return - } - - var viewControllers = navigationController.viewControllers - viewControllers.append(self) - navigationController.setViewControllers(viewControllers, animated: false) - + if reviewId == nil, isReviewStarted { checkReviewStatusAndConfirmExit { [weak self] shouldPop in guard let self = self else { return } - if shouldPop { - var controllers = navigationController.viewControllers - if let index = controllers.firstIndex(of: self) { - controllers.remove(at: index) - - navigationController.delegate = self.originalNavigationDelegate - navigationController.setViewControllers(controllers, animated: true) - } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - navigationController.delegate = self - } + self.navigationController?.popViewController(animated: true) } } + return false } - } - - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return true } } From 4f44f2e89f85b866119c9fef1a7c9ca35baa24cb Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 16:24:33 +0900 Subject: [PATCH 41/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=A0=90?= =?UTF-8?q?=EC=88=98=20=EC=B0=A8=ED=8A=B8=20=EB=86=92=EC=9D=B4=20=EB=B0=8F?= =?UTF-8?q?=20=EB=91=A5=EA=B7=BC=20=EC=A0=95=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewRateViewCell.swift | 24 +++++++++---------- fastlane/Matchfile | 2 +- fastlane/report.xml | 6 ++--- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 6790c676..243b9585 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -177,12 +177,12 @@ final class ReviewRateViewCell: UITableViewCell { private func makeChartBar() -> (container: UIView, foreground: UIView) { let container = UIView() container.backgroundColor = .gray200 - container.layer.cornerRadius = 5 + container.layer.cornerRadius = 2 container.layer.masksToBounds = true let foreground = UIView() foreground.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color - foreground.layer.cornerRadius = 5 + foreground.layer.cornerRadius = 2 foreground.layer.masksToBounds = true container.addSubview(foreground) @@ -284,36 +284,36 @@ final class ReviewRateViewCell: UITableViewCell { oneChartBar.snp.makeConstraints { make in make.centerY.equalTo(onePointLabel) make.leading.equalTo(onePointLabel.snp.trailing).offset(7) - make.height.equalTo(10) - make.width.equalTo(126) + make.height.equalTo(5) + make.width.equalTo(115) } twoChartBar.snp.makeConstraints { make in make.centerY.equalTo(twoPointLabel) make.leading.equalTo(twoPointLabel.snp.trailing).offset(7) - make.height.equalTo(10) - make.width.equalTo(126) + make.height.equalTo(5) + make.width.equalTo(115) } threeChartBar.snp.makeConstraints { make in make.centerY.equalTo(threePointLabel) make.leading.equalTo(threePointLabel.snp.trailing).offset(7) - make.height.equalTo(10) - make.width.equalTo(126) + make.height.equalTo(5) + make.width.equalTo(115) } fourChartBar.snp.makeConstraints { make in make.centerY.equalTo(fourPointLabel) make.leading.equalTo(fourPointLabel.snp.trailing).offset(7) - make.height.equalTo(10) - make.width.equalTo(126) + make.height.equalTo(5) + make.width.equalTo(115) } fiveChartBar.snp.makeConstraints { make in make.centerY.equalTo(fivePointLabel) make.leading.equalTo(fivePointLabel.snp.trailing).offset(7) - make.height.equalTo(10) - make.width.equalTo(126) + make.height.equalTo(5) + make.width.equalTo(115) } // 포인트 레이블 높이 diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 3e9e52b3..eba50a3a 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -9,4 +9,4 @@ type("development") # 관리할 앱의 Bundle Identifier app_identifier(["com.jiwoo.EatSSU", "com.jiwoo.EatSSU.EatSSUwidget2025"]) -api_key_path("api_key.json") +api_key_path("fastlane/api_key.json") diff --git a/fastlane/report.xml b/fastlane/report.xml index 59665bd7..fe3415f9 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,14 +5,12 @@ - + - - - + From c5f366a1fa9e08df39687d43d34dd4902dfe3c56 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 16:30:14 +0900 Subject: [PATCH 42/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20hint=20text=20font=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Presentation/Review/View/RateReview/SetRateView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift index c64f99db..53b9fd1c 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift @@ -275,6 +275,7 @@ final class SetRateView: UIView { func setInitialTextViewState() { userReviewTextView.text = "메뉴에 대한 상세한 리뷰를 작성해주세요" userReviewTextView.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color + userReviewTextView.font = .body2 } /// 이미지 뷰와 관련 UI를 업데이트합니다. From 93c4690bf68bff89bb0868d232369bcd07c93541 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 16:33:52 +0900 Subject: [PATCH 43/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=EC=88=98=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20font=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Presentation/Review/View/RateReview/SetRateView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift index 53b9fd1c..c26966e0 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift @@ -71,8 +71,8 @@ final class SetRateView: UIView { let maximumWordLabel: UILabel = { let label = UILabel() label.text = "0 / 300" - label.font = .caption2 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray600.color + label.font = .caption3 + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color return label }() From 6785413b8ec1b6eb0c833a393fca14f804da14e1 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 16:43:37 +0900 Subject: [PATCH 44/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=82=AC=EC=A7=84=20=EB=B2=84=ED=8A=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/RateReview/SetRateView.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift index c26966e0..20591f8d 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/SetRateView.swift @@ -81,11 +81,16 @@ final class SetRateView: UIView { let button = UIButton() var config = UIButton.Configuration.plain() config.image = EATSSUDesignAsset.Images.addImageButton.image - config.contentInsets = NSDirectionalEdgeInsets(top: -5, leading: 0, bottom: 5, trailing: 0) + config.imagePlacement = .top + config.imagePadding = 3.5 + config.image?.withTintColor(EATSSUDesignAsset.Color.GrayScale.gray300.color) + // Leave space at bottom for "사진 0/1" + config.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 0, bottom: 19, trailing: 0) button.configuration = config button.layer.borderWidth = 1 button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor - button.layer.cornerRadius = 8 + button.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color + button.layer.cornerRadius = 5 button.clipsToBounds = true return button }() @@ -94,7 +99,7 @@ final class SetRateView: UIView { let label = UILabel() label.text = "사진 0/1" label.font = .caption3 - label.textColor = EATSSUDesignAsset.Color.GrayScale.gray500.color + label.textColor = EATSSUDesignAsset.Color.GrayScale.gray400.color label.textAlignment = .center return label }() @@ -174,12 +179,13 @@ final class SetRateView: UIView { userReviewTextView, maximumWordLabel, selectImageButton, - imageCountLabel, userReviewImageView, closeButton, deleteMethodLabel ) + selectImageButton.addSubview(imageCountLabel) + // 1. ScrollView & ContentView scrollView.snp.makeConstraints { $0.top.leading.trailing.equalToSuperview() @@ -246,8 +252,9 @@ final class SetRateView: UIView { } imageCountLabel.snp.makeConstraints { - $0.top.equalTo(selectImageButton.snp.bottom).offset(5) - $0.centerX.equalTo(selectImageButton) + $0.centerX.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(4) + $0.bottom.equalToSuperview().inset(7) } userReviewImageView.snp.makeConstraints { @@ -257,7 +264,7 @@ final class SetRateView: UIView { } deleteMethodLabel.snp.makeConstraints { - $0.top.equalTo(imageCountLabel.snp.bottom).offset(7) + $0.top.equalTo(selectImageButton.snp.bottom).offset(9) $0.leading.equalTo(selectImageButton) $0.bottom.equalTo(contentView.snp.bottom).offset(-50) } From 54ea975f93bacd55da720376bf1b11b6481dd760 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 16:51:09 +0900 Subject: [PATCH 45/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=9C=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../SetRateViewController.swift | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index a7f0de35..852fa5f9 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -275,18 +275,12 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel /// 리뷰 작성/수정 버튼 탭 시 호출됩니다. @objc func tappedNextButton() { - - // 1. 유효성 검증 - if setRateView.userReviewTextView.text == "메뉴에 대한 상세한 리뷰를 작성해주세요" || (setRateView.userReviewTextView.text ?? "").count < 3 { - showToast(message: "리뷰를 3글자 이상 작성해주세요!", type: .info) - return - } - + // 1. 유효성 검증 (별점만 필수) guard setRateView.rateView.currentStar != 0 else { showToast(message: "별점을 입력해주세요!", type: .info) return } - + // 2. 리뷰 전송 분기 if reviewId != nil { sendFixReview() @@ -385,7 +379,12 @@ extension SetRateViewController { showToast(message: "식단 정보가 없습니다.") return } - + + // Normalize review text: send nil if empty or placeholder + let rawText = setRateView.userReviewTextView.text ?? "" + let trimmedText = rawText.trimmingCharacters(in: .whitespacesAndNewlines) + let content = trimmedText.isEmpty || trimmedText == placeholderText ? nil : trimmedText + _Concurrency.Task { do { var imageUrl: String? @@ -395,21 +394,21 @@ extension SetRateViewController { let menuLikes = validMenuIDList.enumerated().map { (index, menuId) in MenuLike(menuId: menuId, isLike: likedStates[index]) } - + let request = WriteReviewMealRequest( mealId: mealId, rating: setRateView.rateView.currentStar, menuLikes: menuLikes, - content: setRateView.userReviewTextView.text, + content: content, imageUrls: imageUrl != nil ? [imageUrl!] : [] ) try await postMealReview(request: request) - + await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() } - + } catch { await MainActor.run { print("❌ Meal 리뷰 업로드 실패: \(error)") @@ -424,33 +423,38 @@ extension SetRateViewController { showToast(message: "메뉴 정보가 없습니다.") return } - + + // Normalize review text: send nil if empty or placeholder + let rawText = setRateView.userReviewTextView.text ?? "" + let trimmedText = rawText.trimmingCharacters(in: .whitespacesAndNewlines) + let content = trimmedText.isEmpty || trimmedText == placeholderText ? nil : trimmedText + _Concurrency.Task { do { var imageUrl: String? if let image = userPickedImage { imageUrl = try await uploadImage(image: image) } - + let menuLike = MenuLike( menuId: menuId, isLike: likedStates.first ?? false ) - + let request = WriteReviewMenuRequest( rating: setRateView.rateView.currentStar, menuLike: menuLike, - content: setRateView.userReviewTextView.text, + content: content, imageUrls: imageUrl != nil ? [imageUrl!] : [] ) - + try await postMenuReview(request: request) - + await MainActor.run { self.isReviewSubmitted = true self.moveToReviewVC() } - + } catch { await MainActor.run { print("❌ Menu 리뷰 업로드 실패: \(error)") From c8701ec8c795ef81f899e9a948c7521804b0ce8e Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 16:55:58 +0900 Subject: [PATCH 46/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20font=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/SeeReview/ReviewRateViewCell.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 243b9585..edb62586 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -40,7 +40,7 @@ final class ReviewRateViewCell: UITableViewCell { var menuLabel: UILabel = { let label = UILabel() label.text = "김치볶음밥 & 계란국" - label.font = .header2 + label.font = .body1 label.textColor = .black label.numberOfLines = 0 label.textAlignment = .center @@ -58,7 +58,7 @@ final class ReviewRateViewCell: UITableViewCell { private let menuTitleLabel: UILabel = { let label = UILabel() label.text = "오늘의 메뉴" - label.font = .body1 + label.font = .subtitle2 label.textColor = .black return label }() From ce695a0d2e4cb0378f5ca33eaeddfe7e4393d592 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 18:18:18 +0900 Subject: [PATCH 47/69] =?UTF-8?q?[#321]=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=B9=84=EC=9C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/RateReview/MenuLikeCell.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift index 9e4a2767..7c4bd4f4 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -42,6 +42,7 @@ final class MenuLikeCell: UITableViewCell { button.tintColor = .gray button.backgroundColor = .clear button.isUserInteractionEnabled = false // Container가 이벤트를 받도록 설정 + button.imageView?.contentMode = .scaleAspectFit return button }() @@ -100,7 +101,7 @@ final class MenuLikeCell: UITableViewCell { likeButton.snp.makeConstraints { $0.center.equalToSuperview() - $0.size.equalTo(18) + $0.size.equalTo(CGSize(width: 18, height: 18)) } } @@ -140,7 +141,8 @@ final class MenuLikeCell: UITableViewCell { DispatchQueue.main.async { // 버튼 이미지 업데이트 - self.likeButton.setImage(image.withRenderingMode(.alwaysOriginal), for: .normal) + let resizedImage = image.withRenderingMode(.alwaysOriginal) + self.likeButton.setImage(resizedImage, for: .normal) // 컨테이너 스타일 업데이트 if self.isLiked { @@ -148,7 +150,7 @@ final class MenuLikeCell: UITableViewCell { self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor // 테두리 색 } else { self.likeContainer.backgroundColor = .clear // 배경색 제거 - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray500.color.cgColor // 테두리 색 + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor // 테두리 색 } } } From 99a59d12bf19442d9879da792355f91edffe4957 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 18:52:06 +0900 Subject: [PATCH 48/69] =?UTF-8?q?[#321]=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=95=88=EB=88=84=EB=A5=B8=20=EB=B2=84=ED=8A=BC=EB=8F=84=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=ED=83=9C=EA=B7=B8=EB=A1=9C=20=ED=91=9C?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../ReviewTagCollectionViewCell.swift | 38 +++++++++++-------- .../ViewController/ReviewViewController.swift | 9 ++--- .../Sources/Utility/Extension/UIColor+.swift | 6 ++- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift index 079d3b07..5ddd93f5 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -8,6 +8,8 @@ import UIKit import SnapKit +import EATSSUDesign + // MARK: - ReviewTagCollectionViewCell /// 리뷰의 메뉴 태그를 표시하는 컬렉션뷰 셀 @@ -22,8 +24,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { /// 좋아요 아이콘 private let iconImageView: UIImageView = { let iv = UIImageView() - iv.image = UIImage(systemName: "hand.thumbsup") - iv.tintColor = .systemTeal + iv.image = EATSSUDesignAsset.Images.thumbUp.image iv.isHidden = true iv.contentMode = .scaleAspectFit return iv @@ -32,8 +33,8 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { /// 태그 이름 레이블 private let titleLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 10, weight: .medium) - label.textColor = .systemTeal + label.font = .caption3 + label.textColor = .primary label.numberOfLines = 1 label.lineBreakMode = .byClipping label.adjustsFontSizeToFitWidth = false @@ -86,8 +87,8 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { /// UI 컴포넌트 설정 private func setupViews() { // 배경 및 테두리 설정 - contentView.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.1) - contentView.layer.borderColor = UIColor.systemTeal.cgColor + contentView.backgroundColor = UIColor.secondary + contentView.layer.borderColor = UIColor.primary.cgColor contentView.layer.borderWidth = 1 titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) @@ -103,14 +104,19 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { // 레이아웃 설정 iconImageView.snp.makeConstraints { make in - make.width.height.equalTo(10) + make.width.height.equalTo(12) } - + stackView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(8) make.trailing.equalToSuperview().inset(8) - make.top.equalToSuperview().offset(6) - make.bottom.equalToSuperview().inset(6) + make.top.equalToSuperview().offset(5) + make.bottom.equalToSuperview().inset(5) + } + + // (Optional) Ensure cell height is fixed at 22pt in self-sizing contexts + contentView.snp.makeConstraints { make in + make.height.equalTo(22) } } @@ -125,7 +131,7 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { if isLiked { iconImageView.isHidden = false - iconImageView.image = UIImage(systemName: "hand.thumbsup") + iconImageView.image = EATSSUDesignAsset.Images.thumbUp.image } else { iconImageView.isHidden = true } @@ -136,17 +142,17 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { static func estimatedSize(for text: String, isLiked: Bool, maxWidth: CGFloat) -> CGSize { let label = UILabel() - label.font = .systemFont(ofSize: 10, weight: .medium) + label.font = .caption3 label.text = text label.numberOfLines = 1 - + let labelSize = label.sizeThatFits(CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) - + // 아이콘(10) + spacing(4) + 여백(8+8) = 30 let iconWidth: CGFloat = isLiked ? 14 : 0 let totalWidth = labelSize.width + iconWidth + 16 - let height: CGFloat = 26 // 고정 높이 - + let height: CGFloat = 22 // 고정 높이 + return CGSize(width: ceil(totalWidth), height: height) } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 9fbf1231..cbb32271 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -548,12 +548,9 @@ extension ReviewViewController: UITableViewDataSource { for: indexPath ) as? ReviewTableCell ?? ReviewTableCell() - // 좋아요한 메뉴만 필터링 - var filteredReviewItem = reviewList[indexPath.row] - let likedMenus = filteredReviewItem.menu?.filter { $0.isLike } - filteredReviewItem.menu = likedMenus - - cell.dataBind(response: filteredReviewItem) + // 좋아요 여부와 무관하게 모든 메뉴 태그 표시 (isLike == true 인 경우에만 thumbUp 아이콘 표시) + let reviewItem = reviewList[indexPath.row] + cell.dataBind(response: reviewItem) // 더보기 버튼 핸들러 cell.handler = { [weak self] in diff --git a/EATSSU/App/Sources/Utility/Extension/UIColor+.swift b/EATSSU/App/Sources/Utility/Extension/UIColor+.swift index 082d4e3f..dd667fe0 100644 --- a/EATSSU/App/Sources/Utility/Extension/UIColor+.swift +++ b/EATSSU/App/Sources/Utility/Extension/UIColor+.swift @@ -33,7 +33,11 @@ extension UIColor { } static var primary: UIColor { - UIColor(hex: "#DF5757") + UIColor(hex: "#66D4C2") + } + + static var secondary: UIColor { + UIColor(hex: "#EEFBF8") } } From 917233c08db73e4a49e0a4522f9685320fb17d60 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 19:04:30 +0900 Subject: [PATCH 49/69] =?UTF-8?q?[#321]=20=ED=8F=89=EC=A0=90=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B0=A8=ED=8A=B8=EB=B0=94=20=EC=A4=91=EC=95=99=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/SeeReview/ReviewRateViewCell.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index edb62586..938a33e2 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -91,7 +91,7 @@ final class ReviewRateViewCell: UITableViewCell { var rateNumLabel: UILabel = { let label = UILabel() label.text = "4.3" - label.font = .bold(size: 36) + label.font = .rate label.textColor = .black return label }() @@ -263,7 +263,8 @@ final class ReviewRateViewCell: UITableViewCell { // 별점 섹션 rateSectionContainer.snp.makeConstraints { make in make.top.equalTo(menuLabel.snp.bottom).offset(40) - make.leading.trailing.equalToSuperview().inset(60) + make.centerX.equalToSuperview().offset(16) + make.width.equalToSuperview().inset(16) } totalRateStackView.snp.makeConstraints { make in From eba548fb5243c4538c8d0d4dcc01c063096117ae Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 19:19:05 +0900 Subject: [PATCH 50/69] =?UTF-8?q?[#321]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20x=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../SetRateViewController.swift | 29 ++++++++++++------ .../ic_close.imageset/Contents.json | 12 ++++++++ .../ic_close.imageset/ic_close.png | Bin 0 -> 607 bytes 3 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/ic_close.png diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 852fa5f9..afbae9f6 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -120,17 +120,28 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = reviewId != nil ? "리뷰 수정하기" : "리뷰 남기기" - + navigationItem.hidesBackButton = true + navigationItem.leftBarButtonItem = nil + + let closeImage = EATSSUDesignAsset.Images.icClose.image.withRenderingMode(.alwaysOriginal) + + let closeUIButton = UIButton(type: .system) + closeUIButton.setImage(closeImage, for: .normal) + closeUIButton.tintColor = .clear + closeUIButton.frame = CGRect(x: 0, y: 0, width: 24, height: 24) + closeUIButton.imageView?.contentMode = .scaleAspectFit + + if let imageView = closeUIButton.imageView { + imageView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.height.equalTo(12) + } + } + + closeUIButton.addTarget(self, action: #selector(didTapCustomBackButton), for: .touchUpInside) - let backButton = UIBarButtonItem( - image: UIImage(systemName: "chevron.backward"), - style: .plain, - target: self, - action: #selector(didTapCustomBackButton) - ) - backButton.tintColor = .lightGray - navigationItem.leftBarButtonItem = backButton + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: closeUIButton) } // MARK: - Setup & Delegate diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/Contents.json new file mode 100644 index 00000000..026d4e67 --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_close.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/ic_close.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/ic_close.imageset/ic_close.png new file mode 100644 index 0000000000000000000000000000000000000000..58fdaca8e869b21c28f4a1c31c42bcb4e4849c44 GIT binary patch literal 607 zcmV-l0-*hgP)@~0drDELIAGL9O(c600d`2O+f$vv5yPk6NHZ&#-)Muaq(|-yZdnM9+fDJ443S%DjMh5j zH^xRXLJn8(8G^0yUq!IHWQ?3;wBLO&=t|LBD&~^0a&S5i6FCjk8bWe%(^L(WA~z>Z z-5@0tx#&_2=R%c(E*&5R%Gg>}3KR-;Osy&htR2fMSSoO>7(s$16DZwwuwbb~O5Yt( zg47Asxb>q2sS>Nzc_j&goCIt3%*lcvB~iSoIksjAf>_D*Z?%a-wjhX<41sLwMc6 z+JRU}U%lv$6ZVt?h#gaQ7E&+j*j>&MBN(UgOfX8rC;$GXE_IQ{4IaDeFDR#KN~fRm-zqy002ovPDHLkV1lj=0m1+P literal 0 HcmV?d00001 From 4466f55f719dfda5529d48c3e01b7df21377b8d3 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 19:37:08 +0900 Subject: [PATCH 51/69] =?UTF-8?q?[#321]=20BaseUIView=20=EC=83=81=EC=86=8D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Presentation/Review/View/RateReview/RateView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift index debdff38..8616fa81 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift @@ -10,7 +10,7 @@ import SnapKit import EATSSUDesign -final class RateView: UIView { // BaseUIView 대신 UIView 상속 +final class RateView: BaseUIView { // MARK: - Properties @@ -57,12 +57,12 @@ final class RateView: UIView { // BaseUIView 대신 UIView 상속 // MARK: - UI Configuration /// UI 컴포넌트 설정 - private func configureUI() { + internal override func configureUI() { addSubview(starStackView) } /// 레이아웃 제약조건 설정 - private func setLayout() { + internal override func setLayout() { starStackView.snp.makeConstraints { make in make.edges.equalToSuperview() } From 5ee69874fc9dd36af30bea3d35cf2db5fdbd8d10 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 19:38:39 +0900 Subject: [PATCH 52/69] =?UTF-8?q?[#321]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Home/ViewController/HomeRestaurantViewController.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift index 57062299..5ba3aa20 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift @@ -264,11 +264,6 @@ extension HomeRestaurantViewController: UITableViewDataSource { reviewMenuTypeInfo.menuID = menus[menuIndex].menuId reviewMenuTypeInfo.changeMenuIDList = nil } - -// let reviewViewController = ReviewViewController() -// delegate = reviewViewController -// navigationController?.pushViewController(reviewViewController, animated: true) -// delegate?.didDelegateReviewMenuTypeInfo(for: reviewMenuTypeInfo) let reviewViewController = ReviewViewController() From 7a1d7d4085220193be019b8f83a98fea9ccc13b4 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 19:43:30 +0900 Subject: [PATCH 53/69] =?UTF-8?q?[#321]=20=EC=97=86=EC=96=B4=EC=A7=84=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EB=8B=A4=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../CustomTabBarContainerController.swift | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 2881696e..f88f8b0a 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -32,21 +32,27 @@ final class CustomTabBarContainerController: BaseViewController { guard let self = self else { return } if index == 1 { + // firebase - click_map 이벤트 호출 MapAnalyticsManager.shared.logClickMap() } + // 마이페이지와 지도는 로그인 필요 + // TODO: 지도는 서버팀과 함께 나중에 둘러보기 상태에서보 "전체" 카테고리는 볼 수 있게 수정 if (index == 1 || index == 2), RealmService.shared.isAccessTokenPresent() == false { self.presentLoginAlert() return } + // 같은 탭 다시 클릭 시 처리 if index == self.currentIndex { if index == 0 { + // 학식 탭: 오늘이 아니면 오늘로 이동 if let nav = self.viewControllers[index] as? UINavigationController, let homeVC = nav.viewControllers.first as? HomeViewController { homeVC.resetToToday() } } else if index == 1 { + // 지도 탭: 콘텐츠 리로드 if let nav = self.viewControllers[index] as? UINavigationController, let mapVC = nav.viewControllers.first as? MainMapViewController { mapVC.reloadContent() @@ -57,6 +63,7 @@ final class CustomTabBarContainerController: BaseViewController { self.switchToViewController(at: index) } + // 각 네비게이션 컨트롤러의 delegate 설정 viewControllers.forEach { navController in navController.delegate = self navController.setNavigationBarHidden(false, animated: false) @@ -74,6 +81,8 @@ final class CustomTabBarContainerController: BaseViewController { contentBottomConstraint = $0.bottom.equalTo(tabBarView.snp.top).constraint } } + + // MARK: - Life Cycle override func viewDidLoad() { super.viewDidLoad() @@ -81,7 +90,8 @@ final class CustomTabBarContainerController: BaseViewController { } // MARK: - Navigation Control - + + /// 탭 전환 처리 private func switchToViewController(at index: Int) { contentContainerView.subviews.forEach { $0.removeFromSuperview() } @@ -95,14 +105,17 @@ final class CustomTabBarContainerController: BaseViewController { tabBarView.setSelectedIndex(index) currentIndex = index + // 현재 표시 중인 VC의 shouldHideTabBar 확인 updateTabBarVisibility(for: selectedNav.topViewController) } + /// 탭바 가시성 업데이트 private func updateTabBarVisibility(for viewController: UIViewController?) { guard let vc = viewController as? BaseViewController else { return } setTabBarHidden(vc.shouldHideTabBar, animated: false) } + /// 로그인 필요 시 알림창 표시 private func presentLoginAlert() { let alert = UIAlertController( title: "로그인이 필요한 서비스입니다", @@ -123,6 +136,7 @@ final class CustomTabBarContainerController: BaseViewController { present(alert, animated: true) } + /// 로그인 화면으로 전환 private func navigateToLogin() { let loginVC = LoginViewController() @@ -133,6 +147,7 @@ final class CustomTabBarContainerController: BaseViewController { } } + /// 공용 다이얼로그(팝업)를 표시하는 함수 public func showDialog( title: String, message: String, @@ -142,13 +157,16 @@ final class CustomTabBarContainerController: BaseViewController { ) { let dialogView = EATSSUDialogView() + // 다이얼로그 내용 설정 dialogView.configure(title: title, message: message) dialogView.setButtonTitles(cancel: cancelButtonTitle, confirm: confirmButtonTitle) + // '취소' 버튼 액션: 팝업 닫기 dialogView.cancelButton.addAction(UIAction { _ in dialogView.removeFromSuperview() }, for: .touchUpInside) + // '확인' 버튼 액션: 전달받은 클로저 실행 후 팝업 닫기 dialogView.confirmButton.addAction(UIAction { _ in confirmAction() dialogView.removeFromSuperview() @@ -162,10 +180,12 @@ final class CustomTabBarContainerController: BaseViewController { // MARK: - Public Interface + /// 외부에서 탭 전환 요청 시 사용 public func setTab(index: Int) { switchToViewController(at: index) } + /// 특정 인덱스의 네비게이션 컨트롤러를 반환 public func getNavController(at index: Int) -> UINavigationController? { guard index < viewControllers.count else { return nil } return viewControllers[index] @@ -210,7 +230,7 @@ final class CustomTabBarContainerController: BaseViewController { extension CustomTabBarContainerController: UINavigationControllerDelegate { - /// ✅ 네비게이션 전환 시 탭바 가시성 자동 업데이트 + /// 네비게이션 전환 시 탭바 가시성 자동 업데이트 func navigationController( _ navigationController: UINavigationController, willShow viewController: UIViewController, From fb3d7d723feb097838ab5d3b3f3ed1ca8bd3c5a5 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 20 Dec 2025 19:57:03 +0900 Subject: [PATCH 54/69] =?UTF-8?q?[#321]=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewEmptyViewCell.swift | 3 +- .../ViewController/ReviewViewController.swift | 21 ++--------- .../CustomTabBarContainerController.swift | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift index 9bca4291..f43ad289 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewEmptyViewCell.swift @@ -102,7 +102,8 @@ final class ReviewEmptyViewCell: UITableViewCell { titleLabel.text = "아직 작성된 리뷰가 없어요" descriptionLabel.text = "메뉴에 가장 먼저 리뷰를 남겨주세요!" } else { - noReviewImageView.image = ImageLiteral.pleaseLogin + titleLabel.text = "로그인이 필요합니다" + descriptionLabel.text = "로그인 후 리뷰를 확인하세요" } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index cbb32271..1527310a 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -10,6 +10,8 @@ import SnapKit import FirebaseAnalytics import Moya +import EATSSUDesign + // MARK: - ReviewViewController /// 메뉴의 리뷰 목록과 통계를 표시하는 뷰 컨트롤러 @@ -67,18 +69,10 @@ final class ReviewViewController: BaseViewController { return tableView }() - /// 로딩 인디케이터 - private var activityIndicatorView: UIActivityIndicatorView = { - let indicator = UIActivityIndicatorView(style: .large) - indicator.startAnimating() - indicator.isHidden = true - return indicator - }() - /// 빈 상태 이미지뷰 private lazy var noReviewImageView: UIImageView = { let imageView = UIImageView() - imageView.image = ImageLiteral.noReview + imageView.image = EATSSUDesignAsset.Images.noReview.image imageView.isHidden = true return imageView }() @@ -140,7 +134,6 @@ final class ReviewViewController: BaseViewController { view.addSubviews( reviewTableView, - activityIndicatorView, noReviewImageView, reviewTabBarContainer ) @@ -154,10 +147,6 @@ final class ReviewViewController: BaseViewController { make.bottom.equalTo(reviewTabBarContainer.snp.top) } - activityIndicatorView.snp.makeConstraints { make in - make.center.equalToSuperview() - } - noReviewImageView.snp.makeConstraints { make in make.center.equalToSuperview() } @@ -312,17 +301,14 @@ final class ReviewViewController: BaseViewController { /// 리뷰 작성 버튼 탭 처리 (로그인 체크 포함) func userTapReviewButton() { if RealmService.shared.isAccessTokenPresent() { - activityIndicatorView.isHidden = false DispatchQueue.global().async { DispatchQueue.main.async { [self] in - if type == "FIXED" { let setRateViewController = SetRateViewController(menuId: menuID) setRateViewController.dataBind( list: menuNameList, idList: menuIDList ?? [] ) - activityIndicatorView.stopAnimating() navigationController?.pushViewController( setRateViewController, animated: true @@ -333,7 +319,6 @@ final class ReviewViewController: BaseViewController { list: validMenusForReview.map { $0.name }, idList: validMenusForReview.map { $0.menuId } ) - activityIndicatorView.stopAnimating() navigationController?.pushViewController( setRateViewController, animated: true diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index d6f979a1..4f78336b 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -133,6 +133,42 @@ final class CustomTabBarContainerController: UITabBarController { } } + + /// 탭바를 숨기거나 표시합니다. + /// - Parameters: + /// - hidden: true면 숨김, false면 표시 + /// - animated: 애니메이션 여부 + public override func setTabBarHidden(_ hidden: Bool, animated: Bool) { + // 이미 원하는 상태라면 종료 + if tabBar.isHidden == hidden, tabBar.alpha == (hidden ? 0 : 1) { + return + } + + let tabBarHeight = tabBar.frame.height + let targetTransform: CGAffineTransform = hidden + ? CGAffineTransform(translationX: 0, y: tabBarHeight) + : .identity + let targetAlpha: CGFloat = hidden ? 0 : 1 + + // 표시로 전환 시에는 먼저 isHidden을 풀어야 애니메이션이 보임 + if !hidden { tabBar.isHidden = false } + + let animations = { + self.tabBar.transform = targetTransform + self.tabBar.alpha = targetAlpha + } + + if animated { + UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut], animations: animations) { _ in + // 숨김 전환 완료 후 isHidden 처리 + if hidden { self.tabBar.isHidden = true } + } + } else { + animations() + tabBar.isHidden = hidden + } + } + // MARK: - Private Helpers /// 로그인 필요 시 알림창 표시 From e9ec5bd16cffd1e1a33e7cce0f49b2aded9e5ebb Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 21 Dec 2025 20:00:27 +0900 Subject: [PATCH 55/69] =?UTF-8?q?[#321]=20=EB=82=B4=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=ED=99=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/MyReviewResponseDTO.swift | 15 ++--- .../Data/Network/Service/NetworkService.swift | 9 +++ .../MyReviewViewController.swift | 2 +- .../View/SeeReview/ReviewTableCell.swift | 7 +-- fastlane/Fastfile | 58 +++++++++++++++++-- fastlane/README.md | 12 +++- fastlane/report.xml | 39 ++++++++++++- 7 files changed, 119 insertions(+), 23 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift index 5a3b176b..ae00eda9 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/MyReviewResponseDTO.swift @@ -7,15 +7,10 @@ import Foundation -// MARK: - API Response DTO +// MARK: - Review List DTO -/// 내가 쓴 리뷰 리스트 전체 응답 구조 +/// 리뷰 리스트 데이터 컨테이너 (NetworkService의 result로 전달됨) struct MyReviewResponseDTO: Codable { - let result: MyReviewList -} - -/// 리뷰 리스트 데이터 컨테이너 -struct MyReviewList: Codable { let numberOfElements: Int let hasNext: Bool let dataList: [MyReviewListItem] @@ -26,10 +21,10 @@ struct MyReviewList: Codable { /// 개별 리뷰 아이템 구조 struct MyReviewListItem: Codable { let reviewId: Int - let rating: Double? + let rating: Int? let writtenAt: String let content: String? - let imageUrls: [String]? + let imageUrls: [String] let menuList: [ReviewMenu] } @@ -37,5 +32,5 @@ struct MyReviewListItem: Codable { struct ReviewMenu: Codable { let id: Int let name: String - let isLike: Bool + let isLike: Bool } diff --git a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift index bcd4e636..1035d7be 100644 --- a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift +++ b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift @@ -48,6 +48,15 @@ final class NetworkService { switch result { case .success(let response): do { + // 🔥 Raw JSON 출력 (디버깅용) + print("------ Raw JSON ------") + if let raw = String(data: response.data, encoding: .utf8) { + print(raw) + } else { + print("❌ Raw JSON 출력 실패: 인코딩 불가") + } + print("-----------------------") + let baseResponse = try response.map(BaseResponse.self) if baseResponse.isSuccess { diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index cacdda97..52568087 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -176,7 +176,7 @@ extension MyReviewViewController { switch result { case .success(let response): - self.reviewList = response.result.dataList + self.reviewList = response.dataList self.myReviewView.myReviewTableView.reloadData() case .failure(let error): diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 61dc8a74..33d32bdf 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -319,12 +319,11 @@ final class ReviewTableCell: UITableViewCell { let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) reviewTextView.frame.size.height = newSize.height - if let imageUrls = response.imageUrls, - let firstImageUrl = imageUrls.first(where: { !$0.isEmpty }) { - + let firstImageUrl = response.imageUrls.first(where: { !$0.isEmpty }) + + if let firstImageUrl { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) - } else { foodImageView.isHidden = true } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7d7a937a..9416644e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,16 +1,66 @@ -# fastlane/Fastfile default_platform(:ios) platform :ios do - desc "개발 환경 세팅" + desc "개발 환경 인증서 세팅" lane :setup_development do match(type: "development") UI.success("🎉 개발 환경 설정 완료!") end - desc "App Store 배포 환경 세팅" + desc "배포 환경 인증서 세팅" lane :setup_appstore do match(type: "appstore") - UI.success("🚀 App Store 배포 환경 설정 완료!") + UI.success("🚀 배포 환경 설정 완료!") + end + + desc "TestFlight에 앱 배포" + lane :release do |options| + target_type = options[:type] || "dev" + target_version = options[:version] || "3.2.0" + + # Project.swift에 정의된 Scheme 선택 + target_scheme = (target_type == "prod") ? "EATSSU-PROD" : "EATSSU-DEV" + + # App Store Connect에서 최신 빌드 번호를 가져와 1을 더함 + build_num = latest_testflight_build_number( + api_key_path: "fastlane/api_key.json", + app_identifier: "com.jiwoo.EatSSU" + ) + 1 + + UI.message("📦 배포 타겟: #{target_type} (Scheme: #{target_scheme})") + UI.message("🚀 버전 주입: #{target_version} (Build: #{build_num})") + + # 상위 폴더(루트)로 이동하여 Tuist 실행 + sh("cd .. && tuist install") + sh("cd .. && tuist generate --no-open") + + # 인증서 동기화 + match(type: "appstore") + + # 앱 빌드 및 버전/빌드번호 강제 주입 + gym( + # workspace가 프로젝트 루트에 있으므로 상위 경로 지정 + workspace: "EATSSU_WORKSPACE.xcworkspace", + scheme: target_scheme, + export_method: "app-store", + clean: true, + xcargs: "MARKETING_VERSION=#{target_version} CURRENT_PROJECT_VERSION=#{build_num}" + ) + + # TestFlight 업로드 + pilot( + api_key_path: "fastlane/api_key.json", + skip_waiting_for_build_processing: true # 업로드만 성공하면 바로 터미널 종료 + ) + + clean_build_artifacts + end + + after_all do |lane| + clean_build_artifacts + end + + error do |lane, exception| + clean_build_artifacts end end \ No newline at end of file diff --git a/fastlane/README.md b/fastlane/README.md index e4b30ae7..f6c028c5 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do [bundle exec] fastlane ios setup_development ``` -개발 환경 세팅 +개발 환경 인증서 세팅 ### ios setup_appstore @@ -29,7 +29,15 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do [bundle exec] fastlane ios setup_appstore ``` -App Store 배포 환경 세팅 +배포 환경 인증서 세팅 + +### ios release + +```sh +[bundle exec] fastlane ios release +``` + +TestFlight에 앱 배포 ---- diff --git a/fastlane/report.xml b/fastlane/report.xml index fe3415f9..f70e2140 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,12 +5,47 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3b7952687419006c7940ff95a05af90e01aa3c4f Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 21 Dec 2025 20:10:02 +0900 Subject: [PATCH 56/69] =?UTF-8?q?[#321]=20=EB=82=B4=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../MyPage/View/MyReviewView.swift | 34 ++++++-- .../MyReviewViewController.swift | 87 ++++++++++++------- .../View/SeeReview/ReviewTableCell.swift | 28 +++++- 3 files changed, 108 insertions(+), 41 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyReviewView.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyReviewView.swift index d4a0432a..8364317f 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyReviewView.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyReviewView.swift @@ -6,13 +6,27 @@ // import UIKit - import SnapKit final class MyReviewView: BaseUIView { // MARK: - UI Components - let myReviewTableView = UITableView() + /// 리뷰 목록 테이블뷰 + let myReviewTableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.backgroundColor = .white + return tableView + }() + + /// 빈 상태 이미지뷰 (필요한 경우) + let noReviewImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.isHidden = true + return imageView + }() // MARK: - Life Cycles @@ -23,15 +37,21 @@ final class MyReviewView: BaseUIView { // MARK: - Functions override func configureUI() { - addSubview(myReviewTableView) - - myReviewTableView.separatorStyle = .none - myReviewTableView.showsVerticalScrollIndicator = false + backgroundColor = .white + + addSubviews(myReviewTableView, noReviewImageView) } override func setLayout() { myReviewTableView.snp.makeConstraints { - $0.edges.equalToSuperview() + $0.top.equalToSuperview().offset(24) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + + noReviewImageView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.height.equalTo(200) } } } diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index 52568087..28a8193f 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -13,12 +13,11 @@ import FirebaseAnalytics final class MyReviewViewController: BaseViewController { override var shouldHideTabBar: Bool { true } + // MARK: - Properties - // DTO 변경에 따라 타입 수정: MyDataList -> MyReviewListItem private var reviewList = [MyReviewListItem]() var nickname: String = .init() - private var menuName: String = .init() // MARK: - UI Components @@ -57,6 +56,7 @@ final class MyReviewViewController: BaseViewController { } override func configureUI() { + view.backgroundColor = .white view.addSubviews(myReviewView) } @@ -67,8 +67,14 @@ final class MyReviewViewController: BaseViewController { } private func setDelegate() { - myReviewView.myReviewTableView.register(ReviewTableCell.self, forCellReuseIdentifier: ReviewTableCell.identifier) - myReviewView.myReviewTableView.register(ReviewEmptyViewCell.self, forCellReuseIdentifier: ReviewEmptyViewCell.identifier) + myReviewView.myReviewTableView.register( + ReviewTableCell.self, + forCellReuseIdentifier: ReviewTableCell.identifier + ) + myReviewView.myReviewTableView.register( + ReviewEmptyViewCell.self, + forCellReuseIdentifier: ReviewEmptyViewCell.identifier + ) myReviewView.myReviewTableView.delegate = self myReviewView.myReviewTableView.dataSource = self } @@ -78,27 +84,35 @@ final class MyReviewViewController: BaseViewController { } private func showFixOrDeleteAlert(reviewID: Int, menuName: String) { - let alert = UIAlertController(title: "리뷰 수정 혹은 삭제", - message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", - preferredStyle: UIAlertController.Style.actionSheet) - - let fixAction = UIAlertAction(title: "수정하기", - style: .default, - handler: { _ in - let setRateViewController = SetRateViewController() - setRateViewController.dataBindForFix(list: [menuName], reviewId: reviewID) - self.navigationController?.pushViewController(setRateViewController, animated: true) - }) - - let deleteAction = UIAlertAction(title: "삭제하기", - style: .default, - handler: { _ in - self.deleteReview(reviewID: reviewID) - }) - - let cancelAction = UIAlertAction(title: "취소하기", - style: .cancel, - handler: nil) + let alert = UIAlertController( + title: "리뷰 수정 혹은 삭제", + message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", + preferredStyle: UIAlertController.Style.actionSheet + ) + + let fixAction = UIAlertAction( + title: "수정하기", + style: .default, + handler: { _ in + let setRateViewController = SetRateViewController() + setRateViewController.dataBindForFix(list: [menuName], reviewId: reviewID) + self.navigationController?.pushViewController(setRateViewController, animated: true) + } + ) + + let deleteAction = UIAlertAction( + title: "삭제하기", + style: .default, + handler: { _ in + self.deleteReview(reviewID: reviewID) + } + ) + + let cancelAction = UIAlertAction( + title: "취소하기", + style: .cancel, + handler: nil + ) alert.addAction(fixAction) alert.addAction(deleteAction) @@ -137,26 +151,32 @@ extension MyReviewViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if reviewList.isEmpty { - let cell = tableView.dequeueReusableCell(withIdentifier: ReviewEmptyViewCell.identifier, for: indexPath) as? ReviewEmptyViewCell ?? ReviewEmptyViewCell() + let cell = tableView.dequeueReusableCell( + withIdentifier: ReviewEmptyViewCell.identifier, + for: indexPath + ) as? ReviewEmptyViewCell ?? ReviewEmptyViewCell() cell.configureForMyReview() cell.selectionStyle = .none return cell } - let cell = tableView.dequeueReusableCell(withIdentifier: ReviewTableCell.identifier, for: indexPath) as? ReviewTableCell ?? ReviewTableCell() + let cell = tableView.dequeueReusableCell( + withIdentifier: ReviewTableCell.identifier, + for: indexPath + ) as? ReviewTableCell ?? ReviewTableCell() - // DTO에 맞게 데이터 바인딩 로직 수정 필요 (ReviewTableCell의 myPageDataBind 함수도 수정되었다고 가정) let reviewItem = reviewList[indexPath.row] cell.myPageDataBind(response: reviewItem, nickname: nickname) cell.handler = { [weak self] in guard let self else { return } - // DTO 구조에 맞게 메뉴 이름을 reviewItem.menuList에서 가져옴 let menuName = reviewItem.menuList.first?.name ?? "알 수 없는 메뉴" - showFixOrDeleteAlert(reviewID: cell.reviewId, - menuName: menuName) + showFixOrDeleteAlert( + reviewID: cell.reviewId, + menuName: menuName + ) } cell.selectionStyle = .none return cell @@ -179,6 +199,9 @@ extension MyReviewViewController { self.reviewList = response.dataList self.myReviewView.myReviewTableView.reloadData() + // 빈 상태 이미지 표시 여부 + self.myReviewView.noReviewImageView.isHidden = !self.reviewList.isEmpty + case .failure(let error): print("내 리뷰 조회 실패: \(error.localizedDescription)") RealmService.shared.resetDB() @@ -187,7 +210,6 @@ extension MyReviewViewController { } } - // 리뷰 삭제 알람 추가 func deleteReview(reviewID: Int) { showCustomDialog( title: "리뷰 삭제하기", @@ -205,6 +227,7 @@ extension MyReviewViewController { switch result { case .success: self.getMyReview() + self.showToast(message: "리뷰가 성공적으로 삭제되었습니다.") case .failure(let error): print("리뷰 삭제 실패: \(error.localizedDescription)") RealmService.shared.resetDB() diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index 33d32bdf..fe1419da 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -319,6 +319,7 @@ final class ReviewTableCell: UITableViewCell { let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) reviewTextView.frame.size.height = newSize.height + // 이미지 처리 let firstImageUrl = response.imageUrls.first(where: { !$0.isEmpty }) if let firstImageUrl { @@ -333,8 +334,31 @@ final class ReviewTableCell: UITableViewCell { sideButton.setTitle("", for: .normal) reviewId = response.reviewId - tags = [] - tagCollectionView.isHidden = true + + // ✅ 태그(메뉴 리스트) 처리 추가 + if !response.menuList.isEmpty { + tags = response.menuList.map { ($0.name, $0.isLike) } + tagCollectionView.isHidden = false + tagCollectionView.reloadData() + tagCollectionView.layoutIfNeeded() + + // 🚀 컬렉션 뷰 높이 업데이트 및 셀 레이아웃 강제 업데이트 (비동기) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + let contentHeight = self.tagCollectionView.collectionViewLayout.collectionViewContentSize.height + + if contentHeight > 0 { + self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) + + // 텍스트뷰와 컬렉션뷰의 높이 변경을 스택뷰와 셀이 반영하도록 강제합니다. + self.contentView.layoutIfNeeded() + } + } + } else { + tags = [] + tagCollectionView.isHidden = true + } // 🚀 3. 셀 레이아웃 강제 업데이트 self.contentView.layoutIfNeeded() From 20c9fdfd0c7b01559f4dfd0c1397188fcafdcdcb Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 21 Dec 2025 20:16:19 +0900 Subject: [PATCH 57/69] =?UTF-8?q?[#321]=20=EB=82=B4=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../MyReviewViewController.swift | 28 ++++++--- .../SetRateViewController.swift | 62 +++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index 28a8193f..51af1398 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -83,7 +83,7 @@ final class MyReviewViewController: BaseViewController { self.nickname = nickname } - private func showFixOrDeleteAlert(reviewID: Int, menuName: String) { + private func showFixOrDeleteAlert(reviewID: Int, reviewItem: MyReviewListItem) { let alert = UIAlertController( title: "리뷰 수정 혹은 삭제", message: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?", @@ -95,7 +95,22 @@ final class MyReviewViewController: BaseViewController { style: .default, handler: { _ in let setRateViewController = SetRateViewController() - setRateViewController.dataBindForFix(list: [menuName], reviewId: reviewID) + + // ✅ 모든 메뉴 정보 전달 + let menuNames = reviewItem.menuList.map { $0.name } + let menuIds = reviewItem.menuList.map { $0.id } + let likedMenuIds = reviewItem.menuList.filter { $0.isLike }.map { $0.id } + + setRateViewController.dataBindForFix( + list: menuNames, + reviewId: reviewID, + rating: reviewItem.rating, + content: reviewItem.content, + imageUrls: reviewItem.imageUrls, + menuIds: menuIds, + likedMenuIds: likedMenuIds + ) + self.navigationController?.pushViewController(setRateViewController, animated: true) } ) @@ -171,11 +186,10 @@ extension MyReviewViewController: UITableViewDataSource { cell.handler = { [weak self] in guard let self else { return } - let menuName = reviewItem.menuList.first?.name ?? "알 수 없는 메뉴" - - showFixOrDeleteAlert( - reviewID: cell.reviewId, - menuName: menuName + // ✅ reviewItem 전체를 전달 + self.showFixOrDeleteAlert( + reviewID: reviewItem.reviewId, + reviewItem: reviewItem ) } cell.selectionStyle = .none diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index afbae9f6..2ecc1a80 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -215,6 +215,68 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.nextButton.setTitle("리뷰 수정 완료하기", for: .normal) } + /// ✅ 새로운 dataBindForFix 메서드 (MyReviewViewController에서 사용) + func dataBindForFix( + list: [String], + reviewId: Int, + rating: Int?, + content: String?, + imageUrls: [String], + menuIds: [Int], + likedMenuIds: [Int] + ) { + // 1. 기본 정보 설정 + self.selectedList = list + self.reviewId = reviewId + self.validMenuIDList = menuIds + + // 2. 리뷰 타입 결정 + if menuIds.count == 1 { + self.reviewType = .fixed + self.menuID = menuIds.first + } else { + self.reviewType = .variable + } + + // 3. 좋아요 상태 복원 + self.likedStates = menuIds.map { menuId in + likedMenuIds.contains(menuId) + } + + // 4. UI 업데이트 + setRateView.menuLabel.text = list.count == 1 + ? "\(list[0]) 를/을 추천하시겠어요?" + : "메뉴를 추천하시겠어요?" + + // 5. 별점 설정 + if let rating = rating { + setRateView.rateView.currentStar = rating + setRateView.rateView.settingStarForFix(currentStar: rating) + } + + // 6. 리뷰 텍스트 설정 + if let content = content, !content.isEmpty { + setRateView.userReviewTextView.text = content + setRateView.userReviewTextView.textColor = .black + setRateView.maximumWordLabel.text = "\(content.count) / 300" + } + + // 7. 이미지 설정 + if let firstImageUrl = imageUrls.first, !firstImageUrl.isEmpty { + setRateView.userReviewImageView.kfSetImage(url: firstImageUrl) + setRateView.updateImageViewState(image: setRateView.userReviewImageView.image, count: 1, isHidden: false) + } else { + setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) + } + + // 8. 버튼 텍스트 변경 + setRateView.nextButton.setTitle("리뷰 수정 완료하기", for: .normal) + + // 9. 테이블뷰 리로드 + setRateView.menuTableView.reloadData() + view.setNeedsLayout() + } + /// 수정할 리뷰의 기존 내용을 화면에 표시합니다. func settingForReviewFix(data: ReviewListItem) { // 별점 설정 From c5daa6782dafe2bcc3771646c77d1a60631a7403 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 21 Dec 2025 20:22:28 +0900 Subject: [PATCH 58/69] =?UTF-8?q?[#321]=20=EB=82=B4=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20=EB=B2=84=ED=8A=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../SetRateViewController.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 2ecc1a80..33fbc47c 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -203,7 +203,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel view.setNeedsLayout() } - /// 리뷰 수정 모드 시작 시 설정합니다. (리뷰 ID 바인딩) + /// 리뷰 수정 모드 시작 시 설정합니다. (리뷰 ID 바인딩) - 기존 방식 func dataBindForFix(list: [String], reviewId: Int) { self.selectedList = list self.reviewId = reviewId @@ -269,7 +269,9 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) } - // 8. 버튼 텍스트 변경 + // 8. 버튼 및 이미지 선택 UI 설정 + setRateView.selectImageButton.isHidden = true + setRateView.deleteMethodLabel.isHidden = true setRateView.nextButton.setTitle("리뷰 수정 완료하기", for: .normal) // 9. 테이블뷰 리로드 @@ -367,17 +369,28 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } } - /// 리뷰 작성/수정 완료 후 ReviewViewController로 돌아갑니다. + /// ✅ 리뷰 작성/수정 완료 후 이전 화면으로 돌아갑니다. private func moveToReviewVC() { + // 1. MyReviewViewController가 네비게이션 스택에 있는지 확인 + if let myReviewVC = navigationController?.viewControllers.first(where: { $0 is MyReviewViewController }) as? MyReviewViewController { + navigationController?.popToViewController(myReviewVC, animated: true) + return + } + + // 2. ReviewViewController가 네비게이션 스택에 있는지 확인 if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) as? ReviewViewController { - reviewVC.setReviewSubmittedSuccessfully() navigationController?.popToViewController(reviewVC, animated: true) + // HomeViewController 새로고침 if let homeVC = navigationController?.viewControllers.first as? HomeViewController { homeVC.refreshAfterReview() } + return } + + // 3. 어느 것도 없으면 그냥 이전 화면으로 + navigationController?.popViewController(animated: true) } } @@ -434,8 +447,8 @@ extension SetRateViewController { await MainActor.run { self.isReviewSubmitted = true - self.moveToReviewVC() self.showToast(message: "리뷰가 성공적으로 수정되었습니다.") + self.moveToReviewVC() } } catch { From c7a488883f36151ba8f7a63e2fc0ad1ead805602 Mon Sep 17 00:00:00 2001 From: Funital Date: Sun, 21 Dec 2025 20:25:22 +0900 Subject: [PATCH 59/69] =?UTF-8?q?[#321]=20=EC=88=98=EC=A0=95=20=EC=8B=9C,?= =?UTF-8?q?=20=EC=82=AC=EC=A7=84=20=EB=B2=84=ED=8A=BC=20=EC=82=AC=EB=9D=BC?= =?UTF-8?q?=EC=A7=90=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Sources/Data/Network/Service/NetworkService.swift | 9 --------- .../Review/ViewController/SetRateViewController.swift | 4 +--- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift index 1035d7be..bcd4e636 100644 --- a/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift +++ b/EATSSU/App/Sources/Data/Network/Service/NetworkService.swift @@ -48,15 +48,6 @@ final class NetworkService { switch result { case .success(let response): do { - // 🔥 Raw JSON 출력 (디버깅용) - print("------ Raw JSON ------") - if let raw = String(data: response.data, encoding: .utf8) { - print(raw) - } else { - print("❌ Raw JSON 출력 실패: 인코딩 불가") - } - print("-----------------------") - let baseResponse = try response.map(BaseResponse.self) if baseResponse.isSuccess { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 33fbc47c..055aef7c 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -270,9 +270,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } // 8. 버튼 및 이미지 선택 UI 설정 - setRateView.selectImageButton.isHidden = true - setRateView.deleteMethodLabel.isHidden = true - setRateView.nextButton.setTitle("리뷰 수정 완료하기", for: .normal) + setRateView.nextButton.setTitle("완료하기", for: .normal) // 9. 테이블뷰 리로드 setRateView.menuTableView.reloadData() From ca60fed609b61d1aa77c045c378b72cdc255ff5b Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 15:17:19 +0900 Subject: [PATCH 60/69] =?UTF-8?q?[#321]=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/NewReviewListResponse.swift | 23 ++++++++++++++++++- .../View/SeeReview/ReviewTableCell.swift | 2 +- .../SetRateViewController.swift | 2 +- Gemfile | 2 ++ Gemfile.lock | 2 ++ fastlane/Fastfile | 16 ++++++------- fastlane/report.xml | 18 +++++++-------- 7 files changed, 44 insertions(+), 21 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift index 9add8337..5b6a78da 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -21,7 +21,8 @@ struct ReviewListItem: Codable { let rating: Double let writtenAt: String let content: String? - let imageUrls: [String]? + /// 유효한 이미지 URL 문자열만 담는 배열 (null / 빈 문자열은 필터링) + let imageUrls: [String] enum CodingKeys: String, CodingKey { case reviewId @@ -34,6 +35,26 @@ struct ReviewListItem: Codable { case content case imageUrls } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + reviewId = try container.decode(Int.self, forKey: .reviewId) + menu = try container.decodeIfPresent([ReviewMenuInfo].self, forKey: .menu) + writerId = try container.decodeIfPresent(Int.self, forKey: .writerId) + isWriter = try container.decode(Bool.self, forKey: .isWriter) + writerNickname = try container.decode(String.self, forKey: .writerNickname) + rating = try container.decode(Double.self, forKey: .rating) + writtenAt = try container.decode(String.self, forKey: .writtenAt) + content = try container.decodeIfPresent(String.self, forKey: .content) + + // imageUrls: [String?] 형태로 받아서 nil / 빈 문자열을 제거해 [String]으로 정제 + let rawImageUrls = try container.decodeIfPresent([String?].self, forKey: .imageUrls) ?? [] + imageUrls = rawImageUrls.compactMap { url in + guard let url, url.isEmpty == false else { return nil } + return url + } + } } struct ReviewMenuInfo: Codable { diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index fe1419da..c313e39d 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -268,7 +268,7 @@ final class ReviewTableCell: UITableViewCell { let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) reviewTextView.frame.size.height = newSize.height - if let firstImageUrl = response.imageUrls?.first(where: { !$0.isEmpty }) { + if let firstImageUrl = response.imageUrls.first(where: { !$0.isEmpty }) { foodImageView.isHidden = false foodImageView.kfSetImage(url: firstImageUrl) } else { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 055aef7c..dd1e20fb 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -289,7 +289,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.maximumWordLabel.text = "\(data.content?.count ?? 0) / 300" // 이미지 설정 (kfSetImage는 Kingfisher 확장 가정) - if let imageUrl = data.imageUrls?.first, !imageUrl.isEmpty { + if let imageUrl = data.imageUrls.first, !imageUrl.isEmpty { setRateView.userReviewImageView.kfSetImage(url: imageUrl) setRateView.updateImageViewState(image: setRateView.userReviewImageView.image, count: 1, isHidden: false) } else { diff --git a/Gemfile b/Gemfile index 7a118b49..6e67cf24 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ source "https://rubygems.org" gem "fastlane" + +gem "abbrev" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 116d7a80..1ef6847d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ GEM base64 nkf rexml + abbrev (0.1.2) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) @@ -223,6 +224,7 @@ PLATFORMS ruby DEPENDENCIES + abbrev fastlane BUNDLED WITH diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9416644e..3f623e69 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,10 +18,8 @@ platform :ios do target_type = options[:type] || "dev" target_version = options[:version] || "3.2.0" - # Project.swift에 정의된 Scheme 선택 target_scheme = (target_type == "prod") ? "EATSSU-PROD" : "EATSSU-DEV" - # App Store Connect에서 최신 빌드 번호를 가져와 1을 더함 build_num = latest_testflight_build_number( api_key_path: "fastlane/api_key.json", app_identifier: "com.jiwoo.EatSSU" @@ -30,16 +28,17 @@ platform :ios do UI.message("📦 배포 타겟: #{target_type} (Scheme: #{target_scheme})") UI.message("🚀 버전 주입: #{target_version} (Build: #{build_num})") - # 상위 폴더(루트)로 이동하여 Tuist 실행 sh("cd .. && tuist install") sh("cd .. && tuist generate --no-open") - # 인증서 동기화 - match(type: "appstore") + # 인증서 동기화 (Matchfile 및 API Key 연결) + match( + type: "appstore", + readonly: true, + api_key_path: "fastlane/api_key.json" + ) - # 앱 빌드 및 버전/빌드번호 강제 주입 gym( - # workspace가 프로젝트 루트에 있으므로 상위 경로 지정 workspace: "EATSSU_WORKSPACE.xcworkspace", scheme: target_scheme, export_method: "app-store", @@ -47,10 +46,9 @@ platform :ios do xcargs: "MARKETING_VERSION=#{target_version} CURRENT_PROJECT_VERSION=#{build_num}" ) - # TestFlight 업로드 pilot( api_key_path: "fastlane/api_key.json", - skip_waiting_for_build_processing: true # 업로드만 성공하면 바로 터미널 종료 + skip_waiting_for_build_processing: true ) clean_build_artifacts diff --git a/fastlane/report.xml b/fastlane/report.xml index f70e2140..14c1f3e9 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,47 +5,47 @@ - + - + - + - + - + - + - + - + - + From 72eac01d061f0f0528db4d0736a7cec838e89e7e Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 16:16:07 +0900 Subject: [PATCH 61/69] =?UTF-8?q?[#321]=20ReviewTableCell=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B2=BD=EA=B3=A0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../View/SeeReview/ReviewTableCell.swift | 57 +++++++------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index c313e39d..e0fe3d9a 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -2,28 +2,19 @@ // ReviewTableCell.swift // EatSSU-iOS // -// Created by 한금준 on 20/11/25. -// import UIKit import SnapKit - import EATSSUDesign -// MARK: - ReviewTableCell - -/// 개별 리뷰를 표시하는 테이블뷰 셀 final class ReviewTableCell: UITableViewCell { - // MARK: - Properties - static let identifier = "ReviewTableCell" var handler: (() -> Void)? var reviewId: Int = 0 var menuName: String = "" private var tags: [(name: String, isLiked: Bool)] = [] - private var tagCollectionViewHeightConstraint: Constraint? // MARK: - UI Components - Profile Section @@ -31,6 +22,7 @@ final class ReviewTableCell: UITableViewCell { private let userProfileImageView: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.profile.image + imageView.contentMode = .scaleAspectFit return imageView }() @@ -104,7 +96,6 @@ final class ReviewTableCell: UITableViewCell { // MARK: - UI Components - Content Section - /// 메뉴 태그 컬렉션뷰 private lazy var tagCollectionView: UICollectionView = { let layout = LeftAlignedCollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -132,6 +123,8 @@ final class ReviewTableCell: UITableViewCell { textView.backgroundColor = .systemBackground textView.font = .body1 textView.text = "여기 계란국 맛집임... 김치볶음밥에 계란후라이 없어서 아쉽 다음에 또 먹어야지" + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 return textView }() @@ -183,12 +176,6 @@ final class ReviewTableCell: UITableViewCell { dateLabel.text = "" userNameLabel.text = "" } - - override func layoutSubviews() { - super.layoutSubviews() - - // layoutSubviews에서 높이 업데이트 로직 제거 (dataBind로 이동) - } // MARK: - UI Configuration @@ -201,14 +188,15 @@ final class ReviewTableCell: UITableViewCell { } func setLayout() { + // 🔧 우선순위 조정: 프로필 이미지 크기 userProfileImageView.snp.makeConstraints { make in - make.width.height.equalTo(30) + make.width.height.equalTo(30).priority(.high) } + // 🔧 우선순위 조정: 프로필 스택뷰 profileStackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(15).priority(.high) make.leading.equalToSuperview().offset(16) - make.height.equalTo(50) } dateReportStackView.snp.makeConstraints { make in @@ -220,18 +208,19 @@ final class ReviewTableCell: UITableViewCell { $0.height.equalTo(12.adjusted) } + // 🔧 우선순위 조정: 컨텐츠 스택뷰 contentStackView.snp.makeConstraints { make in - make.top.equalTo(profileStackView.snp.bottom) + make.top.equalTo(profileStackView.snp.bottom).priority(.high) make.leading.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) + make.bottom.equalToSuperview().offset(-16).priority(.high) make.trailing.equalToSuperview().offset(-16) } - // 태그 컬렉션뷰 + // 🔧 우선순위 조정: 태그 컬렉션뷰 높이 tagCollectionView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() - // 높이 제약을 저장하고 기본 높이를 줍니다. - tagCollectionViewHeightConstraint = make.height.equalTo(26).constraint + // 낮은 우선순위로 설정하여 초기 렌더링 시 충돌 방지 + tagCollectionViewHeightConstraint = make.height.equalTo(26).priority(.medium).constraint } foodImageView.snp.makeConstraints { make in @@ -251,8 +240,7 @@ final class ReviewTableCell: UITableViewCell { // MARK: - Public Methods func dataBind(response: ReviewListItem) { - - // 💡 1. 텍스트뷰의 너비가 계산되도록 레이아웃 업데이트 (시작) + // 레이아웃 업데이트 self.layoutIfNeeded() menuName = response.menu?.map { $0.name }.joined(separator: " + ") ?? "" @@ -263,7 +251,7 @@ final class ReviewTableCell: UITableViewCell { reviewTextView.text = response.content ?? "" reviewId = response.reviewId - // 💡 2. 텍스트뷰 높이를 내용물에 맞게 계산하여 프레임 업데이트 + // 텍스트뷰 높이 계산 let fixedWidth = reviewTextView.frame.size.width let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) reviewTextView.frame.size.height = newSize.height @@ -286,26 +274,25 @@ final class ReviewTableCell: UITableViewCell { tagCollectionView.isHidden = tags.isEmpty tagCollectionView.reloadData() - tagCollectionView.layoutIfNeeded() - // 🚀 3. 컬렉션 뷰 높이 업데이트 및 셀 레이아웃 강제 업데이트 (비동기) + // 컬렉션뷰 높이 업데이트 DispatchQueue.main.async { [weak self] in guard let self = self else { return } let contentHeight = self.tagCollectionView.collectionViewLayout.collectionViewContentSize.height if contentHeight > 0 { + // 🔧 우선순위를 높여서 업데이트 self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) + self.tagCollectionViewHeightConstraint?.layoutConstraints.first?.priority = .required - // 텍스트뷰와 컬렉션뷰의 높이 변경을 스택뷰와 셀이 반영하도록 강제합니다. self.contentView.layoutIfNeeded() } } } func myPageDataBind(response: MyReviewListItem, nickname: String) { - // 💡 1. 텍스트뷰의 너비가 계산되도록 레이아웃 업데이트 (시작) self.layoutIfNeeded() userNameLabel.text = "\(nickname)" @@ -314,12 +301,11 @@ final class ReviewTableCell: UITableViewCell { reviewTextView.text = response.content - // 💡 2. 텍스트뷰 높이를 내용물에 맞게 계산하여 프레임 업데이트 + // 텍스트뷰 높이 계산 let fixedWidth = reviewTextView.frame.size.width let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) reviewTextView.frame.size.height = newSize.height - // 이미지 처리 let firstImageUrl = response.imageUrls.first(where: { !$0.isEmpty }) if let firstImageUrl { @@ -335,14 +321,12 @@ final class ReviewTableCell: UITableViewCell { reviewId = response.reviewId - // ✅ 태그(메뉴 리스트) 처리 추가 if !response.menuList.isEmpty { tags = response.menuList.map { ($0.name, $0.isLike) } tagCollectionView.isHidden = false tagCollectionView.reloadData() tagCollectionView.layoutIfNeeded() - // 🚀 컬렉션 뷰 높이 업데이트 및 셀 레이아웃 강제 업데이트 (비동기) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -350,8 +334,8 @@ final class ReviewTableCell: UITableViewCell { if contentHeight > 0 { self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) + self.tagCollectionViewHeightConstraint?.layoutConstraints.first?.priority = .required - // 텍스트뷰와 컬렉션뷰의 높이 변경을 스택뷰와 셀이 반영하도록 강제합니다. self.contentView.layoutIfNeeded() } } @@ -360,7 +344,6 @@ final class ReviewTableCell: UITableViewCell { tagCollectionView.isHidden = true } - // 🚀 3. 셀 레이아웃 강제 업데이트 self.contentView.layoutIfNeeded() } } From fd03084522b0c11cbffba28906f5dd1aa054b367 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 16:28:57 +0900 Subject: [PATCH 62/69] =?UTF-8?q?[#321]=20MenuLikeCell=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/RateReview/MenuLikeCell.swift | 28 +++++++++++-------- .../View/SeeReview/ReviewTableCell.swift | 3 ++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift index 7c4bd4f4..e85abc69 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -33,6 +33,8 @@ final class MenuLikeCell: UITableViewCell { let label = UILabel() label.font = .body3 label.textColor = .black + label.setContentHuggingPriority(.defaultLow, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return label }() @@ -41,7 +43,7 @@ final class MenuLikeCell: UITableViewCell { let button = UIButton(type: .system) button.tintColor = .gray button.backgroundColor = .clear - button.isUserInteractionEnabled = false // Container가 이벤트를 받도록 설정 + button.isUserInteractionEnabled = false button.imageView?.contentMode = .scaleAspectFit return button }() @@ -52,6 +54,8 @@ final class MenuLikeCell: UITableViewCell { view.layer.cornerRadius = 14 view.layer.borderWidth = 1 view.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + view.setContentHuggingPriority(.required, for: .horizontal) + view.setContentCompressionResistancePriority(.required, for: .horizontal) return view }() @@ -61,6 +65,7 @@ final class MenuLikeCell: UITableViewCell { stack.axis = .horizontal stack.spacing = 12 stack.alignment = .center + stack.distribution = .fill return stack }() @@ -90,13 +95,13 @@ final class MenuLikeCell: UITableViewCell { /// 레이아웃 제약조건 설정 private func setLayout() { hStack.snp.makeConstraints { - $0.verticalEdges.equalToSuperview().inset(12) - $0.horizontalEdges.equalToSuperview() // TableView에서 inset 처리 + $0.verticalEdges.equalToSuperview().inset(12).priority(.high) + $0.horizontalEdges.equalToSuperview() } likeContainer.snp.makeConstraints { - $0.height.equalTo(28) - $0.width.equalTo(58) + $0.height.equalTo(28).priority(.high) + $0.width.equalTo(58).priority(.required) } likeButton.snp.makeConstraints { @@ -136,21 +141,20 @@ final class MenuLikeCell: UITableViewCell { private func updateLikeState() { let image = isLiked - ? EATSSUDesignAsset.Images.thumbUp.image // 채워진 좋아요 - : EATSSUDesignAsset.Images.thumbUpGray.image // 빈 좋아요 + ? EATSSUDesignAsset.Images.thumbUp.image + : EATSSUDesignAsset.Images.thumbUpGray.image DispatchQueue.main.async { // 버튼 이미지 업데이트 let resizedImage = image.withRenderingMode(.alwaysOriginal) self.likeButton.setImage(resizedImage, for: .normal) - // 컨테이너 스타일 업데이트 if self.isLiked { - self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color // 배경색 - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor // 테두리 색 + self.likeContainer.backgroundColor = EATSSUDesignAsset.Color.Main.secondary.color + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.Main.primary.color.cgColor } else { - self.likeContainer.backgroundColor = .clear // 배경색 제거 - self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor // 테두리 색 + self.likeContainer.backgroundColor = .clear + self.likeContainer.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor } } } diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index e0fe3d9a..d6a16836 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -1,10 +1,12 @@ // // ReviewTableCell.swift // EatSSU-iOS +// Created by 한금준 on 20/11/25. // import UIKit import SnapKit + import EATSSUDesign final class ReviewTableCell: UITableViewCell { @@ -218,6 +220,7 @@ final class ReviewTableCell: UITableViewCell { // 🔧 우선순위 조정: 태그 컬렉션뷰 높이 tagCollectionView.snp.makeConstraints { make in + make.top.equalTo(profileStackView.snp.bottom).offset(8) make.leading.trailing.equalToSuperview() // 낮은 우선순위로 설정하여 초기 렌더링 시 충돌 방지 tagCollectionViewHeightConstraint = make.height.equalTo(26).priority(.medium).constraint From e22870e15883cca6275af6146fef4ca34866c52b Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 16:48:56 +0900 Subject: [PATCH 63/69] =?UTF-8?q?[#321]=20=EC=A3=BC=EC=84=9D=20=EB=B0=8F?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/View/RateReview/MenuLikeCell.swift | 21 ++-- .../Review/View/RateReview/RateView.swift | 40 ++---- .../Review/View/RateReview/SetRateView.swift | 49 +++----- .../View/SeeReview/RateNumberView.swift | 24 ++-- .../View/SeeReview/ReviewDividerCell.swift | 9 +- .../View/SeeReview/ReviewEmptyViewCell.swift | 11 +- .../View/SeeReview/ReviewRateViewCell.swift | 24 ++-- .../View/SeeReview/ReviewTableCell.swift | 23 ++-- .../ReviewTagCollectionViewCell.swift | 17 +-- .../ViewController/ReviewViewController.swift | 26 +--- .../SetRateViewController.swift | 114 ++++++------------ 11 files changed, 106 insertions(+), 252 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift index e85abc69..dc5df64e 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/MenuLikeCell.swift @@ -16,10 +16,7 @@ final class MenuLikeCell: UITableViewCell { static let identifier = "MenuLikeCell" - /// 좋아요 버튼 탭 핸들러 (Controller에게 이벤트 전달) var onLikeTapped: (() -> Void)? - - /// 좋아요 상태 var isLiked: Bool = false { didSet { updateLikeState() @@ -48,7 +45,7 @@ final class MenuLikeCell: UITableViewCell { return button }() - /// 좋아요 버튼 컨테이너 (탭 제스처 인식 영역) + /// 좋아요 버튼 컨테이너 private let likeContainer: UIView = { let view = UIView() view.layer.cornerRadius = 14 @@ -59,7 +56,7 @@ final class MenuLikeCell: UITableViewCell { return view }() - /// 가로 스택뷰 (메뉴 레이블 + 좋아요 컨테이너) + /// 스택뷰 (메뉴 레이블 + 좋아요 컨테이너) private lazy var hStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [menuLabel, likeContainer]) stack.axis = .horizontal @@ -83,16 +80,14 @@ final class MenuLikeCell: UITableViewCell { } // MARK: - Configuration - - /// UI 컴포넌트 설정 + private func setupUI() { selectionStyle = .none contentView.addSubview(hStack) likeContainer.addSubview(likeButton) } - - /// 레이아웃 제약조건 설정 + private func setLayout() { hStack.snp.makeConstraints { $0.verticalEdges.equalToSuperview().inset(12).priority(.high) @@ -109,8 +104,7 @@ final class MenuLikeCell: UITableViewCell { $0.size.equalTo(CGSize(width: 18, height: 18)) } } - - /// 제스처 설정 + private func setupGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(likeTapped)) likeContainer.isUserInteractionEnabled = true @@ -119,7 +113,7 @@ final class MenuLikeCell: UITableViewCell { // MARK: - Actions - /// 좋아요 버튼 탭 처리 (Controller에게 이벤트 전달) + /// 좋아요 버튼 탭 처리 @objc private func likeTapped() { onLikeTapped?() } @@ -137,7 +131,7 @@ final class MenuLikeCell: UITableViewCell { // MARK: - Private Methods - /// 좋아요 상태에 따라 UI 업데이트 + /// 좋아요 상태에 따른 UI 업데이트 private func updateLikeState() { let image = isLiked @@ -145,7 +139,6 @@ final class MenuLikeCell: UITableViewCell { : EATSSUDesignAsset.Images.thumbUpGray.image DispatchQueue.main.async { - // 버튼 이미지 업데이트 let resizedImage = image.withRenderingMode(.alwaysOriginal) self.likeButton.setImage(resizedImage, for: .normal) diff --git a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift index 8616fa81..08b1de65 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/RateReview/RateView.swift @@ -2,7 +2,7 @@ // RateView.swift // EatSSU-iOS // -// Created by 박윤빈 on 2023/03/24. +// Created by 한금준 on 2025/09/28. // import UIKit @@ -13,20 +13,12 @@ import EATSSUDesign final class RateView: BaseUIView { // MARK: - Properties - - /// 별 버튼 배열 + var buttons: [UIButton] = [] - - /// 현재 선택된 별점 (1~5) var currentStar: Int = 0 - - /// 별의 개수 (기본값: 5) private var starNumber: Int = 5 // 내부 프로퍼티로 변경 - - /// 채워진 별 이미지 + private lazy var starFillImage: UIImage? = EATSSUDesignAsset.Images.icStarYellow.image - - /// 빈 별 이미지 private lazy var starEmptyImage: UIImage? = EATSSUDesignAsset.Images.icStarGray.image // MARK: - UI Components @@ -55,13 +47,11 @@ final class RateView: BaseUIView { } // MARK: - UI Configuration - - /// UI 컴포넌트 설정 + internal override func configureUI() { addSubview(starStackView) } - - /// 레이아웃 제약조건 설정 + internal override func setLayout() { starStackView.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -69,21 +59,16 @@ final class RateView: BaseUIView { } // MARK: - Private Methods - - /// 별 버튼들 생성 및 설정 + private func setupStars() { - // 기존 버튼 제거 buttons.forEach { $0.removeFromSuperview() } buttons.removeAll() - - // 5개의 별 버튼 생성 및 스택뷰에 추가 + for i in 0.. 0 { - // 🔧 우선순위를 높여서 업데이트 self.tagCollectionViewHeightConstraint?.update(offset: contentHeight) self.tagCollectionViewHeightConstraint?.layoutConstraints.first?.priority = .required @@ -303,8 +295,7 @@ final class ReviewTableCell: UITableViewCell { dateLabel.text = response.writtenAt reviewTextView.text = response.content - - // 텍스트뷰 높이 계산 + let fixedWidth = reviewTextView.frame.size.width let newSize = reviewTextView.sizeThatFits(CGSize(width: fixedWidth, height: .greatestFiniteMagnitude)) reviewTextView.frame.size.height = newSize.height diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift index 5ddd93f5..4f30ba03 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTagCollectionViewCell.swift @@ -10,9 +10,6 @@ import SnapKit import EATSSUDesign -// MARK: - ReviewTagCollectionViewCell - -/// 리뷰의 메뉴 태그를 표시하는 컬렉션뷰 셀 final class ReviewTagCollectionViewCell: UICollectionViewCell { // MARK: - Properties @@ -83,10 +80,8 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { } // MARK: - UI Configuration - - /// UI 컴포넌트 설정 + private func setupViews() { - // 배경 및 테두리 설정 contentView.backgroundColor = UIColor.secondary contentView.layer.borderColor = UIColor.primary.cgColor contentView.layer.borderWidth = 1 @@ -96,13 +91,11 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { iconImageView.setContentCompressionResistancePriority(.required, for: .horizontal) iconImageView.setContentHuggingPriority(.required, for: .horizontal) - - // 스택뷰 구성 + stackView.addArrangedSubview(iconImageView) stackView.addArrangedSubview(titleLabel) contentView.addSubview(stackView) - - // 레이아웃 설정 + iconImageView.snp.makeConstraints { make in make.width.height.equalTo(12) } @@ -114,7 +107,6 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { make.bottom.equalToSuperview().inset(5) } - // (Optional) Ensure cell height is fixed at 22pt in self-sizing contexts contentView.snp.makeConstraints { make in make.height.equalTo(22) } @@ -148,10 +140,9 @@ final class ReviewTagCollectionViewCell: UICollectionViewCell { let labelSize = label.sizeThatFits(CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) - // 아이콘(10) + spacing(4) + 여백(8+8) = 30 let iconWidth: CGFloat = isLiked ? 14 : 0 let totalWidth = labelSize.width + iconWidth + 16 - let height: CGFloat = 22 // 고정 높이 + let height: CGFloat = 22 return CGSize(width: ceil(totalWidth), height: height) } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 1527310a..35cd6fcf 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -12,9 +12,6 @@ import Moya import EATSSUDesign -// MARK: - ReviewViewController - -/// 메뉴의 리뷰 목록과 통계를 표시하는 뷰 컨트롤러 final class ReviewViewController: BaseViewController { // MARK: - Properties @@ -105,7 +102,6 @@ final class ReviewViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - // 데이터 로드 getStatistics() if type == "VARIABLE" { getValidMenusForReview() @@ -178,9 +174,7 @@ final class ReviewViewController: BaseViewController { // MARK: - TableView Setup - /// 테이블뷰 설정 private func setTableView() { - // 셀 등록 reviewTableView.register( ReviewTableCell.self, forCellReuseIdentifier: ReviewTableCell.identifier @@ -198,7 +192,6 @@ final class ReviewViewController: BaseViewController { forCellReuseIdentifier: ReviewDividerCell.identifier ) - // 델리게이트 설정 reviewTableView.delegate = self reviewTableView.dataSource = self } @@ -208,7 +201,6 @@ final class ReviewViewController: BaseViewController { /// 리뷰 작성 버튼 탭 처리 @objc private func handleAddReviewButtonTap() { if type == "VARIABLE" { - // 가변 메뉴 (식사) let reviewVC = SetRateViewController(mealId: menuID) reviewVC.dataBind( list: validMenusForReview.map { $0.name }, @@ -217,7 +209,6 @@ final class ReviewViewController: BaseViewController { navigationController?.pushViewController(reviewVC, animated: true) } else { - // 고정 메뉴 let reviewVC = SetRateViewController(menuId: menuID) reviewVC.dataBind( list: menuNameList, @@ -244,7 +235,6 @@ final class ReviewViewController: BaseViewController { /// 리뷰 삭제 확인 알림 표시 /// - Parameter data: 리뷰 데이터 private func showDeleteAlert(data: ReviewListItem) { - // 작성자가 아니면 신고 알림 표시 if !data.isWriter { self.showReportAlert(reviewID: data.reviewId) return @@ -425,9 +415,9 @@ extension ReviewViewController: UITableViewDataSource { func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: - return 1 // 통계 셀 + return 1 case 1: - return 1 // 구분선 셀 + return 1 case 2: return reviewList.count == 0 ? 1 : reviewList.count // 리뷰 목록 또는 빈 상태 default: @@ -442,15 +432,12 @@ extension ReviewViewController: UITableViewDataSource { ) -> UITableViewCell { switch indexPath.section { case 0: - // 통계 셀 return configureStatisticsCell(tableView, indexPath: indexPath) case 1: - // 구분선 셀 return configureDividerCell(tableView, indexPath: indexPath) case 2: - // 리뷰 목록 또는 빈 상태 셀 return configureReviewCell(tableView, indexPath: indexPath) default: @@ -512,7 +499,6 @@ extension ReviewViewController: UITableViewDataSource { indexPath: IndexPath ) -> UITableViewCell { if reviewList.count == 0 { - // 빈 상태 셀 let cell = tableView.dequeueReusableCell( withIdentifier: ReviewEmptyViewCell.identifier, for: indexPath @@ -527,23 +513,20 @@ extension ReviewViewController: UITableViewDataSource { return cell } else { - // 리뷰 셀 let cell = tableView.dequeueReusableCell( withIdentifier: ReviewTableCell.identifier, for: indexPath ) as? ReviewTableCell ?? ReviewTableCell() - // 좋아요 여부와 무관하게 모든 메뉴 태그 표시 (isLike == true 인 경우에만 thumbUp 아이콘 표시) let reviewItem = reviewList[indexPath.row] cell.dataBind(response: reviewItem) - // 더보기 버튼 핸들러 cell.handler = { [weak self] in guard let self else { return } reviewList[indexPath.row].isWriter - ? self.showDeleteAlert(data: reviewList[indexPath.row]) - : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) + ? self.showDeleteAlert(data: reviewList[indexPath.row]) + : self.showReportAlert(reviewID: reviewList[indexPath.row].reviewId) } cell.selectionStyle = .none @@ -704,7 +687,6 @@ extension ReviewViewController { case .success: print("✅ Review 삭제 성공") - // 데이터 새로고침 self.getStatistics() if self.type == "VARIABLE" { self.getValidMenusForReview() diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index dd1e20fb..47225f4b 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -15,31 +15,27 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel // MARK: - Properties override var shouldHideTabBar: Bool { true } - - // Data Model + private var reviewType: ReviewType = .variable private var mealID: Int? private var menuID: Int? - private var reviewId: Int? // 수정 시 사용되는 리뷰 ID - - // Review Data State + private var reviewId: Int? + private var validMenuIDList: [Int] = [] private var selectedList: [String] = [] private var likedStates: [Bool] = [] private var userPickedImage: UIImage? - - // State Flags + private var isReviewSubmitted = false enum ReviewType { - case fixed // 단일 메뉴 리뷰 - case variable // 식단 리뷰 (여러 메뉴) + case fixed + case variable } // MARK: - UI Components - - // Root View + private let setRateView = SetRateView() private let imagePickerController = UIImagePickerController() @@ -47,7 +43,6 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel init() { super.init(nibName: nil, bundle: nil) - // 리뷰 수정 모드에서는 이 초기화를 사용하며, reviewType 등은 dataBindForFix에서 설정됩니다. } init(mealId: Int) { @@ -90,7 +85,6 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - // 테이블뷰 content size에 따라 높이 제약조건 업데이트 setRateView.menuTableViewHeightConstraint?.update(offset: setRateView.menuTableView.contentSize.height) } @@ -111,8 +105,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.nextButton.addTarget(self, action: #selector(tappedNextButton), for: .touchUpInside) setRateView.selectImageButton.addTarget(self, action: #selector(didSelectedImage), for: .touchUpInside) setRateView.closeButton.addTarget(self, action: #selector(didTappedImageView), for: .touchUpInside) - - // 이미지 뷰 탭 제스처 + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTappedImageView)) setRateView.userReviewImageView.addGestureRecognizer(tapGesture) } @@ -145,21 +138,18 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } // MARK: - Setup & Delegate - - /// 초기 데이터 (유효 메뉴 목록)를 가져오거나 기본 설정을 합니다. + private func setupInitialDataFetch() { if reviewId == nil { if reviewType == .variable, let mealId = mealID { fetchValidMenus(mealId: mealId) } else if reviewType == .fixed { - // Fixed 메뉴는 초기 좋아요 상태만 설정 (메뉴명은 DataBind에서 처리) likedStates = [false] setRateView.menuTableView.reloadData() } } } - - /// Delegate 및 DataSource를 설정합니다. + private func setDelegates() { setRateView.menuTableView.register(MenuLikeCell.self, forCellReuseIdentifier: MenuLikeCell.identifier) setRateView.menuTableView.dataSource = self @@ -174,8 +164,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } // MARK: - Data Binding - - /// 일반 리뷰 작성 시 메뉴 목록 데이터를 바인딩합니다. + func dataBind(list: [String], idList: [Int]) { self.selectedList = list self.validMenuIDList = idList @@ -190,8 +179,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.menuTableView.reloadData() } - - /// 리뷰 수정 (Fixed 타입) 시 데이터를 바인딩합니다. (사용되지 않으나 원본 유지) + func dataBindForFix(menuNames: [String], menuIds: [Int], likedStates: [Bool]) { self.selectedList = menuNames self.validMenuIDList = menuIds @@ -202,8 +190,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.menuTableView.reloadData() view.setNeedsLayout() } - - /// 리뷰 수정 모드 시작 시 설정합니다. (리뷰 ID 바인딩) - 기존 방식 + func dataBindForFix(list: [String], reviewId: Int) { self.selectedList = list self.reviewId = reviewId @@ -214,8 +201,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.deleteMethodLabel.isHidden = true setRateView.nextButton.setTitle("리뷰 수정 완료하기", for: .normal) } - - /// ✅ 새로운 dataBindForFix 메서드 (MyReviewViewController에서 사용) + func dataBindForFix( list: [String], reviewId: Int, @@ -225,59 +211,49 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel menuIds: [Int], likedMenuIds: [Int] ) { - // 1. 기본 정보 설정 self.selectedList = list self.reviewId = reviewId self.validMenuIDList = menuIds - - // 2. 리뷰 타입 결정 + if menuIds.count == 1 { self.reviewType = .fixed self.menuID = menuIds.first } else { self.reviewType = .variable } - - // 3. 좋아요 상태 복원 + self.likedStates = menuIds.map { menuId in likedMenuIds.contains(menuId) } - - // 4. UI 업데이트 + setRateView.menuLabel.text = list.count == 1 ? "\(list[0]) 를/을 추천하시겠어요?" : "메뉴를 추천하시겠어요?" - - // 5. 별점 설정 + if let rating = rating { setRateView.rateView.currentStar = rating setRateView.rateView.settingStarForFix(currentStar: rating) } - - // 6. 리뷰 텍스트 설정 + if let content = content, !content.isEmpty { setRateView.userReviewTextView.text = content setRateView.userReviewTextView.textColor = .black setRateView.maximumWordLabel.text = "\(content.count) / 300" } - - // 7. 이미지 설정 + if let firstImageUrl = imageUrls.first, !firstImageUrl.isEmpty { setRateView.userReviewImageView.kfSetImage(url: firstImageUrl) setRateView.updateImageViewState(image: setRateView.userReviewImageView.image, count: 1, isHidden: false) } else { setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) } - - // 8. 버튼 및 이미지 선택 UI 설정 + setRateView.nextButton.setTitle("완료하기", for: .normal) - - // 9. 테이블뷰 리로드 setRateView.menuTableView.reloadData() view.setNeedsLayout() } - /// 수정할 리뷰의 기존 내용을 화면에 표시합니다. + /// 수정할 리뷰의 기존 내용을 화면에 표시 func settingForReviewFix(data: ReviewListItem) { // 별점 설정 setRateView.rateView.currentStar = Int(data.rating) @@ -287,16 +263,14 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel setRateView.userReviewTextView.text = data.content ?? "" setRateView.userReviewTextView.textColor = .black setRateView.maximumWordLabel.text = "\(data.content?.count ?? 0) / 300" - - // 이미지 설정 (kfSetImage는 Kingfisher 확장 가정) + if let imageUrl = data.imageUrls.first, !imageUrl.isEmpty { setRateView.userReviewImageView.kfSetImage(url: imageUrl) setRateView.updateImageViewState(image: setRateView.userReviewImageView.image, count: 1, isHidden: false) } else { setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) } - - // 좋아요 상태 복원 + if let menuLikes = data.menu { self.likedStates = validMenuIDList.map { menuId in return menuLikes.first(where: { $0.menuId == menuId })?.isLike ?? false @@ -307,7 +281,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel // MARK: - Menu Like Logic - /// 리뷰 좋아요/취소 상태를 토글합니다. + /// 리뷰 좋아요/취소 상태를 토글 private func toggleLike(for index: Int) { likedStates[index].toggle() let idx = IndexPath(row: index, section: 0) @@ -320,19 +294,18 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } // MARK: - Image Handling Actions - - /// 이미지 선택 버튼 탭 시 ImagePicker를 표시합니다. + @objc func didSelectedImage() { present(imagePickerController, animated: true) } - - /// 이미지 뷰 탭 또는 삭제 버튼 탭 시 이미지를 삭제합니다. + @objc func didTappedImageView() { userPickedImage = nil setRateView.updateImageViewState(image: nil, count: 0, isHidden: true) } // MARK: - Custom Back Button Action + @objc private func didTapCustomBackButton() { checkReviewStatusAndConfirmExit { [weak self] shouldPop in guard let self = self else { return } @@ -344,17 +317,14 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } // MARK: - Review Submission Logic - - /// 리뷰 작성/수정 버튼 탭 시 호출됩니다. + @objc func tappedNextButton() { - // 1. 유효성 검증 (별점만 필수) guard setRateView.rateView.currentStar != 0 else { showToast(message: "별점을 입력해주세요!", type: .info) return } - // 2. 리뷰 전송 분기 if reviewId != nil { sendFixReview() } else { @@ -367,27 +337,23 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel } } - /// ✅ 리뷰 작성/수정 완료 후 이전 화면으로 돌아갑니다. + /// 리뷰 작성/수정 완료 후 이전 화면 private func moveToReviewVC() { - // 1. MyReviewViewController가 네비게이션 스택에 있는지 확인 if let myReviewVC = navigationController?.viewControllers.first(where: { $0 is MyReviewViewController }) as? MyReviewViewController { navigationController?.popToViewController(myReviewVC, animated: true) return } - - // 2. ReviewViewController가 네비게이션 스택에 있는지 확인 + if let reviewVC = navigationController?.viewControllers.first(where: { $0 is ReviewViewController }) as? ReviewViewController { reviewVC.setReviewSubmittedSuccessfully() navigationController?.popToViewController(reviewVC, animated: true) - - // HomeViewController 새로고침 + if let homeVC = navigationController?.viewControllers.first as? HomeViewController { homeVC.refreshAfterReview() } return } - - // 3. 어느 것도 없으면 그냥 이전 화면으로 + navigationController?.popViewController(animated: true) } } @@ -396,7 +362,7 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel extension SetRateViewController { - /// Meal(Variable) 리뷰 작성을 위한 유효 메뉴 목록을 요청합니다. + /// Meal(Variable) 리뷰 작성을 위한 유효 메뉴 목록을 요청 private func fetchValidMenus(mealId: Int) { NetworkService.shared.request( ReviewRouter.getValidMenusForReview(mealId), @@ -464,7 +430,6 @@ extension SetRateViewController { return } - // Normalize review text: send nil if empty or placeholder let rawText = setRateView.userReviewTextView.text ?? "" let trimmedText = rawText.trimmingCharacters(in: .whitespacesAndNewlines) let content = trimmedText.isEmpty || trimmedText == placeholderText ? nil : trimmedText @@ -508,7 +473,6 @@ extension SetRateViewController { return } - // Normalize review text: send nil if empty or placeholder let rawText = setRateView.userReviewTextView.text ?? "" let trimmedText = rawText.trimmingCharacters(in: .whitespacesAndNewlines) let content = trimmedText.isEmpty || trimmedText == placeholderText ? nil : trimmedText @@ -632,8 +596,7 @@ extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { } cell.dataBind(menu: selectedList[indexPath.row], isLiked: likedStates[indexPath.row]) - - // Controller가 Cell의 좋아요 탭 이벤트를 처리합니다. + cell.onLikeTapped = { [weak self] in guard let self else { return } self.toggleLike(for: indexPath.row) @@ -651,8 +614,7 @@ extension SetRateViewController: UITableViewDataSource, UITableViewDelegate { // MARK: - UITextViewDelegate extension SetRateViewController: UITextViewDelegate { - - // 플레이스홀더 텍스트 + private var placeholderText: String { return "메뉴에 대한 상세한 리뷰를 작성해주세요" } @@ -747,7 +709,6 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UIGestureRecog // MARK: - Keyboard Handling extension SetRateViewController { - // 키보드 등장 시 View를 위로 올립니다. @objc func keyboardWillShow(_ noti: NSNotification) { if let keyboardFrame: NSValue = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardRectangle = keyboardFrame.cgRectValue @@ -757,8 +718,7 @@ extension SetRateViewController { } } } - - // 키보드 사라질 때 View를 원래 위치로 되돌립니다. + @objc func keyboardWillHide(_: NSNotification) { view.transform = .identity navigationController?.isNavigationBarHidden = false From 31263d2962f6f86274e51323477ca564e30f6771 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 18:56:04 +0900 Subject: [PATCH 64/69] =?UTF-8?q?[#321]=20=EC=9E=91=EC=84=B1=20=EC=A4=91?= =?UTF-8?q?=EB=8B=A8=20alert=20text=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ViewController/SetRateViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 47225f4b..f85ac553 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -666,8 +666,8 @@ extension SetRateViewController: UIImagePickerControllerDelegate, UIGestureRecog let isReviewStarted: Bool = setRateView.rateView.currentStar > 0 || textHasContent if reviewId == nil, isReviewStarted { - let title = "작성 취소" - let message = "작성 중인 리뷰는 저장되지 않습니다. 정말 나가시겠습니까?" + let title = "나가시겠어요?" + let message = "지금 나가면 작성한 내용이 저장되지 않습니다." let confirmButtonTitle = "나가기" let cancelButtonTitle = "계속 작성" From 991fec3b4499b09b76cc3516a579356b445c97ed Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 19:24:06 +0900 Subject: [PATCH 65/69] =?UTF-8?q?[#321]=20=EC=9E=91=EC=84=B1=20=ED=9B=84?= =?UTF-8?q?=20ReviewVC=EC=9D=98=20api=20=EC=83=88=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ViewController/ReviewViewController.swift | 9 +++++++++ .../Review/ViewController/SetRateViewController.swift | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 35cd6fcf..254437ed 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -348,6 +348,15 @@ final class ReviewViewController: BaseViewController { Analytics.logEvent("ReviewViewControllerLoad", parameters: nil) #endif } + + /// 작성 후의 새로고침 함수 + func refreshAllData() { + getStatistics() + if type == "VARIABLE" { + getValidMenusForReview() + } + getReviewList(type: type, menuId: menuID) + } } // MARK: - UITableViewDelegate diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index f85ac553..264dcf82 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -348,6 +348,10 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel reviewVC.setReviewSubmittedSuccessfully() navigationController?.popToViewController(reviewVC, animated: true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + reviewVC.refreshAllData() + } + if let homeVC = navigationController?.viewControllers.first as? HomeViewController { homeVC.refreshAfterReview() } From d49623c82da2115b5cb6b1b812cda26973da61d3 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 19:38:17 +0900 Subject: [PATCH 66/69] =?UTF-8?q?[#321]=20=EC=8B=A0=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9A=94=EC=B2=AD=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ViewController/ReportViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift index 5a19a4bd..7b102f18 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift @@ -301,7 +301,8 @@ extension ReportViewController { NetworkService.shared.request( ReviewRouter.report(param: param), - responseType: Bool.self + responseType: Bool.self, + useAuth: true ) { [weak self] result in switch result { case .success: From bdb626e7c3fca6771781557138b8e4c9a2ad6065 Mon Sep 17 00:00:00 2001 From: Funital Date: Sat, 27 Dec 2025 19:42:47 +0900 Subject: [PATCH 67/69] =?UTF-8?q?[#321]=20ReportVC=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Review/ViewController/ReportViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift index 7b102f18..41233d18 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReportViewController.swift @@ -76,7 +76,7 @@ final class ReportViewController: BaseViewController { sendToEATSSUButton.snp.makeConstraints { make in make.leading.trailing.equalTo(view).inset(24) - make.height.equalTo(52) +// make.height.equalTo(52) make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).inset(24) } From f7500bfa70be2a0fd18277bc9d257de1bf4ed0e9 Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 22:59:20 +0900 Subject: [PATCH 68/69] =?UTF-8?q?[#321]=20=EA=B3=A0=EC=A0=95=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EB=A6=AC=EB=B7=B0=EC=9D=98=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=89=BC=ED=91=9C=EB=A1=9C=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../DTO/Review/NewReviewListResponse.swift | 31 +++++++++++++++++-- .../View/SeeReview/ReviewRateViewCell.swift | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift index 5b6a78da..3c5670a8 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Review/NewReviewListResponse.swift @@ -14,7 +14,7 @@ struct NewReviewListResponse: Codable { struct ReviewListItem: Codable { let reviewId: Int - var menu: [ReviewMenuInfo]? + var menu: [ReviewMenuInfo]? // 항상 배열로 저장 let writerId: Int? let isWriter: Bool let writerNickname: String @@ -26,7 +26,8 @@ struct ReviewListItem: Codable { enum CodingKeys: String, CodingKey { case reviewId - case menu = "menuList" + case menu + case menuList case writerId case isWriter case writerNickname @@ -40,7 +41,6 @@ struct ReviewListItem: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) reviewId = try container.decode(Int.self, forKey: .reviewId) - menu = try container.decodeIfPresent([ReviewMenuInfo].self, forKey: .menu) writerId = try container.decodeIfPresent(Int.self, forKey: .writerId) isWriter = try container.decode(Bool.self, forKey: .isWriter) writerNickname = try container.decode(String.self, forKey: .writerNickname) @@ -48,6 +48,17 @@ struct ReviewListItem: Codable { writtenAt = try container.decode(String.self, forKey: .writtenAt) content = try container.decodeIfPresent(String.self, forKey: .content) + // menu 처리: Fixed 메뉴(객체)와 Variable 메뉴(배열) 모두 처리 + if let menuArray = try? container.decodeIfPresent([ReviewMenuInfo].self, forKey: .menuList) { + // Variable 메뉴: menuList가 배열로 들어오는 경우 + menu = menuArray + } else if let singleMenu = try? container.decodeIfPresent(ReviewMenuInfo.self, forKey: .menu) { + // Fixed 메뉴: menu가 단일 객체로 들어오는 경우 -> 배열로 변환 + menu = [singleMenu] + } else { + menu = nil + } + // imageUrls: [String?] 형태로 받아서 nil / 빈 문자열을 제거해 [String]으로 정제 let rawImageUrls = try container.decodeIfPresent([String?].self, forKey: .imageUrls) ?? [] imageUrls = rawImageUrls.compactMap { url in @@ -55,6 +66,20 @@ struct ReviewListItem: Codable { return url } } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(reviewId, forKey: .reviewId) + try container.encodeIfPresent(menu, forKey: .menuList) + try container.encodeIfPresent(writerId, forKey: .writerId) + try container.encode(isWriter, forKey: .isWriter) + try container.encode(writerNickname, forKey: .writerNickname) + try container.encode(rating, forKey: .rating) + try container.encode(writtenAt, forKey: .writtenAt) + try container.encodeIfPresent(content, forKey: .content) + try container.encode(imageUrls, forKey: .imageUrls) + } } struct ReviewMenuInfo: Codable { diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift index 2a1c9a05..49baeec9 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewRateViewCell.swift @@ -321,7 +321,7 @@ final class ReviewRateViewCell: UITableViewCell { /// - Parameter data: 식사 통계 응답 데이터 func configureWithMealStatistics(_ data: ReviewMealStatisticsResponse) { let menuNames = data.menuList.map { $0.name } - menuLabel.text = menuNames.joined(separator: " + ") + menuLabel.text = menuNames.joined(separator: ", ") setRating(data.rating ?? 0) updateRatingChart(with: data.reviewRatingCount, totalCount: data.totalReviewCount) } From ddb0cd68f2d12c1ac43ccbda6fe1ecce2cfed7f8 Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 23:05:49 +0900 Subject: [PATCH 69/69] =?UTF-8?q?[#321]=20=ED=99=88=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=20=EC=89=BC=ED=91=9C=20=EA=B5=AC=EB=B6=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #321 --- .../Home/View/RestaurantTableView/RestaurantMenuItemView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuItemView.swift b/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuItemView.swift index f34b9ebc..c26ba49b 100644 --- a/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuItemView.swift +++ b/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuItemView.swift @@ -90,7 +90,7 @@ final class RestaurantMenuItemView: BaseUIView { func bind(_ model: MenuTypeInfo) { switch model { case let .change(data): - nameLabel.text = data.briefMenus.map(\.name).joined(separator: "+") + nameLabel.text = data.briefMenus.map(\.name).joined(separator: ", ") priceLabel.text = data.price?.formattedWithCommas ?? "" ratingLabel.text = data.rating != nil ? String(format: "%.1f", data.rating!) : "-" case let .fix(data):