Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Wable-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@
DEE5D6132D9126D8009E5A25 /* WableBadgeSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE5D6122D9126D8009E5A25 /* WableBadgeSegmentedControl.swift */; };
DEE5D6162D9130E5009E5A25 /* GameScheduleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE5D6152D9130E5009E5A25 /* GameScheduleViewModel.swift */; };
DEE5D61A2D91537C009E5A25 /* GameScheduleViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE5D6192D91537C009E5A25 /* GameScheduleViewItem.swift */; };
DEE6CBE32EA0D12E001A83A7 /* CurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE6CBE02EA0D12E001A83A7 /* CurationViewController.swift */; };
DEE6CBE62EA0D1A6001A83A7 /* CurationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE6CBE52EA0D1A6001A83A7 /* CurationCell.swift */; };
DEE6CBE92EA0DD46001A83A7 /* CurationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE6CBE82EA0DD46001A83A7 /* CurationItem.swift */; };
DEE6CBEC2EA0E6AE001A83A7 /* CurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE6CBEB2EA0E6AE001A83A7 /* CurationViewModel.swift */; };
DEF08E232DDB46170024B33C /* MyProfileEmptyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEF08E222DDB46170024B33C /* MyProfileEmptyCell.swift */; };
DEF08E292DDB73200024B33C /* ProfileEmptyCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEF08E282DDB73200024B33C /* ProfileEmptyCellItem.swift */; };
DEF148422DA7B7E5003B2AD8 /* IsUserRegistered.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEF148412DA7B7E5003B2AD8 /* IsUserRegistered.swift */; };
Expand Down Expand Up @@ -767,6 +771,10 @@
DEE5D6122D9126D8009E5A25 /* WableBadgeSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WableBadgeSegmentedControl.swift; sourceTree = "<group>"; };
DEE5D6152D9130E5009E5A25 /* GameScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScheduleViewModel.swift; sourceTree = "<group>"; };
DEE5D6192D91537C009E5A25 /* GameScheduleViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScheduleViewItem.swift; sourceTree = "<group>"; };
DEE6CBE02EA0D12E001A83A7 /* CurationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurationViewController.swift; sourceTree = "<group>"; };
DEE6CBE52EA0D1A6001A83A7 /* CurationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurationCell.swift; sourceTree = "<group>"; };
DEE6CBE82EA0DD46001A83A7 /* CurationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurationItem.swift; sourceTree = "<group>"; };
DEE6CBEB2EA0E6AE001A83A7 /* CurationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurationViewModel.swift; sourceTree = "<group>"; };
DEF08E222DDB46170024B33C /* MyProfileEmptyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileEmptyCell.swift; sourceTree = "<group>"; };
DEF08E282DDB73200024B33C /* ProfileEmptyCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEmptyCellItem.swift; sourceTree = "<group>"; };
DEF148412DA7B7E5003B2AD8 /* IsUserRegistered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsUserRegistered.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1877,6 +1885,7 @@
DE70048A2D8EA82400B7AB71 /* News */,
DE33902A2D8EC38F00638BB4 /* Notice */,
DE3390322D8EF3E100638BB4 /* Detail */,
DEE6CBE22EA0D12E001A83A7 /* Curation */,
);
path = Overview;
sourceTree = "<group>";
Expand Down Expand Up @@ -2550,6 +2559,49 @@
path = Model;
sourceTree = "<group>";
};
DEE6CBE12EA0D12E001A83A7 /* View */ = {
isa = PBXGroup;
children = (
DEE6CBE42EA0D19E001A83A7 /* Cell */,
DEE6CBE02EA0D12E001A83A7 /* CurationViewController.swift */,
);
path = View;
sourceTree = "<group>";
};
DEE6CBE22EA0D12E001A83A7 /* Curation */ = {
isa = PBXGroup;
children = (
DEE6CBE72EA0DD3A001A83A7 /* Model */,
DEE6CBEA2EA0E694001A83A7 /* ViewModel */,
DEE6CBE12EA0D12E001A83A7 /* View */,
);
path = Curation;
sourceTree = "<group>";
};
DEE6CBE42EA0D19E001A83A7 /* Cell */ = {
isa = PBXGroup;
children = (
DEE6CBE52EA0D1A6001A83A7 /* CurationCell.swift */,
);
path = Cell;
sourceTree = "<group>";
};
DEE6CBE72EA0DD3A001A83A7 /* Model */ = {
isa = PBXGroup;
children = (
DEE6CBE82EA0DD46001A83A7 /* CurationItem.swift */,
);
path = Model;
sourceTree = "<group>";
};
DEE6CBEA2EA0E694001A83A7 /* ViewModel */ = {
isa = PBXGroup;
children = (
DEE6CBEB2EA0E6AE001A83A7 /* CurationViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
DEF08E202DDB45380024B33C /* Component */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2848,6 +2900,7 @@
DE0D0C9C2DD1E35B00FB64DC /* UserRole.swift in Sources */,
DE0D0CAA2DD2041100FB64DC /* LikeViewitUseCase.swift in Sources */,
DD29683D2D6DAD2F00143851 /* FetchUserContents.swift in Sources */,
DEE6CBEC2EA0E6AE001A83A7 /* CurationViewModel.swift in Sources */,
DD29683E2D6DAD2F00143851 /* UpdateToken.swift in Sources */,
DE20BAD72D903985000126A4 /* AnnouncementDetailView.swift in Sources */,
DE70048D2D8EA85400B7AB71 /* NewsViewController.swift in Sources */,
Expand Down Expand Up @@ -2942,6 +2995,7 @@
DD2968622D6DAD5500143851 /* CommentLikedTargetType.swift in Sources */,
DD2968632D6DAD5500143851 /* ContentTargetType.swift in Sources */,
DD547AB92D7E43A600B8BA5A /* GhostButton.swift in Sources */,
DEE6CBE92EA0DD46001A83A7 /* CurationItem.swift in Sources */,
DD2968642D6DAD5500143851 /* NotificationTargetType.swift in Sources */,
DD94FF7F2E8C4766002D512E /* UIImageView+.swift in Sources */,
DD2968652D6DAD5500143851 /* ReportTargetType.swift in Sources */,
Expand Down Expand Up @@ -3011,6 +3065,7 @@
DE7C53142D761EE500076E5D /* UIView+.swift in Sources */,
DDCCA3932D738CD500658122 /* UserSessionRepository.swift in Sources */,
DD005BA92D7FFFE400B1661F /* LikeButton.swift in Sources */,
DEE6CBE62EA0D1A6001A83A7 /* CurationCell.swift in Sources */,
DEB60AFF2DD238AF00FE8BFD /* ViewitListCell.swift in Sources */,
DD9EAE2E2D9A63FD00803A1A /* WritePostViewController.swift in Sources */,
DD51A44C2DD458A8004295B6 /* ProfileEditViewController.swift in Sources */,
Expand All @@ -3036,6 +3091,7 @@
DD69C5902D71A3BE000A3349 /* OAuthTokenProvider.swift in Sources */,
DEA10B472DD3B9BA001EBBA9 /* WithdrawalGuideDescriptionView.swift in Sources */,
DE64D7CA2DCBA2D700F55859 /* CreateViewitUseCase.swift in Sources */,
DEE6CBE32EA0D12E001A83A7 /* CurationViewController.swift in Sources */,
DD8261D92E407E7900617F86 /* Likable.swift in Sources */,
DE3CA2872DD4571E00BF5E87 /* FetchAccountInfoUseCase.swift in Sources */,
DD29681D2D6DAD0200143851 /* CommentRepository.swift in Sources */,
Expand Down
39 changes: 39 additions & 0 deletions Wable-iOS/Presentation/Overview/Curation/Model/CurationItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// CurationItem.swift
// Wable-iOS
//
// Created by 김진웅 on 10/16/25.
//

import Foundation

struct CurationItem: Hashable, Identifiable {
let id: UUID = UUID()
let time: String
let title: String
let source: String
let thumbnailURL: URL?
}

extension CurationItem {
static let mocks: [CurationItem] = [
CurationItem(
time: "1시간 전",
title: "Ad ea excepteur nostrud sint commodo duis labore nostrud veniam cillum eu irure quis veniam dolor et",
source: "Esse minim proident aliquip deserunt id magna sunt aliquip elit fugiat officia sunt consequat elit l",
thumbnailURL: URL(string: "https://fastly.picsum.photos/id/176/343/220.jpg?hmac=h_eZSSP2OjzuGIVmDs1OZ_dYT3BzPbCC_QAnMZp5Sn8")
),
CurationItem(
time: "2시간 전",
title: "Excepteur nulla sint commodo ea labore nostrud veniam commodo eu irure reprehenderit veniam dolor eu",
source: "LMagna tempor aliquip elit id officia sunt aliquip elit fugiat officia sunt consequat eiusmod laborum",
thumbnailURL: URL(string: "https://fastly.picsum.photos/id/176/343/220.jpg?hmac=h_eZSSP2OjzuGIVmDs1OZ_dYT3BzPbCC_QAnMZp5Sn8")
),
CurationItem(
time: "3시간 전",
title: "Ea labore nulla voluptate commodo eu labore reprehenderit veniam commodo eu irure reprehenderit veni",
source: "Labore sed voluptate dolore ex non reprehenderit cillum dolore ipsum non velit aute est ipsum qui ut",
thumbnailURL: URL(string: "https://fastly.picsum.photos/id/176/343/220.jpg?hmac=h_eZSSP2OjzuGIVmDs1OZ_dYT3BzPbCC_QAnMZp5Sn8")
)
]
}
234 changes: 234 additions & 0 deletions Wable-iOS/Presentation/Overview/Curation/View/Cell/CurationCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
//
// CurationCell.swift
// Wable-iOS
//
// Created by 김진웅 on 10/16/25.
//

import UIKit

import Kingfisher
import SnapKit
import Then

final class CurationCell: UICollectionViewCell {

// MARK: - UIComponents

private let profileImageView = UIImageView(image: .logoSymbolSmall).then {
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true
$0.layer.cornerRadius = 20
}

private let authorLabel = UILabel().then {
$0.attributedText = "와블 큐레이터".pretendardString(with: .body3)
$0.textColor = .wableBlack
}

private let timeLabel = UILabel().then {
$0.attributedText = "".pretendardString(with: .caption4)
$0.textColor = .gray500
}

private let cardButton = UIButton().then {
$0.layer.cornerRadius = 12
$0.clipsToBounds = true
$0.layer.borderWidth = 1
$0.layer.borderColor = UIColor.gray200.cgColor
$0.backgroundColor = .gray100
}

private let thumbnailImageView = UIImageView().then {
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true
$0.backgroundColor = .gray200
}

private let descriptionView = UIView(backgroundColor: UIColor("F7F7F7")).then {
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The color value 'F7F7F7' is a magic value. Consider defining it as a named color in the asset catalog or as a constant to improve maintainability.

Copilot uses AI. Check for mistakes.
$0.isUserInteractionEnabled = false
}

private let titleLabel = UILabel().then {
$0.attributedText = "".pretendardString(with: .body3)
$0.textColor = .wableBlack
$0.numberOfLines = 1
$0.lineBreakMode = .byTruncatingTail
}

private let sourceLabel = UILabel().then {
$0.attributedText = "".pretendardString(with: .body4)
$0.textColor = .gray600
$0.numberOfLines = 1
$0.lineBreakMode = .byTruncatingTail
}

private let openIconImageView = UIImageView(image: .btnCuration).then {
$0.contentMode = .scaleAspectFit
$0.isUserInteractionEnabled = false
}

private let pressedOverlay = UIView().then {
$0.backgroundColor = UIColor.black.withAlphaComponent(0.08)
$0.alpha = 0
$0.isUserInteractionEnabled = false
}

// MARK: - Properties

private var onTapCard: (() -> Void)?

// MARK: - Initializer

override init(frame: CGRect) {
super.init(frame: frame)

setupView()
setupConstraint()
setupActions()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func prepareForReuse() {
super.prepareForReuse()

thumbnailImageView.kf.cancelDownloadTask()
thumbnailImageView.image = nil
onTapCard = nil
pressedOverlay.alpha = 0
}

func configure(
time: String,
thumbnailURL: URL?,
title: String,
source: String,
onTap: @escaping () -> Void
) {
timeLabel.text = "· \(time)"
titleLabel.text = title
sourceLabel.text = source
onTapCard = onTap

thumbnailImageView.kf.setImage(with: thumbnailURL) { [weak self] result in
switch result {
case .success:
self?.thumbnailImageView.isHidden = false
case .failure:
self?.thumbnailImageView.isHidden = false
}
}
}
}

// MARK: - Setup Method

private extension CurationCell {
func setupView() {
contentView.addSubviews(
profileImageView,
authorLabel,
timeLabel,
cardButton
)

cardButton.addSubviews(
thumbnailImageView,
descriptionView,
pressedOverlay
)

descriptionView.addSubviews(
titleLabel,
sourceLabel,
openIconImageView
)
}

func setupConstraint() {
profileImageView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.adjustedWidthEqualTo(28)
make.leading.equalToSuperview().offset(16)
make.height.equalTo(profileImageView.snp.width)
}

authorLabel.snp.makeConstraints { make in
make.centerY.equalTo(profileImageView)
make.leading.equalTo(profileImageView.snp.trailing).offset(8)
}

timeLabel.snp.makeConstraints { make in
make.centerY.equalTo(profileImageView)
make.leading.equalTo(authorLabel.snp.trailing).offset(8)
}

cardButton.snp.makeConstraints { make in
make.top.equalTo(profileImageView.snp.bottom).offset(8)
make.horizontalEdges.equalToSuperview().inset(16)
make.bottom.equalToSuperview()
}

thumbnailImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}

titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.leading.equalToSuperview().offset(16)
make.trailing.equalTo(openIconImageView.snp.leading).offset(-16)
}

sourceLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.leading.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-12)
make.trailing.equalTo(openIconImageView.snp.leading).offset(-16)
}

openIconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().inset(16)
make.adjustedWidthEqualTo(32)
make.adjustedHeightEqualTo(32)
}

descriptionView.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview()
make.bottom.equalToSuperview()
}

pressedOverlay.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}

func setupActions() {
cardButton.addTarget(self, action: #selector(cardTapped), for: .touchUpInside)
cardButton.addTarget(self, action: #selector(cardTouchDown), for: [.touchDown])
cardButton.addTarget(self, action: #selector(cardTouchUpCancel), for: [.touchDragExit, .touchCancel, .touchUpOutside])
}

@objc func cardTapped() {
animatePressed(false)
onTapCard?()
}

@objc func cardTouchDown() {
animatePressed(true)
}

@objc func cardTouchUpCancel() {
animatePressed(false)
}

func animatePressed(_ pressed: Bool) {
UIView.animate(withDuration: 0.12, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) {
self.pressedOverlay.alpha = pressed ? 1 : 0
}
}
}
Loading