Skip to content

Commit

Permalink
Merge pull request #218 from ku-ring/feature/kuringbot/UI
Browse files Browse the repository at this point in the history
[2.2.0] 쿠링봇 UI
  • Loading branch information
wonniiii authored Aug 22, 2024
2 parents 91cf4e4 + 5b1ce13 commit d7bb69a
Show file tree
Hide file tree
Showing 31 changed files with 663 additions and 23 deletions.
5 changes: 0 additions & 5 deletions KuringApp/KuringApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,6 @@ struct ContentView: View {
}
}
.tint(Color.Kuring.gray600)

/// 팝업 추가
if PopupView.checkShowPopup() {
PopupView()
}
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions package-kuring/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let package = Package(
/// ```swift
/// import NoticeUI
/// ```
"BotUI",
"NoticeUI",
"SubscriptionUI",
"DepartmentUI",
Expand Down Expand Up @@ -47,6 +48,15 @@ let package = Package(
],
targets: [
// MARK: App Library Dependencies
.target(
name: "BotUI",
dependencies: [
"ColorSet", "BotFeatures",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
],
path: "Sources/UIKit/BotUI",
resources: [.process("Resources")]
),
.target(
name: "NoticeUI",
dependencies: [
Expand Down Expand Up @@ -138,6 +148,14 @@ let package = Package(
),

// MARK: Features
.target(
name: "BotFeatures",
dependencies: [
"Networks",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
],
path: "Sources/Features/BotFeatures"
),
.target(
name: "NoticeFeatures",
dependencies: [
Expand Down
129 changes: 129 additions & 0 deletions package-kuring/Sources/Features/BotFeatures/BotFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// File.swift
//
//
// Created by 최효원 on 8/5/24.
//

import Foundation
import ComposableArchitecture

@Reducer
public struct BotFeature {
@ObservableState
public struct State: Equatable {
public var chatInfo: ChatInfo = .init()
public var chatHistory: [ChatInfo] = []
public var focus: Field? = .question

public struct ChatInfo: Equatable {
public var limit: Int = 2
public var text: String = ""
public var messageType: MessageType = .question
public var chatStatus: ChatStatus = .before

public enum MessageType: String, Equatable {
case question
case answer
}

public enum ChatStatus {
/// 질문 보내기 전
case before
/// 질문 보낸 후
case waiting
/// 답변 완료
case complete
/// 답변 실패
case failure
}

public init(
limit: Int = 2,
text: String = "",
messageType: MessageType = .question,
chatStatus: ChatStatus = .before
) {
self.limit = limit
self.text = text
self.messageType = messageType
self.chatStatus = chatStatus
}
}

public enum Field {
case question
}

public init(
chatInfo: ChatInfo = .init(),
chatHistory: [ChatInfo] = [],
focus: Field? = .question
) {
self.chatInfo = chatInfo
self.chatHistory = chatHistory
self.focus = focus
}
}

public enum Action: BindableAction, Equatable {
case sendMessage
case messageResponse(Result<String, ChatError>)
case updateQuestion(String)
case binding(BindingAction<State>)

public enum ChatError: Error, Equatable {
case serverError(Error)

public static func == (lhs: ChatError, rhs: ChatError) -> Bool {
switch (lhs, rhs) {
case let (.serverError(lhsError), .serverError(rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
}
}
}
}

public var body: some ReducerOf<Self> {
BindingReducer()

Reduce { state, action in
switch action {
case .binding:
return .none

case .sendMessage:
guard !state.chatInfo.text.isEmpty else { return .none }
state.focus = nil
state.chatInfo.chatStatus = .waiting
return .run { [question = state.chatInfo.text] send in
do {
// let answer = try await chatService.sendQuestion(question)
/// 임시 응답
await send(.messageResponse(.success(question)))
} catch {
await send(.messageResponse(.failure(.serverError(error))))
}
}

case let .messageResponse(.success(answer)):
state.chatInfo.text = answer
state.chatInfo.chatStatus = .complete
state.chatHistory.append(state.chatInfo)
state.chatInfo = .init()
return .none

case let .messageResponse(.failure(error)):
state.chatInfo.chatStatus = .failure
print(error.localizedDescription)
return .none

case let .updateQuestion(question):
state.chatInfo.text = question
return .none
}
}
}

public init() { }
}
177 changes: 177 additions & 0 deletions package-kuring/Sources/UIKit/BotUI/BotView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//
// SwiftUIView.swift
//
//
// Created by 최효원 on 8/5/24.
//

import SwiftUI
import ComposableArchitecture
import ColorSet
import BotFeatures

public struct BotView: View {
@Bindable var store: StoreOf<BotFeature>
@FocusState private var isInputFocused: Bool
@State private var isPopoverVisible = false
@State private var isSendPopupVisible = false
@State private var messageCountRemaining = 2
@State private var chatMessages: [Message] = []

public var body: some View {
ZStack {
Color.Kuring.bg
.ignoresSafeArea()
.onTapGesture {
isInputFocused = false
}

VStack(alignment: .center) {
headerView
chatView
inputView
infoText
}
.padding(.bottom, 16)

if isSendPopupVisible {
sendPopup
.transition(.opacity)
.zIndex(1)
}
}
}

private var headerView: some View {
HStack {
backButton
Spacer()
titleText
Spacer()
infoButton
}
.padding(.horizontal, 18)
}

private var backButton: some View {
Button {
// 뒤로 가기 버튼 동작 구현
} label: {
Image(systemName: "chevron.backward")
.padding()
.frame(width: 20, height: 11)
.foregroundStyle(Color.black)
}
}

private var titleText: some View {
Text("쿠링봇")
.padding()
.font(.system(size: 18, weight: .semibold))
}

private var infoButton: some View {
Button {
isPopoverVisible.toggle()
} label: {
Image("icon_info_circle", bundle: Bundle.bots)
}
.popover(isPresented: $isPopoverVisible, arrowEdge: .top) {
popoverContent
}
}

private var popoverContent: some View {
VStack(spacing: 10) {
Text("• 쿠링봇은 2024년 6월 이후의 공지\n 사항 내용을 기준으로 답변할 수 있\n 어요.")
Text("• 테스트 기간인 관계로 한 달에 2회\n 까지만 질문 가능해요.")
}
.lineSpacing(5)
.font(.system(size: 15, weight: .medium))
.padding(20)
.presentationBackground(Color.Kuring.gray100)
.presentationCompactAdaptation(.popover)
}

private var chatView: some View {
if !chatMessages.isEmpty {
AnyView(ChatView(messages: chatMessages))
} else {
AnyView(ChatEmptyView())
}
}

private var inputView: some View {
HStack(alignment: .bottom, spacing: 12) {
TextField("질문을 입력해주세요", text: $store.chatInfo.question.limit(to: 300), axis: .vertical)
.lineLimit(5)
.focused($isInputFocused)
.padding(.horizontal)
.padding(.vertical, 12)
.overlay(RoundedRectangle(cornerRadius: 20).strokeBorder(Color.Kuring.gray200, style: StrokeStyle(lineWidth: 1.0)))
.disabled(messageCountRemaining == 0)

sendButton
}
.padding(.horizontal, 20)
.disabled(messageCountRemaining == 0)
}

private var sendButton: some View {
Button {
isInputFocused = false
isSendPopupVisible = true
} label: {
Image(systemName: "arrow.up.circle.fill")
.resizable()
.foregroundStyle(Color.Kuring.gray400)
.scaledToFit()
.frame(width: 40, height: 40)
}
}

private var infoText: some View {
Text("쿠링봇은 실수를 할 수 있습니다. 중요한 정보를 확인하세요.")
.foregroundStyle(Color.Kuring.caption2)
.font(.system(size: 12, weight: .medium))
.padding(.top, 8)
}

private var sendPopup: some View {
SendPopup(isVisible: $isSendPopupVisible) {
if messageCountRemaining > 0 {
messageCountRemaining -= 1
let userMessage = Message(text: store.chatInfo.question, type: .question, sendCount: messageCountRemaining)
let botResponse = Message(text: "자동 응답입니다.", type: .answer, sendCount: messageCountRemaining)
chatMessages.append(contentsOf: [userMessage, botResponse])
store.chatInfo.question = ""
}
}
}

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

/// 글자 수 max 판단
extension Binding where Value == String {
func limit(to maxLength: Int) -> Self {
if self.wrappedValue.count > maxLength {
DispatchQueue.main.async {
self.wrappedValue = String(self.wrappedValue.prefix(maxLength))
}
}
return self
}
}


#Preview {
BotView(
store: Store(
initialState: BotFeature.State(),
reducer: { BotFeature() }
)
)
}
12 changes: 12 additions & 0 deletions package-kuring/Sources/UIKit/BotUI/Bundle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// File.swift
//
//
// Created by 최효원 on 8/5/24.
//

import Foundation

extension Bundle {
public static var bots: Bundle { .module }
}
Loading

0 comments on commit d7bb69a

Please sign in to comment.