Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature/#324] 문답 화면 새로운 디자인 적용 #325

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension BottleAPI: BaseTargetType {
case .fetchBottles:
return "api/v1/bottles"
case .fetchBottleStorageList:
return "api/v1/bottles/ping-pong"
return "api/v2/bottles/ping-pong"
case let .fetchBottlePingPong(bottleID):
return "api/v1/bottles/ping-pong/\(bottleID)"
case let .readBottle(bottleID):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import Foundation
// MARK: - Bottle Storage List

public struct BottleStorageListResponseDTO: Decodable {
let activeBottles: [BottleStorageItemResponseDTO]?
let doneBottles: [BottleStorageItemResponseDTO]?
let pingPongBottles: [BottleStorageItemResponseDTO]?

public struct BottleStorageItemResponseDTO: Decodable {
let age: Int?
Expand All @@ -21,6 +20,8 @@ public struct BottleStorageListResponseDTO: Decodable {
let mbti: String?
let userImageUrl: String?
let userName: String?
let lastActivatedAt: String?
let lastStatus: String?

public func toDomain() -> BottleStorageItem {
return BottleStorageItem(
Expand All @@ -30,15 +31,15 @@ public struct BottleStorageListResponseDTO: Decodable {
keyword: keyword ?? [],
mbti: mbti ?? "",
userImageUrl: userImageUrl ?? "",
userName: userName ?? ""
userName: userName ?? "",
lastActivatedAt: lastActivatedAt ?? "",
lastStatus: PingPongLastStatus(rawValue: lastStatus ?? "")
)
}
}

public func toDomain() -> BottleStorageList {
return BottleStorageList(
activeBottles: activeBottles?.map { $0.toDomain() } ?? [],
doneBottles: doneBottles?.map { $0.toDomain() } ?? []
)
pingPongBottles: pingPongBottles?.map { $0.toDomain() } ?? [])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@
//

public struct BottleStorageList: Decodable {
public let activeBottles: [BottleStorageItem]
public let doneBottles: [BottleStorageItem]
public let pingPongBottles: [BottleStorageItem]

public init(
activeBottles: [BottleStorageItem],
doneBottles: [BottleStorageItem]
pingPongBottles: [BottleStorageItem]
) {
self.activeBottles = activeBottles
self.doneBottles = doneBottles
self.pingPongBottles = pingPongBottles
}

}

public struct BottleStorageItem: Decodable, Equatable {
Expand All @@ -27,6 +23,8 @@ public struct BottleStorageItem: Decodable, Equatable {
public let mbti: String
public let userImageUrl: String
public let userName: String?
public let lastActivatedAt: String?
public let lastStatus: PingPongLastStatus?

public init(
age: Int?,
Expand All @@ -35,7 +33,9 @@ public struct BottleStorageItem: Decodable, Equatable {
keyword: [String],
mbti: String,
userImageUrl: String,
userName: String?
userName: String?,
lastActivatedAt: String?,
lastStatus: PingPongLastStatus?
) {
self.age = age
self.id = id
Expand All @@ -44,5 +44,26 @@ public struct BottleStorageItem: Decodable, Equatable {
self.mbti = mbti
self.userImageUrl = userImageUrl
self.userName = userName
self.lastActivatedAt = lastActivatedAt
self.lastStatus = lastStatus
}
}

public enum PingPongLastStatus: String, Decodable {
/// 대화는 시작했으나 두 사람 모두 문답을 작성하지 않았을 때
case noAnswerFromBoth = "NO_ANSWER_FROM_BOTH"
/// 상대방이 새로운 문답을 작성했을 때
case answerFromOther = "ANSWER_FROM_OTHER"
/// 상대방이 사진을 공유했을 때
case photoSharedByOther = "PHOTO_SHARED_BY_OTHER"
/// 상대방이 연락처를 공유했을 때
case contactSharedByOther = "CONTACT_SHARED_BY_OTHER"
/// 내가 문답을 작성했을 때 (상대방은 작성X)
case answerFromMeOnly = "ANSWER_FROM_ME_ONLY"
/// 내가 사진을 공유했을 때 (상대방은 공유X)
case photoSharedByMeOnly = "PHOTO_SHARED_BY_ME_ONLY"
/// 내가 연락처를 공유했을 때 (상대방은 공유X)
case contactSharedByMeOnly = "CONTACT_SHARED_BY_ME_ONLY"
/// 대화가 중단됐을 때
case conversationStopped = "CONVERSATION_STOPPED"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import CoreLoggerInterface
import DomainBottle
import FeatureReportInterface
import FeatureBottleArrivalInterface

import ComposableArchitecture

Expand All @@ -18,11 +19,12 @@ extension BottleStorageFeature {
let reducer = Reduce<State, Action> { state, action in
switch action {
case .onAppear:
state.isLoading = true
return popToRootAndReload(state: &state)

case let .bottleStorageListFetched(bottleStorageList):
state.activeBottleList = bottleStorageList.activeBottles
state.doneBottlsList = bottleStorageList.doneBottles
state.pingPongBottleList = bottleStorageList.pingPongBottles
state.isLoading = false
return .none

case let .bottleStorageItemDidTapped(bottleID, isRead, userName):
Expand All @@ -33,9 +35,8 @@ extension BottleStorageFeature {
)))
return .none

case let .bottleActiveStateTabButtonTapped(activeState):
state.selectedActiveStateTab = activeState
return .none
case .sandBeachButtonDidTapped:
return .send(.delegate(.sandBeachButtonDidTapped))

case let .path(.element(id: _, action: .pingPongDetail(.delegate(delegate)))):

Expand Down Expand Up @@ -74,7 +75,6 @@ extension BottleStorageFeature {
self.init(reducer: reducer)

func popToRootAndReload(state: inout State) -> Effect<Action> {
state.selectedActiveStateTab = .active
state.path.removeAll()
return .run { send in
let bottleStorageList = try await bottleClient.fetchBottleStorageList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,44 +22,30 @@ public struct BottleStorageFeature {

@ObservableState
public struct State: Equatable {
// 보틀 상태 선택 탭(대화 중, 완료)
let bottleActiveStateTabs: [BottleActiveState]
var selectedActiveStateTab: BottleActiveState
var currentSelectedBottles: [BottleStorageItem] {
switch selectedActiveStateTab {
case .active:
return activeBottleList ?? []
case .done:
return doneBottlsList ?? []
}
}

// 보틀 리스트
var activeBottleList: [BottleStorageItem]?
var doneBottlsList: [BottleStorageItem]?
var pingPongBottleList: [BottleStorageItem]

var path = StackState<Path.State>()
var isLoading: Bool = false

public init() {
self.bottleActiveStateTabs = BottleActiveState.allCases
self.selectedActiveStateTab = .active
self.pingPongBottleList = []
}
}

public enum Action: BindableAction {
// View Life Cycle
case onAppear

// 보틀 상태 선택 탭(대화 중, 완료)
case bottleActiveStateTabButtonTapped(BottleActiveState)

// 보틀 리스트
case bottleStorageListFetched(BottleStorageList)
case bottleStorageItemDidTapped(
bottleID: Int,
isRead: Bool,
userName: String
)
case sandBeachButtonDidTapped
case selectedTabDidChanged(selectedTab: TabType)
case delegate(Delegate)
// ETC.
Expand All @@ -68,6 +54,7 @@ public struct BottleStorageFeature {

public enum Delegate {
case selectedTabDidChanged(selectedTab: TabType)
case sandBeachButtonDidTapped
}
}

Expand All @@ -78,16 +65,25 @@ public struct BottleStorageFeature {
}
}

public enum BottleActiveState: String, CaseIterable, Equatable {
case active
case done

extension PingPongLastStatus {
var title: String {
switch self {
case .active:
return "대화 중"
case .done:
return "완료"
case .noAnswerFromBoth:
return "문답을 시작해 주세요"
case .answerFromOther:
return "새로운 문답이 도착했어요"
case .photoSharedByOther:
return "사진이 도착했어요"
case .contactSharedByOther:
return "연락처가 도착했어요"
case .answerFromMeOnly:
return "문답을 보냈어요"
case .photoSharedByMeOnly:
return "사진을 공유했어요"
case .contactSharedByMeOnly:
return "연락처를 공유했어요"
case .conversationStopped:
return "대화가 중단됐어요"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,37 @@ import SwiftUI
import SharedDesignSystem
import FeatureReportInterface
import FeatureTabBarInterface
import FeatureBottleArrivalInterface

import ComposableArchitecture

public struct BottleStorageView: View {
@Perception.Bindable private var store: StoreOf<BottleStorageFeature>

public init(store: StoreOf<BottleStorageFeature>) {
self.store = store
self.store = store
}

public var body: some View {
WithPerceptionTracking {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
VStack(spacing: 0.0) {
bottleActiveStateSelectTab

bottlsList
.padding(.horizontal, .md)
.padding(.top, 32.0)
}
.padding(.top, 72)
.frame(maxHeight: .infinity, alignment: .top)
.background(to: ColorToken.background(.primary))
.padding(.bottom, BottleConstants.bottomTabBarHeight.value)
.setTabBar(selectedTab: .bottleStorage) { selectedTab in
store.send(.selectedTabDidChanged(selectedTab: selectedTab))
}
.overlay {
if store.pingPongBottleList.isEmpty && store.isLoading {
LoadingIndicator()
}
}
} destination: { store in
WithPerceptionTracking {
switch store.state {
Expand Down Expand Up @@ -64,62 +69,48 @@ public struct BottleStorageView: View {
// MARK: - Private Views

private extension BottleStorageView {
var bottleActiveStateSelectTab: some View {
HStack(spacing: .xs) {
OutlinedStyleButton(
.small(contentType: .text),
title: BottleActiveState.active.title,
buttonType: .throttle,
isSelected: store.selectedActiveStateTab == BottleActiveState.active,
action: {
store.send(.bottleActiveStateTabButtonTapped(.active))
}
)

OutlinedStyleButton(
.small(contentType: .text),
title: BottleActiveState.done.title,
buttonType: .throttle,
isSelected: store.selectedActiveStateTab == BottleActiveState.done,
action: {
store.send(.bottleActiveStateTabButtonTapped(.done))
}
)

Spacer()
}
.padding(.md)
}

@ViewBuilder
var bottlsList: some View {
if store.currentSelectedBottles.isEmpty && store.activeBottleList != nil {
VStack(spacing: .xxl) {
HStack(spacing: 0.0) {
if store.pingPongBottleList.isEmpty && !store.isLoading {
VStack(alignment: .center, spacing: 0.0) {

Spacer()
BottleImageView(type: .local(bottleImageSystem: .illustraition(.basket)))
.frame(height: 180)
.frame(width: 180)
.aspectRatio(1.0, contentMode: .fit)
.padding(.bottom, .xl)

WantedSansStyleText(
"아직 보관 중인\n보틀이 없어요!",
style: .title1,
"아직 대화를 시작하지 않으셨군요!",
style: .subTitle1,
color: .primary
)

Spacer()
}
.padding(.bottom, .xs)

WantedSansStyleText(
"마음에 드는 상대를 찾아\n가치관 문답을 시작해 볼까요?",
style: .body,
color: .tertiary
)
.lineSpacing(5)
.multilineTextAlignment(.center)
.padding(.bottom, .xl)

SolidButton(title: "모래사장 바로가기", sizeType: .extraSmall, buttonType: .throttle, action: { store.send(.sandBeachButtonDidTapped) })

GeometryReader { geometry in
BottleImageView(type: .local(bottleImageSystem: .illustraition(.basket)))
.frame(height: geometry.size.width)
Spacer()
}
.aspectRatio(1.0, contentMode: .fit)
}
} else {
ScrollView {
VStack(spacing: .md) {
ForEach(store.currentSelectedBottles, id: \.id) { bottle in
BottleStorageItem(
ForEach(store.pingPongBottleList, id: \.id) { bottle in
PingPongUserView(
status: bottle.lastStatus?.title ?? "",
lastPingPongTime: bottle.lastActivatedAt ?? "",
userName: bottle.userName ?? "(없음)",
age: bottle.age ?? 0,
mbti: bottle.mbti,
keywords: bottle.keyword,
imageURL: bottle.userImageUrl,
isRead: bottle.isRead
)
Expand Down
Loading