diff --git a/src/Projects/BKData/Sources/API/AuthAPI.swift b/src/Projects/BKData/Sources/API/AuthAPI.swift index c9709acf..d4664a68 100644 --- a/src/Projects/BKData/Sources/API/AuthAPI.swift +++ b/src/Projects/BKData/Sources/API/AuthAPI.swift @@ -16,7 +16,7 @@ public enum AuthAPI { extension AuthAPI: RequestTarget { public var baseURL: String { - return "\(APIConfig.baseURL)/auth" + return "\(APIConfig.baseURLv1)/auth" } public var path: String { diff --git a/src/Projects/BKData/Sources/API/BookAPI.swift b/src/Projects/BKData/Sources/API/BookAPI.swift index 50183d56..675b56e0 100644 --- a/src/Projects/BKData/Sources/API/BookAPI.swift +++ b/src/Projects/BKData/Sources/API/BookAPI.swift @@ -14,7 +14,7 @@ enum BookAPI { extension BookAPI: RequestTarget { var baseURL: String { - return "\(APIConfig.baseURL)/books" + return "\(APIConfig.baseURLv1)/books" } var path: String { diff --git a/src/Projects/BKData/Sources/API/EmotionAPI.swift b/src/Projects/BKData/Sources/API/EmotionAPI.swift new file mode 100644 index 00000000..4193da3d --- /dev/null +++ b/src/Projects/BKData/Sources/API/EmotionAPI.swift @@ -0,0 +1,41 @@ +// Copyright © 2025 Booket. All rights reserved + +import Foundation + +enum EmotionAPI { + case fetchEmotions +} + +extension EmotionAPI: RequestTarget { + var baseURL: String { + return "\(APIConfig.baseURLv2)/emotions" + } + + var path: String { + switch self { + case .fetchEmotions: + return "" + } + } + + var method: HTTPMethod { + switch self { + case .fetchEmotions: + return .get + } + } + + var headers: [String: String] { + return [ + "Content-Type": "application/json" + ] + } + + var body: Encodable? { + return nil + } + + var query: [String: Any] { + return [:] + } +} diff --git a/src/Projects/BKData/Sources/API/HomeAPI.swift b/src/Projects/BKData/Sources/API/HomeAPI.swift index 37c3356b..0eef7be8 100644 --- a/src/Projects/BKData/Sources/API/HomeAPI.swift +++ b/src/Projects/BKData/Sources/API/HomeAPI.swift @@ -8,7 +8,7 @@ enum HomeAPI { extension HomeAPI: RequestTarget { var baseURL: String { - return "\(APIConfig.baseURL)/home" + return "\(APIConfig.baseURLv1)/home" } var path: String { diff --git a/src/Projects/BKData/Sources/API/RecordAPI.swift b/src/Projects/BKData/Sources/API/RecordAPI.swift index 4aaab2e7..6dbfcf29 100644 --- a/src/Projects/BKData/Sources/API/RecordAPI.swift +++ b/src/Projects/BKData/Sources/API/RecordAPI.swift @@ -14,12 +14,7 @@ enum RecordAPI { extension RecordAPI: RequestTarget { var baseURL: String { - switch self { - case .fetch, .seed: - return "\(APIConfig.baseV2URL)/reading-records" - default: - return "\(APIConfig.baseURL)/reading-records" - } + return "\(APIConfig.baseURLv2)/reading-records" } var path: String { @@ -46,7 +41,7 @@ extension RecordAPI: RequestTarget { case .fetch, .detail, .seed: return .get case .patch: - return .patch + return .put // V2 API uses PUT instead of PATCH case .delete: return .delete } diff --git a/src/Projects/BKData/Sources/API/UserAPI.swift b/src/Projects/BKData/Sources/API/UserAPI.swift index d6d7cb41..b9beb9e2 100644 --- a/src/Projects/BKData/Sources/API/UserAPI.swift +++ b/src/Projects/BKData/Sources/API/UserAPI.swift @@ -11,7 +11,7 @@ enum UserAPI { extension UserAPI: RequestTarget { var baseURL: String { - return "\(APIConfig.baseURL)/users/me" + return "\(APIConfig.baseURLv1)/users/me" } var path: String { diff --git a/src/Projects/BKData/Sources/Constant/APIConfig.swift b/src/Projects/BKData/Sources/Constant/APIConfig.swift index 561d71d3..5a1d6b04 100644 --- a/src/Projects/BKData/Sources/Constant/APIConfig.swift +++ b/src/Projects/BKData/Sources/Constant/APIConfig.swift @@ -6,18 +6,22 @@ private final class BKDataBundleToken {} enum APIConfig { private static let bundle = Bundle(for: BKDataBundleToken.self) - - static let baseURL: String = { + + /// API Base URL (xcconfig에서 /api까지만 포함) + private static let baseURL: String = { guard let value = bundle.object(forInfoDictionaryKey: "BASE_API_URL") as? String else { fatalError("Can't load environment: BKData.BASE_API_URL") } return value }() - - static let baseV2URL: String = { - guard let value = bundle.object(forInfoDictionaryKey: "BASE_API_V2_URL") as? String else { - fatalError("Can't load environment: BKData.BASE_API_V2_URL") - } - return value + + /// V1 API Base URL (auth, books, users, home) + static let baseURLv1: String = { + return baseURL + "/v1" + }() + + /// V2 API Base URL (emotions, reading-records) + static let baseURLv2: String = { + return baseURL + "/v2" }() } diff --git a/src/Projects/BKData/Sources/DTO/Request/InsertRecordRequestDTO.swift b/src/Projects/BKData/Sources/DTO/Request/InsertRecordRequestDTO.swift index 6b5d6efc..efa86302 100644 --- a/src/Projects/BKData/Sources/DTO/Request/InsertRecordRequestDTO.swift +++ b/src/Projects/BKData/Sources/DTO/Request/InsertRecordRequestDTO.swift @@ -4,29 +4,33 @@ import BKDomain import Foundation struct InsertRecordRequestDTO: Encodable { - let pageNumber: Int + let pageNumber: Int? let quote: String let review: String? - let emotionTags: [String] - + let primaryEmotion: String + let detailEmotionTagIds: [String] + init( - pageNumber: Int, + pageNumber: Int?, quote: String, review: String?, - emotionTags: [String] + primaryEmotion: String, + detailEmotionTagIds: [String] ) { self.pageNumber = pageNumber self.quote = quote self.review = review - self.emotionTags = emotionTags + self.primaryEmotion = primaryEmotion + self.detailEmotionTagIds = detailEmotionTagIds } - + init(data: RecordVO) { self.init( pageNumber: data.pageNumber, quote: data.quote, - review: data.review, - emotionTags: data.emotionTags + review: data.memo, + primaryEmotion: data.primaryEmotion.rawValue, + detailEmotionTagIds: data.detailEmotionIds ) } } diff --git a/src/Projects/BKData/Sources/DTO/Response/DetailRecordResponseDTO.swift b/src/Projects/BKData/Sources/DTO/Response/DetailRecordResponseDTO.swift index cc64b1dd..3415ffdb 100644 --- a/src/Projects/BKData/Sources/DTO/Response/DetailRecordResponseDTO.swift +++ b/src/Projects/BKData/Sources/DTO/Response/DetailRecordResponseDTO.swift @@ -3,19 +3,20 @@ import BKDomain import Foundation -public struct DetailRecordResponseDTO: Decodable { - public let id: String - public let userBookId: String - public let pageNumber: Int - public let quote: String - public let review: String? - public let emotionTags: [Emotion] - public let createdAt: String - public let updatedAt: String - public let bookTitle: String - public let bookPublisher: String - public let bookCoverImageUrl: URL - public let author: String +struct DetailRecordResponseDTO: Decodable { + let id: String + let userBookId: String + let pageNumber: Int? + let quote: String + let review: String? + let primaryEmotion: PrimaryEmotionDTO + let detailEmotions: [DetailEmotionDTO] + let createdAt: String + let updatedAt: String + let bookTitle: String + let bookPublisher: String + let bookCoverImageUrl: URL + let author: String } extension DetailRecordResponseDTO { @@ -26,7 +27,8 @@ extension DetailRecordResponseDTO { pageNumber: pageNumber, quote: quote, review: review, - emotionTags: emotionTags, + primaryEmotion: primaryEmotion.toDomain() ?? .other, + detailEmotions: detailEmotions.map { $0.toDomain() }, createdAt: DateParser.parseISO8601(createdAt) ?? .distantPast, updatedAt: DateParser.parseISO8601(updatedAt), bookTitle: bookTitle, @@ -41,11 +43,11 @@ extension DetailRecordResponseDTO { public struct DetailRecordV2ResponseDTO: Decodable { public let id: String public let userBookId: String - public let pageNumber: Int + public let pageNumber: Int? public let quote: String public let review: String? public let primaryEmotion: PrimaryEmotionResponseDTO - public let detailEmotions: [DetailEmotionResponseDTO?] + public let detailEmotions: [DetailEmotionResponseDTO] public let createdAt: String public let updatedAt: String public let bookTitle: String @@ -62,7 +64,8 @@ extension DetailRecordV2ResponseDTO { pageNumber: pageNumber, quote: quote, review: review, - emotionTags: [primaryEmotion.displayName], + primaryEmotion: primaryEmotion.toDomain(), + detailEmotions: detailEmotions.map { $0.toDomain() }, createdAt: DateParser.parseISO8601(createdAt) ?? .distantPast, updatedAt: DateParser.parseISO8601(updatedAt), bookTitle: bookTitle, diff --git a/src/Projects/BKData/Sources/DTO/Response/EmotionResponseDTO.swift b/src/Projects/BKData/Sources/DTO/Response/EmotionResponseDTO.swift new file mode 100644 index 00000000..d46eea78 --- /dev/null +++ b/src/Projects/BKData/Sources/DTO/Response/EmotionResponseDTO.swift @@ -0,0 +1,53 @@ +// Copyright © 2025 Booket. All rights reserved + +import BKDomain +import Foundation + +// MARK: - API Response DTO + +struct EmotionListResponseDTO: Decodable { + let emotions: [EmotionGroupDTO] +} + +struct EmotionGroupDTO: Decodable { + let code: String + let displayName: String + let detailEmotions: [DetailEmotionDTO] +} + +struct DetailEmotionDTO: Decodable { + let id: String + let name: String +} + +// MARK: - Mapping to Domain + +extension EmotionGroupDTO { + func toDomain() -> EmotionGroup? { + guard let primaryEmotion = PrimaryEmotion(rawValue: code) else { + return nil + } + return EmotionGroup( + primaryEmotion: primaryEmotion, + displayName: displayName, + detailEmotions: detailEmotions.map { $0.toDomain() } + ) + } +} + +extension DetailEmotionDTO { + func toDomain() -> DetailEmotion { + return DetailEmotion(id: id, name: name) + } +} + +// MARK: - Response에서 사용하는 DTO (기록 조회 시) + +struct PrimaryEmotionDTO: Decodable { + let code: String + let displayName: String + + func toDomain() -> PrimaryEmotion? { + return PrimaryEmotion(rawValue: code) + } +} diff --git a/src/Projects/BKData/Sources/DTO/Response/InsertRecordResponseDTO.swift b/src/Projects/BKData/Sources/DTO/Response/InsertRecordResponseDTO.swift index ec86c90a..1d5c2869 100644 --- a/src/Projects/BKData/Sources/DTO/Response/InsertRecordResponseDTO.swift +++ b/src/Projects/BKData/Sources/DTO/Response/InsertRecordResponseDTO.swift @@ -3,37 +3,23 @@ import BKDomain import Foundation -public struct InsertRecordResponseDTO: Decodable { +struct InsertRecordResponseDTO: Decodable { let id: String let userBookId: String - let pageNumber: Int + let pageNumber: Int? let quote: String let review: String? - let emotionTags: [Emotion] + let primaryEmotion: PrimaryEmotionDTO + let detailEmotions: [DetailEmotionDTO] let createdAt: String let updatedAt: String let bookTitle: String let bookPublisher: String let bookCoverImageUrl: URL let author: String - - enum CodingKeys: String, CodingKey { - case id - case userBookId - case pageNumber - case quote - case review - case emotionTags - case createdAt - case updatedAt - case bookTitle - case bookPublisher - case bookCoverImageUrl - case author - } } -public extension InsertRecordResponseDTO { +extension InsertRecordResponseDTO { func toRecordInfo() -> RecordInfo { return RecordInfo( recordId: id, @@ -41,7 +27,8 @@ public extension InsertRecordResponseDTO { pageNumber: pageNumber, quote: quote, review: review, - emotionTags: emotionTags, + primaryEmotion: primaryEmotion.toDomain() ?? .other, + detailEmotions: detailEmotions.map { $0.toDomain() }, createdAt: DateParser.parseISO8601(createdAt) ?? .distantPast, updatedAt: DateParser.parseISO8601(updatedAt), bookTitle: bookTitle, diff --git a/src/Projects/BKData/Sources/DTO/Response/PrimaryEmotionResponseDTO.swift b/src/Projects/BKData/Sources/DTO/Response/PrimaryEmotionResponseDTO.swift index d10ab8e3..a5e54be0 100644 --- a/src/Projects/BKData/Sources/DTO/Response/PrimaryEmotionResponseDTO.swift +++ b/src/Projects/BKData/Sources/DTO/Response/PrimaryEmotionResponseDTO.swift @@ -4,10 +4,18 @@ import BKDomain public struct PrimaryEmotionResponseDTO: Decodable { let code: String - let displayName: Emotion + let displayName: String + + func toDomain() -> PrimaryEmotion { + return PrimaryEmotion(rawValue: code) ?? .other + } } public struct DetailEmotionResponseDTO: Decodable { let id: String let name: String + + func toDomain() -> DetailEmotion { + return DetailEmotion(id: id, name: name) + } } diff --git a/src/Projects/BKData/Sources/DataAssembly.swift b/src/Projects/BKData/Sources/DataAssembly.swift index 52d6ea7c..9bcbf531 100644 --- a/src/Projects/BKData/Sources/DataAssembly.swift +++ b/src/Projects/BKData/Sources/DataAssembly.swift @@ -165,9 +165,17 @@ public struct DataAssembly: Assembly { pushTokenStore: pushTokenStore ) } - + container.register(type: ExternalLinkRepository.self) { _ in return DefaultExternalLinkRepository() } + + container.register( + type: EmotionRepository.self, + scope: .singleton + ) { _ in + @Autowired(name: "OAuth") var networkProvider: NetworkProvider + return DefaultEmotionRepository(networkProvider: networkProvider) + } } } diff --git a/src/Projects/BKData/Sources/Repository/DefaultEmotionRepository.swift b/src/Projects/BKData/Sources/Repository/DefaultEmotionRepository.swift new file mode 100644 index 00000000..fc978c88 --- /dev/null +++ b/src/Projects/BKData/Sources/Repository/DefaultEmotionRepository.swift @@ -0,0 +1,77 @@ +// Copyright © 2025 Booket. All rights reserved + +import BKCore +import BKDomain +import Combine +import Foundation + +public final class DefaultEmotionRepository: EmotionRepository { + private let networkProvider: NetworkProvider + private var cachedEmotions: [EmotionGroup]? + private let cacheQueue = DispatchQueue(label: "com.booket.emotion.cache") + + public init(networkProvider: NetworkProvider) { + self.networkProvider = networkProvider + } + + public func fetchEmotions() -> AnyPublisher<[EmotionGroup], DomainError> { + networkProvider.request( + target: EmotionAPI.fetchEmotions, + type: EmotionListResponseDTO.self + ) + .mapError { $0.toDomainError() } + .debugError(logger: AppLogger.network) + .map { [weak self] response in + let emotions = response.emotions.compactMap { $0.toDomain() } + self?.cacheQueue.sync { + self?.cachedEmotions = emotions + } + return emotions + } + .eraseToAnyPublisher() + } + + public func getEmotions() -> AnyPublisher<[EmotionGroup], DomainError> { + var cached: [EmotionGroup]? + cacheQueue.sync { + cached = cachedEmotions + } + + if let cached { + return Just(cached) + .setFailureType(to: DomainError.self) + .eraseToAnyPublisher() + } + + return fetchEmotions() + } + + public func getDetailEmotions(for primaryEmotion: PrimaryEmotion) -> AnyPublisher<[DetailEmotion], DomainError> { + getEmotions() + .map { groups in + groups.first { $0.primaryEmotion == primaryEmotion }?.detailEmotions ?? [] + } + .eraseToAnyPublisher() + } + + public func findDetailEmotionId(name: String, in primaryEmotion: PrimaryEmotion) -> AnyPublisher { + getDetailEmotions(for: primaryEmotion) + .map { detailEmotions in + detailEmotions.first { $0.name == name }?.id + } + .eraseToAnyPublisher() + } + + public func findDetailEmotionName(id: String) -> AnyPublisher { + getEmotions() + .map { groups in + for group in groups { + if let emotion = group.detailEmotions.first(where: { $0.id == id }) { + return emotion.name + } + } + return nil + } + .eraseToAnyPublisher() + } +} diff --git a/src/Projects/BKData/Sources/Repository/DefaultRecordRepository.swift b/src/Projects/BKData/Sources/Repository/DefaultRecordRepository.swift index 2093ed2b..1ae73c93 100644 --- a/src/Projects/BKData/Sources/Repository/DefaultRecordRepository.swift +++ b/src/Projects/BKData/Sources/Repository/DefaultRecordRepository.swift @@ -47,11 +47,14 @@ public final class DefaultRecordRepository: RecordRepository { .mapError { $0.toDomainError() } .debugError(logger: AppLogger.network) .map { - RecordFetchResult( + let emotion: Emotion? = $0.representativeEmotion.flatMap { + Emotion(rawValue: $0.displayName) + } + return RecordFetchResult( infos: $0.readingRecords.map { $0.toRecordInfo() }, hasMore: !$0.lastPage, totalCount: $0.totalResults, - mainEmotion: $0.representativeEmotion?.displayName + mainEmotion: emotion ) } .eraseToAnyPublisher() diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/Contents.json new file mode 100644 index 00000000..5b499040 --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "note_default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "note_default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "note_default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default.png new file mode 100644 index 00000000..e47ab69a Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default@2x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default@2x.png new file mode 100644 index 00000000..0690d3ea Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default@2x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default@3x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default@3x.png new file mode 100644 index 00000000..27fe9d79 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_default.imageset/note_default@3x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/Contents.json new file mode 100644 index 00000000..84d99e59 --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "note_insight.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "note_insight@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "note_insight@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight.png new file mode 100644 index 00000000..955eff94 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight@2x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight@2x.png new file mode 100644 index 00000000..402631f5 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight@2x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight@3x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight@3x.png new file mode 100644 index 00000000..a693d46b Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_insight.imageset/note_insight@3x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/Contents.json new file mode 100644 index 00000000..7acd213e --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "note_joy.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "note_joy@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "note_joy@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy.png new file mode 100644 index 00000000..a18d33dc Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy@2x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy@2x.png new file mode 100644 index 00000000..d4b44820 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy@2x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy@3x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy@3x.png new file mode 100644 index 00000000..7c610ff0 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_joy.imageset/note_joy@3x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/Contents.json new file mode 100644 index 00000000..aa2ec241 --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "note_sad.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "note_sad@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "note_sad@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad.png new file mode 100644 index 00000000..695955e3 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad@2x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad@2x.png new file mode 100644 index 00000000..f04fd846 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad@2x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad@3x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad@3x.png new file mode 100644 index 00000000..af0732d3 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_sad.imageset/note_sad@3x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/Contents.json new file mode 100644 index 00000000..24be27ef --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "note_warm.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "note_warm@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "note_warm@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm.png new file mode 100644 index 00000000..c9af191e Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm@2x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm@2x.png new file mode 100644 index 00000000..510e6b32 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm@2x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm@3x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm@3x.png new file mode 100644 index 00000000..a4cb3395 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/note/note_warm.imageset/note_warm@3x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/Contents.json b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/Contents.json new file mode 100644 index 00000000..5551d1e6 --- /dev/null +++ b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "recordcard_etc.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "recordcard_etc@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "recordcard_etc@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc.png new file mode 100644 index 00000000..d5c0f0a6 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc@2x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc@2x.png new file mode 100644 index 00000000..311c33e2 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc@2x.png differ diff --git a/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc@3x.png b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc@3x.png new file mode 100644 index 00000000..c0fd2c08 Binary files /dev/null and b/src/Projects/BKDesign/Resources/Assets.xcassets/graphics/recordcard_etc.imageset/recordcard_etc@3x.png differ diff --git a/src/Projects/BKDesign/Sources/Components/Chip/BKChip.swift b/src/Projects/BKDesign/Sources/Components/Chip/BKChip.swift index ab401170..4534d757 100644 --- a/src/Projects/BKDesign/Sources/Components/Chip/BKChip.swift +++ b/src/Projects/BKDesign/Sources/Components/Chip/BKChip.swift @@ -20,9 +20,11 @@ public final class BKChip: UIView { } } + // swiftlint:disable empty_count public var count: Int = 0 { didSet { countLabel.setText(text: "\(count)") + countLabel.isHidden = (count == 0) } } @@ -50,16 +52,18 @@ public final class BKChip: UIView { updateAppearance() } + // swiftlint:disable empty_count private func setupViews() { titleLabel.setText(text: title) countLabel.setText(text: "\(count)") countLabel.setFontStyle(style: .label1(weight: .semiBold)) - + countLabel.isHidden = (count == 0) + backgroundColor = .bkBaseColor(.primary) - + labelContainer.addSubviews(titleLabel, countLabel) addSubviews(labelContainer) - + layer.borderWidth = 1 layer.borderColor = UIColor.bkBorderColor(.primary).cgColor } @@ -67,18 +71,21 @@ public final class BKChip: UIView { private func setupLayout() { titleLabel.snp.makeConstraints { $0.leading.top.bottom.equalToSuperview() + if count == 0 { + $0.trailing.equalToSuperview() + } } - + countLabel.snp.makeConstraints { $0.leading.equalTo(titleLabel.snp.trailing).offset(BKSpacing.spacing1) - $0.trailing.top.bottom.equalToSuperview() + $0.trailing.equalToSuperview() $0.centerY.equalTo(titleLabel) } - + labelContainer.snp.makeConstraints { $0.center.equalToSuperview() } - + self.snp.makeConstraints { $0.width.equalTo(labelContainer.snp.width).offset(BKSpacing.spacing3 * 2) $0.height.equalTo(36) diff --git a/src/Projects/BKDesign/Sources/Components/Chip/BKRemovableChip.swift b/src/Projects/BKDesign/Sources/Components/Chip/BKRemovableChip.swift new file mode 100644 index 00000000..e167c1ef --- /dev/null +++ b/src/Projects/BKDesign/Sources/Components/Chip/BKRemovableChip.swift @@ -0,0 +1,86 @@ +// Copyright © 2025 Booket. All rights reserved + +import SnapKit +import UIKit + +/// 삭제 버튼이 있는 선택된 상태의 Chip +/// EmotionRegistrationView에서 선택된 세부 감정을 표시하는 용도로 사용 +public final class BKRemovableChip: UIView { + private let titleLabel = BKLabel2() + + private let removeButton: UIImageView = { + let imageView = UIImageView(image: BKImage.Icon.x) + imageView.contentMode = .scaleAspectFit + imageView.tintColor = .bkContentColor(.brand) + return imageView + }() + + private let stackView: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.spacing = 4 + stack.alignment = .center + return stack + }() + + public var title: String = "" { + didSet { + titleLabel.setText(text: title) + } + } + + public var onRemove: (() -> Void)? + + public init(title: String, onRemove: (() -> Void)? = nil) { + super.init(frame: .zero) + self.title = title + self.onRemove = onRemove + setupUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + setupViews() + setupLayout() + setupGesture() + } + + private func setupViews() { + backgroundColor = UIColor(hex: "#E3F8E9") + layer.cornerRadius = 14 + layer.borderWidth = 1 + layer.borderColor = UIColor.bkBorderColor(.brand).cgColor + + titleLabel.setText(text: title) + titleLabel.setFontStyle(style: .label1(weight: .medium)) + titleLabel.setColor(color: .bkContentColor(.brand)) + + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(removeButton) + addSubview(stackView) + } + + private func setupLayout() { + stackView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 8)) + } + + removeButton.snp.makeConstraints { + $0.size.equalTo(14) + } + } + + private func setupGesture() { + isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapGesture) + } + + @objc private func handleTap() { + onRemove?() + } +} diff --git a/src/Projects/BKDesign/Sources/Foundation/GraphicSystem/BKImage.swift b/src/Projects/BKDesign/Sources/Foundation/GraphicSystem/BKImage.swift index a422f52d..c5c14c75 100644 --- a/src/Projects/BKDesign/Sources/Foundation/GraphicSystem/BKImage.swift +++ b/src/Projects/BKDesign/Sources/Foundation/GraphicSystem/BKImage.swift @@ -78,10 +78,19 @@ public enum BKImage { public static let joyCard = BKDesignAsset.recordcardJoy.image public static let sadCard = BKDesignAsset.recordcardSad.image public static let warmCard = BKDesignAsset.recordcardWarm.image + public static let etcCard = BKDesignAsset.recordcardEtc.image public static let warmCircle = BKDesignAsset.warmCircle.image public static let joyCircle = BKDesignAsset.joyCircle.image public static let sadCircle = BKDesignAsset.sadCircle.image public static let insightCircle = BKDesignAsset.insightCircle.image + + public enum Note { + public static let joy = BKDesignAsset.noteJoy.image + public static let sad = BKDesignAsset.noteSad.image + public static let warm = BKDesignAsset.noteWarm.image + public static let insight = BKDesignAsset.noteInsight.image + public static let `default` = BKDesignAsset.noteDefault.image + } } public enum Logos { diff --git a/src/Projects/BKDomain/Sources/DomainAssembly.swift b/src/Projects/BKDomain/Sources/DomainAssembly.swift index 9961b0a0..b60f3a43 100644 --- a/src/Projects/BKDomain/Sources/DomainAssembly.swift +++ b/src/Projects/BKDomain/Sources/DomainAssembly.swift @@ -255,7 +255,14 @@ public struct DomainAssembly: Assembly { notificationRepository: notificationRepository ) } - + + container.register( + type: FetchDetailEmotionsUseCase.self + ) { _ in + @Autowired var repository: EmotionRepository + return DefaultFetchDetailEmotionsUseCase(repository: repository) + } + container.register(type: OpenExternalLinkUseCase.self) { _ in @Autowired var repository: ExternalLinkRepository return DefaultOpenExternalLinkUseCase(repository: repository) diff --git a/src/Projects/BKDomain/Sources/Entity/DetailEmotion.swift b/src/Projects/BKDomain/Sources/Entity/DetailEmotion.swift new file mode 100644 index 00000000..b451d3cb --- /dev/null +++ b/src/Projects/BKDomain/Sources/Entity/DetailEmotion.swift @@ -0,0 +1,31 @@ +// Copyright © 2025 Booket. All rights reserved + +import Foundation + +/// 세부 감정 (서버에서 가져온 데이터) +public struct DetailEmotion: Codable, Equatable, Hashable { + public let id: String + public let name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } +} + +/// 감정 그룹 (대분류 + 세부감정 목록) +public struct EmotionGroup: Codable, Equatable { + public let primaryEmotion: PrimaryEmotion + public let displayName: String + public let detailEmotions: [DetailEmotion] + + public init( + primaryEmotion: PrimaryEmotion, + displayName: String, + detailEmotions: [DetailEmotion] + ) { + self.primaryEmotion = primaryEmotion + self.displayName = displayName + self.detailEmotions = detailEmotions + } +} diff --git a/src/Projects/BKDomain/Sources/Entity/Emotion.swift b/src/Projects/BKDomain/Sources/Entity/Emotion.swift index 9641d909..9226d3bc 100644 --- a/src/Projects/BKDomain/Sources/Entity/Emotion.swift +++ b/src/Projects/BKDomain/Sources/Entity/Emotion.swift @@ -5,5 +5,53 @@ public enum Emotion: String, CaseIterable, Decodable { case joy = "즐거움" case sad = "슬픔" case insight = "깨달음" - case etc = "기타" + case other = "기타" +} + +public enum SubEmotion: String, CaseIterable { + // 따뜻함 + case comforted = "위로받은" + case cozy = "포근한" + case tender = "다정한" + case grateful = "고마운" + case relieved = "마음이 놓이는" + case peaceful = "편안한" + + // 즐거움 + case excited = "설레는" + case satisfied = "뿌듯한" + case cheerful = "유쾌한" + case joyful = "기쁜" + case thrilling = "흥미진진한" + + // 슬픔 + case hollow = "허무한" + case lonely = "외로운" + case regretful = "아쉬운" + case stunned = "먹먹한" + case bittersweet = "애틋한" + case pitiful = "안타까운" + case nostalgic = "그리운" + + // 깨달음 + case amazed = "감탄한" + case insightful = "통찰력을 얻은" + case inspired = "영감을 받은" + case deepened = "생각이 깊어진" + case understood = "새롭게 이해한" + + public static func subEmotions(for emotion: Emotion) -> [SubEmotion] { + switch emotion { + case .warmth: + return [.comforted, .cozy, .tender, .grateful, .relieved, .peaceful] + case .joy: + return [.excited, .satisfied, .cheerful, .joyful, .thrilling] + case .sad: + return [.hollow, .lonely, .regretful, .stunned, .bittersweet, .pitiful, .nostalgic] + case .insight: + return [.amazed, .insightful, .inspired, .deepened, .understood] + case .other: + return [] + } + } } diff --git a/src/Projects/BKDomain/Sources/Entity/PrimaryEmotion.swift b/src/Projects/BKDomain/Sources/Entity/PrimaryEmotion.swift new file mode 100644 index 00000000..399b7666 --- /dev/null +++ b/src/Projects/BKDomain/Sources/Entity/PrimaryEmotion.swift @@ -0,0 +1,45 @@ +// Copyright © 2025 Booket. All rights reserved + +import Foundation + +/// 대분류 감정 (API V2 스펙) +public enum PrimaryEmotion: String, CaseIterable, Codable { + case warmth = "WARMTH" + case joy = "JOY" + case sadness = "SADNESS" + case insight = "INSIGHT" + case other = "OTHER" + + /// 한글 표시명 + public var displayName: String { + switch self { + case .warmth: return "따뜻함" + case .joy: return "즐거움" + case .sadness: return "슬픔" + case .insight: return "깨달음" + case .other: return "기타" + } + } + + /// 설명 + public var description: String { + switch self { + case .warmth: return "공감과 위로가 된 순간" + case .joy: return "흥미롭고 유쾌한 순간" + case .sadness: return "눈물이 고인 순간" + case .insight: return "생각이 깊어지는 순간" + case .other: return "네 가지 감정으로 표현하기 어려울 때" + } + } + + /// Emotion으로 변환 + public func toEmotion() -> Emotion { + switch self { + case .warmth: return .warmth + case .joy: return .joy + case .sadness: return .sad + case .insight: return .insight + case .other: return .other + } + } +} diff --git a/src/Projects/BKDomain/Sources/Entity/RecordInfo.swift b/src/Projects/BKDomain/Sources/Entity/RecordInfo.swift index 1ce26dd6..c30e509e 100644 --- a/src/Projects/BKDomain/Sources/Entity/RecordInfo.swift +++ b/src/Projects/BKDomain/Sources/Entity/RecordInfo.swift @@ -5,24 +5,26 @@ import Foundation public struct RecordInfo: Decodable, Equatable { public let recordId: String public let bookId: String - public let pageNumber: Int + public let pageNumber: Int? public let quote: String public let review: String? - public let emotionTags: [Emotion] + public let primaryEmotion: PrimaryEmotion + public let detailEmotions: [DetailEmotion] public let createdAt: Date public let updatedAt: Date? public let bookTitle: String public let bookPublisher: String public let bookCoverImageUrl: URL public let author: String - + public init( recordId: String, bookId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, review: String?, - emotionTags: [Emotion], + primaryEmotion: PrimaryEmotion, + detailEmotions: [DetailEmotion], createdAt: Date, updatedAt: Date?, bookTitle: String, @@ -35,7 +37,8 @@ public struct RecordInfo: Decodable, Equatable { self.pageNumber = pageNumber self.quote = quote self.review = review - self.emotionTags = emotionTags + self.primaryEmotion = primaryEmotion + self.detailEmotions = detailEmotions self.createdAt = createdAt self.updatedAt = updatedAt self.bookTitle = bookTitle @@ -43,4 +46,9 @@ public struct RecordInfo: Decodable, Equatable { self.bookCoverImageUrl = bookCoverImageUrl self.author = author } + + /// 이전 API와의 호환성을 위한 computed property + public var emotionTags: [Emotion] { + [primaryEmotion.toEmotion()] + } } diff --git a/src/Projects/BKDomain/Sources/Interface/Repository/EmotionRepository.swift b/src/Projects/BKDomain/Sources/Interface/Repository/EmotionRepository.swift new file mode 100644 index 00000000..a9715d9b --- /dev/null +++ b/src/Projects/BKDomain/Sources/Interface/Repository/EmotionRepository.swift @@ -0,0 +1,20 @@ +// Copyright © 2025 Booket. All rights reserved + +import Combine + +public protocol EmotionRepository { + /// 서버에서 감정 목록 가져오기 + func fetchEmotions() -> AnyPublisher<[EmotionGroup], DomainError> + + /// 캐시된 감정 목록 가져오기 (없으면 서버에서 가져옴) + func getEmotions() -> AnyPublisher<[EmotionGroup], DomainError> + + /// 특정 대분류 감정의 세부감정 목록 가져오기 + func getDetailEmotions(for primaryEmotion: PrimaryEmotion) -> AnyPublisher<[DetailEmotion], DomainError> + + /// 세부감정 이름으로 ID 찾기 + func findDetailEmotionId(name: String, in primaryEmotion: PrimaryEmotion) -> AnyPublisher + + /// 세부감정 ID로 이름 찾기 + func findDetailEmotionName(id: String) -> AnyPublisher +} diff --git a/src/Projects/BKDomain/Sources/Interface/Usecase/FetchDetailEmotionsUseCase.swift b/src/Projects/BKDomain/Sources/Interface/Usecase/FetchDetailEmotionsUseCase.swift new file mode 100644 index 00000000..ca73445e --- /dev/null +++ b/src/Projects/BKDomain/Sources/Interface/Usecase/FetchDetailEmotionsUseCase.swift @@ -0,0 +1,7 @@ +// Copyright © 2025 Booket. All rights reserved + +import Combine + +public protocol FetchDetailEmotionsUseCase { + func execute(for primaryEmotion: PrimaryEmotion) -> AnyPublisher<[DetailEmotion], DomainError> +} diff --git a/src/Projects/BKDomain/Sources/UseCase/DefaultFetchDetailEmotionsUseCase.swift b/src/Projects/BKDomain/Sources/UseCase/DefaultFetchDetailEmotionsUseCase.swift new file mode 100644 index 00000000..0004c71f --- /dev/null +++ b/src/Projects/BKDomain/Sources/UseCase/DefaultFetchDetailEmotionsUseCase.swift @@ -0,0 +1,15 @@ +// Copyright © 2025 Booket. All rights reserved + +import Combine + +public struct DefaultFetchDetailEmotionsUseCase: FetchDetailEmotionsUseCase { + private let repository: EmotionRepository + + public init(repository: EmotionRepository) { + self.repository = repository + } + + public func execute(for primaryEmotion: PrimaryEmotion) -> AnyPublisher<[DetailEmotion], DomainError> { + repository.getDetailEmotions(for: primaryEmotion) + } +} diff --git a/src/Projects/BKDomain/Sources/VO/RecordDetails/RecordVO.swift b/src/Projects/BKDomain/Sources/VO/RecordDetails/RecordVO.swift index e43607d4..faf134a8 100644 --- a/src/Projects/BKDomain/Sources/VO/RecordDetails/RecordVO.swift +++ b/src/Projects/BKDomain/Sources/VO/RecordDetails/RecordVO.swift @@ -3,20 +3,37 @@ import Foundation public struct RecordVO { - public let pageNumber: Int + public let pageNumber: Int? public let quote: String - public let review: String? - public let emotionTags: [String] - + public let memo: String? + public let primaryEmotion: PrimaryEmotion + public let detailEmotionIds: [String] + + public init( + pageNumber: Int?, + quote: String, + memo: String?, + primaryEmotion: PrimaryEmotion, + detailEmotionIds: [String] + ) { + self.pageNumber = pageNumber + self.quote = quote + self.memo = memo + self.primaryEmotion = primaryEmotion + self.detailEmotionIds = detailEmotionIds + } + + /// 이전 API와의 호환성을 위한 생성자 public init( - pageNumber: Int, + pageNumber: Int?, quote: String, review: String?, emotionTags: [String] ) { self.pageNumber = pageNumber self.quote = quote - self.review = review - self.emotionTags = emotionTags + self.memo = review + self.primaryEmotion = .other + self.detailEmotionIds = emotionTags } } diff --git a/src/Projects/BKPresentation/Sources/Common/Extension/BKBottomSheetViewController+.swift b/src/Projects/BKPresentation/Sources/Common/Extension/BKBottomSheetViewController+.swift index b3b3b4a9..749be7cc 100644 --- a/src/Projects/BKPresentation/Sources/Common/Extension/BKBottomSheetViewController+.swift +++ b/src/Projects/BKPresentation/Sources/Common/Extension/BKBottomSheetViewController+.swift @@ -242,13 +242,214 @@ extension BKBottomSheetViewController { fileprivate class BKBottomSheetMenuActionTarget { static let shared = BKBottomSheetMenuActionTarget() private var actions: [UIView: () -> Void] = [:] - + func setAction(for view: UIView, action: @escaping () -> Void) { actions[view] = action } - + @objc func handleTap(_ gesture: UITapGestureRecognizer) { guard let view = gesture.view else { return } actions[view]?() } } + +// MARK: - DetailEmotion Selection BottomSheet + +import BKDomain + +extension BKBottomSheetViewController { + static func makeDetailEmotionSheet( + primaryEmotion: PrimaryEmotion, + detailEmotions: [DetailEmotion], + initialSelectedDetailEmotions: [DetailEmotion] = [], + skipAction: @escaping () -> Void, + confirmAction: @escaping ([DetailEmotion]) -> Void + ) -> BKBottomSheetViewController { + var selectedDetailEmotions: Set = Set(initialSelectedDetailEmotions) + + let containerView = UIView() + + // Chip들을 담을 FlowLayout 스택뷰 + let chipContainerView = UIView() + var chipViews: [BKChip] = [] + + let sheet = BKBottomSheetViewController( + title: "어떤 '\(primaryEmotion.displayName)'을 느꼈나요?", + subtitle: "더 자세한 감정을 선택 기록할 수 있어요.", + style: .leadingCloseButton, + suppliedContentStyle: .lower(containerView), + buttonConfiguration: .twoButtonGroup( + leftTitle: "건너뛰기", + rightTitle: "선택 완료", + leftAction: skipAction, + rightAction: { + confirmAction(Array(selectedDetailEmotions)) + } + ) + ) + + // Chip 생성 + for detailEmotion in detailEmotions { + let chip = BKChip(title: detailEmotion.name) + // 초기 선택 상태 설정 + if initialSelectedDetailEmotions.contains(where: { $0.id == detailEmotion.id }) { + chip.isSelected = true + } + chip.onTap = { [weak sheet] in + if selectedDetailEmotions.contains(detailEmotion) { + selectedDetailEmotions.remove(detailEmotion) + chip.isSelected = false + } else { + selectedDetailEmotions.insert(detailEmotion) + chip.isSelected = true + } + // 1개 이상 선택되어야 버튼 활성화 + sheet?.button?.setPrimaryButtonState(!selectedDetailEmotions.isEmpty) + } + chipViews.append(chip) + } + + containerView.addSubview(chipContainerView) + + chipContainerView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(BKInset.inset5) + } + + // Chip들을 FlowLayout처럼 배치 + layoutChips(chipViews, in: chipContainerView) + + // 초기 선택 상태에 따라 버튼 활성화 + sheet.button?.setPrimaryButtonState(!selectedDetailEmotions.isEmpty) + return sheet + } + + private static func layoutChips(_ chips: [BKChip], in containerView: UIView) { + let flowLayoutView = CenteredFlowLayoutView() + flowLayoutView.setChips(chips) + + containerView.addSubview(flowLayoutView) + flowLayoutView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } +} + +// MARK: - CenteredFlowLayoutView + +private final class CenteredFlowLayoutView: UIView { + private var chipViews: [UIView] = [] + private let horizontalSpacing: CGFloat = 8 + private let verticalSpacing: CGFloat = 8 + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setChips(_ views: [UIView]) { + chipViews.forEach { $0.removeFromSuperview() } + chipViews = views + chipViews.forEach { addSubview($0) } + setNeedsLayout() + invalidateIntrinsicContentSize() + } + + override func layoutSubviews() { + super.layoutSubviews() + + let maxWidth = bounds.width + guard maxWidth > 0 else { return } + + // 각 칩의 크기를 먼저 계산 + let chipSizes = chipViews.map { $0.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) } + + // 줄별로 칩을 그룹화 + var rows: [[Int]] = [] + var currentRow: [Int] = [] + var currentRowWidth: CGFloat = 0 + + for (index, chipSize) in chipSizes.enumerated() { + let chipWidth = chipSize.width + let neededWidth = currentRow.isEmpty ? chipWidth : horizontalSpacing + chipWidth + + if currentRowWidth + neededWidth > maxWidth, !currentRow.isEmpty { + rows.append(currentRow) + currentRow = [index] + currentRowWidth = chipWidth + } else { + currentRow.append(index) + currentRowWidth += neededWidth + } + } + if !currentRow.isEmpty { + rows.append(currentRow) + } + + // 각 줄을 중앙 정렬하여 배치 + var currentY: CGFloat = 0 + + for row in rows { + let rowWidth = row.reduce(CGFloat(0)) { total, index in + total + chipSizes[index].width + } + CGFloat(max(0, row.count - 1)) * horizontalSpacing + + var currentX = (maxWidth - rowWidth) / 2 + var rowHeight: CGFloat = 0 + + for index in row { + let chipSize = chipSizes[index] + chipViews[index].frame = CGRect( + x: currentX, + y: currentY, + width: chipSize.width, + height: chipSize.height + ) + currentX += chipSize.width + horizontalSpacing + rowHeight = max(rowHeight, chipSize.height) + } + + currentY += rowHeight + verticalSpacing + } + + invalidateIntrinsicContentSize() + } + + override var intrinsicContentSize: CGSize { + let maxWidth = bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width - 40 + + let chipSizes = chipViews.map { $0.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) } + + var rows: [[Int]] = [] + var currentRow: [Int] = [] + var currentRowWidth: CGFloat = 0 + + for (index, chipSize) in chipSizes.enumerated() { + let chipWidth = chipSize.width + let neededWidth = currentRow.isEmpty ? chipWidth : horizontalSpacing + chipWidth + + if currentRowWidth + neededWidth > maxWidth, !currentRow.isEmpty { + rows.append(currentRow) + currentRow = [index] + currentRowWidth = chipWidth + } else { + currentRow.append(index) + currentRowWidth += neededWidth + } + } + if !currentRow.isEmpty { + rows.append(currentRow) + } + + var totalHeight: CGFloat = 0 + for row in rows { + let rowHeight = row.map { chipSizes[$0].height }.max() ?? 0 + totalHeight += rowHeight + } + totalHeight += CGFloat(max(0, rows.count - 1)) * verticalSpacing + + return CGSize(width: UIView.noIntrinsicMetric, height: totalHeight) + } +} diff --git a/src/Projects/BKPresentation/Sources/Common/Extension/PrimaryEmotion+UI.swift b/src/Projects/BKPresentation/Sources/Common/Extension/PrimaryEmotion+UI.swift new file mode 100644 index 00000000..a9952fd9 --- /dev/null +++ b/src/Projects/BKPresentation/Sources/Common/Extension/PrimaryEmotion+UI.swift @@ -0,0 +1,142 @@ +// Copyright © 2025 Booket. All rights reserved + +import BKDesign +import BKDomain +import UIKit + +// MARK: - UI Properties (EmotionSeed, EmotionIcon 통합) + +public extension PrimaryEmotion { + /// 기본 이미지 + var image: UIImage { + switch self { + case .warmth: return BKImage.Graphics.warm + case .joy: return BKImage.Graphics.joy + case .insight: return BKImage.Graphics.insight + case .sadness: return BKImage.Graphics.sad + case .other: return BKImage.Graphics.Note.default + } + } + + /// 원형 이미지 + var circleImage: UIImage { + switch self { + case .warmth: return BKImage.Graphics.warmCircle + case .joy: return BKImage.Graphics.joyCircle + case .sadness: return BKImage.Graphics.sadCircle + case .insight: return BKImage.Graphics.insightCircle + case .other: return BKImage.Graphics.Note.default + } + } + + /// Note 화면용 이미지 + var noteImage: UIImage { + switch self { + case .warmth: return BKImage.Graphics.Note.warm + case .joy: return BKImage.Graphics.Note.joy + case .sadness: return BKImage.Graphics.Note.sad + case .insight: return BKImage.Graphics.Note.insight + case .other: return BKImage.Graphics.Note.default + } + } + + /// 감정 색상 + var color: UIColor { + switch self { + case .warmth: return .bkEmotionColor(.warmth) + case .joy: return .bkEmotionColor(.joy) + case .insight: return .bkEmotionColor(.insight) + case .sadness: return .bkEmotionColor(.sadness) + case .other: return .bkContentColor(.tertiary) + } + } + + /// 감정 베이스 색상 + var baseColor: UIColor { + switch self { + case .warmth: return .bkEmotionBaseColor(.warmth) + case .joy: return .bkEmotionBaseColor(.joy) + case .insight: return .bkEmotionBaseColor(.insight) + case .sadness: return .bkEmotionBaseColor(.sadness) + case .other: return .bkBaseColor(.secondary) + } + } + + /// 해시태그 형식 + var hashtag: String { + return "#\(displayName)" + } + + /// 공유 카드 배경 이미지 + var cardImage: UIImage { + switch self { + case .warmth: return BKImage.Graphics.warmCard + case .joy: return BKImage.Graphics.joyCard + case .insight: return BKImage.Graphics.insightCard + case .sadness: return BKImage.Graphics.sadCard + case .other: return BKImage.Graphics.etcCard + } + } + + /// 인스타 스토리 공유 시 배경 컬러 + var shareBackgroundColor: UIColor { + switch self { + case .warmth: return UIColor(hex: "FEFCF1") + case .joy: return UIColor(hex: "FFF7F5") + case .insight: return UIColor(hex: "FBF8FF") + case .sadness: return UIColor(hex: "F4F8FF") + case .other: return .white + } + } +} + +// MARK: - Conversion from legacy Emotion + +public extension PrimaryEmotion { + /// 기존 Emotion에서 변환 + init?(from legacyEmotion: Emotion) { + switch legacyEmotion { + case .warmth: self = .warmth + case .joy: self = .joy + case .sad: self = .sadness + case .insight: self = .insight + case .other: self = .other + } + } + + /// 기존 Emotion으로 변환 (하위 호환성) + var toLegacyEmotion: Emotion { + switch self { + case .warmth: return .warmth + case .joy: return .joy + case .sadness: return .sad + case .insight: return .insight + case .other: return .other + } + } +} + +// MARK: - Conversion from Seed + +public extension PrimaryEmotion { + /// Seed 이름에서 변환 + static func from(seedName: String) -> Self? { + switch seedName.lowercased() { + case "warmth", "따뜻함": return .warmth + case "joy", "즐거움": return .joy + case "sad", "sadness", "슬픔": return .sadness + case "insight", "깨달음": return .insight + default: return nil + } + } + + /// Seed 엔티티에서 변환 + static func from(seed: Seed) -> Self? { + return from(seedName: seed.name) + } + + /// 기본 4가지 감정 (other 제외) + static var displayCases: [PrimaryEmotion] { + return [.warmth, .joy, .sadness, .insight] + } +} diff --git a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/BookDetailItem.swift b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/BookDetailItem.swift index 6bd4f321..2d6ec7eb 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/BookDetailItem.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/BookDetailItem.swift @@ -12,17 +12,17 @@ struct BookDetailItem: Hashable { /// 수집한 문장 let note: String /// 감정 - let emotion: EmotionSeed? + let primaryEmotion: PrimaryEmotion let createdAt: Date let page: Int? let bookTitle: String - + static func from(recordInfo: RecordInfo) -> Self { return Self( id: recordInfo.bookId, recordId: recordInfo.recordId, note: recordInfo.quote, - emotion: EmotionSeed.from(emotion: recordInfo.emotionTags.first ?? .joy), + primaryEmotion: recordInfo.primaryEmotion, createdAt: recordInfo.createdAt, page: recordInfo.pageNumber, bookTitle: recordInfo.bookTitle diff --git a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/EmotionSeed.swift b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/EmotionSeed.swift index e42a23dd..5a294903 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/EmotionSeed.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/Models/EmotionSeed.swift @@ -62,13 +62,13 @@ enum EmotionSeed: String, CaseIterable { } } - static func from(emotion: Emotion) -> Self { + static func from(emotion: Emotion) -> Self? { switch emotion { case .joy: return .joy case .sad: return .sad case .insight: return .insight case .warmth: return .warmth - case .etc: return .etc + case .other: return .etc } } @@ -94,7 +94,7 @@ enum EmotionSeed: String, CaseIterable { case .joy: return BKImage.Graphics.joyCard case .insight: return BKImage.Graphics.insightCard case .sad: return BKImage.Graphics.sadCard - case .etc: return BKImage.Graphics.sadCard + case .etc: return BKImage.Graphics.etcCard } } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailView.swift b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailView.swift index cbc24a48..556e34e6 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailView.swift @@ -19,16 +19,7 @@ enum SortOption: String, CaseIterable { case .newest: return { $0.createdAt > $1.createdAt } case .pageDescending: - return { - let p0 = $0.page ?? Int.max - let p1 = $1.page ?? Int.max - - if p0 == p1 { - return $0.createdAt > $1.createdAt - } - - return p0 > p1 - } + return { ($0.page ?? 0) > ($1.page ?? 0) } } } } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailViewCell.swift b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailViewCell.swift index a51c9ee8..ef88607c 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailViewCell.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/BookDetailViewCell.swift @@ -7,12 +7,12 @@ import UIKit final class BookDetailViewCell: UICollectionViewCell { static let reuseIdentifier: String = "BookDetailViewCell" - + private let noteLabel = BKLabel( fontStyle: .body2(weight: .medium), color: .bkContentColor(.secondary) ) - + private let lowerStack: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -20,22 +20,22 @@ final class BookDetailViewCell: UICollectionViewCell { stackView.distribution = .equalSpacing return stackView }() - + private let pageLabel = BKLabel2( fontStyle: .italic, color: .bkContentColor(.brand) ) - + private let creationLabel = BKLabel2( fontStyle: .label1(weight: .medium), color: .bkContentColor(.tertiary) ) - + private let emotionTagLabel = BKLabel2( fontStyle: .label1(weight: .medium), color: .bkContentColor(.tertiary) ) - + private let moreButton: UIImageView = { let imageView = UIImageView( image: BKImage.Icon.moreVertical @@ -45,9 +45,9 @@ final class BookDetailViewCell: UICollectionViewCell { imageView.isUserInteractionEnabled = true return imageView }() - + private var moreButtonAction: (() -> Void)? - + private let upperStack: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -55,18 +55,18 @@ final class BookDetailViewCell: UICollectionViewCell { stackView.distribution = .equalSpacing return stackView }() - + override init(frame: CGRect) { super.init(frame: frame) setupViews() configure() setupLayout() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func prepareForReuse() { super.prepareForReuse() noteLabel.setText(text: "") @@ -74,19 +74,23 @@ final class BookDetailViewCell: UICollectionViewCell { creationLabel.setText(text: "") emotionTagLabel.setText(text: "") } - + func configure( with item: BookDetailItem ) { - let emotion = item.emotion ?? .joy - + let emotion = item.primaryEmotion + let displayedNote = "\"\(item.note)\"" noteLabel.setText(text: displayedNote) - pageLabel.setText(text: item.page.toPageString) + emotionTagLabel.setText(text: "#\(emotion.displayName)") creationLabel.setText(text: item.createdAt.toKoreanDotDateString()) - emotionTagLabel.setText(text: "#\(emotion.rawValue)") + if let page = item.page { + pageLabel.setText(text: "\(page)p") + } else { + pageLabel.setText(text: "-p") + } } - + func applyMoreButtonGesture( action: @escaping () -> Void ) { @@ -105,7 +109,7 @@ private extension BookDetailViewCell { [pageLabel, moreButton].forEach(upperStack.addArrangedSubview) [emotionTagLabel, creationLabel].forEach(lowerStack.addArrangedSubview) } - + func configure() { noteLabel.numberOfLines = Constants.noteMaxNumberOfLines noteLabel.lineBreakMode = .byTruncatingTail @@ -113,27 +117,27 @@ private extension BookDetailViewCell { layer.cornerRadius = LayoutConstants.cornerRadius clipsToBounds = true } - + func setupLayout() { contentView.snp.makeConstraints { $0.edges.equalToSuperview() $0.width.equalTo(UIScreen.main.bounds.width - LayoutConstants.horizontalInset * 2) } - + upperStack.snp.makeConstraints { $0.top.equalToSuperview() .inset(LayoutConstants.topInset) $0.horizontalEdges.equalToSuperview() .inset(LayoutConstants.horizontalInset) } - + noteLabel.snp.makeConstraints { $0.top.equalTo(upperStack.snp.bottom) .offset(LayoutConstants.noteLabelTopInset) $0.horizontalEdges.equalToSuperview() .inset(LayoutConstants.horizontalInset) } - + lowerStack.snp.makeConstraints { $0.top.equalTo(noteLabel.snp.bottom) .offset(LayoutConstants.lowerLabelTopInset) @@ -143,12 +147,12 @@ private extension BookDetailViewCell { $0.bottom.equalToSuperview() .inset(LayoutConstants.bottomInset) } - + moreButton.snp.makeConstraints { $0.size.equalTo(LayoutConstants.moreButtonSize) } } - + @objc func handleMoreButtonTapped() { moreButtonAction?() } @@ -165,7 +169,7 @@ private extension BookDetailViewCell { static let lowerLabelHeight = 22 static let moreButtonSize: CGSize = CGSize(width: 20, height: 20) } - + enum Constants { static let noteMaxNumberOfLines: Int = 4 } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SeedReportView.swift b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SeedReportView.swift index 89ddbf90..1d21b0f4 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SeedReportView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SeedReportView.swift @@ -8,7 +8,7 @@ import UIKit final class SeedReportView: BaseView { // MARK: - Properties private var isExpanded = false - + // MARK: - UI Components private let containerStackView: UIStackView = { let stackView = UIStackView() @@ -17,17 +17,17 @@ final class SeedReportView: BaseView { stackView.alignment = .fill return stackView }() - + private let headerView = UIView() private let emotionImageView = UIImageView() - + private let reportLabel = BKLabel2( fontStyle: .label1(weight: .medium), color: .bkContentColor(.secondary), highlightColor: .bkContentColor(.brand), highlightFont: BKTextStyle.label1(weight: .semiBold).uiFont ) - + private let foldButton: UIImageView = { let imageView = UIImageView( image: BKImage.Icon.chevronDown @@ -37,18 +37,18 @@ final class SeedReportView: BaseView { imageView.isUserInteractionEnabled = true return imageView }() - + private let divider = BKDivider(type: .small) - + private let expandedView: UIView = { let view = UIView() view.isHidden = true view.alpha = 0 return view }() - + private let graphView = SeedGraphView() - + private let labelsStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -57,33 +57,33 @@ final class SeedReportView: BaseView { stackView.spacing = 4 return stackView }() - + override func setupView() { addSubview(containerStackView) - + headerView.addSubviews(emotionImageView, reportLabel, foldButton) [headerView, expandedView].forEach(containerStackView.addArrangedSubview) expandedView.addSubviews(divider, graphView, labelsStackView) - + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleExpansion)) foldButton.addGestureRecognizer(tapGesture) } - + override func configure() { layer.cornerRadius = LayoutConstants.cornerRadius clipsToBounds = true backgroundColor = .bkBaseColor(.secondary) } - + override func setupLayout() { containerStackView.snp.makeConstraints { $0.edges.equalToSuperview().inset(LayoutConstants.contentInset) } - + headerView.snp.makeConstraints { $0.height.equalTo(LayoutConstants.emotionStackHeight) } - + emotionImageView.snp.makeConstraints { $0.leading.equalToSuperview() $0.centerY.equalToSuperview() @@ -94,30 +94,30 @@ final class SeedReportView: BaseView { ) ) } - + reportLabel.snp.makeConstraints { $0.leading .equalTo(emotionImageView.snp.trailing) .offset(LayoutConstants.labelLeadingInset) $0.centerY.equalToSuperview() } - + foldButton.snp.makeConstraints { $0.trailing.equalToSuperview() $0.centerY.equalToSuperview() $0.size.equalTo(LayoutConstants.iconSize) } - + divider.snp.makeConstraints { $0.top.horizontalEdges.equalToSuperview() } - + graphView.snp.makeConstraints { $0.top.equalTo(divider.snp.bottom).offset(LayoutConstants.graphTopOffset) $0.horizontalEdges.equalToSuperview() $0.height.equalTo(LayoutConstants.graphHeight) } - + labelsStackView.snp.makeConstraints { $0.top.equalTo(graphView.snp.bottom).offset(LayoutConstants.labelsTopOffset) $0.height.equalTo(72) @@ -125,25 +125,25 @@ final class SeedReportView: BaseView { $0.bottom.equalToSuperview() } } - + func setEmotionHeader(with emotion: Emotion) { - let emotion = EmotionSeed.from(emotion: emotion) + guard let emotion = EmotionSeed.from(emotion: emotion) else { return } let emotionText = "\'\(emotion.rawValue)\'" emotionImageView.image = emotion.circleImage reportLabel.highlightColor = emotion.color reportLabel.setText(text: "\(emotionText) \(emotion.descriptionText)") reportLabel.highlightedWord = emotionText } - + func applyGraph(with seeds: [Seed]) { graphView.applyGraph(with: seeds) - + labelsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - + let seedDict = Dictionary(seeds.map { ($0.name, $0) }, uniquingKeysWith: { first, last in last }) - + EmotionSeed.allCases.forEach { emotionCase in if let seed = seedDict[emotionCase.rawValue], seed.count >= 1 { let itemView = SeedItemView() @@ -151,7 +151,7 @@ final class SeedReportView: BaseView { labelsStackView.addArrangedSubview(itemView) } } - + self.setNeedsLayout() self.layoutIfNeeded() } @@ -160,14 +160,14 @@ final class SeedReportView: BaseView { private extension SeedReportView { @objc private func toggleExpansion() { isExpanded.toggle() - + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) { self.expandedView.isHidden = !self.isExpanded self.expandedView.alpha = self.isExpanded ? 1 : 0 - + let angle: CGFloat = self.isExpanded ? .pi : 0 self.foldButton.transform = CGAffineTransform(rotationAngle: angle) - + self.layoutIfNeeded() } } @@ -180,7 +180,7 @@ private extension SeedReportView { static let emotionStackHeight = 36 static let labelLeadingInset = BKSpacing.spacing2 static let iconSize: CGSize = CGSize(width: 24, height: 24) - + static let graphHeight: CGFloat = 12 static let graphTopOffset: CGFloat = BKSpacing.spacing5 static let labelsTopOffset: CGFloat = BKSpacing.spacing4 diff --git a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SentenceCardView.swift b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SentenceCardView.swift index 95d20742..e42eee39 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SentenceCardView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/BookDetail/View/SentenceCardView.swift @@ -115,8 +115,8 @@ final class SentenceCardView: BaseView { public func configure(_ data: BookDetailItem) { sentenceLabel.setText(text: data.note) titleLabel.setText(text: data.bookTitle.withCornerBrackets()) - if let emotion = data.emotion { - emotionBackgroundImageView.image = emotion.cardImage + if data.primaryEmotion != .other { + emotionBackgroundImageView.image = data.primaryEmotion.cardImage } else { emotionBackgroundImageView.backgroundColor = .bkBaseColor(.secondary) } diff --git a/src/Projects/BKPresentation/Sources/MainFlow/NoteCompletion/View/CollectedSentenceView.swift b/src/Projects/BKPresentation/Sources/MainFlow/NoteCompletion/View/CollectedSentenceView.swift index e0b0e405..1052e5fa 100644 --- a/src/Projects/BKPresentation/Sources/MainFlow/NoteCompletion/View/CollectedSentenceView.swift +++ b/src/Projects/BKPresentation/Sources/MainFlow/NoteCompletion/View/CollectedSentenceView.swift @@ -76,11 +76,15 @@ final class CollectedSentenceView: BaseView { func apply( sentence: String, - page: Int + page: Int? ) { let displayedText = "\"\(sentence)\"" collectedSentenceLabel.setText(text: displayedText) - pageLabel.setText(text: "\(page)p") + if let page = page { + pageLabel.setText(text: "\(page)p") + } else { + pageLabel.isHidden = true + } } }