From 3c5f43c9374f53bd2e882bb1b0769179e8a1a9f6 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Thu, 28 May 2026 12:22:01 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[feat]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80=20=EC=85=80=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomPageControl.swift | 2 +- .../SelectedContentReasonTableViewCell.swift | 326 ++++++++++++++++-- ...lectedContentReasonTableViewCellItem.swift | 20 +- .../48/ic_add_photo.imageset/Contents.json | 23 ++ .../48/ic_add_photo.imageset/photo_btn.png | Bin 0 -> 1189 bytes .../48/ic_add_photo.imageset/photo_btn@2x.png | Bin 0 -> 2215 bytes .../48/ic_add_photo.imageset/photo_btn@3x.png | Bin 0 -> 3277 bytes .../ic_primary+xmark.imageset/Contents.json | 23 ++ .../Frame 2087330175.png | Bin 0 -> 695 bytes .../Frame 2087330175@2x.png | Bin 0 -> 1274 bytes .../Frame 2087330175@3x.png | Bin 0 -> 1783 bytes .../Sources/View/Scene/Home/HomeView.swift | 1 - .../CreateCollectionViewController.swift | 61 +++- 13 files changed, 414 insertions(+), 42 deletions(-) create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/Contents.json create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn@2x.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn@3x.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_primary+xmark.imageset/Contents.json create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_primary+xmark.imageset/Frame 2087330175.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_primary+xmark.imageset/Frame 2087330175@2x.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_primary+xmark.imageset/Frame 2087330175@3x.png diff --git a/FLINT/Presentation/Sources/View/Component/FlinerRecommendCard/CustomPageControl.swift b/FLINT/Presentation/Sources/View/Component/FlinerRecommendCard/CustomPageControl.swift index 33347eb7..55d919d3 100644 --- a/FLINT/Presentation/Sources/View/Component/FlinerRecommendCard/CustomPageControl.swift +++ b/FLINT/Presentation/Sources/View/Component/FlinerRecommendCard/CustomPageControl.swift @@ -7,7 +7,7 @@ import UIKit -final class CustomPageControl: BaseView { +public final class CustomPageControl: BaseView { // MARK: - Property diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift index 771086b0..98a8efbb 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift @@ -1,4 +1,3 @@ - // // SelectedContentReasonTableViewCell.swift // FLINT @@ -17,8 +16,14 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { public var onToggleSpoiler: ((Bool) -> Void)? public var onChangeReasonText: ((String) -> Void)? public var onTapCloseWithDraft: (() -> Void)? + public var onTapAddPhoto: (() -> Void)? private var isSpoilerOn: Bool = false + private var photos: [UIImage] = [] + + private var photoScrollHeightConstraint: Constraint? + private var pageControlTopConstraint: Constraint? + private var pageControlHeightConstraint: Constraint? //MARK: - UI @@ -36,8 +41,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } private let closeButton = UIButton().then { - $0.setImage(.icCancel, for: .normal) - $0.tintColor = .flintWhite + $0.setImage(.icPrimaryXmark, for: .normal) } private let titleLabel = UILabel().then { @@ -55,16 +59,35 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } private let sectionTitleLabel = UILabel() - private let spoilerLabel = UILabel() + private let textView = FlintTextView(placeholder: "이 작품의 매력 포인트를 적어주세요.") + + private let photoScrollView = UIScrollView().then { + $0.showsHorizontalScrollIndicator = false + $0.isPagingEnabled = true + $0.isHidden = true + } + + private let photoStackView = UIStackView().then { + $0.axis = .horizontal + $0.spacing = 0 + } + + private let pageControl = CustomPageControl().then { + $0.isHidden = true + } + + private let addPhotoButton = UIButton().then { + $0.setImage(.icAddPhoto, for: .normal) + } + private let checkboxToggleView = ToggleBarView( type: .primary, isOn: false, knobSize: 24, contentInset: 2 ) - private let textView = FlintTextView(placeholder: "이 작품의 매력 포인트를 적어주세요.") //MARK: - Setup @@ -74,8 +97,11 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { sectionTitleLabel.attributedText = .pretendard(.head3_m_18, text: "이 작품을 선택한 이유", color: .flintWhite) spoilerLabel.attributedText = .pretendard(.caption1_m_12, text: "스포일러", color: .flintWhite) - + addPhotoButton.imageView?.contentMode = .scaleAspectFill closeButton.addTarget(self, action: #selector(didTapClose), for: .touchUpInside) + addPhotoButton.addTarget(self, action: #selector(didTapAddPhoto), for: .touchUpInside) + + photoScrollView.delegate = self checkboxToggleView.onValueChanged = { [weak self] isOn in self?.onToggleSpoiler?(isOn) @@ -83,15 +109,19 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } public override func setHierarchy() { - contentView.addSubviews(containerView, closeButton) + contentView.addSubview(containerView) containerView.addSubviews( posterImageView, infoContainerView, + photoScrollView, + pageControl, sectionTitleLabel, + textView, + addPhotoButton, spoilerLabel, checkboxToggleView, - textView + closeButton // 항상 최상단 ) infoContainerView.addSubviews( @@ -99,6 +129,8 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { directorLabel, yearLabel ) + + photoScrollView.addSubview(photoStackView) } public override func setLayout() { @@ -111,6 +143,23 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.size.equalTo(24) } + photoScrollView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + photoScrollHeightConstraint = $0.height.equalTo(0).constraint + } + + photoStackView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.height.equalToSuperview() + } + + pageControl.snp.makeConstraints { + pageControlTopConstraint = $0.top.equalTo(photoScrollView.snp.bottom).offset(0).constraint + $0.centerX.equalToSuperview() + pageControlHeightConstraint = $0.height.equalTo(0).constraint + } + posterImageView.snp.makeConstraints { $0.top.equalToSuperview().inset(64) $0.leading.equalToSuperview().inset(16) @@ -145,49 +194,57 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.leading.equalToSuperview().inset(16) } + textView.snp.makeConstraints { + $0.top.equalTo(sectionTitleLabel.snp.bottom).offset(16) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.greaterThanOrEqualTo(104) + } + + addPhotoButton.snp.makeConstraints { + $0.centerY.equalTo(checkboxToggleView.snp.centerY) + $0.leading.equalToSuperview().inset(16) + $0.width.equalTo(48) + $0.height.equalTo(28) + } + checkboxToggleView.snp.makeConstraints { - $0.centerY.equalTo(sectionTitleLabel.snp.centerY) + $0.top.equalTo(textView.snp.bottom).offset(12) $0.trailing.equalToSuperview().inset(16) $0.width.equalTo(44) $0.height.equalTo(28) + $0.bottom.equalToSuperview().inset(16) } spoilerLabel.snp.makeConstraints { - $0.centerY.equalTo(sectionTitleLabel.snp.centerY) + $0.centerY.equalTo(checkboxToggleView.snp.centerY) $0.trailing.equalTo(checkboxToggleView.snp.leading).offset(-8) } - - textView.snp.makeConstraints { - $0.top.equalTo(sectionTitleLabel.snp.bottom).offset(16) - $0.horizontalEdges.equalToSuperview().inset(16) - $0.bottom.equalToSuperview() - $0.height.greaterThanOrEqualTo(104) - } } public override func prepare() { super.prepare() posterImageView.image = nil - titleLabel.attributedText = nil directorLabel.attributedText = nil yearLabel.attributedText = nil - sectionTitleLabel.attributedText = nil - spoilerLabel.attributedText = nil - isSpoilerOn = false checkboxToggleView.setOn(false, animated: true) - textView.text = "" + photos = [] + photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + applyLayout(hasPhotos: false) } public override func prepareForReuse() { super.prepareForReuse() posterImageView.kf.cancelDownloadTask() posterImageView.image = nil + photos = [] + photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + applyLayout(hasPhotos: false) } //MARK: - Configure @@ -198,32 +255,237 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } else { posterImageView.image = item.posterImage } - + titleLabel.attributedText = .pretendard(.head3_m_18, text: item.title, color: .flintWhite) directorLabel.attributedText = .pretendard(.body1_r_16, text: item.director, color: .flintGray300) yearLabel.attributedText = .pretendard(.body1_r_16, text: item.year, color: .flintGray300) - + sectionTitleLabel.attributedText = .pretendard(.head3_m_18, text: "이 작품을 선택한 이유", color: .flintWhite) spoilerLabel.attributedText = .pretendard(.caption1_m_12, text: "스포일러", color: .flintWhite) - - checkboxToggleView.setOn(item.isSpoiler, animated: false) + checkboxToggleView.setOn(item.isSpoiler, animated: false) textView.text = item.reasonText ?? "" - + + configurePhotos(item.photos) + } + + private func configurePhotos(_ newPhotos: [UIImage]) { + photos = newPhotos + photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + let hasPhotos = !photos.isEmpty + applyLayout(hasPhotos: hasPhotos) + + guard hasPhotos else { return } + + pageControl.numberOfPages = photos.count + pageControl.currentPage = 0 + + photos.enumerated().forEach { index, image in + let wrapper = makePhotoWrapper(image: image, index: index) + photoStackView.addArrangedSubview(wrapper) + } + } + + private func applyLayout(hasPhotos: Bool) { + if hasPhotos { + posterImageView.isHidden = true + infoContainerView.isHidden = false // 유지 + + photoScrollView.isHidden = false + photoScrollHeightConstraint?.update(offset: 270) + + pageControl.isHidden = false + pageControlTopConstraint?.update(offset: 8) + pageControlHeightConstraint?.update(offset: 8) + + // infoContainerView → photoScrollView 기준 + infoContainerView.snp.remakeConstraints { + $0.top.equalTo(photoScrollView.snp.bottom).offset(8) + $0.leading.trailing.equalToSuperview().inset(16) + } + + sectionTitleLabel.snp.remakeConstraints { + $0.top.equalTo(infoContainerView.snp.bottom).offset(16) + $0.leading.equalToSuperview().inset(16) + } + + } else { + posterImageView.isHidden = false + infoContainerView.isHidden = false + + photoScrollView.isHidden = true + photoScrollHeightConstraint?.update(offset: 0) + + pageControl.isHidden = true + pageControlTopConstraint?.update(offset: 0) + pageControlHeightConstraint?.update(offset: 0) + + // infoContainerView → posterImageView 기준 (원래대로) + infoContainerView.snp.remakeConstraints { + $0.top.equalTo(posterImageView.snp.top) + $0.leading.equalTo(posterImageView.snp.trailing).offset(16) + $0.trailing.equalToSuperview().inset(24) + } + + sectionTitleLabel.snp.remakeConstraints { + $0.top.equalTo(posterImageView.snp.bottom).offset(16) + $0.leading.equalToSuperview().inset(16) + } + } + } + + private func makePhotoWrapper(image: UIImage, index: Int) -> UIView { + let wrapper = UIView().then { + $0.clipsToBounds = true + } + + let imageView = UIImageView().then { + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + $0.image = image + } + + let deleteButton = UIButton().then { + $0.setImage(.icDeselect, for: .normal) + $0.tag = index + $0.addTarget(self, action: #selector(didTapDeletePhoto(_:)), for: .touchUpInside) + } + + wrapper.addSubview(imageView) + wrapper.addSubview(deleteButton) + + imageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + deleteButton.snp.makeConstraints { + $0.top.trailing.equalToSuperview().inset(12) + $0.size.equalTo(48) + } + + wrapper.snp.makeConstraints { + $0.width.equalTo(UIScreen.main.bounds.width) + $0.height.equalTo(270) + } + + return wrapper } //MARK: - Action @objc private func didTapClose() { let text = (textView.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - let hasDraft = !text.isEmpty - - if hasDraft { - onTapCloseWithDraft?() - } else { + if text.isEmpty { onTapClose?() + } else { + onTapCloseWithDraft?() } } + + @objc private func didTapAddPhoto() { + onTapAddPhoto?() + } + + @objc private func didTapDeletePhoto(_ sender: UIButton) { + let index = sender.tag + guard index < photos.count else { return } + photos.remove(at: index) + configurePhotos(photos) + } +} + +// MARK: - UIScrollViewDelegate +extension SelectedContentReasonTableViewCell: UIScrollViewDelegate { + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard scrollView.bounds.width > 0 else { return } + let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width)) + pageControl.currentPage = page + } } + +#if DEBUG +import UIKit +import PhotosUI + +public final class SelectedContentReasonPreviewViewController: UIViewController { + + private let tableView = UITableView() + + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + + tableView.backgroundColor = .black + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 600 + tableView.register( + SelectedContentReasonTableViewCell.self, + forCellReuseIdentifier: "cell" + ) + + view.addSubview(tableView) + tableView.snp.makeConstraints { $0.edges.equalToSuperview() } + } +} + +extension SelectedContentReasonPreviewViewController: UITableViewDataSource { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell( + withIdentifier: "cell", + for: indexPath + ) as! SelectedContentReasonTableViewCell + cell.configure(with: .mock) + cell.onTapAddPhoto = { [weak self] in + var config = PHPickerConfiguration() + config.selectionLimit = 5 + config.filter = .images + + let picker = PHPickerViewController(configuration: config) + picker.delegate = self + self?.present(picker, animated: true) + } + return cell + } +} + +extension SelectedContentReasonPreviewViewController: PHPickerViewControllerDelegate { + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true) + + guard let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? SelectedContentReasonTableViewCell else { return } + + let group = DispatchGroup() + var images: [UIImage] = [] + let lock = NSLock() + + for result in results { + group.enter() + result.itemProvider.loadObject(ofClass: UIImage.self) { object, _ in + if let image = object as? UIImage { + lock.lock() + images.append(image) + lock.unlock() + } + group.leave() + } + } + + group.notify(queue: .main) { [weak self] in + var item = SelectedContentReasonTableViewCellItem.mock + item.photos = images + cell.configure(with: item) + + // 셀 높이 재계산 + self?.tableView.beginUpdates() + self?.tableView.endUpdates() + } + } +} +#endif diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift index 8d80bc89..99f0c084 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift @@ -17,6 +17,7 @@ public struct SelectedContentReasonTableViewCellItem { public var isSpoiler: Bool public var reasonText: String? + public var photos: [UIImage] public init( contentId: Int64, @@ -26,7 +27,8 @@ public struct SelectedContentReasonTableViewCellItem { director: String, year: String, isSpoiler: Bool = false, - reasonText: String? = nil + reasonText: String? = nil, + photos: [UIImage] = [] ) { self.contentId = contentId self.posterURL = posterURL @@ -36,5 +38,21 @@ public struct SelectedContentReasonTableViewCellItem { self.year = year self.isSpoiler = isSpoiler self.reasonText = reasonText + self.photos = photos } } + +#if DEBUG +extension SelectedContentReasonTableViewCellItem { + @MainActor static let mock = SelectedContentReasonTableViewCellItem( + contentId: 0, + posterURL: nil, + posterImage: .imgTving, + title: "컨택트", + director: "드니 빌뇌브", + year: "2016", + isSpoiler: false, + reasonText: "" + ) +} +#endif diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/Contents.json b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/Contents.json new file mode 100644 index 00000000..2cc5e6f1 --- /dev/null +++ b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "photo_btn.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "photo_btn@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "photo_btn@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn.png b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn.png new file mode 100644 index 0000000000000000000000000000000000000000..21bc4689a315b875cccaf0c0e230f81d855f8aec GIT binary patch literal 1189 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 literal 0 HcmV?d00001 diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn@3x.png b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_add_photo.imageset/photo_btn@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eccf7c7e11cb55b81658212eef78c1c691deb653 GIT binary patch literal 3277 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 literal 0 HcmV?d00001 diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_primary+xmark.imageset/Frame 2087330175@3x.png b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_primary+xmark.imageset/Frame 2087330175@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..29868b805830218b1661756b62503788e5832f94 GIT binary patch literal 1783 zcmVz-O5?LW*BI|cKKWfE;l)_=Xe4r66Qbvp=@a4>v01bP0#tv1H~v48cW2FT*HfA1qI;6 zHSy&@rCjMkm)$EW${V=(LR2JH0WWkP#qP=$D@|x5a$8k27DcvL;Xu)QZq0_i7Itp4um27WJxtNqC`oCd zBT!!!gtw+)dcADDT1se?61lVq<3Z`hs&J!3#XOA6v5`q&0Notg?s?%fu&_|7Ge^Ie zr@>Y_Fm+h;=t4p%VIS27s0eiJl+hG9c3>S!d8QBkYnXsy`PN^~Y_5x3p;5>0$#C6D zN~MjB)R%dURBn-{7JVYKk2z|j@(#!pO7$j;SBqW{wzBA6>}38nZD*8k{s|MZ$SnG) z5sLNZHq_)t`#${s)x+S=?$ZaL8^Px@7xnqx;ZB5#h)z1;ZPD)GGKndeT1BCH2HvVlHB}}- z)7UM{p`L*z%1Sj^W&mT4@97@mhy`Sc>fLX&OoVZ;NvI8)DI+xv6J!$P(qe`T!VN0^ zS(qX-#xiMCv#eCpWCBGf7)DX42FeUDPUx^>oY11f7P`i$20=nSFod#WKn6-}8xx^b z2h>7nF+vbU3il7F2oKf1ioT8EaEscYb`1TBjm z8EfF78BMZ<7VqJ_sOfj(v=(+;EUo5p(PbpLhFKf9MlRPc+EPNmUP9R2}#$ zOd>i7>+UhE_L4hkbL@4E0cA-QF~>d`okVKC-J*L*&okCq$ZF^5efT=6M@ewqXM*Y? z{CqGv&sJx?xw|`s1^3*Zf~m<<$T`qVyJT@EQink3Ky2~fz=RC=ZkkLYx2t~1y}&4C zW4J6v9^5-~W}jsJHg}M&3;w#uWPjLUz}r6fPTn!OLc_4;wOjpB!N+@+tfC$>a2Wa2 znSISB^(jzb4O(x6C_LPWz54FBu8c!BJEzW!Q{>pf6CG$RIE+;7t8lA_x9Hl79$jic zp7K+?-*=Nsp^q-yqef^@z1d5tsW|k;^a0}$sk~KICQ*8B6MaAhVyh!Fd>;V}e`6KYm+MuE zcoMdhnGA z`hqHIWEd7{{!XRD{&O&ZCHH#80^bgw-Dl6}`P1<0)aW%Sn|t&#nt4q_HScL#*Ef76 Z`46&ft;2XBb1MJ<002ovPDHLkV1g<`O4k4Y literal 0 HcmV?d00001 diff --git a/FLINT/Presentation/Sources/View/Scene/Home/HomeView.swift b/FLINT/Presentation/Sources/View/Scene/Home/HomeView.swift index 2c94b57e..de26ca08 100644 --- a/FLINT/Presentation/Sources/View/Scene/Home/HomeView.swift +++ b/FLINT/Presentation/Sources/View/Scene/Home/HomeView.swift @@ -22,7 +22,6 @@ public final class HomeView: BaseView { $0.showsVerticalScrollIndicator = false } - // 이부분이추가 public let flinerCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index 360034b6..12cdbeea 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -12,6 +12,8 @@ import Domain import View import ViewModel +import PhotosUI + public protocol CreateCollectionViewControllerFactory { func makeCreateCollectionViewController() -> CreateCollectionViewController } @@ -36,6 +38,7 @@ public final class CreateCollectionViewController: BaseViewController [CreateCollectionEntity.CreateCollectionContents] { return selectedReasonItems.map { item in - return CreateCollectionEntity.CreateCollectionContents( contentId: item.contentId, isSpoiler: item.isSpoiler, @@ -168,7 +171,8 @@ private extension CreateCollectionViewController { return SelectedContentReasonTableViewCellItem( contentId: model.contentId, posterURL: model.posterURL, - posterImage: model.posterImage, title: model.title, + posterImage: model.posterImage, + title: model.title, director: model.director, year: model.year, isSpoiler: false, @@ -179,7 +183,6 @@ private extension CreateCollectionViewController { updateCreatePayload() } - func presentAddContentSelect() { guard let factory = viewControllerFactory else { return } @@ -239,6 +242,16 @@ private extension CreateCollectionViewController { modalRef = modal modal.show(in: hostView) } + + func presentPhotoPicker() { + var config = PHPickerConfiguration() + config.selectionLimit = 5 + config.filter = .images + + let picker = PHPickerViewController(configuration: config) + picker.delegate = self + present(picker, animated: true) + } } // MARK: - UITableViewDataSource @@ -341,7 +354,6 @@ extension CreateCollectionViewController: UITableViewDataSource { cell.onTapClose = { [weak self, weak cell] in guard let self, let cell, let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } - let reasonIndex = indexPath.row - 1 let item = self.selectedReasonItems[reasonIndex] self.deleteReasonItem(item, at: reasonIndex) @@ -350,7 +362,6 @@ extension CreateCollectionViewController: UITableViewDataSource { cell.onTapCloseWithDraft = { [weak self, weak cell] in guard let self, let cell, let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } - let reasonIndex = indexPath.row - 1 let item = self.selectedReasonItems[reasonIndex] self.presentDeleteConfirmModal { @@ -361,7 +372,6 @@ extension CreateCollectionViewController: UITableViewDataSource { cell.onToggleSpoiler = { [weak self, weak cell] isOn in guard let self, let cell, let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } - let reasonIndex = indexPath.row - 1 self.selectedReasonItems[reasonIndex].isSpoiler = isOn self.updateCreatePayload() @@ -370,12 +380,18 @@ extension CreateCollectionViewController: UITableViewDataSource { cell.onChangeReasonText = { [weak self, weak cell] text in guard let self, let cell, let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } - let reasonIndex = indexPath.row - 1 self.selectedReasonItems[reasonIndex].reasonText = text self.updateCreatePayload() } + cell.onTapAddPhoto = { [weak self, weak cell] in + guard let self, let cell, + let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } + self.currentPhotoPickerIndex = indexPath.row - 1 + self.presentPhotoPicker() + } + return cell } @@ -404,3 +420,34 @@ extension CreateCollectionViewController: UITableViewDelegate { return UITableView.automaticDimension } } + +// MARK: - PHPickerViewControllerDelegate + +extension CreateCollectionViewController: PHPickerViewControllerDelegate { + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true) + guard let index = currentPhotoPickerIndex else { return } + + let group = DispatchGroup() + var images: [UIImage] = [] + + for result in results { + group.enter() + result.itemProvider.loadObject(ofClass: UIImage.self) { object, _ in + if let image = object as? UIImage { + images.append(image) + } + group.leave() + } + } + + group.notify(queue: .main) { [weak self] in + guard let self else { return } + self.selectedReasonItems[index].photos = images + self.rootView.tableView.reloadRows( + at: [IndexPath(row: index + 1, section: 1)], + with: .none + ) + } + } +} From ce1f19e20a1250c3cea8b9c9fb9c4222df161f95 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Thu, 28 May 2026 17:14:02 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[fix]=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=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 --- .../SelectedContentReasonTableViewCell.swift | 113 ++++++++---------- .../CreateCollectionViewController.swift | 1 + 2 files changed, 48 insertions(+), 66 deletions(-) diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift index 98a8efbb..76fbd527 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift @@ -17,6 +17,11 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { public var onChangeReasonText: ((String) -> Void)? public var onTapCloseWithDraft: (() -> Void)? public var onTapAddPhoto: (() -> Void)? + public var onPhotosChanged: (() -> Void)? + + public var currentReasonText: String { + return textView.text ?? "" + } private var isSpoilerOn: Bool = false private var photos: [UIImage] = [] @@ -121,7 +126,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { addPhotoButton, spoilerLabel, checkboxToggleView, - closeButton // 항상 최상단 + closeButton ) infoContainerView.addSubviews( @@ -143,23 +148,6 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.size.equalTo(24) } - photoScrollView.snp.makeConstraints { - $0.top.equalToSuperview() - $0.horizontalEdges.equalToSuperview() - photoScrollHeightConstraint = $0.height.equalTo(0).constraint - } - - photoStackView.snp.makeConstraints { - $0.edges.equalToSuperview() - $0.height.equalToSuperview() - } - - pageControl.snp.makeConstraints { - pageControlTopConstraint = $0.top.equalTo(photoScrollView.snp.bottom).offset(0).constraint - $0.centerX.equalToSuperview() - pageControlHeightConstraint = $0.height.equalTo(0).constraint - } - posterImageView.snp.makeConstraints { $0.top.equalToSuperview().inset(64) $0.leading.equalToSuperview().inset(16) @@ -189,8 +177,25 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.bottom.equalToSuperview().inset(24) } - sectionTitleLabel.snp.makeConstraints { + photoScrollView.snp.makeConstraints { $0.top.equalTo(posterImageView.snp.bottom).offset(16) + $0.horizontalEdges.equalToSuperview() + photoScrollHeightConstraint = $0.height.equalTo(0).constraint + } + + photoStackView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.height.equalToSuperview() + } + + pageControl.snp.makeConstraints { + pageControlTopConstraint = $0.top.equalTo(photoScrollView.snp.bottom).offset(0).constraint + $0.centerX.equalToSuperview() + pageControlHeightConstraint = $0.height.equalTo(0).constraint + } + + sectionTitleLabel.snp.makeConstraints { + $0.top.equalTo(pageControl.snp.bottom).offset(16) $0.leading.equalToSuperview().inset(16) } @@ -288,50 +293,19 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } private func applyLayout(hasPhotos: Bool) { - if hasPhotos { - posterImageView.isHidden = true - infoContainerView.isHidden = false // 유지 - - photoScrollView.isHidden = false - photoScrollHeightConstraint?.update(offset: 270) - - pageControl.isHidden = false - pageControlTopConstraint?.update(offset: 8) - pageControlHeightConstraint?.update(offset: 8) - - // infoContainerView → photoScrollView 기준 - infoContainerView.snp.remakeConstraints { - $0.top.equalTo(photoScrollView.snp.bottom).offset(8) - $0.leading.trailing.equalToSuperview().inset(16) - } - - sectionTitleLabel.snp.remakeConstraints { - $0.top.equalTo(infoContainerView.snp.bottom).offset(16) - $0.leading.equalToSuperview().inset(16) - } - - } else { - posterImageView.isHidden = false - infoContainerView.isHidden = false - - photoScrollView.isHidden = true - photoScrollHeightConstraint?.update(offset: 0) - - pageControl.isHidden = true - pageControlTopConstraint?.update(offset: 0) - pageControlHeightConstraint?.update(offset: 0) - - // infoContainerView → posterImageView 기준 (원래대로) - infoContainerView.snp.remakeConstraints { - $0.top.equalTo(posterImageView.snp.top) - $0.leading.equalTo(posterImageView.snp.trailing).offset(16) - $0.trailing.equalToSuperview().inset(24) - } - - sectionTitleLabel.snp.remakeConstraints { - $0.top.equalTo(posterImageView.snp.bottom).offset(16) - $0.leading.equalToSuperview().inset(16) - } + posterImageView.isHidden = false + infoContainerView.isHidden = false + + photoScrollView.isHidden = !hasPhotos + photoScrollHeightConstraint?.update(offset: hasPhotos ? 270 : 0) + + pageControl.isHidden = !hasPhotos + pageControlTopConstraint?.update(offset: hasPhotos ? 8 : 0) + pageControlHeightConstraint?.update(offset: hasPhotos ? 8 : 0) + + sectionTitleLabel.snp.remakeConstraints { + $0.top.equalTo(pageControl.snp.bottom).offset(16) + $0.leading.equalToSuperview().inset(16) } } @@ -392,6 +366,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { guard index < photos.count else { return } photos.remove(at: index) configurePhotos(photos) + onPhotosChanged?() } } @@ -451,6 +426,10 @@ extension SelectedContentReasonPreviewViewController: UITableViewDataSource { picker.delegate = self self?.present(picker, animated: true) } + cell.onPhotosChanged = { [weak self] in + self?.tableView.beginUpdates() + self?.tableView.endUpdates() + } return cell } } @@ -478,13 +457,15 @@ extension SelectedContentReasonPreviewViewController: PHPickerViewControllerDele } group.notify(queue: .main) { [weak self] in + guard let self else { return } + var item = SelectedContentReasonTableViewCellItem.mock item.photos = images + item.reasonText = cell.currentReasonText cell.configure(with: item) - // 셀 높이 재계산 - self?.tableView.beginUpdates() - self?.tableView.endUpdates() + self.tableView.beginUpdates() + self.tableView.endUpdates() } } } diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index 12cdbeea..5cf58bd3 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -386,6 +386,7 @@ extension CreateCollectionViewController: UITableViewDataSource { } cell.onTapAddPhoto = { [weak self, weak cell] in + guard let self, let cell, let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } self.currentPhotoPickerIndex = indexPath.row - 1 From d750805d89ea53da9b1c5da3c9091cfcbf67c9c8 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Thu, 28 May 2026 19:50:48 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[feat]=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SelectedContentReasonTableViewCell.swift | 42 +++++++++++++++---- ...lectedContentReasonTableViewCellItem.swift | 4 +- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift index 76fbd527..4c30af31 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift @@ -286,10 +286,20 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { pageControl.numberOfPages = photos.count pageControl.currentPage = 0 - photos.enumerated().forEach { index, image in - let wrapper = makePhotoWrapper(image: image, index: index) + let infinitePhotos = [photos.last!] + photos + [photos.first!] + + infinitePhotos.enumerated().forEach { index, image in + let isReal = index >= 1 && index <= photos.count + let realIndex = isReal ? index - 1 : (index == 0 ? photos.count - 1 : 0) + let wrapper = makePhotoWrapper(image: image, realIndex: realIndex) photoStackView.addArrangedSubview(wrapper) } + + DispatchQueue.main.async { + let width = self.photoScrollView.bounds.width + guard width > 0 else { return } + self.photoScrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) + } } private func applyLayout(hasPhotos: Bool) { @@ -309,7 +319,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } } - private func makePhotoWrapper(image: UIImage, index: Int) -> UIView { + private func makePhotoWrapper(image: UIImage, realIndex: Int) -> UIView { let wrapper = UIView().then { $0.clipsToBounds = true } @@ -322,7 +332,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { let deleteButton = UIButton().then { $0.setImage(.icDeselect, for: .normal) - $0.tag = index + $0.tag = realIndex $0.addTarget(self, action: #selector(didTapDeletePhoto(_:)), for: .touchUpInside) } @@ -374,9 +384,27 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { extension SelectedContentReasonTableViewCell: UIScrollViewDelegate { public func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard scrollView.bounds.width > 0 else { return } - let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width)) - pageControl.currentPage = page + let width = scrollView.bounds.width + guard width > 0, photos.count > 0 else { return } + + let page = Int(round(scrollView.contentOffset.x / width)) + let realPage = (page - 1 + photos.count) % photos.count + pageControl.currentPage = realPage + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + let width = scrollView.bounds.width + guard width > 0, photos.count > 0 else { return } + + let page = Int(round(scrollView.contentOffset.x / width)) + + if page == 0 { + scrollView.setContentOffset(CGPoint(x: width * CGFloat(photos.count), y: 0), animated: false) + } + + else if page == photos.count + 1 { + scrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) + } } } diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift index 99f0c084..d420ac53 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift @@ -48,8 +48,8 @@ extension SelectedContentReasonTableViewCellItem { contentId: 0, posterURL: nil, posterImage: .imgTving, - title: "컨택트", - director: "드니 빌뇌브", + title: "영화이름 어어어어어엄 청길게 ", + director: "감독이름도 어어어엄청 긴이름", year: "2016", isSpoiler: false, reasonText: "" From 7d8198d98c78d6b9ce56e766c7d2901b574c2ab4 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Fri, 29 May 2026 22:24:36 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[refactor]=20createviewcontroller=20=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 --- .../SelectedContentReasonTableViewCell.swift | 181 +++++++++--------- .../48/ic_black_xmark.imageset/Contents.json | 23 +++ .../Frame 2087330175.png | Bin 0 -> 675 bytes .../Frame 2087330175@2x.png | Bin 0 -> 1139 bytes .../Frame 2087330175@3x.png | Bin 0 -> 1682 bytes .../CreateCollectionViewController.swift | 25 ++- 6 files changed, 132 insertions(+), 97 deletions(-) create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Contents.json create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175@2x.png create mode 100644 FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175@3x.png diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift index 4c30af31..4ff302f7 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift @@ -12,6 +12,19 @@ import Then public final class SelectedContentReasonTableViewCell: BaseTableViewCell { + // MARK: - Metric + + private enum Metric { + static let posterTop: CGFloat = 64 + static let posterLeading: CGFloat = 16 + static let posterWidth: CGFloat = 100 + static let posterHeight: CGFloat = 150 + static let photoHeight: CGFloat = 270 + static let horizontalInset: CGFloat = 16 + } + + // MARK: - Closure + public var onTapClose: (() -> Void)? public var onToggleSpoiler: ((Bool) -> Void)? public var onChangeReasonText: ((String) -> Void)? @@ -19,23 +32,26 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { public var onTapAddPhoto: (() -> Void)? public var onPhotosChanged: (() -> Void)? - public var currentReasonText: String { - return textView.text ?? "" - } + public var currentReasonText: String { textView.text ?? "" } + public var currentPhotos: [UIImage] { photos } - private var isSpoilerOn: Bool = false - private var photos: [UIImage] = [] + // MARK: - State + private var photos: [UIImage] = [] private var photoScrollHeightConstraint: Constraint? private var pageControlTopConstraint: Constraint? private var pageControlHeightConstraint: Constraint? - //MARK: - UI + // MARK: - UI private let containerView = UIView().then { $0.backgroundColor = .clear } + private let closeButton = UIButton().then { + $0.setImage(.icPrimaryXmark, for: .normal) + } + private let posterImageView = UIImageView().then { $0.contentMode = .scaleAspectFill $0.clipsToBounds = true @@ -45,10 +61,6 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.backgroundColor = .clear } - private let closeButton = UIButton().then { - $0.setImage(.icPrimaryXmark, for: .normal) - } - private let titleLabel = UILabel().then { $0.numberOfLines = 2 $0.lineBreakMode = .byTruncatingTail @@ -63,11 +75,6 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.numberOfLines = 1 } - private let sectionTitleLabel = UILabel() - private let spoilerLabel = UILabel() - - private let textView = FlintTextView(placeholder: "이 작품의 매력 포인트를 적어주세요.") - private let photoScrollView = UIScrollView().then { $0.showsHorizontalScrollIndicator = false $0.isPagingEnabled = true @@ -83,10 +90,16 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { $0.isHidden = true } + private let sectionTitleLabel = UILabel() + + private let textView = FlintTextView(placeholder: "이 작품의 매력 포인트를 적어주세요.") + private let addPhotoButton = UIButton().then { $0.setImage(.icAddPhoto, for: .normal) } + private let spoilerLabel = UILabel() + private let checkboxToggleView = ToggleBarView( type: .primary, isOn: false, @@ -94,7 +107,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { contentInset: 2 ) - //MARK: - Setup + // MARK: - Setup public override func setStyle() { backgroundColor = .clear @@ -103,9 +116,9 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { sectionTitleLabel.attributedText = .pretendard(.head3_m_18, text: "이 작품을 선택한 이유", color: .flintWhite) spoilerLabel.attributedText = .pretendard(.caption1_m_12, text: "스포일러", color: .flintWhite) addPhotoButton.imageView?.contentMode = .scaleAspectFill + closeButton.addTarget(self, action: #selector(didTapClose), for: .touchUpInside) addPhotoButton.addTarget(self, action: #selector(didTapAddPhoto), for: .touchUpInside) - photoScrollView.delegate = self checkboxToggleView.onValueChanged = { [weak self] isOn in @@ -129,12 +142,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { closeButton ) - infoContainerView.addSubviews( - titleLabel, - directorLabel, - yearLabel - ) - + infoContainerView.addSubviews(titleLabel, directorLabel, yearLabel) photoScrollView.addSubview(photoStackView) } @@ -149,14 +157,14 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } posterImageView.snp.makeConstraints { - $0.top.equalToSuperview().inset(64) - $0.leading.equalToSuperview().inset(16) - $0.width.equalTo(100) - $0.height.equalTo(150) + $0.top.equalToSuperview().inset(Metric.posterTop) + $0.leading.equalToSuperview().inset(Metric.posterLeading) + $0.width.equalTo(Metric.posterWidth) + $0.height.equalTo(Metric.posterHeight) } infoContainerView.snp.makeConstraints { - $0.top.equalTo(posterImageView.snp.top) + $0.top.equalTo(posterImageView) $0.leading.equalTo(posterImageView.snp.trailing).offset(16) $0.trailing.equalToSuperview().inset(24) } @@ -168,7 +176,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { directorLabel.snp.makeConstraints { $0.top.equalTo(titleLabel.snp.bottom).offset(12) - $0.leading.trailing.equalToSuperview() + $0.horizontalEdges.equalToSuperview() } yearLabel.snp.makeConstraints { @@ -196,63 +204,56 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { sectionTitleLabel.snp.makeConstraints { $0.top.equalTo(pageControl.snp.bottom).offset(16) - $0.leading.equalToSuperview().inset(16) + $0.leading.equalToSuperview().inset(Metric.horizontalInset) } textView.snp.makeConstraints { $0.top.equalTo(sectionTitleLabel.snp.bottom).offset(16) - $0.horizontalEdges.equalToSuperview().inset(16) + $0.horizontalEdges.equalToSuperview().inset(Metric.horizontalInset) $0.height.greaterThanOrEqualTo(104) } addPhotoButton.snp.makeConstraints { - $0.centerY.equalTo(checkboxToggleView.snp.centerY) - $0.leading.equalToSuperview().inset(16) + $0.centerY.equalTo(checkboxToggleView) + $0.leading.equalToSuperview().inset(Metric.horizontalInset) $0.width.equalTo(48) $0.height.equalTo(28) } checkboxToggleView.snp.makeConstraints { $0.top.equalTo(textView.snp.bottom).offset(12) - $0.trailing.equalToSuperview().inset(16) + $0.trailing.equalToSuperview().inset(Metric.horizontalInset) $0.width.equalTo(44) $0.height.equalTo(28) $0.bottom.equalToSuperview().inset(16) } spoilerLabel.snp.makeConstraints { - $0.centerY.equalTo(checkboxToggleView.snp.centerY) + $0.centerY.equalTo(checkboxToggleView) $0.trailing.equalTo(checkboxToggleView.snp.leading).offset(-8) } } public override func prepare() { super.prepare() - posterImageView.image = nil titleLabel.attributedText = nil directorLabel.attributedText = nil yearLabel.attributedText = nil - isSpoilerOn = false checkboxToggleView.setOn(false, animated: true) textView.text = "" - - photos = [] - photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - applyLayout(hasPhotos: false) + resetPhotos() } public override func prepareForReuse() { super.prepareForReuse() posterImageView.kf.cancelDownloadTask() posterImageView.image = nil - photos = [] - photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - applyLayout(hasPhotos: false) + resetPhotos() } - //MARK: - Configure + // MARK: - Configure public func configure(with item: SelectedContentReasonTableViewCellItem) { if let url = item.posterURL { @@ -264,7 +265,6 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { titleLabel.attributedText = .pretendard(.head3_m_18, text: item.title, color: .flintWhite) directorLabel.attributedText = .pretendard(.body1_r_16, text: item.director, color: .flintGray300) yearLabel.attributedText = .pretendard(.body1_r_16, text: item.year, color: .flintGray300) - sectionTitleLabel.attributedText = .pretendard(.head3_m_18, text: "이 작품을 선택한 이유", color: .flintWhite) spoilerLabel.attributedText = .pretendard(.caption1_m_12, text: "스포일러", color: .flintWhite) @@ -274,55 +274,62 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { configurePhotos(item.photos) } + // MARK: - Private + + private var isSpoilerOn: Bool = false + + private func resetPhotos() { + photos = [] + photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + applyPhotoLayout(hasPhotos: false) + } + private func configurePhotos(_ newPhotos: [UIImage]) { photos = newPhotos photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - let hasPhotos = !photos.isEmpty - applyLayout(hasPhotos: hasPhotos) - - guard hasPhotos else { return } + applyPhotoLayout(hasPhotos: !photos.isEmpty) + guard !photos.isEmpty else { return } pageControl.numberOfPages = photos.count pageControl.currentPage = 0 + setupInfiniteScroll() + } + + private func setupInfiniteScroll() { let infinitePhotos = [photos.last!] + photos + [photos.first!] infinitePhotos.enumerated().forEach { index, image in - let isReal = index >= 1 && index <= photos.count - let realIndex = isReal ? index - 1 : (index == 0 ? photos.count - 1 : 0) - let wrapper = makePhotoWrapper(image: image, realIndex: realIndex) - photoStackView.addArrangedSubview(wrapper) + let realIndex: Int + if index == 0 { realIndex = photos.count - 1 } + else if index == photos.count + 1 { realIndex = 0 } + else { realIndex = index - 1 } + + photoStackView.addArrangedSubview(makePhotoWrapper(image: image, realIndex: realIndex)) } - DispatchQueue.main.async { - let width = self.photoScrollView.bounds.width - guard width > 0 else { return } - self.photoScrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) - } + layoutIfNeeded() + let width = photoScrollView.bounds.width + guard width > 0 else { return } + photoScrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) } - private func applyLayout(hasPhotos: Bool) { - posterImageView.isHidden = false - infoContainerView.isHidden = false - + private func applyPhotoLayout(hasPhotos: Bool) { photoScrollView.isHidden = !hasPhotos - photoScrollHeightConstraint?.update(offset: hasPhotos ? 270 : 0) - + photoScrollHeightConstraint?.update(offset: hasPhotos ? Metric.photoHeight : 0) pageControl.isHidden = !hasPhotos pageControlTopConstraint?.update(offset: hasPhotos ? 8 : 0) pageControlHeightConstraint?.update(offset: hasPhotos ? 8 : 0) sectionTitleLabel.snp.remakeConstraints { $0.top.equalTo(pageControl.snp.bottom).offset(16) - $0.leading.equalToSuperview().inset(16) + $0.leading.equalToSuperview().inset(Metric.horizontalInset) } } private func makePhotoWrapper(image: UIImage, realIndex: Int) -> UIView { - let wrapper = UIView().then { - $0.clipsToBounds = true - } + let wrapper = UIView().then { $0.clipsToBounds = true } let imageView = UIImageView().then { $0.contentMode = .scaleAspectFill @@ -331,17 +338,14 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { } let deleteButton = UIButton().then { - $0.setImage(.icDeselect, for: .normal) + $0.setImage(.icBlackXmark, for: .normal) $0.tag = realIndex $0.addTarget(self, action: #selector(didTapDeletePhoto(_:)), for: .touchUpInside) } - wrapper.addSubview(imageView) - wrapper.addSubview(deleteButton) + wrapper.addSubviews(imageView, deleteButton) - imageView.snp.makeConstraints { - $0.edges.equalToSuperview() - } + imageView.snp.makeConstraints { $0.edges.equalToSuperview() } deleteButton.snp.makeConstraints { $0.top.trailing.equalToSuperview().inset(12) @@ -350,21 +354,17 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { wrapper.snp.makeConstraints { $0.width.equalTo(UIScreen.main.bounds.width) - $0.height.equalTo(270) + $0.height.equalTo(Metric.photoHeight) } return wrapper } - //MARK: - Action + // MARK: - Action @objc private func didTapClose() { let text = (textView.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - if text.isEmpty { - onTapClose?() - } else { - onTapCloseWithDraft?() - } + text.isEmpty ? onTapClose?() : onTapCloseWithDraft?() } @objc private func didTapAddPhoto() { @@ -385,24 +385,19 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { extension SelectedContentReasonTableViewCell: UIScrollViewDelegate { public func scrollViewDidScroll(_ scrollView: UIScrollView) { let width = scrollView.bounds.width - guard width > 0, photos.count > 0 else { return } - + guard width > 0, !photos.isEmpty else { return } let page = Int(round(scrollView.contentOffset.x / width)) - let realPage = (page - 1 + photos.count) % photos.count - pageControl.currentPage = realPage + pageControl.currentPage = (page - 1 + photos.count) % photos.count } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let width = scrollView.bounds.width - guard width > 0, photos.count > 0 else { return } - + guard width > 0, !photos.isEmpty else { return } let page = Int(round(scrollView.contentOffset.x / width)) if page == 0 { scrollView.setContentOffset(CGPoint(x: width * CGFloat(photos.count), y: 0), animated: false) - } - - else if page == photos.count + 1 { + } else if page == photos.count + 1 { scrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) } } @@ -449,7 +444,6 @@ extension SelectedContentReasonPreviewViewController: UITableViewDataSource { var config = PHPickerConfiguration() config.selectionLimit = 5 config.filter = .images - let picker = PHPickerViewController(configuration: config) picker.delegate = self self?.present(picker, animated: true) @@ -465,7 +459,6 @@ extension SelectedContentReasonPreviewViewController: UITableViewDataSource { extension SelectedContentReasonPreviewViewController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) - guard let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? SelectedContentReasonTableViewCell else { return } let group = DispatchGroup() @@ -486,12 +479,10 @@ extension SelectedContentReasonPreviewViewController: PHPickerViewControllerDele group.notify(queue: .main) { [weak self] in guard let self else { return } - var item = SelectedContentReasonTableViewCellItem.mock item.photos = images item.reasonText = cell.currentReasonText cell.configure(with: item) - self.tableView.beginUpdates() self.tableView.endUpdates() } diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Contents.json b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Contents.json new file mode 100644 index 00000000..8bd8636b --- /dev/null +++ b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Frame 2087330175.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 2087330175@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 2087330175@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175.png b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175.png new file mode 100644 index 0000000000000000000000000000000000000000..ac40bcf9f613e4b879f347ff34e29c6d794a83f5 GIT binary patch literal 675 zcmV;U0$lxxP)Bb5S=@|gaS@zD9_G@1Ut(swbGxU9adU_ zR)&HoR7ypsrFPoc5Vat&fapJvk_t{55CZsy+?@y9!4h~0VRrWRz1^AFS&8OARaK?k zZl6k$q>V-+m0iY^4&N7i&wjuEsnKZE-8dvCXFMKP`~ChM2s9D^#O{4QU)D*;Gwp~* zqo;$x-~r|~i6eth`i4T@XihLRiQ!T$2`e6 z2ORTWulEU4q4hvV@^?5VGLMkZ%QccS6fP1rBxCpAPG6-`DT+iQl+WkMZJtpol^o~8 z;qaym@#mlWfBA**U4T4K0%#aa$v$9b92mat1cUK zFLqNmI^{xgdssw7xZ`?ot+!xn%aGfv38iRNn*(k=fk5C3O6N|qi9~|Rm literal 0 HcmV?d00001 diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175@2x.png b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..66b8a73b0ee6cd4ad9685d6e9fbfa0b005fee932 GIT binary patch literal 1139 zcmV-(1dRKMP)1(2Zn3YFJ7nhGr@oW81%=Fd%g!NGzRd!~z3g!9WXw^!v4JE5GCS>?H0-3Vwe6 zb?3Wxckh{mM1X0U>Few3g+wCpq*|?-_|2j_?fyJ09Otisq}vjQC7ec9L@Jq|As>WMYf?xK~)hY1tumY)+A!VTadrFxF`fY zIgkRGOvb#uz5R;jtn>$KczF0_5Njm6oiI5$xd`D8q<_K_zOVy4d!gHjMyG?W%WUG8 zlK#VIz8)PNm7ANJy>=(HRlpTKFM}uBNr{#UxTgCo@gX}lHfBMI!^Q(mgV-Ig^!Z*! z8Xas5?5B+byJ-+Z_#YBK;Yr$!gmu)pkn8tdNB#iGU_2z?O1oZ%??SHY{FeA*kB^Vf zSe9jT$G5=9$jB$@Z>-lP^%rB04D2JCM1Qdh<1*K7V9Z=R_OGE4XQ*d;Wsum zL>G+PUk2jp8?>K^CP9^mT6luz92=-H=)9(cpqmoC5T0P{p3wq%iJ)|7rv&$VAw0qT zQwfw0Rf&*J5h+UHU2M++kF5zl(Ais3YsTWp#twD zqWZs8w4uPN$xZ=_VKj@zV+l#SP^p^iRNyitE%01IBr?)1&i6_Q+~YefkdY9xu%Jn* z7fRszGHW5O7=I8AR5NVUN<2I~EE|7-I}S{RYn}PP`ue)RA2iOlQsU&~#IJ4@s9auN z7QOkvo`gU(N%36O2~ZrfL-Cd4-KF@xz8} zFB?0Xme-LMY~+PAiP;({!e$O}|7T-=%kKkNwySUv70s-~g92Qq<@buVUud}Y7!xHZ ze7@bmuG1ubtwE;#BK;4qHr!_U{QP{o(@BBmc}Ni^as)qon9?6A3?{YVvK@v5fCC#Y zRqBIFG5QtZGC4jo$OXzXZPyxkG#4d0oFt03zUd~^z-TWOP!Yp0vhK>ZR^o$?@3Ios z5!}CrL_m!)v);elNd0)Ba5nn+-zmqO%VI_Ro37Bc`~&1j1QdU%N$UUr002ovPDHLk FV1gXL8K(dM literal 0 HcmV?d00001 diff --git a/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175@3x.png b/FLINT/Presentation/Sources/View/Resource/Assets.xcassets/Icon/Common/48/ic_black_xmark.imageset/Frame 2087330175@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..48cb09cc7d7ae8b98666ba97a937a27ac4f7b194 GIT binary patch literal 1682 zcmV;D25tF?P)8?wz zgX&C|0*OOHN*Pf=a#ex^5~*S;a#eB-QmQjuQXLW$mnwT5hrqXogyBG#d;h&zS&~9&UGx2!*^Siq{jkm9?m#a?Kc{5_M z7`B5_sq}{5b8vcknx_DXkzydq`T2R4zvQ<-d2MO{BS`9~)pV-H2Q~0gM4t%D$zP`Tn=;-LbhGATh2RyZ) z@$qr(`uh4MwO*@nLTBGJ>%}+4sYzf`EY2%xA8`PF&{xC5!{1SpG-YB#Lqkh!Qh)Gn zirNo+=DD7pp7)oRm;bnZlWT!koFDn&XKH^Qu}H)s?YlYIl|X1Y6b*4KGHzQ^LnjTp zWwZK|A_zy}*+#itU4cZjf{hg+JoDm@Xf#J}f zFF#meVr`gX$;KEM7?{*`-LMW+m>7%mggRFofV$Sx@&naAC`%HVnwl~ z7lp&DPeIs_UEebnD5z4`P8{$||r=Pft@mpRYK!ySqz^ zi;E-!h%+}gS8*(p$PVe~WD26sWhi7ac>5Bmxm+ zisU2?gjrcxA=$8*rC6XHQy{L5Kg1D~JQw^}Kz+ ze*&!-T3-8RPQC{}e)kicVv-NKS+yjzEVG`jtY!$=s?5Q5=wN zdgxgu_~P|?z#)rc5~@G|KBtulRiH+Rr>q3m5AZ=*3SsQaZC36#!S#tAN(-vj&$07K zA+{vr0p=iMzA^=b+fJZUKPgD3jXBydv2Q4VR0FsbM|P>iD=g3p2=oO7FvQ$!`p_aG zJx}!a_g}GzeNPJUnp=e}5)Vf|OC*!YD{e{*)KTOeL#&!nhOET1QGzQ2XaRAse?R?2rkP<}Y;bUpW@cv0=d!bMm{a-n zWsi$l=4I|%2ZfoTcqT0`vnCcC9UYMh6kT606bh5pllvBgXx4)n)@;rs3A7|^s!(+O zjq*uU70D_)CR7&<>RfczAXk2%LLfM6e%PSSWyf;P7B%HO;fvaBHIED`e2+yXGQLBfW`+2Fj(c67(FV%|Zf2FLtR_}?1p4k@rknh+_n zi&=y?y3+}DYtm*e5|2Noh{C3|P){7ta6z5$NAD?m3tN^sYULW83wGYGdD{?CPNVRB z6MDqYxYaDMGk#4zkvcg?_@*rHhOzF!|M3EaE}6pI zU!N3%R+UI3G9*tdL(E7~CQ3Ph&wmqltis*!G)%J<(~z-gRj2Cs!>=Fkd2i@bsLtb2 cwJ7C$1epy#mf%a&>Hq)$07*qoM6N<$f(uO(`2YX_ literal 0 HcmV?d00001 diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index 5cf58bd3..51204710 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -386,13 +386,21 @@ extension CreateCollectionViewController: UITableViewDataSource { } cell.onTapAddPhoto = { [weak self, weak cell] in - guard let self, let cell, let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } self.currentPhotoPickerIndex = indexPath.row - 1 self.presentPhotoPicker() } + cell.onPhotosChanged = { [weak self, weak cell] in + guard let self, let cell, + let indexPath = self.rootView.tableView.indexPath(for: cell) else { return } + let reasonIndex = indexPath.row - 1 + self.selectedReasonItems[reasonIndex].photos = cell.currentPhotos + self.rootView.tableView.beginUpdates() + self.rootView.tableView.endUpdates() + } + return cell } @@ -431,12 +439,25 @@ extension CreateCollectionViewController: PHPickerViewControllerDelegate { let group = DispatchGroup() var images: [UIImage] = [] + let lock = NSLock() for result in results { group.enter() result.itemProvider.loadObject(ofClass: UIImage.self) { object, _ in if let image = object as? UIImage { - images.append(image) + let queue = DispatchQueue(label: "imageQueue") + + for result in results { + group.enter() + result.itemProvider.loadObject(ofClass: UIImage.self) { object, _ in + if let image = object as? UIImage { + queue.sync { + images.append(image) + } + } + group.leave() + } + } } group.leave() } From 6425b88e8181a6ded0c895850b55c82cba32e995 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sat, 30 May 2026 00:24:34 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feat]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Collection/CreateCollectionEntity.swift | 20 ++-- .../UploadCollectionImageUseCase.swift | 39 ++++++++ FLINT/FLINT/Dependency/DIContainer.swift | 10 +- .../UploadCollectionImageUseCaseFactory.swift | 20 ++++ ...eateCollectionViewControllerFactory+.swift | 8 +- .../SelectedContentReasonTableViewCell.swift | 97 ++----------------- ...lectedContentReasonTableViewCellItem.swift | 16 +-- .../CreateCollectionViewController.swift | 85 ++++++++++------ .../CreateCollectionViewModel.swift | 6 +- 9 files changed, 153 insertions(+), 148 deletions(-) create mode 100644 FLINT/Domain/Sources/UseCase/Collection/UploadCollectionImageUseCase.swift create mode 100644 FLINT/FLINT/Dependency/Factory/UseCase/Collection/UploadCollectionImageUseCaseFactory.swift diff --git a/FLINT/Domain/Sources/Entity/Collection/CreateCollectionEntity.swift b/FLINT/Domain/Sources/Entity/Collection/CreateCollectionEntity.swift index 70d8407f..b77e6061 100644 --- a/FLINT/Domain/Sources/Entity/Collection/CreateCollectionEntity.swift +++ b/FLINT/Domain/Sources/Entity/Collection/CreateCollectionEntity.swift @@ -14,11 +14,12 @@ public struct CreateCollectionEntity: Encodable { public let isPublic: Bool public let contentList: [CreateCollectionContents] - public init(imgaeUrl: String, - title: String, - description: String, - isPublic: Bool, - contentList: [CreateCollectionContents] + public init( + imgaeUrl: String, + title: String, + description: String, + isPublic: Bool, + contentList: [CreateCollectionContents] ) { self.imageUrl = imgaeUrl self.title = title @@ -34,11 +35,18 @@ public extension CreateCollectionEntity { public let contentId: Int64 public let isSpoiler: Bool public let reason: String + public let customImage: String? - public init(contentId: Int64, isSpoiler: Bool, reason: String) { + public init( + contentId: Int64, + isSpoiler: Bool, + reason: String, + customImage customImageKey: String? = nil + ) { self.contentId = contentId self.isSpoiler = isSpoiler self.reason = reason + self.customImage = customImageKey } } } diff --git a/FLINT/Domain/Sources/UseCase/Collection/UploadCollectionImageUseCase.swift b/FLINT/Domain/Sources/UseCase/Collection/UploadCollectionImageUseCase.swift new file mode 100644 index 00000000..14e6e6b1 --- /dev/null +++ b/FLINT/Domain/Sources/UseCase/Collection/UploadCollectionImageUseCase.swift @@ -0,0 +1,39 @@ +// +// UploadCollectionImageUseCase.swift +// Domain +// +// Created by 소은 on 5/29/26. +// + +import Combine +import Foundation +import UIKit + +import Entity +import Repository + +public protocol UploadCollectionImageUseCase { + func callAsFunction(_ image: UIImage) -> AnyPublisher +} + +public final class DefaultUploadCollectionImageUseCase: UploadCollectionImageUseCase { + + private let storageRepository: StorageRepository + + public init(storageRepository: StorageRepository) { + self.storageRepository = storageRepository + } + + public func callAsFunction(_ image: UIImage) -> AnyPublisher { + storageRepository.fetchPresignedURL(uploadType: .collectionContent, fileExtension: .png) + .flatMap { [storageRepository] presignedUrlInfoEntity in + guard let imageData = image.pngData() else { + return Fail(error: FlintError.imageEncodingFailed).eraseToAnyPublisher() + } + return storageRepository.uploadImageToS3(imageData: imageData, uploadUrl: presignedUrlInfoEntity.uploadUrl, fileExtension: .png) + .map { _ in return presignedUrlInfoEntity.key } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} diff --git a/FLINT/FLINT/Dependency/DIContainer.swift b/FLINT/FLINT/Dependency/DIContainer.swift index ac8255cf..1340f325 100644 --- a/FLINT/FLINT/Dependency/DIContainer.swift +++ b/FLINT/FLINT/Dependency/DIContainer.swift @@ -27,7 +27,9 @@ typealias DependencyFactory = ViewControllerFactory & CreateCollectionViewModelFactory & AddContentSelectViewModelFactory & CollectionFolderListViewModelFactory & - CollectionDetailViewModelFactory + CollectionDetailViewModelFactory & + StorageRepositoryFactory & + UploadCollectionImageUseCaseFactory final class DIContainer: DependencyFactory { @@ -54,6 +56,12 @@ final class DIContainer: DependencyFactory { lazy var presignedUrlService: any PresignedUrlService = DefaultPresignedUrlService() + // MARK: - UseCase + + lazy var uploadCollectionImageUseCase: UploadCollectionImageUseCase = DefaultUploadCollectionImageUseCase( + storageRepository: makeStorageRepository() + ) + // MARK: - Init init() { diff --git a/FLINT/FLINT/Dependency/Factory/UseCase/Collection/UploadCollectionImageUseCaseFactory.swift b/FLINT/FLINT/Dependency/Factory/UseCase/Collection/UploadCollectionImageUseCaseFactory.swift new file mode 100644 index 00000000..2271dff8 --- /dev/null +++ b/FLINT/FLINT/Dependency/Factory/UseCase/Collection/UploadCollectionImageUseCaseFactory.swift @@ -0,0 +1,20 @@ +// +// UploadCollectionImageUseCaseFactory.swift +// FLINT +// +// Created by 소은 on 2026.05.29. +// + +import Foundation + +import Domain + +protocol UploadCollectionImageUseCaseFactory: StorageRepositoryFactory { + func makeUploadCollectionImageUseCase() -> UploadCollectionImageUseCase +} + +extension UploadCollectionImageUseCaseFactory { + func makeUploadCollectionImageUseCase() -> UploadCollectionImageUseCase { + return DefaultUploadCollectionImageUseCase(storageRepository: makeStorageRepository()) + } +} diff --git a/FLINT/FLINT/Dependency/Factory/ViewController/CreateCollectionViewControllerFactory+.swift b/FLINT/FLINT/Dependency/Factory/ViewController/CreateCollectionViewControllerFactory+.swift index 5a83079a..da5d3303 100644 --- a/FLINT/FLINT/Dependency/Factory/ViewController/CreateCollectionViewControllerFactory+.swift +++ b/FLINT/FLINT/Dependency/Factory/ViewController/CreateCollectionViewControllerFactory+.swift @@ -9,9 +9,13 @@ import Foundation import Presentation -extension CreateCollectionViewControllerFactory where Self: CreateCollectionViewModelFactory & ViewControllerFactory { +extension CreateCollectionViewControllerFactory where Self: CreateCollectionViewModelFactory & ViewControllerFactory & UploadCollectionImageUseCaseFactory { func makeCreateCollectionViewController() -> CreateCollectionViewController { let vm = makeCreateCollectionViewModel() - return CreateCollectionViewController(viewModel: vm, viewControllerFactory: self) + return CreateCollectionViewController( + viewModel: vm, + uploadImageUseCase: makeUploadCollectionImageUseCase(), + viewControllerFactory: self + ) } } diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift index 4ff302f7..d6b1791a 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift @@ -309,10 +309,13 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { photoStackView.addArrangedSubview(makePhotoWrapper(image: image, realIndex: realIndex)) } - layoutIfNeeded() - let width = photoScrollView.bounds.width - guard width > 0 else { return } - photoScrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.layoutIfNeeded() + let width = self.photoScrollView.bounds.width + guard width > 0 else { return } + self.photoScrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) + } } private func applyPhotoLayout(hasPhotos: Bool) { @@ -403,89 +406,3 @@ extension SelectedContentReasonTableViewCell: UIScrollViewDelegate { } } - -#if DEBUG -import UIKit -import PhotosUI - -public final class SelectedContentReasonPreviewViewController: UIViewController { - - private let tableView = UITableView() - - public override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .black - - tableView.backgroundColor = .black - tableView.dataSource = self - tableView.separatorStyle = .none - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 600 - tableView.register( - SelectedContentReasonTableViewCell.self, - forCellReuseIdentifier: "cell" - ) - - view.addSubview(tableView) - tableView.snp.makeConstraints { $0.edges.equalToSuperview() } - } -} - -extension SelectedContentReasonPreviewViewController: UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell( - withIdentifier: "cell", - for: indexPath - ) as! SelectedContentReasonTableViewCell - cell.configure(with: .mock) - cell.onTapAddPhoto = { [weak self] in - var config = PHPickerConfiguration() - config.selectionLimit = 5 - config.filter = .images - let picker = PHPickerViewController(configuration: config) - picker.delegate = self - self?.present(picker, animated: true) - } - cell.onPhotosChanged = { [weak self] in - self?.tableView.beginUpdates() - self?.tableView.endUpdates() - } - return cell - } -} - -extension SelectedContentReasonPreviewViewController: PHPickerViewControllerDelegate { - public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.dismiss(animated: true) - guard let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? SelectedContentReasonTableViewCell else { return } - - let group = DispatchGroup() - var images: [UIImage] = [] - let lock = NSLock() - - for result in results { - group.enter() - result.itemProvider.loadObject(ofClass: UIImage.self) { object, _ in - if let image = object as? UIImage { - lock.lock() - images.append(image) - lock.unlock() - } - group.leave() - } - } - - group.notify(queue: .main) { [weak self] in - guard let self else { return } - var item = SelectedContentReasonTableViewCellItem.mock - item.photos = images - item.reasonText = cell.currentReasonText - cell.configure(with: item) - self.tableView.beginUpdates() - self.tableView.endUpdates() - } - } -} -#endif diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift index d420ac53..ff65ab0d 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCellItem.swift @@ -18,6 +18,7 @@ public struct SelectedContentReasonTableViewCellItem { public var isSpoiler: Bool public var reasonText: String? public var photos: [UIImage] + public var customImageKey: String? = nil public init( contentId: Int64, @@ -41,18 +42,3 @@ public struct SelectedContentReasonTableViewCellItem { self.photos = photos } } - -#if DEBUG -extension SelectedContentReasonTableViewCellItem { - @MainActor static let mock = SelectedContentReasonTableViewCellItem( - contentId: 0, - posterURL: nil, - posterImage: .imgTving, - title: "영화이름 어어어어어엄 청길게 ", - director: "감독이름도 어어어엄청 긴이름", - year: "2016", - isSpoiler: false, - reasonText: "" - ) -} -#endif diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index 51204710..a525f21e 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -41,11 +41,16 @@ public final class CreateCollectionViewController: BaseViewController [UIImage] { + await withTaskGroup(of: (Int, UIImage?).self) { group in + for (index, result) in results.enumerated() { + group.addTask { + await withCheckedContinuation { continuation in result.itemProvider.loadObject(ofClass: UIImage.self) { object, _ in - if let image = object as? UIImage { - queue.sync { - images.append(image) - } - } - group.leave() + continuation.resume(returning: (index, object as? UIImage)) } } } - group.leave() } - } - - group.notify(queue: .main) { [weak self] in - guard let self else { return } - self.selectedReasonItems[index].photos = images - self.rootView.tableView.reloadRows( - at: [IndexPath(row: index + 1, section: 1)], - with: .none - ) + + var indexedImages: [(Int, UIImage)] = [] + for await (index, image) in group { + if let image = image { + indexedImages.append((index, image)) + } + } + + return indexedImages + .sorted { $0.0 < $1.0 } + .map { $0.1 } } } } diff --git a/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift b/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift index e1d5307a..6e36732b 100644 --- a/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift +++ b/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift @@ -92,9 +92,9 @@ public final class DefaultCreateCollectionViewModel: CreateCollectionViewModel { private func evaluateDoneEnabled() { let titleValid = !titleText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let countValid = contentList.count >= 2 - let visibilityValid = isPublic == true // (정책 나중에) - let descriptionValid = true // (정책 나중에) - let imageValid = true // (정책 나중에) + let visibilityValid = isPublic == true + let descriptionValid = true + let imageValid = true let canCreate = titleValid && countValid && visibilityValid && descriptionValid && imageValid From 7fbaadede4ca95311c96091e3ac7034f3ac184ca Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sat, 30 May 2026 00:52:22 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[fix]=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=ED=95=84=EB=93=9C=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 --- .../SelectedContentReasonTableViewCell.swift | 18 +++++++++++++++++- .../CreateCollectionViewController.swift | 5 +++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift index d6b1791a..766baf37 100644 --- a/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift +++ b/FLINT/Presentation/Sources/View/Component/SelectedContentReasonTableViewCell/SelectedContentReasonTableViewCell.swift @@ -124,6 +124,7 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { checkboxToggleView.onValueChanged = { [weak self] isOn in self?.onToggleSpoiler?(isOn) } + textView.delegate = self } public override func setHierarchy() { @@ -269,8 +270,12 @@ public final class SelectedContentReasonTableViewCell: BaseTableViewCell { spoilerLabel.attributedText = .pretendard(.caption1_m_12, text: "스포일러", color: .flintWhite) checkboxToggleView.setOn(item.isSpoiler, animated: false) - textView.text = item.reasonText ?? "" + if textView.text.isEmpty || textView.text != item.reasonText { + textView.text = item.reasonText ?? "" + textView.subviews.compactMap { $0 as? UILabel }.first?.isHidden = !(item.reasonText ?? "").isEmpty + } + configurePhotos(item.photos) } @@ -403,6 +408,17 @@ extension SelectedContentReasonTableViewCell: UIScrollViewDelegate { } else if page == photos.count + 1 { scrollView.setContentOffset(CGPoint(x: width, y: 0), animated: false) } + } + } +extension SelectedContentReasonTableViewCell: UITextViewDelegate { + public func textViewDidChange(_ textView: UITextView) { + if let flintTextView = textView as? FlintTextView { + let placeholderLabel = flintTextView.subviews.compactMap { $0 as? UILabel }.first + placeholderLabel?.isHidden = !textView.text.isEmpty + } + onChangeReasonText?(textView.text ?? "") + } +} diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index a525f21e..475be40f 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -459,6 +459,11 @@ extension CreateCollectionViewController: PHPickerViewControllerDelegate { }, receiveValue: { [weak self] keys in guard let self else { return } + + if let cell = self.rootView.tableView.cellForRow(at: IndexPath(row: index + 1, section: 1)) as? SelectedContentReasonTableViewCell { + self.selectedReasonItems[index].reasonText = cell.currentReasonText + } + self.selectedReasonItems[index].photos = images self.selectedReasonItems[index].customImageKey = keys.first self.rootView.tableView.reloadRows( From 68e3eda1b2df54fd1b6b89ee6e863f957af71201 Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sat, 30 May 2026 01:09:15 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[feat]=20=ED=97=A4=EB=8D=94=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=8F=84=20=EB=B3=80=EA=B2=BD=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateCollectionHeaderImageCell.swift | 25 +++++++- .../CreateCollectionViewController.swift | 60 ++++++++++++++++--- .../CreateCollectionViewModel.swift | 6 ++ 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift b/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift index d88c0b22..b08670d7 100644 --- a/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift +++ b/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift @@ -12,6 +12,8 @@ import Then public final class CreateCollectionHeaderImageCell: BaseTableViewCell { + public var onTapAddPhoto: (() -> Void)? + private let headerImageView = UIImageView().then { $0.contentMode = .scaleAspectFill $0.clipsToBounds = true @@ -24,13 +26,19 @@ public final class CreateCollectionHeaderImageCell: BaseTableViewCell { $0.clipsToBounds = true } + private let addPhotoButton = UIButton().then { + $0.setImage(.icBackgroundPhoto, for: .normal) + } + + public override func setStyle() { backgroundColor = .flintBackground contentView.backgroundColor = .flintBackground + addPhotoButton.addTarget(self, action: #selector(didTapAddPhoto), for: .touchUpInside) } public override func setHierarchy() { - contentView.addSubviews(headerImageView, blackOverlayView) + contentView.addSubviews(headerImageView, blackOverlayView, addPhotoButton) } public override func setLayout() { @@ -42,5 +50,20 @@ public final class CreateCollectionHeaderImageCell: BaseTableViewCell { blackOverlayView.snp.makeConstraints { $0.edges.equalToSuperview() } + + addPhotoButton.snp.makeConstraints { + $0.center.equalToSuperview() + $0.size.equalTo(48) + } + } + + public func configure(with image: UIImage?) { + if let image { + headerImageView.image = image + } + } + + @objc private func didTapAddPhoto() { + onTapAddPhoto?() } } diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index 475be40f..8e1295c2 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -43,6 +43,9 @@ public final class CreateCollectionViewController: BaseViewController Date: Sat, 30 May 2026 01:50:29 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[feat]=20=EB=B2=84=ED=8A=BC=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=20alertcontroller=20=EC=98=AC=EB=9D=BC?= =?UTF-8?q?=EC=98=A4=EB=8F=84=EB=A1=9D=20=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/Component/BottomSheet/.swift | 7 +++++ .../CreateCollectionHeaderImageCell.swift | 6 ++-- .../CreateCollectionView.swift | 24 +++++++++++--- .../CreateCollectionViewController.swift | 31 ++++++++++++++++--- .../CreateCollectionViewModel.swift | 12 +++---- 5 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 FLINT/Presentation/Sources/View/Component/BottomSheet/.swift diff --git a/FLINT/Presentation/Sources/View/Component/BottomSheet/.swift b/FLINT/Presentation/Sources/View/Component/BottomSheet/.swift new file mode 100644 index 00000000..b16f260a --- /dev/null +++ b/FLINT/Presentation/Sources/View/Component/BottomSheet/.swift @@ -0,0 +1,7 @@ +// +// PhotoOptionsView.swift +// Presentation +// +// Created by 소은 on 5/30/26. +// + diff --git a/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift b/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift index b08670d7..893b5685 100644 --- a/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift +++ b/FLINT/Presentation/Sources/View/Scene/CreateCollection/Cell/CreateCollectionHeaderImageCell.swift @@ -13,6 +13,8 @@ import Then public final class CreateCollectionHeaderImageCell: BaseTableViewCell { public var onTapAddPhoto: (() -> Void)? + public var onTapSelectPhoto: (() -> Void)? + public var onTapDeletePhoto: (() -> Void)? private let headerImageView = UIImageView().then { $0.contentMode = .scaleAspectFill @@ -58,9 +60,7 @@ public final class CreateCollectionHeaderImageCell: BaseTableViewCell { } public func configure(with image: UIImage?) { - if let image { - headerImageView.image = image - } + headerImageView.image = image ?? .imgBackgroundGradiantMiddle } @objc private func didTapAddPhoto() { diff --git a/FLINT/Presentation/Sources/View/Scene/CreateCollection/CreateCollectionView/CreateCollectionView.swift b/FLINT/Presentation/Sources/View/Scene/CreateCollection/CreateCollectionView/CreateCollectionView.swift index 973306c4..007c7a79 100644 --- a/FLINT/Presentation/Sources/View/Scene/CreateCollection/CreateCollectionView/CreateCollectionView.swift +++ b/FLINT/Presentation/Sources/View/Scene/CreateCollection/CreateCollectionView/CreateCollectionView.swift @@ -87,7 +87,22 @@ public final class CreateCollectionView: BaseView { footerContainerView.subviews.forEach { $0.removeFromSuperview() } footerContainerView.backgroundColor = .clear - footerContainerView.addSubview(button) + let copyrightLabel = UILabel().then { + $0.attributedText = .pretendard( + .caption1_r_12, + text: "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. 컬렉션 이용 시 저작권을 준수해 주세요.", + color: .flintGray300 + ) + $0.textAlignment = .left + $0.numberOfLines = 3 + } + + footerContainerView.addSubviews(copyrightLabel, button) + + copyrightLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(4) + $0.horizontalEdges.equalToSuperview().inset(footerSideInset) + } button.addAction( UIAction { [weak self] _ in @@ -97,21 +112,20 @@ public final class CreateCollectionView: BaseView { ) button.snp.makeConstraints { - $0.top.equalToSuperview() + $0.top.equalTo(copyrightLabel.snp.bottom).offset(4) $0.horizontalEdges.equalToSuperview().inset(footerSideInset) $0.height.equalTo(footerButtonHeight) $0.bottom.equalToSuperview().inset(footerBottomInset) } - let footerHeight = footerButtonHeight + footerBottomInset + let footerHeight = footerButtonHeight + footerBottomInset + 8 + 8 footerContainerView.frame = CGRect( x: 0, y: 0, width: tableView.bounds.width, - height: footerHeight + height: footerHeight + 40 ) tableView.tableFooterView = footerContainerView } - } diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index 8e1295c2..a95e1b3a 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -120,10 +120,10 @@ private extension CreateCollectionViewController { viewModel.createSuccess .receive(on: RunLoop.main) - .sink { [weak self] in -#warning("TODO: - 성공 처리") - self?.navigationController?.popViewController(animated: true) - print("CreateCollection 성공") + .sink { [weak self] collectionId in + guard let self, let factory = self.viewControllerFactory else { return } + let detailVC = factory.makeCollectionDetailViewController(collectionId: collectionId) + self.navigationController?.pushViewController(detailVC, animated: true) } .store(in: &cancellables) @@ -304,7 +304,28 @@ extension CreateCollectionViewController: UITableViewDataSource { ) as! CreateCollectionHeaderImageCell cell.configure(with: headerImage) cell.onTapAddPhoto = { [weak self] in - self?.presentHeaderPhotoPicker() + guard let self else { return } + let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + sheet.overrideUserInterfaceStyle = .dark // 추가 + + sheet.addAction(UIAlertAction(title: "앨범에서 선택", style: .default) { [weak self] _ in + self?.presentHeaderPhotoPicker() + }) + + sheet.addAction(UIAlertAction(title: "커버 사진 삭제", style: .destructive) { [weak self] _ in + guard let self else { return } + self.headerImage = nil + self.headerImageKey = nil + self.updateCreatePayload() + self.rootView.tableView.reloadRows( + at: [IndexPath(row: 0, section: 0)], + with: .none + ) + }) + + sheet.addAction(UIAlertAction(title: "닫기", style: .cancel)) + + self.present(sheet, animated: true) } return cell diff --git a/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift b/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift index c782f428..1a313755 100644 --- a/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift +++ b/FLINT/Presentation/Sources/ViewModel/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewModel.swift @@ -21,7 +21,7 @@ public protocol CreateCollectionViewModelInput { public protocol CreateCollectionViewModelOutput { var isDoneEnabled: CurrentValueSubject { get } - var createSuccess: PassthroughSubject { get } + var createSuccess: PassthroughSubject { get } var createFailure: PassthroughSubject { get } } @@ -32,7 +32,7 @@ public final class DefaultCreateCollectionViewModel: CreateCollectionViewModel { private let createCollectionUseCase: CreateCollectionUseCase public var isDoneEnabled: CurrentValueSubject = .init(false) - public var createSuccess: PassthroughSubject = .init() + public var createSuccess: PassthroughSubject = .init() public var createFailure: PassthroughSubject = .init() // MARK: - State @@ -81,12 +81,12 @@ public final class DefaultCreateCollectionViewModel: CreateCollectionViewModel { createCollectionUseCase(collectionInfo: entity) .manageThread() - .map { _ in Result.success(()) } - .catch { Just(Result.failure($0)) } + .map { collectionId in Result.success(collectionId) } + .catch { Just(Result.failure($0)) } .sinkHandledCompletion { [weak self] result in switch result { - case .success: - self?.createSuccess.send(()) + case .success(let collectionId): + self?.createSuccess.send(collectionId) case .failure(let error): self?.createFailure.send(error) } From 5ca78a6302f69bf9436b15ff9d701c09ffed76ee Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sat, 30 May 2026 01:57:59 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[chore]=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateCollectionView/CreateCollectionViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift index a95e1b3a..4b3a7640 100644 --- a/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift +++ b/FLINT/Presentation/Sources/ViewController/Scene/CreateCollection/CreateCollectionView/CreateCollectionViewController.swift @@ -306,7 +306,7 @@ extension CreateCollectionViewController: UITableViewDataSource { cell.onTapAddPhoto = { [weak self] in guard let self else { return } let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - sheet.overrideUserInterfaceStyle = .dark // 추가 + sheet.overrideUserInterfaceStyle = .dark sheet.addAction(UIAlertAction(title: "앨범에서 선택", style: .default) { [weak self] _ in self?.presentHeaderPhotoPicker() From 7fa87e0ddc0a21babdbbbfe71ec97f166cee727e Mon Sep 17 00:00:00 2001 From: soonny <134983918+soeun11@users.noreply.github.com> Date: Sat, 30 May 2026 02:01:11 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[chore]=EC=95=88=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Sources/View/Component/BottomSheet/.swift | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 FLINT/Presentation/Sources/View/Component/BottomSheet/.swift diff --git a/FLINT/Presentation/Sources/View/Component/BottomSheet/.swift b/FLINT/Presentation/Sources/View/Component/BottomSheet/.swift deleted file mode 100644 index b16f260a..00000000 --- a/FLINT/Presentation/Sources/View/Component/BottomSheet/.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// PhotoOptionsView.swift -// Presentation -// -// Created by 소은 on 5/30/26. -// -