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
86 changes: 86 additions & 0 deletions ONMIR/Domain/Representation/BookRepresentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Foundation

public struct BookRepresentation: Sendable, Hashable {
public let id: String
public let volumeInfo: VolumeInfo
public let saleInfo: SaleInfo?

public init(
id: String,
volumeInfo: VolumeInfo,
saleInfo: SaleInfo?
) {
self.id = id
self.volumeInfo = volumeInfo
self.saleInfo = saleInfo
}

public struct VolumeInfo: Sendable, Hashable {
public let title: String
public let subtitle: String?
public let authors: [String]?
public let publisher: String?
public let publishedDate: String?
public let description: String?
public let pageCount: Int?
public let categories: [String]?
public let language: String?
public let imageLinks: ImageLinks?

public init(
title: String,
subtitle: String?,
authors: [String]?,
publisher: String?,
publishedDate: String?,
description: String?,
pageCount: Int?,
categories: [String]?,
language: String?,
imageLinks: ImageLinks?
) {
self.title = title
self.subtitle = subtitle
self.authors = authors
self.publisher = publisher
self.publishedDate = publishedDate
self.description = description
self.pageCount = pageCount
self.categories = categories
self.language = language
self.imageLinks = imageLinks
}

public struct ImageLinks: Sendable, Hashable {
public let smallThumbnail: String?
public let thumbnail: String?

public init(smallThumbnail: String?, thumbnail: String?) {
self.smallThumbnail = smallThumbnail
self.thumbnail = thumbnail
}
}
}

public struct SaleInfo: Sendable, Hashable {
public let listPrice: Price?
public let retailPrice: Price?
public let buyLink: String?

public init(listPrice: Price?, retailPrice: Price?, buyLink: String?) {
self.listPrice = listPrice
self.retailPrice = retailPrice
self.buyLink = buyLink
}

public struct Price: Sendable, Hashable {
public let amount: Double?
public let currencyCode: String?

public init(amount: Double?, currencyCode: String?) {
self.amount = amount
self.currencyCode = currencyCode
}
}
}
}
116 changes: 116 additions & 0 deletions ONMIR/Feature/NewBookRecord/Components/BookInfoCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import Nuke
import SnapKit
import UIKit

extension NewBookRecordViewController {
final class BookInfoCell: UICollectionViewCell {
private let containerView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.cornerRadius = 12
return view
}()

private let coverImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .tertiarySystemBackground
imageView.layer.cornerRadius = 8
imageView.clipsToBounds = true
return imageView
}()

private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .bold)
label.numberOfLines = 0
label.textAlignment = .left
return label
}()

private let authorsLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .secondaryLabel
label.numberOfLines = 0
label.textAlignment = .left
return label
}()

private var imageLoadTask: Task<Void, Never>?

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

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func prepareForReuse() {
super.prepareForReuse()
imageLoadTask?.cancel()
imageLoadTask = nil
coverImageView.image = nil
titleLabel.text = nil
authorsLabel.text = nil
}

private func setupView() {
contentView.addSubview(containerView)
containerView.addSubview(coverImageView)
containerView.addSubview(titleLabel)
containerView.addSubview(authorsLabel)

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

coverImageView.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview()
make.height.equalTo(100)
make.width.equalTo(coverImageView.snp.height).multipliedBy(0.67)
}

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

authorsLabel.snp.makeConstraints { make in
make.leading.equalTo(coverImageView.snp.trailing).offset(16)
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.trailing.equalToSuperview()
}
}

func configure(with book: BookRepresentation) {
titleLabel.text = book.volumeInfo.title

if let authors = book.volumeInfo.authors, !authors.isEmpty {
authorsLabel.text = authors.joined(separator: ", ")
}

if let thumbnailURLString = book.volumeInfo.imageLinks?.thumbnail {
let secureURL = thumbnailURLString.replacingOccurrences(
of: "http://",
with: "https://"
)
if let thumbnailURL = URL(string: secureURL) {
imageLoadTask = Task {
do {
let image = try await ImagePipeline.shared.image(for: thumbnailURL)
if !Task.isCancelled {
self.coverImageView.image = image
}
} catch {
print("이미지 로딩 실패: \(error)")
}
}
}
}
}
}
}
165 changes: 165 additions & 0 deletions ONMIR/Feature/NewBookRecord/Components/NoteCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import SnapKit
import UIKit

