From b3a222f1f4f2c5eb834eb7b3b7efd7d2ea111f93 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:55:33 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[BOOK-476]=20feat:=20=EB=8F=84=EC=84=9C?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Search/View/SearchEmptyView.swift | 135 ++++++++++++++++++ .../MainFlow/Search/View/SearchView.swift | 12 +- .../Search/View/SearchViewController.swift | 56 ++++++++ .../Search/ViewModel/SearchViewModel.swift | 8 ++ 4 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift new file mode 100644 index 00000000..60ec4c3c --- /dev/null +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift @@ -0,0 +1,135 @@ +// Copyright © 2025 Booket. All rights reserved + +import BKDesign +import BKDomain +import Combine +import SnapKit +import UIKit + +final class SearchEmptyView: BaseView { + var onActionButtonTapped: (() -> Void)? + + private let scrollView: UIScrollView = { + let view = UIScrollView() + view.showsVerticalScrollIndicator = false + view.alwaysBounceVertical = true + return view + }() + + private let contentView = UIView() + + private let textGroupView = UIView() + + private let titleLabel = BKLabel( + text: "", + fontStyle: .headline1(weight: .semiBold), + color: .bkContentColor(.primary), + alignment: .center + ) + + private let descriptionLabel = BKLabel( + text: "", + fontStyle: .body1(weight: .medium), + color: .bkContentColor(.secondary), + alignment: .center + ) + + private let actionButton: BKButton = { + let button = BKButton(style: .secondary, size: .small) + return button + }() + + override func setupView() { + addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(textGroupView) + textGroupView.addSubviews(titleLabel, descriptionLabel, actionButton) + } + + override func setupLayout() { + scrollView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.edges.equalTo(scrollView.contentLayoutGuide) + $0.width.equalTo(scrollView.frameLayoutGuide) + $0.height.greaterThanOrEqualTo(scrollView.frameLayoutGuide).priority(.low) + } + + textGroupView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(BKSpacing.spacing5) + $0.top.greaterThanOrEqualToSuperview().offset(20) + $0.bottom.lessThanOrEqualToSuperview().inset(20) + } + } + + override func configure() { + actionButton.addAction(UIAction { [weak self] _ in + self?.onActionButtonTapped?() + }, for: .touchUpInside) + } + + func setContent(title: String, description: String? = nil, buttonTitle: String? = nil) { + titleLabel.setText(text: title) + + if let desc = description { + descriptionLabel.setText(text: desc) + descriptionLabel.isHidden = false + } else { + descriptionLabel.isHidden = true + } + + if let btnTitle = buttonTitle { + actionButton.setTitle(btnTitle, for: .normal) + actionButton.isHidden = false + } else { + actionButton.isHidden = true + } + + updateConstraintsManually(hasDescription: description != nil, hasButton: buttonTitle != nil) + } +} + +private extension SearchEmptyView { + func updateConstraintsManually(hasDescription: Bool, hasButton: Bool) { + let horizontalInset = BKSpacing.spacing5 + let textSpacing = BKSpacing.spacing2 + let buttonSpacing = BKSpacing.spacing5 + + // 1. Title Label + titleLabel.snp.remakeConstraints { + $0.top.equalToSuperview() + $0.leading.trailing.equalToSuperview() + + if !hasDescription && !hasButton { + $0.bottom.equalToSuperview() + } + } + + // 2. Description Label + if hasDescription { + descriptionLabel.snp.remakeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(textSpacing) + $0.leading.trailing.equalToSuperview().inset(horizontalInset) + + if !hasButton { + $0.bottom.equalToSuperview() + } + } + } + + // 3. Action Button + if hasButton { + actionButton.snp.remakeConstraints { + let topTarget = hasDescription ? descriptionLabel.snp.bottom : titleLabel.snp.bottom + + $0.top.equalTo(topTarget).offset(buttonSpacing) + $0.centerX.equalToSuperview() + + $0.bottom.equalToSuperview() + } + } + } +} diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift index e153b08a..7421f2ef 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift @@ -82,6 +82,10 @@ final class SearchView: BaseView { searchBar.text = text } + func updateEmptyView(with view: UIView?) { + collectionView.backgroundView = view + } + func applySnapshot( with state: SearchViewModel.SearchState, count: Int = 0 @@ -101,13 +105,7 @@ final class SearchView: BaseView { case .result(let state): let isEmpty = state.books.isEmpty && state.bookInfos.isEmpty - if isEmpty { - header.layoutIfNeeded() - let headerHeight = header.bounds.height - let offset = -(headerHeight / 2.0) - collectionView.backgroundView = makeEmptyLabel(state.placeholder, verticalOffset: offset) - searchBar.setClearButtonMode(.whileEditing) - } else { + if !isEmpty { collectionView.backgroundView = nil snapshot.appendSections([.result]) // Book과 BookInfo 모두 처리 diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift index 5dfe24c4..a77d96f1 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift @@ -247,6 +247,30 @@ final class SearchViewController: BaseViewController, ScreenLoggable ) } .store(in: &cancellable) + + viewModel.statePublisher + .receive(on: DispatchQueue.main) + .map { state -> (isEmpty: Bool, viewType: SearchViewType) in + if case .result(let resultState) = state.searchState { + let isEmpty = resultState.books.isEmpty && resultState.bookInfos.isEmpty + return (isEmpty, state.viewType ?? .defaultSearch) + } + return (false, state.viewType ?? .defaultSearch) + } + .removeDuplicates { prev, curr in + return prev.isEmpty == curr.isEmpty && prev.viewType == curr.viewType + } + .sink { [weak self] (isEmpty, viewType) in + guard let self = self else { return } + + if isEmpty { + let emptyView = self.makeEmptyView(for: viewType) + self.contentView.updateEmptyView(with: emptyView) + } else { + self.contentView.updateEmptyView(with: nil) + } + } + .store(in: &cancellable) } } @@ -329,3 +353,35 @@ private extension SearchViewController { sheet.show(from: self, animated: true) } } + +private extension SearchViewController { + // 뷰 타입에 따라 내용을 다르게 설정하여 SearchEmptyView 반환 + func makeEmptyView(for type: SearchViewType) -> UIView { + let emptyView = SearchEmptyView() + + switch type { + case .defaultSearch: + emptyView.setContent( + title: "검색어와 일치하는 도서가 없습니다.", + description: "찾으시는 도서가 없다면 직접 등록해보세요.", + buttonTitle: "도서 등록 요청하기" + ) + emptyView.onActionButtonTapped = { [weak self] in + // 뷰모델로 액션 전달 + self?.viewModel.send(.emptyViewButtonTapped) + } + + case .myLibrarySearch: + emptyView.setContent( + title: "내 서재에 해당 도서가 없습니다.", + description: nil, // 설명 없음 + buttonTitle: nil + ) + emptyView.onActionButtonTapped = { [weak self] in + self?.viewModel.send(.emptyViewButtonTapped) + } + } + + return emptyView + } +} diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift index 7a06c0f3..b68fa781 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift @@ -114,6 +114,7 @@ final class SearchViewModel: BaseViewModel { case errorHandled case retryTapped case lastRetryTapped + case emptyViewButtonTapped } enum SideEffect { @@ -317,6 +318,13 @@ final class SearchViewModel: BaseViewModel { case .errorHandled: newState.error = nil + + case .emptyViewButtonTapped: + if searchViewType == .myLibrarySearch { + print("내 서재 검색 결과 없음 -> 전체 검색으로 이동") + } else { + print("검색 결과 없음 -> 도서 등록 요청") + } } return (newState, effects) From 5d927f5c29a6b4a4f6508413153c6ae6ba44146c Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:13:06 +0900 Subject: [PATCH 02/13] =?UTF-8?q?Revert=20"[BOOK-476]=20feat:=20=EB=8F=84?= =?UTF-8?q?=EC=84=9C=20=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B0=9C=EB=B0=9C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b3a222f1f4f2c5eb834eb7b3b7efd7d2ea111f93. --- .../Search/View/SearchEmptyView.swift | 135 ------------------ .../MainFlow/Search/View/SearchView.swift | 12 +- .../Search/View/SearchViewController.swift | 56 -------- .../Search/ViewModel/SearchViewModel.swift | 8 -- 4 files changed, 7 insertions(+), 204 deletions(-) delete mode 100644 src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift deleted file mode 100644 index 60ec4c3c..00000000 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchEmptyView.swift +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright © 2025 Booket. All rights reserved - -import BKDesign -import BKDomain -import Combine -import SnapKit -import UIKit - -final class SearchEmptyView: BaseView { - var onActionButtonTapped: (() -> Void)? - - private let scrollView: UIScrollView = { - let view = UIScrollView() - view.showsVerticalScrollIndicator = false - view.alwaysBounceVertical = true - return view - }() - - private let contentView = UIView() - - private let textGroupView = UIView() - - private let titleLabel = BKLabel( - text: "", - fontStyle: .headline1(weight: .semiBold), - color: .bkContentColor(.primary), - alignment: .center - ) - - private let descriptionLabel = BKLabel( - text: "", - fontStyle: .body1(weight: .medium), - color: .bkContentColor(.secondary), - alignment: .center - ) - - private let actionButton: BKButton = { - let button = BKButton(style: .secondary, size: .small) - return button - }() - - override func setupView() { - addSubview(scrollView) - scrollView.addSubview(contentView) - contentView.addSubview(textGroupView) - textGroupView.addSubviews(titleLabel, descriptionLabel, actionButton) - } - - override func setupLayout() { - scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - contentView.snp.makeConstraints { - $0.edges.equalTo(scrollView.contentLayoutGuide) - $0.width.equalTo(scrollView.frameLayoutGuide) - $0.height.greaterThanOrEqualTo(scrollView.frameLayoutGuide).priority(.low) - } - - textGroupView.snp.makeConstraints { - $0.centerY.equalToSuperview() - $0.leading.trailing.equalToSuperview().inset(BKSpacing.spacing5) - $0.top.greaterThanOrEqualToSuperview().offset(20) - $0.bottom.lessThanOrEqualToSuperview().inset(20) - } - } - - override func configure() { - actionButton.addAction(UIAction { [weak self] _ in - self?.onActionButtonTapped?() - }, for: .touchUpInside) - } - - func setContent(title: String, description: String? = nil, buttonTitle: String? = nil) { - titleLabel.setText(text: title) - - if let desc = description { - descriptionLabel.setText(text: desc) - descriptionLabel.isHidden = false - } else { - descriptionLabel.isHidden = true - } - - if let btnTitle = buttonTitle { - actionButton.setTitle(btnTitle, for: .normal) - actionButton.isHidden = false - } else { - actionButton.isHidden = true - } - - updateConstraintsManually(hasDescription: description != nil, hasButton: buttonTitle != nil) - } -} - -private extension SearchEmptyView { - func updateConstraintsManually(hasDescription: Bool, hasButton: Bool) { - let horizontalInset = BKSpacing.spacing5 - let textSpacing = BKSpacing.spacing2 - let buttonSpacing = BKSpacing.spacing5 - - // 1. Title Label - titleLabel.snp.remakeConstraints { - $0.top.equalToSuperview() - $0.leading.trailing.equalToSuperview() - - if !hasDescription && !hasButton { - $0.bottom.equalToSuperview() - } - } - - // 2. Description Label - if hasDescription { - descriptionLabel.snp.remakeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(textSpacing) - $0.leading.trailing.equalToSuperview().inset(horizontalInset) - - if !hasButton { - $0.bottom.equalToSuperview() - } - } - } - - // 3. Action Button - if hasButton { - actionButton.snp.remakeConstraints { - let topTarget = hasDescription ? descriptionLabel.snp.bottom : titleLabel.snp.bottom - - $0.top.equalTo(topTarget).offset(buttonSpacing) - $0.centerX.equalToSuperview() - - $0.bottom.equalToSuperview() - } - } - } -} diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift index 7421f2ef..e153b08a 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift @@ -82,10 +82,6 @@ final class SearchView: BaseView { searchBar.text = text } - func updateEmptyView(with view: UIView?) { - collectionView.backgroundView = view - } - func applySnapshot( with state: SearchViewModel.SearchState, count: Int = 0 @@ -105,7 +101,13 @@ final class SearchView: BaseView { case .result(let state): let isEmpty = state.books.isEmpty && state.bookInfos.isEmpty - if !isEmpty { + if isEmpty { + header.layoutIfNeeded() + let headerHeight = header.bounds.height + let offset = -(headerHeight / 2.0) + collectionView.backgroundView = makeEmptyLabel(state.placeholder, verticalOffset: offset) + searchBar.setClearButtonMode(.whileEditing) + } else { collectionView.backgroundView = nil snapshot.appendSections([.result]) // Book과 BookInfo 모두 처리 diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift index a77d96f1..5dfe24c4 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift @@ -247,30 +247,6 @@ final class SearchViewController: BaseViewController, ScreenLoggable ) } .store(in: &cancellable) - - viewModel.statePublisher - .receive(on: DispatchQueue.main) - .map { state -> (isEmpty: Bool, viewType: SearchViewType) in - if case .result(let resultState) = state.searchState { - let isEmpty = resultState.books.isEmpty && resultState.bookInfos.isEmpty - return (isEmpty, state.viewType ?? .defaultSearch) - } - return (false, state.viewType ?? .defaultSearch) - } - .removeDuplicates { prev, curr in - return prev.isEmpty == curr.isEmpty && prev.viewType == curr.viewType - } - .sink { [weak self] (isEmpty, viewType) in - guard let self = self else { return } - - if isEmpty { - let emptyView = self.makeEmptyView(for: viewType) - self.contentView.updateEmptyView(with: emptyView) - } else { - self.contentView.updateEmptyView(with: nil) - } - } - .store(in: &cancellable) } } @@ -353,35 +329,3 @@ private extension SearchViewController { sheet.show(from: self, animated: true) } } - -private extension SearchViewController { - // 뷰 타입에 따라 내용을 다르게 설정하여 SearchEmptyView 반환 - func makeEmptyView(for type: SearchViewType) -> UIView { - let emptyView = SearchEmptyView() - - switch type { - case .defaultSearch: - emptyView.setContent( - title: "검색어와 일치하는 도서가 없습니다.", - description: "찾으시는 도서가 없다면 직접 등록해보세요.", - buttonTitle: "도서 등록 요청하기" - ) - emptyView.onActionButtonTapped = { [weak self] in - // 뷰모델로 액션 전달 - self?.viewModel.send(.emptyViewButtonTapped) - } - - case .myLibrarySearch: - emptyView.setContent( - title: "내 서재에 해당 도서가 없습니다.", - description: nil, // 설명 없음 - buttonTitle: nil - ) - emptyView.onActionButtonTapped = { [weak self] in - self?.viewModel.send(.emptyViewButtonTapped) - } - } - - return emptyView - } -} diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift index b68fa781..7a06c0f3 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift @@ -114,7 +114,6 @@ final class SearchViewModel: BaseViewModel { case errorHandled case retryTapped case lastRetryTapped - case emptyViewButtonTapped } enum SideEffect { @@ -318,13 +317,6 @@ final class SearchViewModel: BaseViewModel { case .errorHandled: newState.error = nil - - case .emptyViewButtonTapped: - if searchViewType == .myLibrarySearch { - print("내 서재 검색 결과 없음 -> 전체 검색으로 이동") - } else { - print("검색 결과 없음 -> 도서 등록 요청") - } } return (newState, effects) From 611979daa3ed35a80ba15688e42478970bd147c6 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:27:21 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[BOOK-476]=20feat:=20EmptyStateView=20?= =?UTF-8?q?=EB=82=B4=20=EB=B2=84=ED=8A=BC=20General=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ArchiveFlow/View/EmptyStateView.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift b/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift index 20ccf6c0..a582aa9e 100644 --- a/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift +++ b/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift @@ -6,7 +6,7 @@ import UIKit import SnapKit final class EmptyStateView: BaseView { - var onTapLogin: (() -> Void)? + var onTapActionButton: (() -> Void)? private var cancellables = Set() private let titleLabel = BKLabel( @@ -23,13 +23,13 @@ final class EmptyStateView: BaseView { alignment: .center ) - private let loginButton: BKButton = { + private let actionButton: BKButton = { let button = BKButton(style: .secondary, size: .small) return button }() private lazy var stackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, loginButton]) + let stackView = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, actionButton]) stackView.axis = .vertical stackView.alignment = .center stackView.spacing = 8 @@ -50,8 +50,8 @@ final class EmptyStateView: BaseView { } override func configure() { - loginButton.title = Constants.loginButtonTitle - loginButton.addTarget(self, action: #selector(tapLogin), for: .touchUpInside) + actionButton.isHidden = true + actionButton.addTarget(self, action: #selector(tapActionButton), for: .touchUpInside) AccessModeCenter.shared.mode .receive(on: DispatchQueue.main) @@ -71,6 +71,7 @@ private extension EmptyStateView { static let memberTitle = "아직 등록된 책이 없어요" static let memberSubtitle = "도서 등록 후 나만의 아카이브를 만들어보세요" static let loginButtonTitle = "로그인하기" + static let requestButtonTitle = "문의하기" } func apply(mode: AppAccessMode) { @@ -78,15 +79,15 @@ private extension EmptyStateView { case .guest: titleLabel.setText(text: Constants.guestTitle) descriptionLabel.setText(text: Constants.guestSubtitle) - loginButton.isHidden = false + actionButton.title = Constants.requestButtonTitle case .member: titleLabel.setText(text: Constants.memberTitle) descriptionLabel.setText(text: Constants.memberSubtitle) - loginButton.isHidden = true + actionButton.title = Constants.loginButtonTitle } } - @objc func tapLogin() { - onTapLogin?() + @objc func tapActionButton() { + onTapActionButton?() } } From dbdfb5176260c3152f6203307be1225fe771e56d Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:39:22 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[BOOK-476]=20feat:=20EmptyStateView?= =?UTF-8?q?=EC=97=90=20=EC=99=B8=EB=B6=80=EC=97=90=EC=84=9C=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A3=BC=EC=9E=85=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ArchiveFlow/View/ArchiveView.swift | 2 +- .../ArchiveFlow/View/EmptyStateView.swift | 42 +++++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Projects/BKPresentation/Sources/ArchiveFlow/View/ArchiveView.swift b/src/Projects/BKPresentation/Sources/ArchiveFlow/View/ArchiveView.swift index 034e8d89..41281e37 100644 --- a/src/Projects/BKPresentation/Sources/ArchiveFlow/View/ArchiveView.swift +++ b/src/Projects/BKPresentation/Sources/ArchiveFlow/View/ArchiveView.swift @@ -73,7 +73,7 @@ final class ArchiveView: BaseView, UIGestureRecognizerDelegate { setupLayout() updateEmptyState() - emptyStateView.onTapLogin = { [weak self] in + emptyStateView.onTapActionButton = { [weak self] in self?.eventPublisher.send(.loginButtonTapped) } diff --git a/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift b/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift index a582aa9e..e3a0ec68 100644 --- a/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift +++ b/src/Projects/BKPresentation/Sources/ArchiveFlow/View/EmptyStateView.swift @@ -8,6 +8,10 @@ import SnapKit final class EmptyStateView: BaseView { var onTapActionButton: (() -> Void)? + private var memberCustomTitle: String? + private var memberCustomDescription: String? + private var memberCustomActionTitle: String? + private var cancellables = Set() private let titleLabel = BKLabel( text: "", @@ -50,18 +54,28 @@ final class EmptyStateView: BaseView { } override func configure() { - actionButton.isHidden = true actionButton.addTarget(self, action: #selector(tapActionButton), for: .touchUpInside) AccessModeCenter.shared.mode .receive(on: DispatchQueue.main) .sink { [weak self] mode in - self?.apply(mode: mode) + self?.updateUI(for: mode) } .store(in: &cancellables) - apply(mode: AccessModeCenter.shared.mode.value) + updateUI(for: AccessModeCenter.shared.mode.value) + } + + public func setContent(title: String, description: String, actionTitle: String? = nil) { + self.memberCustomTitle = title + self.memberCustomDescription = description + self.memberCustomActionTitle = actionTitle + + if AccessModeCenter.shared.mode.value == .member { + updateUI(for: .member) + } } + } private extension EmptyStateView { @@ -71,19 +85,29 @@ private extension EmptyStateView { static let memberTitle = "아직 등록된 책이 없어요" static let memberSubtitle = "도서 등록 후 나만의 아카이브를 만들어보세요" static let loginButtonTitle = "로그인하기" - static let requestButtonTitle = "문의하기" } - func apply(mode: AppAccessMode) { + private func updateUI(for mode: AppAccessMode) { switch mode { case .guest: titleLabel.setText(text: Constants.guestTitle) descriptionLabel.setText(text: Constants.guestSubtitle) - actionButton.title = Constants.requestButtonTitle - case .member: - titleLabel.setText(text: Constants.memberTitle) - descriptionLabel.setText(text: Constants.memberSubtitle) actionButton.title = Constants.loginButtonTitle + actionButton.isHidden = false + + case .member: + let title = memberCustomTitle ?? Constants.memberTitle + let desc = memberCustomDescription ?? Constants.memberSubtitle + + titleLabel.setText(text: title) + descriptionLabel.setText(text: desc) + + if let actionTitle = memberCustomActionTitle { + actionButton.title = actionTitle + actionButton.isHidden = false + } else { + actionButton.isHidden = true + } } } From b7db61d4ebb84f186c3a6b3126b7d8518255c998 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:39:52 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[BOOK-476]=20feat:=20SearchView=EC=97=90?= =?UTF-8?q?=20EmptyView=20=EC=83=81=ED=83=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainFlow/Search/View/SearchView.swift | 46 ++++++++++++++++--- .../Search/View/SearchViewController.swift | 17 +++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift index e153b08a..26a6fcd1 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift @@ -26,6 +26,7 @@ final class SearchView: BaseView { private let divider = BKDivider(type: .medium) private let header = SearchSectionHeaderView() + private let emptyView = EmptyStateView() private lazy var collectionView: UICollectionView = { return setupCollectionView() @@ -38,13 +39,23 @@ final class SearchView: BaseView { private var layoutMode = CollectionLayoutMode.beforeSearch override func setupView() { - addSubviews(searchBar, divider, header, collectionView) + addSubviews(searchBar, divider, header, collectionView, emptyView) } override func configure() { searchBar.setOnReturn { [weak self] text in self?.eventPublisher.send(.search(text)) } + + emptyView.onTapActionButton = { [weak self] in + let currentMode = AccessModeCenter.shared.mode.value + + if currentMode == .guest { + self?.eventPublisher.send(.goToLogin) + } else { + self?.eventPublisher.send(.goToRequestPage) + } + } } override func setupLayout() { @@ -72,6 +83,12 @@ final class SearchView: BaseView { .offset(LayoutConstants.headerOffset) $0.leading.trailing.bottom.equalToSuperview() } + + emptyView.snp.makeConstraints { + $0.top.equalTo(header.snp.bottom) + .offset(LayoutConstants.headerOffset) + $0.leading.trailing.bottom.equalToSuperview() + } } func setSearchBarPlaceholder(with placeholder: String) { @@ -90,6 +107,9 @@ final class SearchView: BaseView { switch state { case .recent(let state): + emptyView.isHidden = true + collectionView.isHidden = false + if state.queries.isEmpty { collectionView.backgroundView = makeEmptyLabel(state.placeholder) } else { @@ -102,12 +122,15 @@ final class SearchView: BaseView { case .result(let state): let isEmpty = state.books.isEmpty && state.bookInfos.isEmpty if isEmpty { - header.layoutIfNeeded() - let headerHeight = header.bounds.height - let offset = -(headerHeight / 2.0) - collectionView.backgroundView = makeEmptyLabel(state.placeholder, verticalOffset: offset) + updateEmptyState( + isHidden: false, + title: "아직 등록된 책이 없어요", + description: "카카오톡 채널로 문의를 남겨주세요", + buttontitle: "문의하기" + ) searchBar.setClearButtonMode(.whileEditing) } else { + collectionView.isHidden = false collectionView.backgroundView = nil snapshot.appendSections([.result]) // Book과 BookInfo 모두 처리 @@ -277,7 +300,18 @@ private extension SearchView { return container } - @objc func searchButtonTapped() { + private func updateEmptyState(isHidden: Bool, title: String = "", description: String = "", buttontitle: String = "") { + emptyView.isHidden = isHidden + collectionView.isHidden = !isHidden + + if !isHidden { + emptyView.setContent(title: title, description: description, actionTitle: buttontitle) + bringSubviewToFront(emptyView) + } + } + + @objc + func searchButtonTapped() { guard let text = searchBar.text, !text.isEmpty else { return } eventPublisher.send(.search(text)) } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift index 5dfe24c4..800e68f0 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift @@ -11,6 +11,8 @@ enum SearchViewEvent: Equatable { case deleteRecentQuery(String) case upsertBook(String) case goToBookDetail(isbn: String, userBookId: String) + case goToLogin + case goToRequestPage } final class SearchViewController: BaseViewController, ScreenLoggable { @@ -70,6 +72,21 @@ final class SearchViewController: BaseViewController, ScreenLoggable } .store(in: &cancellable) + contentView.eventPublisher + .filter { $0 == .goToLogin } + .sink { [weak self] _ in + print("LOG: 게스트 모드 - 로그인 화면으로 이동") + self?.coordinator?.notifyAuthenticationRequired { } + } + .store(in: &cancellable) + + contentView.eventPublisher + .filter { $0 == .goToRequestPage } + .sink { [weak self] _ in + print("LOG: 멤버 모드 - 문의하기 페이지로 이동") + } + .store(in: &cancellable) + contentView.eventPublisher .compactMap { event -> String? in if case let .deleteRecentQuery(query) = event { return query } From 80479a9e25e7b46f562bcf64e2f7046257b31e6b Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:08:24 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[BOOK-476]=20chore:=20kakao=5Fchat=5Furl?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Copyright=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SupportingFiles/Booket/Info.plist | 2 ++ src/Workspace.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SupportingFiles/Booket/Info.plist b/src/SupportingFiles/Booket/Info.plist index 22b7c5c4..b579cdb8 100644 --- a/src/SupportingFiles/Booket/Info.plist +++ b/src/SupportingFiles/Booket/Info.plist @@ -43,6 +43,8 @@ KAKAO_NATIVE_APP_KEY $(KAKAO_NATIVE_APP_KEY) + KAKAO_CHAT_URL + $(KAKAO_CHAT_URL) LSApplicationQueriesSchemes kakaokompassauth diff --git a/src/Workspace.swift b/src/Workspace.swift index b179edf5..ec7a4f49 100644 --- a/src/Workspace.swift +++ b/src/Workspace.swift @@ -6,5 +6,5 @@ let workspace = Workspace( projects: BKModule.allCases.map { .relativeToRoot("Projects/\($0.rawValue)") }, - fileHeaderTemplate: "Copyright © 2025 Booket. All rights reserved" + fileHeaderTemplate: "Copyright © 2026 Booket. All rights reserved" ) From 17136284b3ee725e3950d956f1854d649e1efba2 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:09:02 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[BOOK-476]=20feat:=20ExternalLinkReposito?= =?UTF-8?q?ry=20&=20OpenExternalLinkUseCase=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BKDomain/Sources/DomainAssembly.swift | 5 +++++ .../Repository/ExternalLinkRepository.swift | 9 +++++++++ .../Usecase/OpenExternalLinkUseCase.swift | 8 ++++++++ .../UseCase/DefaultOpenExternalLinkUseCase.swift | 16 ++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift create mode 100644 src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift create mode 100644 src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift diff --git a/src/Projects/BKDomain/Sources/DomainAssembly.swift b/src/Projects/BKDomain/Sources/DomainAssembly.swift index dbf3d40b..9961b0a0 100644 --- a/src/Projects/BKDomain/Sources/DomainAssembly.swift +++ b/src/Projects/BKDomain/Sources/DomainAssembly.swift @@ -255,5 +255,10 @@ public struct DomainAssembly: Assembly { notificationRepository: notificationRepository ) } + + container.register(type: OpenExternalLinkUseCase.self) { _ in + @Autowired var repository: ExternalLinkRepository + return DefaultOpenExternalLinkUseCase(repository: repository) + } } } diff --git a/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift b/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift new file mode 100644 index 00000000..79964e5d --- /dev/null +++ b/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift @@ -0,0 +1,9 @@ +// Copyright © 2026 Booket. All rights reserved + +import Combine +import Foundation + +public protocol ExternalLinkRepository { + /// 전달받은 URL 문자열을 통해 외부 링크를 실행합니다. + func open(_ urlString: String) -> AnyPublisher +} diff --git a/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift b/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift new file mode 100644 index 00000000..55318641 --- /dev/null +++ b/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift @@ -0,0 +1,8 @@ +// Copyright © 2026 Booket. All rights reserved + +import Combine +import Foundation + +public protocol OpenExternalLinkUseCase { + func execute(url: String) -> AnyPublisher +} diff --git a/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift b/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift new file mode 100644 index 00000000..4614caf1 --- /dev/null +++ b/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift @@ -0,0 +1,16 @@ +// Copyright © 2026 Booket. All rights reserved + +import Combine +import Foundation + +public struct DefaultOpenExternalLinkUseCase: OpenExternalLinkUseCase { + private let repository: ExternalLinkRepository + + init(repository: ExternalLinkRepository) { + self.repository = repository + } + + public func execute(url: String) -> AnyPublisher { + repository.open(url) + } +} From 945e3216b6fc781029f8ffe2593d5e8721c99bde Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:16:52 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[BOOK-476]=20feat:=20DefaultExternalLinkR?= =?UTF-8?q?epository=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BKData/Sources/DataAssembly.swift | 4 +++ .../DefaultExternalLinkRepository.swift | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift diff --git a/src/Projects/BKData/Sources/DataAssembly.swift b/src/Projects/BKData/Sources/DataAssembly.swift index ac54e2c5..52d6ea7c 100644 --- a/src/Projects/BKData/Sources/DataAssembly.swift +++ b/src/Projects/BKData/Sources/DataAssembly.swift @@ -165,5 +165,9 @@ public struct DataAssembly: Assembly { pushTokenStore: pushTokenStore ) } + + container.register(type: ExternalLinkRepository.self) { _ in + return DefaultExternalLinkRepository() + } } } diff --git a/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift b/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift new file mode 100644 index 00000000..df57306b --- /dev/null +++ b/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift @@ -0,0 +1,28 @@ +// Copyright © 2026 Booket. All rights reserved + +import BKCore +import BKDomain +import Combine +import UIKit + +final class DefaultExternalLinkRepository: ExternalLinkRepository { + func open(_ urlString: String) -> AnyPublisher { + return Future { promise in + guard let url = URL(string: urlString) else { + promise(.success(false)) + return + } + + DispatchQueue.main.async { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) { success in + promise(.success(success)) + } + } else { + promise(.success(false)) + } + } + } + .eraseToAnyPublisher() + } +} From cd75dc47e8a62f1d776f479da6646d453b0ce295 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:53:12 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[BOOK-476]=20feat:=20AppScheme=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/DefaultExternalLinkRepository.swift | 9 +++++++++ .../Interface/Repository/ExternalLinkRepository.swift | 11 +++++++++++ .../Interface/Usecase/OpenExternalLinkUseCase.swift | 4 +++- .../UseCase/DefaultOpenExternalLinkUseCase.swift | 8 ++++++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift b/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift index df57306b..251996f8 100644 --- a/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift +++ b/src/Projects/BKData/Sources/Repository/DefaultExternalLinkRepository.swift @@ -6,9 +6,18 @@ import Combine import UIKit final class DefaultExternalLinkRepository: ExternalLinkRepository { + func canOpen(_ urlString: String) -> Bool { + guard let url = URL(string: urlString) else { + Log.error("유효하지 않은 URL 형식: \(urlString)", logger: AppLogger.network) + return false + } + return UIApplication.shared.canOpenURL(url) + } + func open(_ urlString: String) -> AnyPublisher { return Future { promise in guard let url = URL(string: urlString) else { + Log.error("URL 객체 생성 실패: \(urlString)", logger: AppLogger.network) promise(.success(false)) return } diff --git a/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift b/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift index 79964e5d..e09f7b8a 100644 --- a/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift +++ b/src/Projects/BKDomain/Sources/Interface/Repository/ExternalLinkRepository.swift @@ -3,7 +3,18 @@ import Combine import Foundation +/// 외부 시스템(앱 스킴 또는 웹 브라우저)으로 링크를 연결하고 상태를 확인하는 인터페이스입니다. public protocol ExternalLinkRepository { + + /// 전달받은 URL 문자열이 현재 시스템에서 실행 가능한지 여부를 확인합니다. + /// + /// - Parameter urlString: 확인할 대상 URL 문자열 (예: "kakaoplus://...", "https://...") + /// - Returns: 실행 가능 여부 (true: 실행 가능, false: 실행 불가 또는 스킴 미등록) + func canOpen(_ urlString: String) -> Bool + /// 전달받은 URL 문자열을 통해 외부 링크를 실행합니다. + /// + /// - Parameter urlString: 실행할 대상 URL 문자열 + /// - Returns: 실행 성공 여부를 전달하는 Publisher (true: 실행 성공, false: 실행 실패) func open(_ urlString: String) -> AnyPublisher } diff --git a/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift b/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift index 55318641..3affdb0e 100644 --- a/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift +++ b/src/Projects/BKDomain/Sources/Interface/Usecase/OpenExternalLinkUseCase.swift @@ -4,5 +4,7 @@ import Combine import Foundation public protocol OpenExternalLinkUseCase { - func execute(url: String) -> AnyPublisher + /// 외부 링크를 실행합니다. + /// appScheme이 있고 실행 가능한 경우 우선 실행하며, 실패 시 urlString을 실행합니다. + func execute(urlString: String, appScheme: String?) -> AnyPublisher } diff --git a/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift b/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift index 4614caf1..60874a60 100644 --- a/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift +++ b/src/Projects/BKDomain/Sources/UseCase/DefaultOpenExternalLinkUseCase.swift @@ -10,7 +10,11 @@ public struct DefaultOpenExternalLinkUseCase: OpenExternalLinkUseCase { self.repository = repository } - public func execute(url: String) -> AnyPublisher { - repository.open(url) + public func execute(urlString: String, appScheme: String?) -> AnyPublisher { + if let appScheme = appScheme, repository.canOpen(appScheme) { + return repository.open(appScheme) + } + + return repository.open(urlString) } } From dc5a21c8150e6e8e1f593825599225ef44681401 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:53:42 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[BOOK-476]=20feat:=20SearchCoordinator?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9B=B9=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Coordinator/SearchCoordinator.swift | 25 +++++++++++++++++++ .../Search/View/SearchViewController.swift | 4 ++- .../Search/ViewModel/SearchViewModel.swift | 5 +++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift index e3a71a2b..109bb033 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/Coordinator/SearchCoordinator.swift @@ -1,5 +1,9 @@ // Copyright © 2025 Booket. All rights reserved +import BKCore +import BKDesign +import BKDomain +import Combine import UIKit final class SearchCoordinator: Coordinator { @@ -8,6 +12,9 @@ final class SearchCoordinator: Coordinator { var navigationController: UINavigationController private let searchViewType: SearchViewType + private var cancellables = Set() + + @Autowired var openExternalLinkUseCase: OpenExternalLinkUseCase init( parentCoordinator: Coordinator?, @@ -57,4 +64,22 @@ extension SearchCoordinator { addChildCoordinator(bookDetailCoordinator) bookDetailCoordinator.start() } + + func showRequestPage() { + let webURL = URLConstants.kakaoChatURL + let appScheme = URLConstants.kakaoAppScheme + + Log.debug("외부 링크 오픈 시도 - Web: \(webURL), App: \(appScheme)", logger: AppLogger.ui) + + openExternalLinkUseCase.execute(urlString: webURL, appScheme: appScheme) + .receive(on: DispatchQueue.main) + .sink { success in + if success { + Log.debug("외부 링크 오픈 성공", logger: AppLogger.ui) + } else { + Log.error("외부 링크 오픈 실패 (URL 스킴 확인 필요)", logger: AppLogger.ui) + } + } + .store(in: &cancellables) + } } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift index 800e68f0..388b0045 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchViewController.swift @@ -83,7 +83,9 @@ final class SearchViewController: BaseViewController, ScreenLoggable contentView.eventPublisher .filter { $0 == .goToRequestPage } .sink { [weak self] _ in - print("LOG: 멤버 모드 - 문의하기 페이지로 이동") + self?.viewModel.send(.requestPageTapped) + + self?.coordinator?.showRequestPage() } .store(in: &cancellable) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift index 7a06c0f3..98173e90 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/ViewModel/SearchViewModel.swift @@ -70,7 +70,6 @@ enum SearchViewType: String { } } - final class SearchViewModel: BaseViewModel { enum SearchState: Equatable { case recent(RecentState) @@ -114,6 +113,7 @@ final class SearchViewModel: BaseViewModel { case errorHandled case retryTapped case lastRetryTapped + case requestPageTapped } enum SideEffect { @@ -317,6 +317,9 @@ final class SearchViewModel: BaseViewModel { case .errorHandled: newState.error = nil + + case .requestPageTapped: + Log.debug("문의하기 버튼 클릭 액션 수신", logger: AppLogger.viewModel) } return (newState, effects) From 5a934c50a13a8cd4d2b36e74763e6e1b45be7777 Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:04:49 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[BOOK-476]=20chore:=20kakao=20plus=20chat?= =?UTF-8?q?=20account=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Constant/URLConstants.swift | 19 +++++++++++++++++++ src/SupportingFiles/Booket/Info.plist | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/Projects/BKPresentation/Sources/Constant/URLConstants.swift diff --git a/src/Projects/BKPresentation/Sources/Constant/URLConstants.swift b/src/Projects/BKPresentation/Sources/Constant/URLConstants.swift new file mode 100644 index 00000000..9a0dd0ab --- /dev/null +++ b/src/Projects/BKPresentation/Sources/Constant/URLConstants.swift @@ -0,0 +1,19 @@ +// Copyright © 2026 Booket. All rights reserved + +import Foundation + +private final class PresentationBundleToken {} + +public enum URLConstants { + private static let bundle = Bundle(for: PresentationBundleToken.self) + + private static let kakaoAccount: String = { + guard let value = bundle.object(forInfoDictionaryKey: "KAKAO_ACCOUNT") as? String else { + fatalError("Can't load KAKAO_ACCOUNT") + } + return value + }() + + public static let kakaoAppScheme = "kakaoplus://plusfriend/home/\(kakaoAccount)" + public static let kakaoChatURL = "https://pf.kakao.com/\(kakaoAccount)/chat" +} diff --git a/src/SupportingFiles/Booket/Info.plist b/src/SupportingFiles/Booket/Info.plist index b579cdb8..607cf91b 100644 --- a/src/SupportingFiles/Booket/Info.plist +++ b/src/SupportingFiles/Booket/Info.plist @@ -43,8 +43,8 @@ KAKAO_NATIVE_APP_KEY $(KAKAO_NATIVE_APP_KEY) - KAKAO_CHAT_URL - $(KAKAO_CHAT_URL) + KAKAO_ACCOUNT + $(KAKAO_ACCOUNT) LSApplicationQueriesSchemes kakaokompassauth From 68422636ccbe3fc1c40e302a3facc7fa69a2677d Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:22:16 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[BOOK-476]=20fix:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainFlow/Search/View/SearchView.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift index 26a6fcd1..6d893478 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift @@ -126,7 +126,7 @@ final class SearchView: BaseView { isHidden: false, title: "아직 등록된 책이 없어요", description: "카카오톡 채널로 문의를 남겨주세요", - buttontitle: "문의하기" + buttonTitle: "문의하기" ) searchBar.setClearButtonMode(.whileEditing) } else { @@ -296,16 +296,21 @@ private extension SearchView { $0.centerX.equalToSuperview() $0.centerY.equalToSuperview().offset(verticalOffset) } - + return container } - private func updateEmptyState(isHidden: Bool, title: String = "", description: String = "", buttontitle: String = "") { + private func updateEmptyState( + isHidden: Bool, + title: String = "", + description: String = "", + buttonTitle: String = "" + ) { emptyView.isHidden = isHidden collectionView.isHidden = !isHidden if !isHidden { - emptyView.setContent(title: title, description: description, actionTitle: buttontitle) + emptyView.setContent(title: title, description: description, actionTitle: buttonTitle) bringSubviewToFront(emptyView) } } From 5e153a32f87cadc06b05bdd1c7ac387d1db762ba Mon Sep 17 00:00:00 2001 From: doyeonk429 <80318425+doyeonk429@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:28:54 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[BOOK-476]=20fix:=20view=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EC=9D=BC=EA=B4=84=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MainFlow/Search/View/SearchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift index 6d893478..01b20fd1 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/Search/View/SearchView.swift @@ -130,7 +130,7 @@ final class SearchView: BaseView { ) searchBar.setClearButtonMode(.whileEditing) } else { - collectionView.isHidden = false + updateEmptyState(isHidden: true) collectionView.backgroundView = nil snapshot.appendSections([.result]) // Book과 BookInfo 모두 처리