diff --git a/ChatBot/ChatBot.xcodeproj/project.pbxproj b/ChatBot/ChatBot.xcodeproj/project.pbxproj index 0eb42b2c..a7bfa90e 100644 --- a/ChatBot/ChatBot.xcodeproj/project.pbxproj +++ b/ChatBot/ChatBot.xcodeproj/project.pbxproj @@ -19,9 +19,13 @@ 70E87ED22BBAA6F800E27E43 /* RequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70E87ED12BBAA6F800E27E43 /* RequestDTO.swift */; }; B4B3E2BD2B42D1BB00818B3C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3E2BC2B42D1BB00818B3C /* AppDelegate.swift */; }; B4B3E2BF2B42D1BB00818B3C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3E2BE2B42D1BB00818B3C /* SceneDelegate.swift */; }; - B4B3E2C12B42D1BB00818B3C /* ChatbotMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3E2C02B42D1BB00818B3C /* ChatbotMainViewController.swift */; }; + B4B3E2C12B42D1BB00818B3C /* DetailChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3E2C02B42D1BB00818B3C /* DetailChatViewController.swift */; }; B4B3E2C62B42D1BC00818B3C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4B3E2C52B42D1BC00818B3C /* Assets.xcassets */; }; B4B3E2C92B42D1BC00818B3C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B4B3E2C72B42D1BC00818B3C /* LaunchScreen.storyboard */; }; + E67855E82BC7B35500488352 /* DetailChatViewUserInputSectionStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E67855E72BC7B35500488352 /* DetailChatViewUserInputSectionStackView.swift */; }; + E67855EA2BC7B84400488352 /* MessageCellStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E67855E92BC7B84400488352 /* MessageCellStackView.swift */; }; + E67855EC2BC7B9AB00488352 /* DetailMessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E67855EB2BC7B9AB00488352 /* DetailMessageCollectionViewCell.swift */; }; + E67855EE2BC7BA9700488352 /* ChatMessageCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E67855ED2BC7BA9700488352 /* ChatMessageCollectionView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -38,10 +42,14 @@ B4B3E2B92B42D1BB00818B3C /* ChatBot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatBot.app; sourceTree = BUILT_PRODUCTS_DIR; }; B4B3E2BC2B42D1BB00818B3C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; B4B3E2BE2B42D1BB00818B3C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - B4B3E2C02B42D1BB00818B3C /* ChatbotMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatbotMainViewController.swift; sourceTree = ""; }; + B4B3E2C02B42D1BB00818B3C /* DetailChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailChatViewController.swift; sourceTree = ""; }; B4B3E2C52B42D1BC00818B3C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B4B3E2C82B42D1BC00818B3C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; B4B3E2CA2B42D1BC00818B3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E67855E72BC7B35500488352 /* DetailChatViewUserInputSectionStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailChatViewUserInputSectionStackView.swift; sourceTree = ""; }; + E67855E92BC7B84400488352 /* MessageCellStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellStackView.swift; sourceTree = ""; }; + E67855EB2BC7B9AB00488352 /* DetailMessageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailMessageCollectionViewCell.swift; sourceTree = ""; }; + E67855ED2BC7BA9700488352 /* ChatMessageCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageCollectionView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -136,12 +144,24 @@ 70E483A32BB5AB3900931F01 /* Model */, 70E483A22BB5AB3700931F01 /* Repository */, 70E483A12BB5AB3400931F01 /* Network */, - B4B3E2C02B42D1BB00818B3C /* ChatbotMainViewController.swift */, + E67855E62BC7B31700488352 /* View */, B4B3E2CA2B42D1BC00818B3C /* Info.plist */, ); path = ChatBot; sourceTree = ""; }; + E67855E62BC7B31700488352 /* View */ = { + isa = PBXGroup; + children = ( + B4B3E2C02B42D1BB00818B3C /* DetailChatViewController.swift */, + E67855E72BC7B35500488352 /* DetailChatViewUserInputSectionStackView.swift */, + E67855E92BC7B84400488352 /* MessageCellStackView.swift */, + E67855EB2BC7B9AB00488352 /* DetailMessageCollectionViewCell.swift */, + E67855ED2BC7BA9700488352 /* ChatMessageCollectionView.swift */, + ); + path = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -212,7 +232,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B4B3E2C12B42D1BB00818B3C /* ChatbotMainViewController.swift in Sources */, + E67855EE2BC7BA9700488352 /* ChatMessageCollectionView.swift in Sources */, + B4B3E2C12B42D1BB00818B3C /* DetailChatViewController.swift in Sources */, 70A610432BBE6061009B08ED /* NetworkErrorEnum.swift in Sources */, 70E87ED22BBAA6F800E27E43 /* RequestDTO.swift in Sources */, 70E4839C2BB5A88900931F01 /* RequestMessageModel.swift in Sources */, @@ -220,11 +241,14 @@ 70E483A72BB5ABBA00931F01 /* APIKeyManager.swift in Sources */, 70A610412BBE6034009B08ED /* URLRequestBuilder.swift in Sources */, 70E483A02BB5A8E700931F01 /* MessageRepository.swift in Sources */, + E67855EA2BC7B84400488352 /* MessageCellStackView.swift in Sources */, 70E4839A2BB5A88000931F01 /* OpenAICheatResponseDTO.swift in Sources */, 70E483AB2BB5AC7900931F01 /* OpenAIService.swift in Sources */, 70E483A92BB5ABC400931F01 /* OpenAIEndPoint.swift in Sources */, + E67855E82BC7B35500488352 /* DetailChatViewUserInputSectionStackView.swift in Sources */, 70E483AE2BB5AEB100931F01 /* ChatViewModel.swift in Sources */, B4B3E2BF2B42D1BB00818B3C /* SceneDelegate.swift in Sources */, + E67855EC2BC7B9AB00488352 /* DetailMessageCollectionViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ChatBot/ChatBot.xcodeproj/xcshareddata/xcschemes/ChatBot.xcscheme b/ChatBot/ChatBot.xcodeproj/xcshareddata/xcschemes/ChatBot.xcscheme new file mode 100644 index 00000000..5ac22da7 --- /dev/null +++ b/ChatBot/ChatBot.xcodeproj/xcshareddata/xcschemes/ChatBot.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChatBot/ChatBot/App/SceneDelegate.swift b/ChatBot/ChatBot/App/SceneDelegate.swift index 33a172ab..0090270d 100644 --- a/ChatBot/ChatBot/App/SceneDelegate.swift +++ b/ChatBot/ChatBot/App/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let repo = MessageRepository() let apiService = OpenAIService() let viewModel = ChatViewModel(repository: repo, apiService: apiService) - let rootViewController = ChatbotMainViewController(viewModel: viewModel, repo: repo, apiService: apiService) + let rootViewController = DetailChatViewController(viewModel: viewModel, repo: repo, apiService: apiService) self.window?.rootViewController = rootViewController self.window?.makeKeyAndVisible() diff --git a/ChatBot/ChatBot/ChatbotMainViewController.swift b/ChatBot/ChatBot/ChatbotMainViewController.swift deleted file mode 100644 index 87fed78f..00000000 --- a/ChatBot/ChatBot/ChatbotMainViewController.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// ViewController.swift -// ChatBot -// -// Created by Tacocat on 1/1/24. -// - -import UIKit - -final class ChatbotMainViewController: UIViewController { - private var viewModel: ChatViewModel - private var repo: MessageRepository - private let apiService: OpenAIService - - init(viewModel: ChatViewModel, repo: MessageRepository, apiService: OpenAIService) { - self.apiService = apiService - self.viewModel = viewModel - self.repo = repo - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - repo = MessageRepository() - - viewModel = ChatViewModel(repository: repo, apiService: apiService) - - setupSendMessageButton() - setupCheckStoregeButton() - setupclearRepoButton() - setupErrorAlert() - } - - - // MARK: - func - private func setupSendMessageButton() { - let sendButton = UIButton(type: .system) - sendButton.setTitle("Send Message", for: .normal) - sendButton.addTarget(self, action: #selector(sendMessage), for: .touchUpInside) - - sendButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(sendButton) - - NSLayoutConstraint.activate([ - sendButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - sendButton.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - } - - private func setupCheckStoregeButton() { - let sendButton = UIButton(type: .system) - sendButton.setTitle("checkStorege", for: .normal) - sendButton.addTarget(self, action: #selector(printMessageRepositoryContents), for: .touchUpInside) - - sendButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(sendButton) - - NSLayoutConstraint.activate([ - sendButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - sendButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 40) - ]) - } - - private func setupclearRepoButton() { - let sendButton = UIButton(type: .system) - sendButton.setTitle("clearRepo", for: .normal) - sendButton.addTarget(self, action: #selector(messageClear), for: .touchUpInside) - - sendButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(sendButton) - - NSLayoutConstraint.activate([ - sendButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - sendButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 80) - ]) - } - - private func configuerErrorAlert(message: String) { - let alertController = UIAlertController(title: "Error발생", message: message, preferredStyle: .alert) - let action = UIAlertAction(title: "확인", style: .default) - alertController.addAction(action) - self.present(alertController, animated: true) - } - - private func setupErrorAlert() { - viewModel.onError = { [weak self] errorMessage in - DispatchQueue.main.async { - self?.configuerErrorAlert(message: errorMessage) - } - } - } - - // MARK: - objc func - @objc private func sendMessage() { - let message = "IOS 개발자가 되기 위한 구체적인 계획" - - DispatchQueue.main.async { - self.viewModel.processUserMessage(message: message, model: .gpt3Turbo) - } - } - @objc private func printMessageRepositoryContents() { - let messages = repo.getMessages() - print(""" - 😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃 - \(messages) - 😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃 - """) - } - @objc private func messageClear() { - repo.clearStorage() - } -} diff --git a/ChatBot/ChatBot/Repository/MessageRepository.swift b/ChatBot/ChatBot/Repository/MessageRepository.swift index 71a08441..1b65c622 100644 --- a/ChatBot/ChatBot/Repository/MessageRepository.swift +++ b/ChatBot/ChatBot/Repository/MessageRepository.swift @@ -7,13 +7,19 @@ import Foundation +protocol MessageRepositoryDeletage: AnyObject { + func messageDidUpdate() +} + class MessageRepository { private var messagesStorage: [RequestMessageModel] = [] private let repoQueue = DispatchQueue(label: "repoQueue") + weak var delegate: MessageRepositoryDeletage? func addMessage(_ message: RequestMessageModel) { repoQueue.async { self.messagesStorage.append(message) + self.delegate?.messageDidUpdate() } } @@ -26,6 +32,7 @@ class MessageRepository { func clearStorage() { repoQueue.sync { self.messagesStorage.removeAll() + self.delegate?.messageDidUpdate() } } } diff --git a/ChatBot/ChatBot/View/ChatMessageCollectionView.swift b/ChatBot/ChatBot/View/ChatMessageCollectionView.swift new file mode 100644 index 00000000..a9f68ff8 --- /dev/null +++ b/ChatBot/ChatBot/View/ChatMessageCollectionView.swift @@ -0,0 +1,33 @@ +// +// ChatMessageCollectionView.swift +// ChatBot +// +// Created by 권태호 on 4/11/24. +// + +import UIKit + +class ChatMessageCollectionView: UICollectionView { + init() { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + super.init(frame: .zero, collectionViewLayout: layout) + + self.register(DetailMessageCollectionViewCell.self, forCellWithReuseIdentifier: DetailMessageCollectionViewCell.identifier) + self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColor = .systemBackground + + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.minimumLineSpacing = 5 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { return } + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) // 상, 하, 좌, 우 여백 설정 + } +} diff --git a/ChatBot/ChatBot/View/DetailChatViewController.swift b/ChatBot/ChatBot/View/DetailChatViewController.swift new file mode 100644 index 00000000..1168b0b0 --- /dev/null +++ b/ChatBot/ChatBot/View/DetailChatViewController.swift @@ -0,0 +1,218 @@ +// +// ViewController.swift +// ChatBot +// +// Created by Tacocat on 1/1/24. +// + +import UIKit + +final class DetailChatViewController: UIViewController { + // MARK: - Property + + private var viewModel: ChatViewModel + private var repo: MessageRepository + private let apiService: OpenAIService + private lazy var openAIAPIResponseIndicatorView: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.center = self.view.center + return indicator + }() + + private var detailChatStackView = DetailChatViewUserInputSectionStackView() + private var chatMessageCollectionView = ChatMessageCollectionView() + + + init(viewModel: ChatViewModel, repo: MessageRepository, apiService: OpenAIService) { + self.apiService = apiService + self.viewModel = viewModel + self.repo = repo + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .white + setupDetailChatStackView() + setupChatMessageCollectionView() + configureDetailChatStackView() + bindViewModel() + } + + override func viewWillAppear(_ animated: Bool) { + keyboardAppear() + } + + override func viewWillDisappear(_ animated: Bool) { + keyboardDisappear() + } + + // MARK: - Autolayout + private func setupDetailChatStackView() { + self.view.addSubview(detailChatStackView) + detailChatStackView.translatesAutoresizingMaskIntoConstraints = false + detailChatStackView.userInputTextView.delegate = self + + NSLayoutConstraint.activate([ + detailChatStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), + detailChatStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + detailChatStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + detailChatStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20) + ]) + } + + private func setupChatMessageCollectionView() { + view.addSubview(chatMessageCollectionView) + chatMessageCollectionView.dataSource = self + chatMessageCollectionView.delegate = self + + NSLayoutConstraint.activate([ + chatMessageCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), + chatMessageCollectionView.leadingAnchor.constraint(equalTo: detailChatStackView.leadingAnchor), + chatMessageCollectionView.trailingAnchor.constraint(equalTo: detailChatStackView.trailingAnchor), + chatMessageCollectionView.bottomAnchor.constraint(equalTo: detailChatStackView.topAnchor, constant: -10) + ]) + } + // MARK: - Indicator + private func showOpenAIAPIResponseIndicator() { + self.view.addSubview(openAIAPIResponseIndicatorView) + openAIAPIResponseIndicatorView.startAnimating() + } + + private func hideOpenAIAPIResponseIndicator() { + openAIAPIResponseIndicatorView.stopAnimating() + openAIAPIResponseIndicatorView.removeFromSuperview() + } + + + // MARK: - configureButton + private func configureDetailChatStackView() { + detailChatStackView.doneButton.addTarget(self, action: #selector(doneButtonTapped(_:)), for: .touchUpInside) + } + + + @objc private func doneButtonTapped(_ sender: UIButton) { + guard let userInput = detailChatStackView.userInputTextView.text, !userInput.isEmpty else { + return + } + detailChatStackView.userInputTextView.text = "" + + DispatchQueue.main.async { + self.showOpenAIAPIResponseIndicator() + } + + viewModel.processUserMessage(message: userInput, model: .gpt3Turbo) { [weak self] in + DispatchQueue.main.async { + self?.hideOpenAIAPIResponseIndicator() + } + } + } + + // MARK: - configureViewModel + private func bindViewModel() { + viewModel.onMessagesUpdated = { [weak self] in + DispatchQueue.main.async { + self?.chatMessageCollectionView.reloadData() + self?.scrollToBottom() + } + } + + DispatchQueue.main.async { + self.viewModel.onError = { [weak self] errorMessage in + DispatchQueue.main.async { + self?.configureErrorAlert() + } + } + } + } + + private func scrollToBottom() { + DispatchQueue.main.async { + if self.viewModel.messageRepository.getMessages().count > 0 { + let indexPath = IndexPath(item: self.viewModel.messageRepository.getMessages().count - 1, section: 0) + self.chatMessageCollectionView.scrollToItem(at: indexPath, at: .bottom, animated: true) + } + } + } + // MARK: - Alert Configure + private func configureErrorAlert() { + let alert = UIAlertController(title: "Error", message: "관리자에게 문의해주세요", preferredStyle: .alert) + let action = UIAlertAction(title: "확인", style: .default) + alert.addAction(action) + present(alert, animated: true) + } + // MARK: - keyBoardAction + @objc func keyboardUp(notification:NSNotification) { + if let keyboardFrame:NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { + let keyboardRectangle = keyboardFrame.cgRectValue + UIView.animate( + withDuration: 0.3 + , animations: { + self.view.transform = CGAffineTransform(translationX: 0, y: -keyboardRectangle.height) + } + ) + } + } + + @objc func keyboardDown() { + self.view.transform = .identity + } + + private func keyboardAppear() { + NotificationCenter.default.addObserver(self, selector: #selector(keyboardUp), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardDown), name: UIResponder.keyboardWillHideNotification, object: nil) + } + private func keyboardDisappear() { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + +} + +extension DetailChatViewController: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + let size = CGSize(width: view.frame.width - 40, height: .infinity) + let estimatedSize = textView.sizeThatFits(size) + let validHeight = estimatedSize.height.isNaN || estimatedSize.height < 0 ? 40 : estimatedSize.height + + textView.constraints.forEach { (constraint) in + if constraint.firstAttribute == .height { + constraint.constant = max(40, min(100, validHeight)) + } + } + } +} + +// MARK: - CollectionView + +extension DetailChatViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel.messageRepository.getMessages().count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DetailMessageCollectionViewCell.identifier, for: indexPath) as? DetailMessageCollectionViewCell else { + return UICollectionViewCell() + } + + let message = viewModel.messageRepository.getMessages()[indexPath.row] + cell.configureMessageCollectionViewCell(with: message) + + return cell + } +} + +extension DetailChatViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let message = viewModel.messageRepository.getMessages()[indexPath.row].content + let width = collectionView.frame.width + let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) + let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)] + let estimatedFrame = NSString(string: message).boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil) + return CGSize(width: width, height: estimatedFrame.height + 20) + } +} diff --git a/ChatBot/ChatBot/View/DetailChatViewUserInputSectionStackView.swift b/ChatBot/ChatBot/View/DetailChatViewUserInputSectionStackView.swift new file mode 100644 index 00000000..30e79ea9 --- /dev/null +++ b/ChatBot/ChatBot/View/DetailChatViewUserInputSectionStackView.swift @@ -0,0 +1,62 @@ +// +// DetailChatViewUserInputSectionStackView.swift +// ChatBot +// +// Created by 권태호 on 4/11/24. +// + +import UIKit + +class DetailChatViewUserInputSectionStackView: UIStackView { + var userInputTextView: UITextView = { + let textView = UITextView() + textView.font = UIFont.systemFont(ofSize: 16) + textView.layer.borderWidth = 0.1 + textView.layer.cornerRadius = 10 + textView.isScrollEnabled = false + return textView + }() + + var doneButton: UIButton = { + let button = UIButton() + let image = UIImage(systemName: "arrowshape.up") + button.setImage(image, for: .normal) + button.backgroundColor = .black + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupStackView() + + } + + override func layoutSubviews() { + super.layoutSubviews() + configureLayoutSubviews() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - configure + private func setupStackView() { + self.axis = .horizontal + self.spacing = 5 + self.distribution = .fill + self.alignment = .center + addArrangedSubview(userInputTextView) + addArrangedSubview(doneButton) + + doneButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + doneButton.widthAnchor.constraint(equalToConstant: 40), + doneButton.heightAnchor.constraint(equalToConstant: 40) + ]) + } + + private func configureLayoutSubviews() { + doneButton.layer.cornerRadius = doneButton.frame.height / 2 + } +} diff --git a/ChatBot/ChatBot/View/DetailMessageCollectionViewCell.swift b/ChatBot/ChatBot/View/DetailMessageCollectionViewCell.swift new file mode 100644 index 00000000..28de4c2f --- /dev/null +++ b/ChatBot/ChatBot/View/DetailMessageCollectionViewCell.swift @@ -0,0 +1,56 @@ +// +// DetailMessageCollectionViewCell.swift +// ChatBot +// +// Created by 권태호 on 4/11/24. +// + +import UIKit + +class DetailMessageCollectionViewCell: UICollectionViewCell { + static var identifier = String(describing: DetailMessageCollectionViewCell.self) + + private let messageCellStackView = MessageCellStackView() + + override init(frame: CGRect) { + super.init(frame: frame) + setupMessageCollectionViewCell() + + } + + override func prepareForReuse() { + super.prepareForReuse() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //셀 높이 동적 제어 + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + super.preferredLayoutAttributesFitting(layoutAttributes) + layoutIfNeeded() + + let size = contentView.systemLayoutSizeFitting(layoutAttributes.size) + var newFrame = layoutAttributes.frame + newFrame.size.height = ceil(size.height) + layoutAttributes.frame = newFrame + + return layoutAttributes + } + + private func setupMessageCollectionViewCell() { + contentView.addSubview(messageCellStackView) + messageCellStackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + messageCellStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), + messageCellStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), + messageCellStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -5), + messageCellStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15) + ]) + } + + func configureMessageCollectionViewCell(with model: RequestMessageModel) { + messageCellStackView.configureStackView(with: model) + } +} diff --git a/ChatBot/ChatBot/View/MessageCellStackView.swift b/ChatBot/ChatBot/View/MessageCellStackView.swift new file mode 100644 index 00000000..389a009f --- /dev/null +++ b/ChatBot/ChatBot/View/MessageCellStackView.swift @@ -0,0 +1,50 @@ +// +// MessageCellStackView.swift +// ChatBot +// +// Created by 권태호 on 4/11/24. +// + +import UIKit + +class MessageCellStackView: UIStackView { + private let contentLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 15) + label.numberOfLines = 0 + label.layer.cornerRadius = 10 + return label + }() + + private let senderLabel: UILabel = { + let label = UILabel() + label.font = UIFont.boldSystemFont(ofSize: 18) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupStackView() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupStackView() { + self.axis = .vertical + self.spacing = 8 + self.distribution = .fill + self.alignment = .fill + self.addArrangedSubview(senderLabel) + self.addArrangedSubview(contentLabel) + } + + func configureStackView(with content: RequestMessageModel) { + let message = content.content + contentLabel.text = message + contentLabel.textColor = .black + contentLabel.textAlignment = .left + senderLabel.text = content.role == .user ? "😁You" : "🤖Bot" + } +} diff --git a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift index c9d49933..6fc64dde 100644 --- a/ChatBot/ChatBot/ViewModel/ChatViewModel.swift +++ b/ChatBot/ChatBot/ViewModel/ChatViewModel.swift @@ -8,31 +8,36 @@ import Foundation final class ChatViewModel { - private let messageRepository: MessageRepository + let messageRepository: MessageRepository private let apiService: OpenAIService var onError:((String) -> Void)? + var onMessagesUpdated: (() -> Void)? init(repository: MessageRepository, apiService: OpenAIService) { self.messageRepository = repository self.apiService = apiService } - func processUserMessage(message content: String, model: GPTModel) { + func processUserMessage(message content: String, model: GPTModel, completion: @escaping () -> Void) { let userMessage = RequestMessageModel(role: .user, content: content) messageRepository.addMessage(userMessage) + self.onMessagesUpdated?() - apiService.sendRequestToOpenAI(messageRepository.getMessages(), model: model, APIkey: APIKeyManager.openAIAPIKey) { [weak self] result in - DispatchQueue.main.async { - switch result { - case .success(let receivedMessages): - receivedMessages.forEach { responseMessage in - self?.messageRepository.addMessage(responseMessage) - } - case .failure(let error): - self?.onError?("Error 발생: 관라자에게 문의해주세요 \(error.localizedDescription)") + apiService.sendRequestToOpenAI(messageRepository.getMessages(), + model: model, + APIkey: APIKeyManager.openAIAPIKey) { [weak self] result in + switch result { + case .success(let receivedMessages): + receivedMessages.forEach { responseMessage in + self?.messageRepository.addMessage(responseMessage) } + self?.onMessagesUpdated?() + case .failure(let error): + self?.onError?(error.localizedDescription) } + completion() } } } +