extension NewBookRecordViewController {
final class NoteCell: UICollectionViewCell {
private let containerView: UIView = {
let view = UIView()
view.backgroundColor = .secondarySystemGroupedBackground
view.layer.cornerRadius = 12
return view
}()

private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = .label
return label
}()

private let textView: UITextView = {
let textView = UITextView()
textView.font = .systemFont(ofSize: 14)
textView.backgroundColor = .clear
textView.isScrollEnabled = false
textView.textContainerInset = UIEdgeInsets(
top: 8,
left: 8,
bottom: 8,
right: 8
)
textView.layer.borderColor = UIColor.systemGray5.cgColor
textView.layer.borderWidth = 0.5
textView.layer.cornerRadius = 8
return textView
}()

private var textChangedHandler: (@MainActor (String) -> Void)?

private var textViewHeightConstraint: Constraint?
private var currentTextHeight: CGFloat = 100

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

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupView() {
contentView.addSubview(containerView)
containerView.addSubview(titleLabel)
containerView.addSubview(textView)

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

titleLabel.snp.makeConstraints { make in
make.top.horizontalEdges.equalToSuperview().inset(16)
}

textView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.horizontalEdges.equalToSuperview().inset(16)
make.bottom.lessThanOrEqualToSuperview().inset(16)
textViewHeightConstraint = make.height.equalTo(100).constraint
}

textView.delegate = self
}

override func layoutSubviews() {
super.layoutSubviews()

if textView.bounds.width > 0 {
updateTextViewHeight()
}
}

func configure(
title: String,
note: String,
textChangedHandler: @MainActor @escaping (String) -> Void
) {
titleLabel.text = title
self.textChangedHandler = textChangedHandler

textView.text = note

setNeedsLayout()
layoutIfNeeded()
updateTextViewHeight()
}

private func updateTextViewHeight() {
guard textView.bounds.width > 0 else {
return
}

let fixedWidth =
textView.frame.width - textView.textContainerInset.left
- textView.textContainerInset.right - 2
* textView.textContainer.lineFragmentPadding

let textString = textView.text ?? ""
let textStorage = NSTextStorage(
string: textString,
attributes: [
.font: textView.font ?? UIFont.systemFont(ofSize: 14)
]
)

let textContainer = NSTextContainer(
size: CGSize(width: fixedWidth, height: .greatestFiniteMagnitude))
textContainer.lineFragmentPadding =
textView.textContainer.lineFragmentPadding
textContainer.lineBreakMode = .byWordWrapping

let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)

layoutManager.ensureLayout(for: textContainer)

let usedRect = layoutManager.usedRect(for: textContainer)
let newHeight = max(
100,
ceil(usedRect.height + textView.textContainerInset.top
+ textView.textContainerInset.bottom)
)

if abs(newHeight - currentTextHeight) > 1 {
textViewHeightConstraint?.update(offset: newHeight)
currentTextHeight = newHeight

invalidateIntrinsicContentSize()
}
}

override func preferredLayoutAttributesFitting(
_ layoutAttributes: UICollectionViewLayoutAttributes
) -> UICollectionViewLayoutAttributes {
let attributes = super.preferredLayoutAttributesFitting(layoutAttributes)

let titleHeight = titleLabel.frame.height
let spacing: CGFloat = 8
let verticalInsets: CGFloat = 32

let totalHeight =
titleHeight + spacing + currentTextHeight + verticalInsets
attributes.frame.size.height = totalHeight

return attributes
}
}
}

extension NewBookRecordViewController.NoteCell: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
textChangedHandler?(textView.text)
updateTextViewHeight()
}
}
Loading