From 2bbd5ca9586e0ac4f07b8ebfa393ba8a68291e0a Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Tue, 9 Sep 2025 23:57:10 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[feat/chat]=20feat:=20=EB=8C=80=ED=99=94?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D=20=EB=A1=9C=EB=94=A9=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/ChatViewController.swift | 47 ++++++++++++++ .../CommonUI.xcodeproj/project.pbxproj | 4 ++ .../View/Chat/ChatAnalysisLoadingView.swift | 62 +++++++++++++++++++ .../CommonUI/Sources/View/Chat/ChatView.swift | 7 +++ 4 files changed, 120 insertions(+) create mode 100644 Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 5811540..6259b43 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -15,6 +15,8 @@ public class ChatViewController: BaseViewController { let chatView = ChatView() private var messages: [ChatMessageVO] = [] + + private let loadingView = ChatAnalysisLoadingView() public init(chatViewModel: ChatViewModel) { self.viewModel = chatViewModel @@ -47,6 +49,7 @@ public class ChatViewController: BaseViewController { public override func setupHierarchy() { view.addSubview(chatView) + view.addSubview(loadingView) } public override func setupDelegate() { @@ -56,6 +59,13 @@ public class ChatViewController: BaseViewController { chatView.snp.makeConstraints { $0.edges.equalToSuperview() } + + loadingView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + // 초기에는 로딩 화면 숨김 + loadingView.isHidden = true } private func bindData() { @@ -80,6 +90,10 @@ public class ChatViewController: BaseViewController { chatView.onSendButtonTapped = { [weak self] message in self?.sendMessage(message) } + + chatView.onEndButtonTapped = { [weak self] in + self?.showAnalysisLoading() + } } private func setupTextFieldActions() { @@ -113,6 +127,39 @@ public class ChatViewController: BaseViewController { messages.append(message) chatView.addMessageToUI(message) } + + private func showAnalysisLoading() { + print("🔄 대화 분석 시작") + + // 로딩 화면 표시 + loadingView.isHidden = false + loadingView.alpha = 0 + + UIView.animate(withDuration: 0.3) { + self.loadingView.alpha = 1 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + self.hideAnalysisLoading() + } + } + + private func hideAnalysisLoading() { + print("✅ 대화 분석 완료") + + UIView.animate(withDuration: 0.3, animations: { + self.loadingView.alpha = 0 + }) { _ in + self.loadingView.isHidden = true + // 여기서 분석 결과 화면으로 이동하거나 다른 액션 수행 + self.navigateToAnalysisResult() + } + } + + private func navigateToAnalysisResult() { + // TODO: 분석 결과 화면으로 이동하는 로직 구현 + print("📊 분석 결과 화면으로 이동") + } private func updateRecommendTopics(_ topics: [String]) { print("🔄 추천 주제 업데이트: \(topics)") diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index 473999c..6ce71f6 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 950A0D602E5C3C7000C07CF2 /* LMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */; }; 950A0D622E5C562700C07CF2 /* LMInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D612E5C561400C07CF2 /* LMInputField.swift */; }; 950A0D962E605CEA00C07CF2 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */; }; + 9516ED442E7075EA00F548A1 /* ChatAnaylsisLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED432E7075DC00F548A1 /* ChatAnaylsisLoadingView.swift */; }; BAD8B768F782046D4AA1C073 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3DE048BEDCB92B48F401061 /* RxCocoa.framework */; }; BE81B1F3E60D37D75A058D2B /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CCDC15081A22BAED6318E3E /* SnapKit.framework */; }; C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012F45F908769FA7C3C0792F /* LoginView.swift */; }; @@ -76,6 +77,7 @@ 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMButton.swift; sourceTree = ""; }; 950A0D612E5C561400C07CF2 /* LMInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMInputField.swift; sourceTree = ""; }; 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = ""; }; + 9516ED432E7075DC00F548A1 /* ChatAnaylsisLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnaylsisLoadingView.swift; sourceTree = ""; }; 9CCDC15081A22BAED6318E3E /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BACC7259FC0C14CB352A4E6B /* OptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionView.swift; sourceTree = ""; }; C28FE6392E1612667826E5C5 /* DiaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryView.swift; sourceTree = ""; }; @@ -198,6 +200,7 @@ isa = PBXGroup; children = ( 3EBDD10D8389EBB23B42C54F /* ChatView.swift */, + 9516ED432E7075DC00F548A1 /* ChatAnaylsisLoadingView.swift */, ); path = Chat; sourceTree = ""; @@ -369,6 +372,7 @@ 65762CE867888754D56BA2CB /* OptionView.swift in Sources */, CEADBDD98AC9921C05AAC1DA /* QuizCollectionViewCell.swift in Sources */, 950A0D512E5AADC400C07CF2 /* SignInView.swift in Sources */, + 9516ED442E7075EA00F548A1 /* ChatAnaylsisLoadingView.swift in Sources */, 592FAEA836FD6B4B3F466D72 /* QuizCompleteAlertView.swift in Sources */, 65B3402D15A9F06B3E88CE19 /* QuizView.swift in Sources */, C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */, diff --git a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift new file mode 100644 index 0000000..f299efb --- /dev/null +++ b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift @@ -0,0 +1,62 @@ +// +// ChatAnalysisLoadingView.swift +// CommonUI +// +// Created by 박지윤 on 7/2/25. +// + +import UIKit +import SnapKit +import Then + +open class ChatAnalysisLoadingView: UIView { + + private let loadingSpinner = UIActivityIndicatorView(style: .large).then { + $0.color = CommonUIAssets.LMGray1 + $0.startAnimating() + } + + private let mainLabel = UILabel().then { + $0.text = "사용자 대화를 분석 중입니다..." + $0.textColor = CommonUIAssets.LMGray1 + $0.font = UIFont.systemFont(ofSize: 18, weight: .medium) + $0.textAlignment = .center + } + + private let subLabel = UILabel().then { + $0.text = "잠시만 기다려 주세요" + $0.textColor = CommonUIAssets.LMGray3 + $0.font = UIFont.systemFont(ofSize: 14, weight: .regular) + $0.textAlignment = .center + } + + public override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + backgroundColor = CommonUIAssets.LMOrange4 + + [loadingSpinner, mainLabel, subLabel].forEach { addSubview($0) } + + loadingSpinner.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.centerY.equalToSuperview().offset(-40) + } + + mainLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalTo(loadingSpinner.snp.bottom).offset(20) + } + + subLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalTo(mainLabel.snp.bottom).offset(8) + } + } +} \ No newline at end of file diff --git a/Projects/CommonUI/Sources/View/Chat/ChatView.swift b/Projects/CommonUI/Sources/View/Chat/ChatView.swift index 3ab2d84..44555f6 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatView.swift @@ -107,6 +107,7 @@ open class ChatView: UIView { let disposeBag = DisposeBag() public var onSendButtonTapped: ((String) -> Void)? + public var onEndButtonTapped: (() -> Void)? public override init(frame: CGRect) { super.init(frame: frame) @@ -123,6 +124,12 @@ open class ChatView: UIView { self?.chatTextField.text = "" }) .disposed(by: disposeBag) + + endButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.onEndButtonTapped?() + }) + .disposed(by: disposeBag) } func initAttribute() { From 66d7723108a604e755579c795f2a60ca44bf49e7 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Thu, 11 Sep 2025 00:06:16 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[feat/chat]=20feat:=20image=20asset=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Icon/bubble.imageset/Contents.json | 22 ++++++++++++++++++ .../Icon/bubble.imageset/bubble@2x.png | Bin 0 -> 1220 bytes .../Icon/bubble.imageset/bubble@3x.png | Bin 0 -> 1645 bytes .../Icon/edit.imageset/Contents.json | 22 ++++++++++++++++++ .../Icon/edit.imageset/edit@2x.png | Bin 0 -> 596 bytes .../Icon/edit.imageset/edit@3x.png | Bin 0 -> 883 bytes .../Icon/message.imageset/Contents.json | 22 ++++++++++++++++++ .../Icon/message.imageset/message@2x.png | Bin 0 -> 2281 bytes .../Icon/message.imageset/message@3x.png | Bin 0 -> 3390 bytes .../Sources/Enum/CommonUIAssets.swift | 3 +++ 10 files changed, 69 insertions(+) create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/Contents.json create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/bubble@2x.png create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/bubble@3x.png create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/edit.imageset/Contents.json create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/edit.imageset/edit@2x.png create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/edit.imageset/edit@3x.png create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/Contents.json create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/message@2x.png create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/message@3x.png diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/Contents.json b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/Contents.json new file mode 100644 index 0000000..c8ed7b4 --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/bubble@2x.png b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/bubble.imageset/bubble@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9493cd56ace7047d3cdd3d731b261460461b7ee9 GIT binary patch literal 1220 zcmV;#1UvhQP)#W`X4_17~xtcflK02gz4n>MJBkzzg9_ZvS|-_AxF6UPXrTU zBElt6phfsLhQ|r0m1;X@ni`|x3=@pYLP?9!tW(cA2raj!xBWz89pyL{hkGy)a z1?*fY?1E#rIjm)0!|Hrfyv2BbuM0BNJd6UYQZh8=C$tp9MEz5_ng1e-3j;ZpRsJ7Y_@f z(Va$8wf8xlaX>Rr;vlF<-hT9Ct1aVUd2p@S0S7?^20wM{V`CL_--3KNns9JlA14fq zQ->Y50xL8UJvRA!m_Nt-6P||2%CU{)5CCfCI**+JP$5s61UbSfOcmIC6X+X`4`Rtj zXxOAO#Yz^;e_Sc3!HXG4m&b9ik9{vi^1yvbyVpUpUNK52kf4Gv>6g5coVz`hQI#BA zvSDQhYB1`u$!$G?ao~9^{knJ(ZVlx@yA5*(3J%U=IKt91AgsdNA@WAStt4!>tFSq? zLC!YjMw(8x*1-kzv2>e$EMfAra+GjCvxani2tE19C*06oce1IKIr`axPPa|46P68H z@5}EVVe0{e3mJKVTmiCpoI0-IVm$8Ubo3vm^g;S8|B~Rny`N!5uw+eYCT+JOVjK$K zQ|J4L>tHj&1+sZ+R@K)hjJP6RBA5ZPINZ}LhX%M&mzpgM4R;z=I`V~bg%V6g!V4L* ztTd9SEq}e)VNLppCcy?ic-X5OCo&GZEidv`t_$cpTnjT*nRa!Ba{PLj$LF-o6;HKN zA(u;4noCvP{;2VSFgt661DF`*N;d^Tw)q8)Vc*}1ix@YnA17Qd4qBivF9cRpK8>&a zpf|=Te>uWWwwWxg*Yrvtr6XIRBzgqW-ll>vFWjA^{3UI;b%7+Y_Kd*$p1@vqp`_~- z7059-3c1r1wCORYHVPhP-hmUqkNWUyTV(6MH-p8z_G_Z6rgv~HIsPH^d51WB4SFy* i?@wF#yv8-YXM6!hu;-|p1Ixz%0000zv?mZ}60CqSG4ct`<$K|X(7N`kg3`{8Mzf!~d-_iRIdbI4kt2sI5T*cmS}g&1WkBya zpe%s|x|R!cs}!2*)0R54D<35KAmm5DM{QjG&4&qNVnoQ(xAfq!0w7kwe&D3=X{#+F z=Mb0wXu$-KDiQLiz5w5MH|S-RzV!u|41AKVW01xDG$kb!BBn$D18jnPw4DYa_a;J~ z*WN=i0(|<^HqGzQ-)As)^OXLT=oPIcUw|tR>iF~SHrxYa6CppmdjUcmjNY))Z8Cb` zwczTujcbh#xU7FvU4Z!_tu4sb;EDq=WMJo(#+8n-h>#z)HprqkVpi>dfPH$1CN4L8 zNCCmggLNY1CBzxuF{L9h5u^%xB+}K0)dB*pM8ox)9|!WZR-w;QDp>-4ABo9GM1(xs zQmRm4ZwlOpFTMIC2{85&_$GIRFu*3h-gyI#a3&kz3K&7{uvQP6~-cLA()xRc?z zObSj0js!AdYQg1uqe7*x(4-W)AsicnN=4?otW@w4!AgU;S5WgH5Mt9B^I(IUY7wCmDy%fjzDk4*Bm%vs zN`VGDz+}=O#2%eGWphj$s->yTjRX~rhMx`Oli|Bg`G{`>A<6cBGOi$pMwkebHJoO# z=3!yl2r2%d<8tU)CL^NP+XSx|GHSP-sm_f8(up{1B0&1aZR6Hyt!@Qemp?v9a&pWf z1|eVRkWYMGf%`25u#GM1-`DKJh89@NSU1k4c;69Q>BZm%kZkf zd18nskOB1jIPCrF)`4h56nJ`8x*J4q<|QJgnPqcSU8cwnd(X{mjuNZcE%4}i=Hq{l zSc4t}SB+6?*p)8MJ}{oxtU!CckGr3E5>y|@Ai|7;EfLB-pa?B!*EaOGFyCDz+p*qH z`nJ;Qf^|W^+xI=`ewCk)teM3oLfeP1!#Rjbzc!m=w-R*Q-l-7jCSI52d94Xz?r>6s z4BDN$(cQiR_wkpVDkOmAVw1tS2#J|b$(hB3z1Du(5~3?pp11C>hmrkK92g>M5>kb7 z#1tuTqKZYX*{(qKC@JKLmn7z82hY~0xohLrX+x%&vZ9kN?b{F|fPn zAsLW>8_*MJE35q6_*nUuE7Gd+zVgX(z0Yt3+R$yh?F|V_0?>Lzg7N_z8oP#;NbAb) zO_2imRcc)z>v|tRei5^E9MAfSW=-Qy3jSd{Kr6dPR(${mh;Fp2H#7iT5NlsytsZE5 zAx8(`4dHMyxUb|Zy7%W$09X9pSA0hop!d%cAq{}q#&?3f75Iv-v;gwHlN1PPBQy&D zjsz{u1_0AQ%nAVd5VHe-F2o`Lpa-!S0O&w03IO9F76*WF5J><)JVY`85C@SI0E9yr z&ac~30c@K31p$i#p%5QT2T-fHh#Q(A{woy1o@s48aj`=vW_SlDN#bJxr1eK94aJWE zkknU6>Wg>Hi9-N}>Z>K40-Y14ra^aa`wM{9e@PfGT=ON5$ZL0O`V?yY+#OtxC>! i$nVOZmA|;_iu4EOim!-9LZTl40000LCbOWgqBpbNANGgm(M^X|$jFJ>S%QQ!OEMEy^dPPc_8TV5?2h38cxoTEQKqB z1iipbP#-T{-c>L~(4{Dag)>J`9~Bmii=by6C$u6=&nqy4LdS!SpJegBBV$X zD~OOFNwgp$wIuO^h?J7D5JaSsl#L)Fg`}(m5wRs@Cy0nCDN8{_EJ@i4B4S9&S`ZOh zl0v&3E=5q^4y^z@W5(BP4)C{Ba)O-oAT`jA)0TSZk+e1ZE%qt49cYJsFKvk-B^BU1 z_Z{(T&}QW0{|PCHI|Y90c!WJSNs%5Rq$I9`VrMaqrLIhRWr-`3-jN`Qr2Ig8-6Bp9 zVfzwS2=-mLl2i~OaZBlvM3DEI!eK}zUg`wg89Vn6J zRxTR~?QxZ`-xjMdlcVJYa*rcCKt;L+a9&{4@Hh!6EKgL803|{p{w?pJqC(&rS|Z<3 zMVB-?OymaRDbRjH;~U!ZT0z!DX4Czo1;4*&|5L;hR#sMW;2)Kb$=B+#)RzDN002ov JPDHLkV1f~afvW%j literal 0 HcmV?d00001 diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/Contents.json b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/Contents.json new file mode 100644 index 0000000..d72b03f --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "message@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "message@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/message@2x.png b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/message.imageset/message@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..55cde1654898c89dd0f45027224e54e472deddaa GIT binary patch literal 2281 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 ScF#}w15F^e4eP*evHt>2JbJ?b literal 0 HcmV?d00001 diff --git a/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift b/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift index 9447024..2d2b4d9 100644 --- a/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift +++ b/Projects/CommonUI/Sources/Enum/CommonUIAssets.swift @@ -50,6 +50,9 @@ public enum CommonUIAssets { public static let IconPlay = image(named: "play") public static let IconBack = image(named: "back") public static let IconSend = image(named: "send") + public static let IconEdit = image(named: "edit") + public static let IconMessage = image(named: "message") + public static let IconBubble = image(named: "bubble") /// color public static let LMOrange1 = color(named: "LMOrange01") From 46280486892a82953031d8a840fda112d3bcc1f4 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Thu, 11 Sep 2025 00:07:37 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[feat/chat]=20feat:=20chat=20=ED=83=AD=20?= =?UTF-8?q?flow=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LearnMate/Sources/Coordinator/TabBarCoordinator.swift | 4 ++-- Projects/LearnMate/Sources/DI/ChatAssembly.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift b/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift index 97d0b2f..a0b0142 100644 --- a/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift +++ b/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift @@ -95,8 +95,8 @@ final class DefaultTabBarController: TabBarCoordinator { let homeViewController = dependency.injector.resolve(HomeViewController.self) tabNavigationController.pushViewController(homeViewController, animated: true) case .chat: - let chatViewController = dependency.injector.resolve(ChatViewController.self) - tabNavigationController.pushViewController(chatViewController, animated: true) + let chatMainViewController = dependency.injector.resolve(ChatMainViewController.self) + tabNavigationController.pushViewController(chatMainViewController, animated: true) default: let viewController = UIViewController() viewController.view.backgroundColor = .black diff --git a/Projects/LearnMate/Sources/DI/ChatAssembly.swift b/Projects/LearnMate/Sources/DI/ChatAssembly.swift index 6459851..b547bb7 100644 --- a/Projects/LearnMate/Sources/DI/ChatAssembly.swift +++ b/Projects/LearnMate/Sources/DI/ChatAssembly.swift @@ -18,9 +18,9 @@ public struct ChatAssembly: Assembly { tokenUseCase: tokenUseCase) } - container.register(ChatViewController.self) { resolver in + container.register(ChatMainViewController.self) { resolver in let chatViewModel = resolver.resolve(ChatViewModel.self)! - return ChatViewController(chatViewModel: chatViewModel) + return ChatMainViewController(chatViewModel: chatViewModel) } } } From 70cb027fed3bba4c0921ce2e6349e7ead522535f Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Thu, 11 Sep 2025 00:08:06 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[feat/chat]=20feat:=20ChatMainView=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Chat/Chat.xcodeproj/project.pbxproj | 46 +-- .../Sources/View/ChatAnalysisController.swift | 44 +++ .../Sources/View/ChatMainViewController.swift | 76 +++++ .../Sources/View/ChatViewController.swift | 7 +- .../CommonUI.xcodeproj/project.pbxproj | 20 +- .../View/Chat/ChatAnalysisLoadingView.swift | 5 +- .../Sources/View/Chat/ChatAnalysisView.swift | 275 ++++++++++++++++++ .../Sources/View/Chat/ChatListCell.swift | 68 +++++ .../Sources/View/Chat/ChatMainView.swift | 160 ++++++++++ 9 files changed, 661 insertions(+), 40 deletions(-) create mode 100644 Projects/Chat/Sources/View/ChatAnalysisController.swift create mode 100644 Projects/Chat/Sources/View/ChatMainViewController.swift create mode 100644 Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift create mode 100644 Projects/CommonUI/Sources/View/Chat/ChatListCell.swift create mode 100644 Projects/CommonUI/Sources/View/Chat/ChatMainView.swift diff --git a/Projects/Chat/Chat.xcodeproj/project.pbxproj b/Projects/Chat/Chat.xcodeproj/project.pbxproj index 4d48404..d5d1742 100644 --- a/Projects/Chat/Chat.xcodeproj/project.pbxproj +++ b/Projects/Chat/Chat.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -12,6 +12,8 @@ 7F3F4DE30EE902DEABC6040E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAFFC509053CC99EAA3B277 /* ChatViewModel.swift */; }; 9179FB9B1C2AEDCB7A3CFC7E /* ChatCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588CFD307FD55B64676DA657 /* ChatCoordinator.swift */; }; 91C973D65FBDD1ABF65A1A2A /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F910F30DEBC80ABF5CC5A6F9 /* Domain.framework */; }; + 9516ED482E7076F800F548A1 /* ChatAnalysisController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED472E7076E900F548A1 /* ChatAnalysisController.swift */; }; + 9516ED4F2E708CE100F548A1 /* ChatMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED4E2E708CD900F548A1 /* ChatMainViewController.swift */; }; B6F14AC32696F30284073B2F /* Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEE28C25EAD3F5DC9E76FFAC /* Chat.framework */; }; B89B886F288E5BDECC82BA50 /* Common.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BD6CA2A2FDA5A5D9C4A87D /* Common.framework */; }; E096380D552BAA6326ED8397 /* CommonUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB61D7206C15B58E35E2DEB /* CommonUI.framework */; }; @@ -60,6 +62,8 @@ 5AAFFC509053CC99EAA3B277 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = ""; }; 5E359E9093579B130E0EDD53 /* Then.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Then.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 64BD6CA2A2FDA5A5D9C4A87D /* Common.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Common.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9516ED472E7076E900F548A1 /* ChatAnalysisController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisController.swift; sourceTree = ""; }; + 9516ED4E2E708CD900F548A1 /* ChatMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMainViewController.swift; sourceTree = ""; }; BC90A71D97E9F538AACFD586 /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C8A6CF81DAC793585959F31D /* Chat-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Chat-Info.plist"; sourceTree = ""; }; E05B48A08FE1A700DE3FEE63 /* RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -154,7 +158,9 @@ B076E5D7F9914D43D18020A7 /* View */ = { isa = PBXGroup; children = ( + 9516ED4E2E708CD900F548A1 /* ChatMainViewController.swift */, E083003E4769EA2B259F4BE8 /* ChatViewController.swift */, + 9516ED472E7076E900F548A1 /* ChatAnalysisController.swift */, ); path = View; sourceTree = ""; @@ -227,8 +233,6 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - TargetAttributes = { - }; }; buildConfigurationList = 5BF2D8A0F20A5563CE9A6AEB /* Build configuration list for PBXProject "Chat" */; compatibilityVersion = "Xcode 14.0"; @@ -278,9 +282,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9516ED4F2E708CE100F548A1 /* ChatMainViewController.swift in Sources */, 9179FB9B1C2AEDCB7A3CFC7E /* ChatCoordinator.swift in Sources */, 11E1631C9C29E4197C2782CB /* ChatViewController.swift in Sources */, 7F3F4DE30EE902DEABC6040E /* ChatViewModel.swift in Sources */, + 9516ED482E7076F800F548A1 /* ChatAnalysisController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -314,21 +320,14 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.ChatTests; PRODUCT_NAME = ChatTests; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -413,11 +412,7 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.Chat; PRODUCT_NAME = Chat; SDKROOT = iphoneos; @@ -511,11 +506,7 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.ChatTests; PRODUCT_NAME = ChatTests; SDKROOT = iphoneos; @@ -553,11 +544,7 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.Chat; PRODUCT_NAME = Chat; SDKROOT = iphoneos; @@ -565,10 +552,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/Projects/Chat/Sources/View/ChatAnalysisController.swift b/Projects/Chat/Sources/View/ChatAnalysisController.swift new file mode 100644 index 0000000..d015257 --- /dev/null +++ b/Projects/Chat/Sources/View/ChatAnalysisController.swift @@ -0,0 +1,44 @@ +// +// ChatAnalysisController.swift +// Chat +// +// Created by 박지윤 on 9/9/25. +// + +import UIKit +import CommonUI +import Domain + +public class ChatAnalysisController: BaseViewController { + + private let chatAnalysisView = ChatAnalysisView() + private var messages: [ChatMessageVO] = [] + + public init(messages: [ChatMessageVO]) { + self.messages = messages + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + chatAnalysisView.setupAnalysisData(messages) + } + + public override func setupViewProperty() { + view.backgroundColor = CommonUIAssets.LMOrange4 + } + + public override func setupHierarchy() { + view.addSubview(chatAnalysisView) + } + + public override func setupLayout() { + chatAnalysisView.snp.makeConstraints { + $0.edges.equalTo(view.safeAreaLayoutGuide) + } + } +} diff --git a/Projects/Chat/Sources/View/ChatMainViewController.swift b/Projects/Chat/Sources/View/ChatMainViewController.swift new file mode 100644 index 0000000..b5a3336 --- /dev/null +++ b/Projects/Chat/Sources/View/ChatMainViewController.swift @@ -0,0 +1,76 @@ +// +// ChatMainViewController.swift +// Chat +// +// Created by 박지윤 on 9/10/25. +// + +import UIKit +import CommonUI +import RxSwift +import Domain + +public class ChatMainViewController: BaseViewController { + let viewModel: ChatViewModel + + let chatLabel = UILabel().then { + $0.text = "대화" + $0.textColor = CommonUIAssets.LMBlack + $0.font = UIFont.systemFont(ofSize: 30, weight: .bold) + } + + let chatMainView = ChatMainView() + + public init(chatViewModel: ChatViewModel) { + self.viewModel = chatViewModel + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: false) + } + + public override func viewDidLoad() { + super.viewDidLoad() + setupViewProperty() + setupHierarchy() + setupLayout() + bindData() + bindActions() + } + + public override func setupViewProperty() { + view.backgroundColor = CommonUIAssets.LMOrange4 + } + + public override func setupHierarchy() { + [chatLabel, chatMainView].forEach { view.addSubview($0) } + } + + public override func setupDelegate() { + } + + public override func setupLayout() { + chatLabel.snp.makeConstraints { + $0.height.equalTo(34) + $0.top.equalTo(view.safeAreaLayoutGuide).offset(10) + $0.leading.equalToSuperview().inset(20) + } + + chatMainView.snp.makeConstraints { + $0.top.equalTo(chatLabel.snp.bottom).offset(10) + $0.horizontalEdges.bottom.equalToSuperview() + } + } + + private func bindData() { + } + + private func bindActions() { + } +} diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 6259b43..479f1e8 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -157,8 +157,12 @@ public class ChatViewController: BaseViewController { } private func navigateToAnalysisResult() { - // TODO: 분석 결과 화면으로 이동하는 로직 구현 print("📊 분석 결과 화면으로 이동") + + let analysisController = ChatAnalysisController(messages: messages) + analysisController.modalPresentationStyle = .fullScreen + + present(analysisController, animated: true) } private func updateRecommendTopics(_ topics: [String]) { @@ -169,7 +173,6 @@ public class ChatViewController: BaseViewController { return } - // ChatView의 recommendTexts 프로퍼티로 간단하게 업데이트 chatView.recommendTexts = topics print("✅ 추천 주제 업데이트 완료: \(topics.count)개") diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index 6ce71f6..475eafe 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -25,7 +25,10 @@ 950A0D602E5C3C7000C07CF2 /* LMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */; }; 950A0D622E5C562700C07CF2 /* LMInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D612E5C561400C07CF2 /* LMInputField.swift */; }; 950A0D962E605CEA00C07CF2 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */; }; - 9516ED442E7075EA00F548A1 /* ChatAnaylsisLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED432E7075DC00F548A1 /* ChatAnaylsisLoadingView.swift */; }; + 9516ED4A2E7076FF00F548A1 /* ChatAnalysisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED492E7076FB00F548A1 /* ChatAnalysisView.swift */; }; + 9516ED4D2E707ACF00F548A1 /* ChatAnalysisLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */; }; + 9516ED512E708CF700F548A1 /* ChatMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED502E708CF200F548A1 /* ChatMainView.swift */; }; + 9516ED532E709BA600F548A1 /* ChatListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED522E709BA300F548A1 /* ChatListCell.swift */; }; BAD8B768F782046D4AA1C073 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3DE048BEDCB92B48F401061 /* RxCocoa.framework */; }; BE81B1F3E60D37D75A058D2B /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CCDC15081A22BAED6318E3E /* SnapKit.framework */; }; C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012F45F908769FA7C3C0792F /* LoginView.swift */; }; @@ -77,7 +80,10 @@ 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMButton.swift; sourceTree = ""; }; 950A0D612E5C561400C07CF2 /* LMInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMInputField.swift; sourceTree = ""; }; 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = ""; }; - 9516ED432E7075DC00F548A1 /* ChatAnaylsisLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnaylsisLoadingView.swift; sourceTree = ""; }; + 9516ED492E7076FB00F548A1 /* ChatAnalysisView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisView.swift; sourceTree = ""; }; + 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisLoadingView.swift; sourceTree = ""; }; + 9516ED502E708CF200F548A1 /* ChatMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMainView.swift; sourceTree = ""; }; + 9516ED522E709BA300F548A1 /* ChatListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListCell.swift; sourceTree = ""; }; 9CCDC15081A22BAED6318E3E /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BACC7259FC0C14CB352A4E6B /* OptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionView.swift; sourceTree = ""; }; C28FE6392E1612667826E5C5 /* DiaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryView.swift; sourceTree = ""; }; @@ -199,8 +205,11 @@ 951F3F852E6DDE7F0022583B /* Chat */ = { isa = PBXGroup; children = ( + 9516ED502E708CF200F548A1 /* ChatMainView.swift */, + 9516ED522E709BA300F548A1 /* ChatListCell.swift */, + 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */, 3EBDD10D8389EBB23B42C54F /* ChatView.swift */, - 9516ED432E7075DC00F548A1 /* ChatAnaylsisLoadingView.swift */, + 9516ED492E7076FB00F548A1 /* ChatAnalysisView.swift */, ); path = Chat; sourceTree = ""; @@ -353,6 +362,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9516ED4A2E7076FF00F548A1 /* ChatAnalysisView.swift in Sources */, 950A0D602E5C3C7000C07CF2 /* LMButton.swift in Sources */, C94951E661D9DC9D28B56273 /* TuistAssets+CommonUI.swift in Sources */, 7DC59B80630854028C7C80F4 /* TuistBundle+CommonUI.swift in Sources */, @@ -363,16 +373,18 @@ 950A0D4F2E5AADB500C07CF2 /* SignUpView.swift in Sources */, F7673E4248628D67F3542848 /* ChatView.swift in Sources */, 950A0D562E5C29D000C07CF2 /* LMTextField.swift in Sources */, + 9516ED4D2E707ACF00F548A1 /* ChatAnalysisLoadingView.swift in Sources */, FB3FE0AB8AE6868B6D5E241C /* DiaryView.swift in Sources */, 70139D721530B3262C44ABC1 /* AnswerView.swift in Sources */, 416C58AEE5E4491991982CFF /* HomeProgressView.swift in Sources */, 7D319882A302F75CCE46A48C /* HomeQuizView.swift in Sources */, 537F80B2F39FD73F6F78F9B2 /* HomeView.swift in Sources */, + 9516ED532E709BA600F548A1 /* ChatListCell.swift in Sources */, 950A0D622E5C562700C07CF2 /* LMInputField.swift in Sources */, + 9516ED512E708CF700F548A1 /* ChatMainView.swift in Sources */, 65762CE867888754D56BA2CB /* OptionView.swift in Sources */, CEADBDD98AC9921C05AAC1DA /* QuizCollectionViewCell.swift in Sources */, 950A0D512E5AADC400C07CF2 /* SignInView.swift in Sources */, - 9516ED442E7075EA00F548A1 /* ChatAnaylsisLoadingView.swift in Sources */, 592FAEA836FD6B4B3F466D72 /* QuizCompleteAlertView.swift in Sources */, 65B3402D15A9F06B3E88CE19 /* QuizView.swift in Sources */, C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */, diff --git a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift index f299efb..560f6dc 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisLoadingView.swift @@ -2,7 +2,7 @@ // ChatAnalysisLoadingView.swift // CommonUI // -// Created by 박지윤 on 7/2/25. +// Created by 박지윤 on 9/10/25. // import UIKit @@ -10,7 +10,6 @@ import SnapKit import Then open class ChatAnalysisLoadingView: UIView { - private let loadingSpinner = UIActivityIndicatorView(style: .large).then { $0.color = CommonUIAssets.LMGray1 $0.startAnimating() @@ -59,4 +58,4 @@ open class ChatAnalysisLoadingView: UIView { $0.top.equalTo(mainLabel.snp.bottom).offset(8) } } -} \ No newline at end of file +} diff --git a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift new file mode 100644 index 0000000..a861686 --- /dev/null +++ b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift @@ -0,0 +1,275 @@ +// +// ChatAnalysisView.swift +// CommonUI +// +// Created by 박지윤 on 9/9/25. +// + +import UIKit +import SnapKit +import Then +import Domain + +open class ChatAnalysisView: UIView { + + private let scrollView = UIScrollView().then { + $0.showsVerticalScrollIndicator = true + $0.alwaysBounceVertical = true + } + + private let contentView = UIView() + + private let titleLabel = UILabel().then { + $0.text = "대화 분석" + $0.textColor = .black + $0.font = UIFont.systemFont(ofSize: 24, weight: .bold) + $0.textAlignment = .center + } + + private let analysisStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 16 + $0.alignment = .fill + } + + public override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + backgroundColor = CommonUIAssets.LMOrange4 + + [scrollView].forEach { addSubview($0) } + [contentView].forEach { scrollView.addSubview($0) } + [titleLabel, analysisStackView].forEach { contentView.addSubview($0) } + + scrollView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.width.equalToSuperview() + } + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.leading.trailing.equalToSuperview().inset(20) + } + + analysisStackView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(30) + $0.leading.trailing.equalToSuperview().inset(20) + $0.bottom.equalToSuperview().offset(-20) + } + } + + public func setupAnalysisData(_ messages: [ChatMessageVO]) { + // 기존 뷰들 제거 + analysisStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + // 샘플 분석 데이터 (실제로는 API에서 받아온 데이터 사용) + let analysisData = createSampleAnalysisData() + + for item in analysisData { + let analysisView = createAnalysisItemView(item) + analysisStackView.addArrangedSubview(analysisView) + } + } + + private func createSampleAnalysisData() -> [AnalysisItem] { + return [ + AnalysisItem( + message: "오늘 힘든 일 없었어?", + author: "HUMAN", + feedback: Feedback( + type: .positive, + icon: "✓", + text: "좋은 표현이에요! �� 상대방의 하루를 살펴보려는 따뜻한 표현이에요.", + suggestion: nil + ) + ), + AnalysisItem( + message: "오늘 배운 수업이 어려워서 조금 힘들었어", + author: "AI", + feedback: nil + ), + AnalysisItem( + message: "그런 일로 왜 그래?", + author: "HUMAN", + feedback: Feedback( + type: .negative, + icon: "!", + text: "고치면 더 좋은 표현이에요 이 말은 상대방의 감정을 가볍게 여기는 것 처럼 들릴 수 있어요.", + suggestion: "이렇게 말하는 건 어떨까요? \"그랬구나. 속상했겠다. 괜찮아?\" 공감하는 말로 시작하면 상대방이 더 편안해질 수 있어요." + ) + ), + AnalysisItem( + message: "내 맘대로 되지 않아서 그랬던 것 같아", + author: "AI", + feedback: nil + ) + ] + } + + private func createAnalysisItemView(_ item: AnalysisItem) -> UIView { + let containerView = UIView() + + // 메시지 뷰 + let messageView = createMessageBubble(item.message, author: item.author) + containerView.addSubview(messageView) + + messageView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + } + + // 피드백이 있는 경우 피드백 뷰 추가 + if let feedback = item.feedback { + let feedbackView = createFeedbackBubble(feedback) + containerView.addSubview(feedbackView) + + feedbackView.snp.makeConstraints { + $0.top.equalTo(messageView.snp.bottom).offset(8) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + } else { + messageView.snp.makeConstraints { + $0.bottom.equalToSuperview() + } + } + + return containerView + } + + private func createMessageBubble(_ text: String, author: String) -> UIView { + let containerView = UIView() + + let bubbleView = UIView().then { + $0.layer.cornerRadius = 16 + $0.backgroundColor = author == "HUMAN" ? CommonUIAssets.LMBlue2 : UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) + } + + let messageLabel = UILabel().then { + $0.text = text + $0.textColor = author == "HUMAN" ? .white : .black + $0.font = .systemFont(ofSize: 16, weight: .regular) + $0.numberOfLines = 0 + } + + containerView.addSubview(bubbleView) + bubbleView.addSubview(messageLabel) + + // 메시지 정렬 (사용자는 오른쪽, AI는 왼쪽) + if author == "HUMAN" { + bubbleView.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.trailing.equalToSuperview() + $0.width.lessThanOrEqualTo(250) + } + } else { + bubbleView.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.leading.equalToSuperview() + $0.width.lessThanOrEqualTo(250) + } + } + + messageLabel.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) + } + + return containerView + } + + private func createFeedbackBubble(_ feedback: Feedback) -> UIView { + let containerView = UIView() + + let bubbleView = UIView().then { + $0.layer.cornerRadius = 16 + $0.backgroundColor = feedback.type == .positive ? + UIColor(red: 0.8, green: 1.0, blue: 0.8, alpha: 1.0) : // 연두색 + UIColor(red: 1.0, green: 0.8, blue: 0.8, alpha: 1.0) // 연한 빨간색 + } + + let iconLabel = UILabel().then { + $0.text = feedback.icon + $0.textColor = feedback.type == .positive ? .green : .red + $0.font = .systemFont(ofSize: 16, weight: .bold) + } + + let feedbackLabel = UILabel().then { + $0.text = feedback.text + $0.textColor = .black + $0.font = .systemFont(ofSize: 14, weight: .regular) + $0.numberOfLines = 0 + } + + containerView.addSubview(bubbleView) + bubbleView.addSubview(iconLabel) + bubbleView.addSubview(feedbackLabel) + + // 피드백은 오른쪽 정렬 + bubbleView.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.trailing.equalToSuperview() + $0.width.lessThanOrEqualTo(280) + } + + iconLabel.snp.makeConstraints { + $0.leading.equalToSuperview().offset(12) + $0.top.equalToSuperview().offset(12) + } + + feedbackLabel.snp.makeConstraints { + $0.leading.equalTo(iconLabel.snp.trailing).offset(8) + $0.trailing.equalToSuperview().offset(-12) + $0.top.equalToSuperview().offset(12) + $0.bottom.equalToSuperview().offset(-12) + } + + // 제안이 있는 경우 추가 + if let suggestion = feedback.suggestion { + let suggestionLabel = UILabel().then { + $0.text = suggestion + $0.textColor = .black + $0.font = .systemFont(ofSize: 13, weight: .regular) + $0.numberOfLines = 0 + } + + bubbleView.addSubview(suggestionLabel) + + suggestionLabel.snp.makeConstraints { + $0.leading.equalTo(feedbackLabel.snp.leading) + $0.trailing.equalTo(feedbackLabel.snp.trailing) + $0.top.equalTo(feedbackLabel.snp.bottom).offset(8) + $0.bottom.equalToSuperview().offset(-12) + } + } + + return containerView + } +} + +struct AnalysisItem { + let message: String + let author: String + let feedback: Feedback? +} + +struct Feedback { + enum FeedbackType { + case positive + case negative + } + + let type: FeedbackType + let icon: String + let text: String + let suggestion: String? +} diff --git a/Projects/CommonUI/Sources/View/Chat/ChatListCell.swift b/Projects/CommonUI/Sources/View/Chat/ChatListCell.swift new file mode 100644 index 0000000..4252fe1 --- /dev/null +++ b/Projects/CommonUI/Sources/View/Chat/ChatListCell.swift @@ -0,0 +1,68 @@ +// +// ChatListCell.swift +// CommonUI +// +// Created by 박지윤 on 9/10/25. +// + +import UIKit + +class ChatListCell: UITableViewCell { + + private let profileIconView = UIImageView().then { + $0.image = CommonUIAssets.IconBubble + } + + private let labelStackView = UIStackView().then { + $0.axis = .vertical + $0.alignment = .leading + $0.spacing = 5 + } + + private let titleLabel = UILabel().then { + $0.textColor = CommonUIAssets.LMGray1 + $0.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + } + + private let dateLabel = UILabel().then { + $0.textColor = CommonUIAssets.LMGray4 + $0.font = UIFont.systemFont(ofSize: 13, weight: .medium) + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0)) + backgroundColor = CommonUIAssets.LMWhite + layer.cornerRadius = 12 + layer.borderWidth = 1 + layer.borderColor = CommonUIAssets.LMGray4?.cgColor + selectionStyle = .none + + [profileIconView, labelStackView].forEach { addSubview($0) } + [titleLabel, dateLabel].forEach { labelStackView.addArrangedSubview($0) } + + profileIconView.snp.makeConstraints { + $0.leading.equalToSuperview().offset(15) + $0.centerY.equalToSuperview() + $0.width.height.equalTo(25) + } + + labelStackView.snp.makeConstraints { + $0.leading.equalTo(profileIconView.snp.trailing).offset(12) + $0.centerY.equalToSuperview() + } + } + + func configure(title: String, date: String) { + titleLabel.text = title + dateLabel.text = date + } +} diff --git a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift new file mode 100644 index 0000000..c3a4e69 --- /dev/null +++ b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift @@ -0,0 +1,160 @@ +// +// ChatMainView.swift +// CommonUI +// +// Created by 박지윤 on 7/2/25. +// + +import UIKit +import SnapKit +import Then + +public class ChatMainView: UIView { + private let emptyStateContainer = UIView() + + private let emptyIconImageView = UIImageView().then { + $0.image = CommonUIAssets.IconMessage + $0.contentMode = .scaleAspectFit + } + + private let emptyMessageLabel = UILabel().then { + $0.text = "저장된 대화가 없어요\n새로운 대화를 시작해보세요" + $0.textColor = CommonUIAssets.LMGray3 + $0.font = UIFont.systemFont(ofSize: 16, weight: .regular) + $0.textAlignment = .center + $0.numberOfLines = 2 + } + + private let chatListContainer = UIView() + + private let chatListTableView = UITableView().then { + $0.separatorStyle = .none + $0.backgroundColor = .clear + $0.showsVerticalScrollIndicator = false + } + + private var newChatButton = LMButton(textColor: CommonUIAssets.LMBlack, + bgColor: CommonUIAssets.LMOrange1) + + private var isShowingEmptyState = true + + public override init(frame: CGRect) { + super.init(frame: frame) + initAttribute() + setupUI() + setupTableView() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func initAttribute() { + newChatButton = newChatButton.then { + $0.setTitle("새 대화 시작하기", for: .normal) + $0.setImage(CommonUIAssets.IconEdit? + .resize(to: CGSize(width: 20, height: 20)), for: .normal) + $0.semanticContentAttribute = .forceLeftToRight + $0.imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 6) + } + } + + private func setupUI() { + backgroundColor = .clear + + [emptyStateContainer, chatListContainer, newChatButton].forEach { addSubview($0) } + + [emptyIconImageView, emptyMessageLabel].forEach { emptyStateContainer.addSubview($0) } + [chatListTableView].forEach { chatListContainer.addSubview($0) } + + setupConstraints() + showChatList() + } + + private func setupConstraints() { + emptyStateContainer.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.centerY.equalToSuperview().offset(-50) + $0.leading.trailing.equalToSuperview().inset(40) + } + + emptyIconImageView.snp.makeConstraints { + $0.top.centerX.equalToSuperview() + $0.width.height.equalTo(60) + } + + emptyMessageLabel.snp.makeConstraints { + $0.top.equalTo(emptyIconImageView.snp.bottom).offset(16) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + + chatListContainer.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(newChatButton.snp.top).offset(-20) + } + + chatListTableView.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(20) + } + + newChatButton.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(20) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) + } + } + + private func setupTableView() { + chatListTableView.delegate = self + chatListTableView.dataSource = self + chatListTableView.register(ChatListCell.self, forCellReuseIdentifier: "ChatListCell") + } + + public func showEmptyState() { + isShowingEmptyState = true + emptyStateContainer.isHidden = false + chatListContainer.isHidden = true + } + + public func showChatList() { + isShowingEmptyState = false + emptyStateContainer.isHidden = true + chatListContainer.isHidden = false + chatListTableView.reloadData() + } + + public func setNewChatButtonAction(_ action: @escaping () -> Void) { + newChatButton.addTarget(self, action: #selector(newChatButtonTapped), for: .touchUpInside) + newChatButtonAction = action + } + + private var newChatButtonAction: (() -> Void)? + + @objc private func newChatButtonTapped() { + newChatButtonAction?() + } +} + +extension ChatMainView: UITableViewDataSource, UITableViewDelegate { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 2 + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "ChatListCell", for: indexPath) as! ChatListCell + + if indexPath.row == 0 { + cell.configure(title: "1", date: "2025-12-30") + } else { + cell.configure(title: "2", date: "2025-12-31") + } + + return cell + } + + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 80 + } +} From 7bbbe8955b5bbf3e105700c94c1fde8bacc33ddb Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sat, 13 Sep 2025 17:16:03 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[feat/chat]=20feat:=20Chat=20API=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Data/Data.xcodeproj/project.pbxproj | 8 ++ Projects/Data/Sources/DTO/ChatDTO.swift | 11 +- Projects/Data/Sources/DTO/ChatDetailDTO.swift | 46 ++++++++ Projects/Data/Sources/DTO/ChatRoomDTO.swift | 29 +++++ .../Sources/Repository/ChatRepository.swift | 111 +++++++++++------- .../Domain/Domain.xcodeproj/project.pbxproj | 8 ++ .../RepositoryProtocol/ChatRepository.swift | 2 + Projects/Domain/Sources/VO/ChatDetailVO.swift | 36 ++++++ .../Domain/Sources/VO/ChatRoomListVO.swift | 26 ++++ 9 files changed, 226 insertions(+), 51 deletions(-) create mode 100644 Projects/Data/Sources/DTO/ChatDetailDTO.swift create mode 100644 Projects/Data/Sources/DTO/ChatRoomDTO.swift create mode 100644 Projects/Domain/Sources/VO/ChatDetailVO.swift create mode 100644 Projects/Domain/Sources/VO/ChatRoomListVO.swift diff --git a/Projects/Data/Data.xcodeproj/project.pbxproj b/Projects/Data/Data.xcodeproj/project.pbxproj index e90f791..6524f3c 100644 --- a/Projects/Data/Data.xcodeproj/project.pbxproj +++ b/Projects/Data/Data.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 901ACA7B98089AB702ADA830 /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FAA1459D11CCE724C34195 /* Domain.framework */; }; 950A0D702E5CCF0200C07CF2 /* SignRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D6F2E5CCEFD00C07CF2 /* SignRepository.swift */; }; 950A0D902E6039D600C07CF2 /* DefaultDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */; }; + 9516ED5D2E71D3FE00F548A1 /* ChatRoomDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED5C2E71D3FB00F548A1 /* ChatRoomDTO.swift */; }; + 9516ED612E71D5B800F548A1 /* ChatDetailDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED602E71D5B200F548A1 /* ChatDetailDTO.swift */; }; 951F3F8F2E6F36450022583B /* ChatDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F3F8E2E6F36440022583B /* ChatDTO.swift */; }; 951F3F912E6F36680022583B /* ChatRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F3F902E6F36640022583B /* ChatRepository.swift */; }; A334985695DC9388841BBC43 /* QuizRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCA951B89F2C3D20AA31F7F /* QuizRepository.swift */; }; @@ -47,6 +49,8 @@ 77810122262C6CB16D4D47DA /* QuizDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizDTO.swift; sourceTree = ""; }; 950A0D6F2E5CCEFD00C07CF2 /* SignRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignRepository.swift; sourceTree = ""; }; 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDTO.swift; sourceTree = ""; }; + 9516ED5C2E71D3FB00F548A1 /* ChatRoomDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRoomDTO.swift; sourceTree = ""; }; + 9516ED602E71D5B200F548A1 /* ChatDetailDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDetailDTO.swift; sourceTree = ""; }; 951F3F8E2E6F36440022583B /* ChatDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDTO.swift; sourceTree = ""; }; 951F3F902E6F36640022583B /* ChatRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRepository.swift; sourceTree = ""; }; A3B0D3D8C7049B6856791C1D /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -82,6 +86,8 @@ isa = PBXGroup; children = ( 951F3F8E2E6F36440022583B /* ChatDTO.swift */, + 9516ED5C2E71D3FB00F548A1 /* ChatRoomDTO.swift */, + 9516ED602E71D5B200F548A1 /* ChatDetailDTO.swift */, 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */, AAAC2885ACFE998578DC25E8 /* CourseDTO.swift */, A44BC1E2E75FC256F832CA38 /* LoginDTO.swift */, @@ -218,7 +224,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9516ED612E71D5B800F548A1 /* ChatDetailDTO.swift in Sources */, 43E9C2380F425520C1FA1AD2 /* CourseDTO.swift in Sources */, + 9516ED5D2E71D3FE00F548A1 /* ChatRoomDTO.swift in Sources */, FF43B3A4D0DC88307E918DB0 /* LoginDTO.swift in Sources */, E9463A3FF42D5F0960245F80 /* QuizDTO.swift in Sources */, 684AAEA9796EED3F9FC592FC /* NetworkConfiguration.swift in Sources */, diff --git a/Projects/Data/Sources/DTO/ChatDTO.swift b/Projects/Data/Sources/DTO/ChatDTO.swift index bad3905..e46337c 100644 --- a/Projects/Data/Sources/DTO/ChatDTO.swift +++ b/Projects/Data/Sources/DTO/ChatDTO.swift @@ -41,16 +41,15 @@ public struct ChatMessageDataDTO: Decodable { public let content: String } -public struct ChatMessageRequestDTO: Encodable { - public let content: String -} - extension ChatMessageDataDTO { func toDomain() -> ChatMessageVO { return ChatMessageVO( chatId: chatId, author: author, - content: content - ) + content: content) } } + +public struct ChatMessageRequestDTO: Encodable { + public let content: String +} diff --git a/Projects/Data/Sources/DTO/ChatDetailDTO.swift b/Projects/Data/Sources/DTO/ChatDetailDTO.swift new file mode 100644 index 0000000..f580849 --- /dev/null +++ b/Projects/Data/Sources/DTO/ChatDetailDTO.swift @@ -0,0 +1,46 @@ +// +// ChatDetailDTO.swift +// Data +// +// Created by 박지윤 on 9/11/25. +// + +import Domain + +public struct ChatDetailResponseDTO: Decodable { + public let is_success: Bool + public let code: String + public let message: String + public let data: ChatDetailDataDTO +} + +public struct ChatDetailDataDTO: Decodable { + public let chatRoom: ChatRoomDataDTO + public let chatList: [ChatListDTO] +} + +public struct ChatListDTO: Decodable { + public let chatId: Int + public let author: Int + public let content: String + public let comment: String + public let createdAt: String +} + +extension ChatDetailDataDTO { + func toDomain() -> ChatDetailVO { + let chatRoomVO = ChatRoomVO(chatRoomId: chatRoom.chatRoomId, + title: chatRoom.title, + createdAt: chatRoom.createdAt) + + let chatListVO = chatList.map { chat in + ChatListVO(chatId: chat.chatId, + author: chat.author, + content: chat.content, + comment: chat.comment, + createdAt: chat.createdAt) + } + + return ChatDetailVO(chatRoom: chatRoomVO, chatList: chatListVO) + } +} diff --git a/Projects/Data/Sources/DTO/ChatRoomDTO.swift b/Projects/Data/Sources/DTO/ChatRoomDTO.swift new file mode 100644 index 0000000..3c1b1da --- /dev/null +++ b/Projects/Data/Sources/DTO/ChatRoomDTO.swift @@ -0,0 +1,29 @@ +// +// ChatRoomDTO.swift +// Data +// +// Created by 박지윤 on 9/11/25. +// + +import Domain + +public struct ChatRoomResponseDTO: Decodable { + public let is_success: Bool + public let code: String + public let message: String + public let data: [ChatRoomDataDTO] +} + +public struct ChatRoomDataDTO: Decodable { + public let chatRoomId: Int + public let title: String + public let createdAt: String +} + +extension ChatRoomDataDTO { + func toDomain() -> ChatRoomVO { + return ChatRoomVO(chatRoomId: chatRoomId, + title: title, + createdAt: createdAt) + } +} diff --git a/Projects/Data/Sources/Repository/ChatRepository.swift b/Projects/Data/Sources/Repository/ChatRepository.swift index 38ac1a8..5ba59d1 100644 --- a/Projects/Data/Sources/Repository/ChatRepository.swift +++ b/Projects/Data/Sources/Repository/ChatRepository.swift @@ -16,69 +16,90 @@ public class DefaultChatRepository: ChatRepository { self.tokenRepository = tokenRepository } + /// 텍스트 대화 시작하기 public func postChatStart() -> Single { + return request(method: .post, + endpoint: "/api/chats/text", + responseType: ChatDataDTO.self + ) + .map { dto in + return dto.toDomain() + } + } + + /// 텍스트 대화하기 + public func postChat(chatRoomId: Int, content: String) -> Single { + let parameter = ["content": content] + + return request(method: .post, + parameters: parameter, + endpoint: "/api/chats/text/\(chatRoomId)", + responseType: ChatMessageDataDTO.self + ) + .map { dto in + return dto.toDomain() + } + } + + /// 저장된 대화 내역 리스트 조회하기 + public func getChatList() -> Single<[ChatRoomVO]> { + return request(endpoint: "/api/chats/", + responseType: [ChatRoomDataDTO].self + ) + .map { dtoList in + dtoList.map { $0.toDomain() } + } + } + + /// 저장된 대화 내역 상세 조회하기 + public func getChatDetail(chatRoomId: Int) -> Single { + return request(endpoint: "/api/chats/\(chatRoomId)", + encoding: URLEncoding.default, + responseType: ChatDetailDataDTO.self + ) + .map { dto in + return dto.toDomain() + } + } + + private func request( + method: HTTPMethod = .get, + parameters: [String: Any]? = nil, + endpoint: String, + encoding: ParameterEncoding = JSONEncoding.default, + responseType: T.Type + ) -> Single { return Single.create { single in - let url = "\(NetworkConfiguration.baseUrl)/api/chats/text" + let url = "\(NetworkConfiguration.baseUrl)\(endpoint)" var headers: HTTPHeaders = [:] if let token = self.tokenRepository.getAccessToken() { + print("🔑 사용할 토큰: \(token)") headers.add(name: "Authorization", value: "Bearer \(token)") + } else { + print("❌ 토큰이 없습니다!") } + print("🌐 API 요청 URL: \(url)") + print("🔑 Authorization 헤더: \(headers)") - print("[텍스트 대화 시작 POST] URL: \(url)") - print("[텍스트 대화 시작 POST] 헤더: \(headers)") - let request = AF.request(url, - method: .post, - encoding: JSONEncoding.default, + method: .get, + parameters: parameters, + encoding: encoding, headers: headers) .validate() - .responseDecodable(of: ChatResponseDTO.self) { response in + .responseDecodable(of: responseType) { response in switch response.result { case .success(let value): - print("[텍스트 대화 시작 POST] 성공: \(value)") - single(.success(value.data.toDomain())) + print("✅ API 응답 성공: \(value)") + single(.success(value)) case .failure(let error): - print("[텍스트 대화 시작 POST] 실패: \(error)") + print("❌ API 응답 실패: \(error)") single(.failure(error)) } } + return Disposables.create { request.cancel() } } } - - public func postChat(chatRoomId: Int, content: String) -> Single { - return Single.create { single in - let url = "\(NetworkConfiguration.baseUrl)/api/chats/text/\(chatRoomId)" - var headers: HTTPHeaders = [:] - - if let token = self.tokenRepository.getAccessToken() { - headers.add(name: "Authorization", value: "Bearer \(token)") - } - - let requestBody = ChatMessageRequestDTO(content: content) - - print("[메시지 전송 POST] URL: \(url)") - print("[메시지 전송 POST] 헤더: \(headers)") - print("[메시지 전송 POST] 요청 내용: \(content)") - - let request = AF.request(url, - method: .post, - parameters: requestBody, - encoder: JSONParameterEncoder.default, - headers: headers) - .validate() - .responseDecodable(of: ChatMessageResponseDTO.self) { response in - switch response.result { - case .success(let value): - print("[메시지 전송 POST] 성공: \(value)") - single(.success(value.data.toDomain())) - case .failure(let error): - print("[메시지 전송 POST] 실패: \(error)") - single(.failure(error)) - } - } - return Disposables.create { request.cancel() } - } - } } diff --git a/Projects/Domain/Domain.xcodeproj/project.pbxproj b/Projects/Domain/Domain.xcodeproj/project.pbxproj index 30ba42e..52beb99 100644 --- a/Projects/Domain/Domain.xcodeproj/project.pbxproj +++ b/Projects/Domain/Domain.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 950A0D6B2E5CCBF000C07CF2 /* SignRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D6A2E5CCBE800C07CF2 /* SignRepository.swift */; }; 950A0D822E5DE10C00C07CF2 /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D812E5DE10700C07CF2 /* LoginUseCase.swift */; }; 950A0D922E603DA100C07CF2 /* DefaultVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D912E603D9C00C07CF2 /* DefaultVO.swift */; }; + 9516ED592E71CE1300F548A1 /* ChatDetailVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED582E71CE1000F548A1 /* ChatDetailVO.swift */; }; + 9516ED5B2E71CF0300F548A1 /* ChatRoomListVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED5A2E71CEFE00F548A1 /* ChatRoomListVO.swift */; }; 951F3F892E6DE1F80022583B /* ChatUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F3F882E6DE1F60022583B /* ChatUseCase.swift */; }; 951F3F8B2E6DE2140022583B /* ChatRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F3F8A2E6DE2100022583B /* ChatRepository.swift */; }; 951F3F8D2E6F360C0022583B /* ChatVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951F3F8C2E6F36090022583B /* ChatVO.swift */; }; @@ -58,6 +60,8 @@ 950A0D6A2E5CCBE800C07CF2 /* SignRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignRepository.swift; sourceTree = ""; }; 950A0D812E5DE10700C07CF2 /* LoginUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCase.swift; sourceTree = ""; }; 950A0D912E603D9C00C07CF2 /* DefaultVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVO.swift; sourceTree = ""; }; + 9516ED582E71CE1000F548A1 /* ChatDetailVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDetailVO.swift; sourceTree = ""; }; + 9516ED5A2E71CEFE00F548A1 /* ChatRoomListVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRoomListVO.swift; sourceTree = ""; }; 951F3F882E6DE1F60022583B /* ChatUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUseCase.swift; sourceTree = ""; }; 951F3F8A2E6DE2100022583B /* ChatRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRepository.swift; sourceTree = ""; }; 951F3F8C2E6F36090022583B /* ChatVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatVO.swift; sourceTree = ""; }; @@ -163,6 +167,8 @@ isa = PBXGroup; children = ( 951F3F8C2E6F36090022583B /* ChatVO.swift */, + 9516ED5A2E71CEFE00F548A1 /* ChatRoomListVO.swift */, + 9516ED582E71CE1000F548A1 /* ChatDetailVO.swift */, 950A0D912E603D9C00C07CF2 /* DefaultVO.swift */, 747DBBCAB797E5E0A82177F2 /* CourseVO.swift */, FD08A7186FB9676854B7AEAC /* LoginVO.swift */, @@ -260,7 +266,9 @@ 950A0D6B2E5CCBF000C07CF2 /* SignRepository.swift in Sources */, 69DAD609572D32F2BA3845AE /* String+Extension.swift in Sources */, 950A0D822E5DE10C00C07CF2 /* LoginUseCase.swift in Sources */, + 9516ED592E71CE1300F548A1 /* ChatDetailVO.swift in Sources */, 92BD46EE48F6C63B6E43D069 /* UIView+Extension.swift in Sources */, + 9516ED5B2E71CF0300F548A1 /* ChatRoomListVO.swift in Sources */, D510BC17C4583615CB60439E /* CourseRepository.swift in Sources */, D7012CC494E56CD8CE58176F /* LoginRepository.swift in Sources */, 1782D2A1A7FB2BD6FFA1DFEA /* QuizRepository.swift in Sources */, diff --git a/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift index d67b153..b030623 100644 --- a/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift +++ b/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift @@ -10,4 +10,6 @@ import RxSwift public protocol ChatRepository { func postChatStart() -> Single func postChat(chatRoomId: Int, content: String) -> Single + func getChatList() -> Single<[ChatRoomVO]> + func getChatDetail(chatRoomId: Int) -> Single } diff --git a/Projects/Domain/Sources/VO/ChatDetailVO.swift b/Projects/Domain/Sources/VO/ChatDetailVO.swift new file mode 100644 index 0000000..b717aab --- /dev/null +++ b/Projects/Domain/Sources/VO/ChatDetailVO.swift @@ -0,0 +1,36 @@ +// +// ChatDetailVO.swift +// Domain +// +// Created by 박지윤 on 9/11/25. +// + +public struct ChatDetailVO { + public let chatRoom: ChatRoomVO + public let chatList: [ChatListVO] + + public init(chatRoom: ChatRoomVO, chatList: [ChatListVO]) { + self.chatRoom = chatRoom + self.chatList = chatList + } +} + +public struct ChatListVO { + public let chatId: Int + public let author: Int + public let content: String + public let comment: String + public let createdAt: String + + public init(chatId: Int, + author: Int, + content: String, + comment: String, + createdAt: String) { + self.chatId = chatId + self.author = author + self.content = content + self.comment = comment + self.createdAt = createdAt + } +} diff --git a/Projects/Domain/Sources/VO/ChatRoomListVO.swift b/Projects/Domain/Sources/VO/ChatRoomListVO.swift new file mode 100644 index 0000000..06d205c --- /dev/null +++ b/Projects/Domain/Sources/VO/ChatRoomListVO.swift @@ -0,0 +1,26 @@ +// +// ChatRoomListVO.swift +// Domain +// +// Created by 박지윤 on 9/11/25. +// + +public struct ChatRoomListVO { + public let chatRoomList: [ChatRoomVO] + + public init(chatRoomList: [ChatRoomVO]) { + self.chatRoomList = chatRoomList + } +} + +public struct ChatRoomVO { + public let chatRoomId: Int + public let title: String + public let createdAt: String + + public init(chatRoomId: Int, title: String, createdAt: String) { + self.chatRoomId = chatRoomId + self.title = title + self.createdAt = createdAt + } +} From 09ba6e9593658858fb1464528109defa0f567024 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sat, 13 Sep 2025 17:25:45 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[feat/chat]=20feat:=20Chat=20API=20Reposi?= =?UTF-8?q?tory=20&=20UseCase=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Data/Data.xcodeproj/project.pbxproj | 14 +++++++++--- .../Data/Sources/DTO/{ => Chat}/ChatDTO.swift | 0 .../DTO/{ => Chat}/ChatDetailDTO.swift | 0 .../Sources/DTO/{ => Chat}/ChatRoomDTO.swift | 0 .../Sources/Repository/ChatRepository.swift | 22 +++++++++++++++++++ .../RepositoryProtocol/ChatRepository.swift | 2 ++ .../Domain/Sources/UseCase/ChatUseCase.swift | 20 +++++++++++++++++ 7 files changed, 55 insertions(+), 3 deletions(-) rename Projects/Data/Sources/DTO/{ => Chat}/ChatDTO.swift (100%) rename Projects/Data/Sources/DTO/{ => Chat}/ChatDetailDTO.swift (100%) rename Projects/Data/Sources/DTO/{ => Chat}/ChatRoomDTO.swift (100%) diff --git a/Projects/Data/Data.xcodeproj/project.pbxproj b/Projects/Data/Data.xcodeproj/project.pbxproj index 6524f3c..048ab16 100644 --- a/Projects/Data/Data.xcodeproj/project.pbxproj +++ b/Projects/Data/Data.xcodeproj/project.pbxproj @@ -85,9 +85,7 @@ 647255CD65221C9CD4A43DED /* DTO */ = { isa = PBXGroup; children = ( - 951F3F8E2E6F36440022583B /* ChatDTO.swift */, - 9516ED5C2E71D3FB00F548A1 /* ChatRoomDTO.swift */, - 9516ED602E71D5B200F548A1 /* ChatDetailDTO.swift */, + 9516ED642E75609400F548A1 /* Chat */, 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */, AAAC2885ACFE998578DC25E8 /* CourseDTO.swift */, A44BC1E2E75FC256F832CA38 /* LoginDTO.swift */, @@ -117,6 +115,16 @@ ); sourceTree = ""; }; + 9516ED642E75609400F548A1 /* Chat */ = { + isa = PBXGroup; + children = ( + 951F3F8E2E6F36440022583B /* ChatDTO.swift */, + 9516ED5C2E71D3FB00F548A1 /* ChatRoomDTO.swift */, + 9516ED602E71D5B200F548A1 /* ChatDetailDTO.swift */, + ); + path = Chat; + sourceTree = ""; + }; A46DA33BCE1E2288153B2AC3 /* Network */ = { isa = PBXGroup; children = ( diff --git a/Projects/Data/Sources/DTO/ChatDTO.swift b/Projects/Data/Sources/DTO/Chat/ChatDTO.swift similarity index 100% rename from Projects/Data/Sources/DTO/ChatDTO.swift rename to Projects/Data/Sources/DTO/Chat/ChatDTO.swift diff --git a/Projects/Data/Sources/DTO/ChatDetailDTO.swift b/Projects/Data/Sources/DTO/Chat/ChatDetailDTO.swift similarity index 100% rename from Projects/Data/Sources/DTO/ChatDetailDTO.swift rename to Projects/Data/Sources/DTO/Chat/ChatDetailDTO.swift diff --git a/Projects/Data/Sources/DTO/ChatRoomDTO.swift b/Projects/Data/Sources/DTO/Chat/ChatRoomDTO.swift similarity index 100% rename from Projects/Data/Sources/DTO/ChatRoomDTO.swift rename to Projects/Data/Sources/DTO/Chat/ChatRoomDTO.swift diff --git a/Projects/Data/Sources/Repository/ChatRepository.swift b/Projects/Data/Sources/Repository/ChatRepository.swift index 5ba59d1..ba3d604 100644 --- a/Projects/Data/Sources/Repository/ChatRepository.swift +++ b/Projects/Data/Sources/Repository/ChatRepository.swift @@ -41,6 +41,28 @@ public class DefaultChatRepository: ChatRepository { } } + /// 대화방 삭제하기 + public func deleteChat(chatRoomId: Int) -> Single { + return request(method: .delete, + endpoint: "/api/chats/\(chatRoomId)", + responseType: DefaultDTO.self + ) + .map { dto in + return dto.getMessage() + } + } + + /// 대화 분석하기 + public func postChatAnalysis(chatRoomId: Int) -> Single { + return request(method: .post, + endpoint: "/api/chats/text/\(chatRoomId)/analysis", + responseType: ChatDetailDataDTO.self + ) + .map { dto in + return dto.toDomain() + } + } + /// 저장된 대화 내역 리스트 조회하기 public func getChatList() -> Single<[ChatRoomVO]> { return request(endpoint: "/api/chats/", diff --git a/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift index b030623..085b03d 100644 --- a/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift +++ b/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift @@ -10,6 +10,8 @@ import RxSwift public protocol ChatRepository { func postChatStart() -> Single func postChat(chatRoomId: Int, content: String) -> Single + func deleteChat(chatRoomId: Int) -> Single + func postChatAnalysis(chatRoomId: Int) -> Single func getChatList() -> Single<[ChatRoomVO]> func getChatDetail(chatRoomId: Int) -> Single } diff --git a/Projects/Domain/Sources/UseCase/ChatUseCase.swift b/Projects/Domain/Sources/UseCase/ChatUseCase.swift index ceb1f46..7ec85ce 100644 --- a/Projects/Domain/Sources/UseCase/ChatUseCase.swift +++ b/Projects/Domain/Sources/UseCase/ChatUseCase.swift @@ -10,6 +10,10 @@ import RxSwift public protocol ChatUseCase { func postChatStart() -> Single func postChat(chatRoomId: Int, content: String) -> Single + func deleteChat(chatRoomId: Int) -> Single + func postChatAnalysis(chatRoomId: Int) -> Single + func getChatList() -> Single<[ChatRoomVO]> + func getChatDetail(chatRoomId: Int) -> Single } public final class DefaultChatUseCase: ChatUseCase { @@ -26,4 +30,20 @@ public final class DefaultChatUseCase: ChatUseCase { public func postChat(chatRoomId: Int, content: String) -> Single { return repository.postChat(chatRoomId: chatRoomId, content: content) } + + public func deleteChat(chatRoomId: Int) -> Single { + return repository.deleteChat(chatRoomId: chatRoomId) + } + + public func postChatAnalysis(chatRoomId: Int) -> Single { + return repository.postChatAnalysis(chatRoomId: chatRoomId) + } + + public func getChatList() -> Single<[ChatRoomVO]> { + return repository.getChatList() + } + + public func getChatDetail(chatRoomId: Int) -> Single { + return repository.getChatDetail(chatRoomId: chatRoomId) + } } From 309fc3fd64dbf21809e236d4ae80c701cc3e945e Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sat, 13 Sep 2025 18:30:08 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[feat/chat]=20fix:=20Chat=20API=20respons?= =?UTF-8?q?e=20body=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/ChatMainViewController.swift | 7 +++++ .../Sources/ViewModel/ChatViewModel.swift | 11 ++++++++ .../Sources/View/Chat/ChatMainView.swift | 26 +++++++++++------ .../Data/Sources/DTO/Chat/ChatRoomDTO.swift | 12 +++++++- .../Sources/Repository/ChatRepository.swift | 28 +++++++++---------- .../RepositoryProtocol/ChatRepository.swift | 2 +- .../Domain/Sources/UseCase/ChatUseCase.swift | 4 +-- 7 files changed, 64 insertions(+), 26 deletions(-) diff --git a/Projects/Chat/Sources/View/ChatMainViewController.swift b/Projects/Chat/Sources/View/ChatMainViewController.swift index b5a3336..fba50ef 100644 --- a/Projects/Chat/Sources/View/ChatMainViewController.swift +++ b/Projects/Chat/Sources/View/ChatMainViewController.swift @@ -33,6 +33,7 @@ public class ChatMainViewController: BaseViewController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: false) + viewModel.getChatList() } public override func viewDidLoad() { @@ -69,6 +70,12 @@ public class ChatMainViewController: BaseViewController { } private func bindData() { + viewModel.chatListSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] chatRoomList in + self?.chatMainView.updateChatList(chatRoomList) + }) + .disposed(by: disposeBag) } private func bindActions() { diff --git a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift index c9000c7..ec67aed 100644 --- a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift +++ b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift @@ -18,6 +18,7 @@ public class ChatViewModel: ChatViewModelProtocol { private let chatUseCase: ChatUseCase private let tokenUseCase: TokenUseCase + let chatListSubject = PublishSubject() let chatSubject = PublishSubject() let messageSubject = PublishSubject() private var currentChatRoomId: Int = 0 @@ -28,6 +29,16 @@ public class ChatViewModel: ChatViewModelProtocol { self.tokenUseCase = tokenUseCase } + func getChatList() { + chatUseCase.getChatList() + .subscribe(onSuccess: { [weak self] chat in + print("✅ 저장된 대화 불러오기 성공: \(chat)") + self?.chatListSubject.onNext(chat) + }, onFailure: { error in + print("❌ 저장된 대화 불러오기 실패: \(error)") + }).disposed(by: disposeBag) + } + func startTextChat() { chatUseCase.postChatStart() .subscribe(onSuccess: { [weak self] chat in diff --git a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift index c3a4e69..36fd422 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit import Then +import Domain public class ChatMainView: UIView { private let emptyStateContainer = UIView() @@ -37,6 +38,7 @@ public class ChatMainView: UIView { bgColor: CommonUIAssets.LMOrange1) private var isShowingEmptyState = true + private var chatRoomList: [ChatRoomVO] = [] public override init(frame: CGRect) { super.init(frame: frame) @@ -68,7 +70,7 @@ public class ChatMainView: UIView { [chatListTableView].forEach { chatListContainer.addSubview($0) } setupConstraints() - showChatList() + showEmptyState() } private func setupConstraints() { @@ -79,7 +81,8 @@ public class ChatMainView: UIView { } emptyIconImageView.snp.makeConstraints { - $0.top.centerX.equalToSuperview() + $0.top.equalToSuperview().offset(-20) + $0.centerX.equalToSuperview() $0.width.height.equalTo(60) } @@ -125,6 +128,16 @@ public class ChatMainView: UIView { chatListTableView.reloadData() } + public func updateChatList(_ chatRoomListVO: ChatRoomListVO) { + self.chatRoomList = chatRoomListVO.chatRoomList + + if chatRoomListVO.chatRoomList.isEmpty { + showEmptyState() + } else { + showChatList() + } + } + public func setNewChatButtonAction(_ action: @escaping () -> Void) { newChatButton.addTarget(self, action: #selector(newChatButtonTapped), for: .touchUpInside) newChatButtonAction = action @@ -139,17 +152,14 @@ public class ChatMainView: UIView { extension ChatMainView: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 2 + return chatRoomList.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ChatListCell", for: indexPath) as! ChatListCell - if indexPath.row == 0 { - cell.configure(title: "1", date: "2025-12-30") - } else { - cell.configure(title: "2", date: "2025-12-31") - } + let chatRoom = chatRoomList[indexPath.row] + cell.configure(title: chatRoom.title, date: chatRoom.createdAt) return cell } diff --git a/Projects/Data/Sources/DTO/Chat/ChatRoomDTO.swift b/Projects/Data/Sources/DTO/Chat/ChatRoomDTO.swift index 3c1b1da..0843d78 100644 --- a/Projects/Data/Sources/DTO/Chat/ChatRoomDTO.swift +++ b/Projects/Data/Sources/DTO/Chat/ChatRoomDTO.swift @@ -11,7 +11,11 @@ public struct ChatRoomResponseDTO: Decodable { public let is_success: Bool public let code: String public let message: String - public let data: [ChatRoomDataDTO] + public let data: ChatRoomListDTO +} + +public struct ChatRoomListDTO: Decodable { + public let chatRoomList: [ChatRoomDataDTO] } public struct ChatRoomDataDTO: Decodable { @@ -27,3 +31,9 @@ extension ChatRoomDataDTO { createdAt: createdAt) } } + +extension ChatRoomListDTO { + func toDomain() -> ChatRoomListVO { + return ChatRoomListVO(chatRoomList: chatRoomList.map { $0.toDomain() }) + } +} diff --git a/Projects/Data/Sources/Repository/ChatRepository.swift b/Projects/Data/Sources/Repository/ChatRepository.swift index ba3d604..66173a3 100644 --- a/Projects/Data/Sources/Repository/ChatRepository.swift +++ b/Projects/Data/Sources/Repository/ChatRepository.swift @@ -20,10 +20,10 @@ public class DefaultChatRepository: ChatRepository { public func postChatStart() -> Single { return request(method: .post, endpoint: "/api/chats/text", - responseType: ChatDataDTO.self + responseType: ChatResponseDTO.self ) .map { dto in - return dto.toDomain() + return dto.data.toDomain() } } @@ -34,10 +34,10 @@ public class DefaultChatRepository: ChatRepository { return request(method: .post, parameters: parameter, endpoint: "/api/chats/text/\(chatRoomId)", - responseType: ChatMessageDataDTO.self + responseType: ChatMessageResponseDTO.self ) .map { dto in - return dto.toDomain() + return dto.data.toDomain() } } @@ -56,20 +56,20 @@ public class DefaultChatRepository: ChatRepository { public func postChatAnalysis(chatRoomId: Int) -> Single { return request(method: .post, endpoint: "/api/chats/text/\(chatRoomId)/analysis", - responseType: ChatDetailDataDTO.self + responseType: ChatDetailResponseDTO.self ) .map { dto in - return dto.toDomain() + return dto.data.toDomain() } } /// 저장된 대화 내역 리스트 조회하기 - public func getChatList() -> Single<[ChatRoomVO]> { - return request(endpoint: "/api/chats/", - responseType: [ChatRoomDataDTO].self + public func getChatList() -> Single { + return request(endpoint: "/api/chats", + responseType: ChatRoomResponseDTO.self ) - .map { dtoList in - dtoList.map { $0.toDomain() } + .map { dto in + return dto.data.toDomain() } } @@ -77,10 +77,10 @@ public class DefaultChatRepository: ChatRepository { public func getChatDetail(chatRoomId: Int) -> Single { return request(endpoint: "/api/chats/\(chatRoomId)", encoding: URLEncoding.default, - responseType: ChatDetailDataDTO.self + responseType: ChatDetailResponseDTO.self ) .map { dto in - return dto.toDomain() + return dto.data.toDomain() } } @@ -105,7 +105,7 @@ public class DefaultChatRepository: ChatRepository { print("🔑 Authorization 헤더: \(headers)") let request = AF.request(url, - method: .get, + method: method, parameters: parameters, encoding: encoding, headers: headers) diff --git a/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift index 085b03d..31524ac 100644 --- a/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift +++ b/Projects/Domain/Sources/RepositoryProtocol/ChatRepository.swift @@ -12,6 +12,6 @@ public protocol ChatRepository { func postChat(chatRoomId: Int, content: String) -> Single func deleteChat(chatRoomId: Int) -> Single func postChatAnalysis(chatRoomId: Int) -> Single - func getChatList() -> Single<[ChatRoomVO]> + func getChatList() -> Single func getChatDetail(chatRoomId: Int) -> Single } diff --git a/Projects/Domain/Sources/UseCase/ChatUseCase.swift b/Projects/Domain/Sources/UseCase/ChatUseCase.swift index 7ec85ce..5cd0ca2 100644 --- a/Projects/Domain/Sources/UseCase/ChatUseCase.swift +++ b/Projects/Domain/Sources/UseCase/ChatUseCase.swift @@ -12,7 +12,7 @@ public protocol ChatUseCase { func postChat(chatRoomId: Int, content: String) -> Single func deleteChat(chatRoomId: Int) -> Single func postChatAnalysis(chatRoomId: Int) -> Single - func getChatList() -> Single<[ChatRoomVO]> + func getChatList() -> Single func getChatDetail(chatRoomId: Int) -> Single } @@ -39,7 +39,7 @@ public final class DefaultChatUseCase: ChatUseCase { return repository.postChatAnalysis(chatRoomId: chatRoomId) } - public func getChatList() -> Single<[ChatRoomVO]> { + public func getChatList() -> Single { return repository.getChatList() } From 93cb385c47c0fc1cd761ebe4667860e87f4c6ddf Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sat, 13 Sep 2025 18:47:18 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[feat/chat]=20feat:=20=EC=83=88=20?= =?UTF-8?q?=EB=8C=80=ED=99=94=20=EC=8B=9C=EC=9E=91=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=EC=8B=9C=20=ED=99=94=EB=A9=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/ChatMainViewController.swift | 16 +++++++++++-- .../Sources/View/ChatViewController.swift | 20 ++++++++++++---- .../Sources/View/Chat/ChatMainView.swift | 24 ++++++++++--------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/Projects/Chat/Sources/View/ChatMainViewController.swift b/Projects/Chat/Sources/View/ChatMainViewController.swift index fba50ef..564652a 100644 --- a/Projects/Chat/Sources/View/ChatMainViewController.swift +++ b/Projects/Chat/Sources/View/ChatMainViewController.swift @@ -12,6 +12,7 @@ import Domain public class ChatMainViewController: BaseViewController { let viewModel: ChatViewModel + public var onPresentNewChat: (() -> Void)? let chatLabel = UILabel().then { $0.text = "대화" @@ -42,7 +43,7 @@ public class ChatMainViewController: BaseViewController { setupHierarchy() setupLayout() bindData() - bindActions() + bindEvents() } public override func setupViewProperty() { @@ -78,6 +79,17 @@ public class ChatMainViewController: BaseViewController { .disposed(by: disposeBag) } - private func bindActions() { + private func bindEvents() { + chatMainView.newChatButtonTapped + .bind { [weak self] in + self?.presentNewChatView() + } + .disposed(by: disposeBag) + } + + private func presentNewChatView() { + let chatViewController = ChatViewController(chatViewModel: viewModel) + chatViewController.hidesBottomBarWhenPushed = true + navigationController?.pushViewController(chatViewController, animated: true) } } diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 479f1e8..35c23a3 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -14,6 +14,11 @@ public class ChatViewController: BaseViewController { let viewModel: ChatViewModel let chatView = ChatView() + let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, + rightImage: nil, + title: nil, + isRightButtonHidden: true) + private var messages: [ChatMessageVO] = [] private let loadingView = ChatAnalysisLoadingView() @@ -48,23 +53,29 @@ public class ChatViewController: BaseViewController { } public override func setupHierarchy() { - view.addSubview(chatView) - view.addSubview(loadingView) + [navigationBar, chatView, loadingView] + .forEach { view.addSubview($0) } } public override func setupDelegate() { } public override func setupLayout() { + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.width.centerX.equalToSuperview() + } + chatView.snp.makeConstraints { - $0.edges.equalToSuperview() + $0.top.equalTo(navigationBar.snp.bottom) + $0.horizontalEdges.bottom.equalToSuperview() } loadingView.snp.makeConstraints { $0.edges.equalToSuperview() } - // 초기에는 로딩 화면 숨김 + navigationBar.isHidden = false loadingView.isHidden = true } @@ -134,6 +145,7 @@ public class ChatViewController: BaseViewController { // 로딩 화면 표시 loadingView.isHidden = false loadingView.alpha = 0 + navigationBar.isHidden = true UIView.animate(withDuration: 0.3) { self.loadingView.alpha = 1 diff --git a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift index 36fd422..783f7b3 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift @@ -9,8 +9,14 @@ import UIKit import SnapKit import Then import Domain +import RxRelay +import RxSwift public class ChatMainView: UIView { + + public let newChatButtonTapped = PublishRelay() + let disposeBag = DisposeBag() + private let emptyStateContainer = UIView() private let emptyIconImageView = UIImageView().then { @@ -45,6 +51,7 @@ public class ChatMainView: UIView { initAttribute() setupUI() setupTableView() + bindEvents() } required public init?(coder: NSCoder) { @@ -109,6 +116,12 @@ public class ChatMainView: UIView { } } + private func bindEvents() { + newChatButton.rx.tap + .bind(to: newChatButtonTapped) + .disposed(by: disposeBag) + } + private func setupTableView() { chatListTableView.delegate = self chatListTableView.dataSource = self @@ -137,17 +150,6 @@ public class ChatMainView: UIView { showChatList() } } - - public func setNewChatButtonAction(_ action: @escaping () -> Void) { - newChatButton.addTarget(self, action: #selector(newChatButtonTapped), for: .touchUpInside) - newChatButtonAction = action - } - - private var newChatButtonAction: (() -> Void)? - - @objc private func newChatButtonTapped() { - newChatButtonAction?() - } } extension ChatMainView: UITableViewDataSource, UITableViewDelegate { From 1e90fff59a4de3737a82863ba35258d0261d1669 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sat, 13 Sep 2025 20:28:50 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[feat/chat]=20feat:=20Chat=20=EB=8C=80?= =?UTF-8?q?=ED=99=94=20=EB=B6=84=EC=84=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/ChatAnalysisController.swift | 22 +++- .../Sources/View/ChatViewController.swift | 29 +++-- .../Sources/ViewModel/ChatViewModel.swift | 11 ++ .../Sources/View/Chat/ChatAnalysisView.swift | 117 ++++-------------- .../CommonUI/Sources/View/Chat/ChatView.swift | 10 +- .../Data/Sources/DTO/Chat/ChatDetailDTO.swift | 2 +- .../Sources/Repository/ChatRepository.swift | 3 +- Projects/Domain/Sources/VO/ChatDetailVO.swift | 4 +- 8 files changed, 80 insertions(+), 118 deletions(-) diff --git a/Projects/Chat/Sources/View/ChatAnalysisController.swift b/Projects/Chat/Sources/View/ChatAnalysisController.swift index d015257..b8b2e4e 100644 --- a/Projects/Chat/Sources/View/ChatAnalysisController.swift +++ b/Projects/Chat/Sources/View/ChatAnalysisController.swift @@ -12,17 +12,23 @@ import Domain public class ChatAnalysisController: BaseViewController { private let chatAnalysisView = ChatAnalysisView() - private var messages: [ChatMessageVO] = [] + + let navigationBar = DefaultNavigationBar(leftImage: nil, + rightImage: nil, + title: "대화 분석", + isRightButtonHidden: true) + + private var messages: [ChatListVO] = [] - public init(messages: [ChatMessageVO]) { + public init(messages: [ChatListVO]) { self.messages = messages super.init() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + public override func viewDidLoad() { super.viewDidLoad() chatAnalysisView.setupAnalysisData(messages) @@ -37,8 +43,14 @@ public class ChatAnalysisController: BaseViewController { } public override func setupLayout() { + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.width.centerX.equalToSuperview() + } + chatAnalysisView.snp.makeConstraints { - $0.edges.equalTo(view.safeAreaLayoutGuide) + $0.top.equalTo(navigationBar.snp.bottom) + $0.horizontalEdges.bottom.equalToSuperview() } } } diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 35c23a3..d1fd295 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -95,6 +95,13 @@ public class ChatViewController: BaseViewController { self?.addMessageToUI(message) }) .disposed(by: disposeBag) + + viewModel.analysisResultSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] analysisResult in + self?.presentAnalysisController(with: analysisResult) + }) + .disposed(by: disposeBag) } private func bindActions() { @@ -104,6 +111,7 @@ public class ChatViewController: BaseViewController { chatView.onEndButtonTapped = { [weak self] in self?.showAnalysisLoading() + self?.viewModel.postChatAnalysis() } } @@ -121,14 +129,14 @@ public class ChatViewController: BaseViewController { if messages.isEmpty { chatView.hideRecommendSection() } - + // 사용자 메시지 UI에 추가 let userMessage = ChatMessageVO(chatId: 0, author: "HUMAN", content: text) addMessageToUI(userMessage) // API 호출 viewModel.sendMessage(content: text) - + // 텍스트 필드 초기화 및 버튼 비활성화 chatView.chatTextField.text = "" chatView.sendButton.isEnabled = false @@ -155,24 +163,21 @@ public class ChatViewController: BaseViewController { self.hideAnalysisLoading() } } - + private func hideAnalysisLoading() { - print("✅ 대화 분석 완료") - UIView.animate(withDuration: 0.3, animations: { self.loadingView.alpha = 0 }) { _ in self.loadingView.isHidden = true - // 여기서 분석 결과 화면으로 이동하거나 다른 액션 수행 - self.navigateToAnalysisResult() } } private func navigateToAnalysisResult() { print("📊 분석 결과 화면으로 이동") - let analysisController = ChatAnalysisController(messages: messages) - analysisController.modalPresentationStyle = .fullScreen + let emptyChatList: [ChatListVO] = [] + let analysisController = ChatAnalysisController(messages: emptyChatList) + analysisController.modalPresentationStyle = UIModalPresentationStyle.fullScreen present(analysisController, animated: true) } @@ -189,4 +194,10 @@ public class ChatViewController: BaseViewController { print("✅ 추천 주제 업데이트 완료: \(topics.count)개") } + + private func presentAnalysisController(with analysisResult: ChatDetailVO) { + let analysisController = ChatAnalysisController(messages: analysisResult.chatList) + analysisController.hidesBottomBarWhenPushed = true + navigationController?.pushViewController(analysisController, animated: true) + } } diff --git a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift index ec67aed..863e19c 100644 --- a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift +++ b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift @@ -21,6 +21,7 @@ public class ChatViewModel: ChatViewModelProtocol { let chatListSubject = PublishSubject() let chatSubject = PublishSubject() let messageSubject = PublishSubject() + let analysisResultSubject = PublishSubject() private var currentChatRoomId: Int = 0 public init(chatUseCase: ChatUseCase, @@ -64,4 +65,14 @@ public class ChatViewModel: ChatViewModelProtocol { print("❌ 메시지 전송 실패: \(error)") }).disposed(by: disposeBag) } + + func postChatAnalysis() { + chatUseCase.postChatAnalysis(chatRoomId: currentChatRoomId) + .subscribe(onSuccess: { [weak self] analysisResult in + print("✅ 대화 분석 성공: \(analysisResult)") + self?.analysisResultSubject.onNext(analysisResult) + }, onFailure: { error in + print("❌ 대화 분석 실패: \(error)") + }).disposed(by: disposeBag) + } } diff --git a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift index a861686..cc9d306 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatAnalysisView.swift @@ -19,13 +19,6 @@ open class ChatAnalysisView: UIView { private let contentView = UIView() - private let titleLabel = UILabel().then { - $0.text = "대화 분석" - $0.textColor = .black - $0.font = UIFont.systemFont(ofSize: 24, weight: .bold) - $0.textAlignment = .center - } - private let analysisStackView = UIStackView().then { $0.axis = .vertical $0.spacing = 16 @@ -44,10 +37,10 @@ open class ChatAnalysisView: UIView { private func setupUI() { backgroundColor = CommonUIAssets.LMOrange4 - [scrollView].forEach { addSubview($0) } - [contentView].forEach { scrollView.addSubview($0) } - [titleLabel, analysisStackView].forEach { contentView.addSubview($0) } - + self.addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(analysisStackView) + scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -57,79 +50,40 @@ open class ChatAnalysisView: UIView { $0.width.equalToSuperview() } - titleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(20) - $0.leading.trailing.equalToSuperview().inset(20) - } - analysisStackView.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(30) + $0.top.equalToSuperview().offset(20) $0.leading.trailing.equalToSuperview().inset(20) $0.bottom.equalToSuperview().offset(-20) } } - - public func setupAnalysisData(_ messages: [ChatMessageVO]) { - // 기존 뷰들 제거 + + public func setupAnalysisData(_ messages: [ChatListVO]) { analysisStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - // 샘플 분석 데이터 (실제로는 API에서 받아온 데이터 사용) - let analysisData = createSampleAnalysisData() - - for item in analysisData { - let analysisView = createAnalysisItemView(item) + + for message in messages { + let analysisView = createAnalysisItemView(from: message) analysisStackView.addArrangedSubview(analysisView) } } - - private func createSampleAnalysisData() -> [AnalysisItem] { - return [ - AnalysisItem( - message: "오늘 힘든 일 없었어?", - author: "HUMAN", - feedback: Feedback( - type: .positive, - icon: "✓", - text: "좋은 표현이에요! �� 상대방의 하루를 살펴보려는 따뜻한 표현이에요.", - suggestion: nil - ) - ), - AnalysisItem( - message: "오늘 배운 수업이 어려워서 조금 힘들었어", - author: "AI", - feedback: nil - ), - AnalysisItem( - message: "그런 일로 왜 그래?", - author: "HUMAN", - feedback: Feedback( - type: .negative, - icon: "!", - text: "고치면 더 좋은 표현이에요 이 말은 상대방의 감정을 가볍게 여기는 것 처럼 들릴 수 있어요.", - suggestion: "이렇게 말하는 건 어떨까요? \"그랬구나. 속상했겠다. 괜찮아?\" 공감하는 말로 시작하면 상대방이 더 편안해질 수 있어요." - ) - ), - AnalysisItem( - message: "내 맘대로 되지 않아서 그랬던 것 같아", - author: "AI", - feedback: nil - ) - ] - } - - private func createAnalysisItemView(_ item: AnalysisItem) -> UIView { + + private func createAnalysisItemView(from message: ChatListVO) -> UIView { let containerView = UIView() // 메시지 뷰 - let messageView = createMessageBubble(item.message, author: item.author) + let author = message.author == 1 ? "HUMAN" : "AI" + let messageView = createMessageBubble(message.content, author: author) containerView.addSubview(messageView) messageView.snp.makeConstraints { $0.top.leading.trailing.equalToSuperview() } - // 피드백이 있는 경우 피드백 뷰 추가 - if let feedback = item.feedback { + // comment가 있는 경우 피드백 뷰 추가 (사용자 메시지에만) + if let comment = message.comment, !comment.isEmpty, message.author == 1 { + let feedback = Feedback( + text: comment, + suggestion: nil + ) let feedbackView = createFeedbackBubble(feedback) containerView.addSubview(feedbackView) @@ -192,15 +146,7 @@ open class ChatAnalysisView: UIView { let bubbleView = UIView().then { $0.layer.cornerRadius = 16 - $0.backgroundColor = feedback.type == .positive ? - UIColor(red: 0.8, green: 1.0, blue: 0.8, alpha: 1.0) : // 연두색 - UIColor(red: 1.0, green: 0.8, blue: 0.8, alpha: 1.0) // 연한 빨간색 - } - - let iconLabel = UILabel().then { - $0.text = feedback.icon - $0.textColor = feedback.type == .positive ? .green : .red - $0.font = .systemFont(ofSize: 16, weight: .bold) + $0.backgroundColor = UIColor(red: 0.9, green: 0.95, blue: 1.0, alpha: 1.0) // 연한 파란색 } let feedbackLabel = UILabel().then { @@ -211,7 +157,6 @@ open class ChatAnalysisView: UIView { } containerView.addSubview(bubbleView) - bubbleView.addSubview(iconLabel) bubbleView.addSubview(feedbackLabel) // 피드백은 오른쪽 정렬 @@ -221,16 +166,8 @@ open class ChatAnalysisView: UIView { $0.width.lessThanOrEqualTo(280) } - iconLabel.snp.makeConstraints { - $0.leading.equalToSuperview().offset(12) - $0.top.equalToSuperview().offset(12) - } - feedbackLabel.snp.makeConstraints { - $0.leading.equalTo(iconLabel.snp.trailing).offset(8) - $0.trailing.equalToSuperview().offset(-12) - $0.top.equalToSuperview().offset(12) - $0.bottom.equalToSuperview().offset(-12) + $0.edges.equalToSuperview().inset(12) } // 제안이 있는 경우 추가 @@ -245,8 +182,7 @@ open class ChatAnalysisView: UIView { bubbleView.addSubview(suggestionLabel) suggestionLabel.snp.makeConstraints { - $0.leading.equalTo(feedbackLabel.snp.leading) - $0.trailing.equalTo(feedbackLabel.snp.trailing) + $0.leading.trailing.equalTo(feedbackLabel) $0.top.equalTo(feedbackLabel.snp.bottom).offset(8) $0.bottom.equalToSuperview().offset(-12) } @@ -263,13 +199,6 @@ struct AnalysisItem { } struct Feedback { - enum FeedbackType { - case positive - case negative - } - - let type: FeedbackType - let icon: String let text: String let suggestion: String? } diff --git a/Projects/CommonUI/Sources/View/Chat/ChatView.swift b/Projects/CommonUI/Sources/View/Chat/ChatView.swift index 44555f6..96b00ec 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatView.swift @@ -13,6 +13,11 @@ import Then import RxRelay open class ChatView: UIView { + + public var onSendButtonTapped: ((String) -> Void)? + public var onEndButtonTapped: (() -> Void)? + let disposeBag = DisposeBag() + let titleLabel = UILabel().then { $0.text = "AI와 텍스트로 대화하세요" $0.textColor = .black @@ -103,12 +108,7 @@ open class ChatView: UIView { $0.layer.cornerRadius = 22 $0.isEnabled = false } - - let disposeBag = DisposeBag() - public var onSendButtonTapped: ((String) -> Void)? - public var onEndButtonTapped: (() -> Void)? - public override init(frame: CGRect) { super.init(frame: frame) initAttribute() diff --git a/Projects/Data/Sources/DTO/Chat/ChatDetailDTO.swift b/Projects/Data/Sources/DTO/Chat/ChatDetailDTO.swift index f580849..70f8985 100644 --- a/Projects/Data/Sources/DTO/Chat/ChatDetailDTO.swift +++ b/Projects/Data/Sources/DTO/Chat/ChatDetailDTO.swift @@ -23,7 +23,7 @@ public struct ChatListDTO: Decodable { public let chatId: Int public let author: Int public let content: String - public let comment: String + public let comment: String? public let createdAt: String } diff --git a/Projects/Data/Sources/Repository/ChatRepository.swift b/Projects/Data/Sources/Repository/ChatRepository.swift index 66173a3..e616bcd 100644 --- a/Projects/Data/Sources/Repository/ChatRepository.swift +++ b/Projects/Data/Sources/Repository/ChatRepository.swift @@ -55,7 +55,7 @@ public class DefaultChatRepository: ChatRepository { /// 대화 분석하기 public func postChatAnalysis(chatRoomId: Int) -> Single { return request(method: .post, - endpoint: "/api/chats/text/\(chatRoomId)/analysis", + endpoint: "/api/chats/\(chatRoomId)/analysis", responseType: ChatDetailResponseDTO.self ) .map { dto in @@ -76,7 +76,6 @@ public class DefaultChatRepository: ChatRepository { /// 저장된 대화 내역 상세 조회하기 public func getChatDetail(chatRoomId: Int) -> Single { return request(endpoint: "/api/chats/\(chatRoomId)", - encoding: URLEncoding.default, responseType: ChatDetailResponseDTO.self ) .map { dto in diff --git a/Projects/Domain/Sources/VO/ChatDetailVO.swift b/Projects/Domain/Sources/VO/ChatDetailVO.swift index b717aab..dffa0e4 100644 --- a/Projects/Domain/Sources/VO/ChatDetailVO.swift +++ b/Projects/Domain/Sources/VO/ChatDetailVO.swift @@ -19,13 +19,13 @@ public struct ChatListVO { public let chatId: Int public let author: Int public let content: String - public let comment: String + public let comment: String? public let createdAt: String public init(chatId: Int, author: Int, content: String, - comment: String, + comment: String?, createdAt: String) { self.chatId = chatId self.author = author From 147acca3072046f8a301bb98a0af74cadc3546a7 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sun, 14 Sep 2025 13:02:31 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[feat/chat]=20feat:=20Chat=20=EB=8C=80?= =?UTF-8?q?=ED=99=94=20=EB=B6=84=EC=84=9D=20=EA=B5=AC=ED=98=84=20-=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/ChatAnalysisController.swift | 30 ++++----- .../Sources/View/ChatViewController.swift | 20 ++++-- .../CommonUI.xcodeproj/project.pbxproj | 2 +- .../Icon/close.imageset/Contents.json | 22 +++++++ .../Icon/close.imageset/close@2x.png | Bin 0 -> 556 bytes .../Icon/close.imageset/close@3x.png | Bin 0 -> 722 bytes .../Sources/Enum/CommonUIAssets.swift | 1 + .../Sources/View/Chat/ChatAnalysisView.swift | 57 +++++++++++++++--- .../CommonUI/Sources/View/Chat/ChatView.swift | 47 ++++++++++++++- .../Navigation/DefaultNavigationBar.swift | 13 ++-- .../Sources/View/QuizViewController.swift | 3 +- .../Coordinator/TabBarCoordinator.swift | 20 +++--- .../Sources/View/SignInViewController.swift | 3 +- .../Sources/View/SignUpViewController.swift | 3 +- 14 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/Contents.json create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/close@2x.png create mode 100644 Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/close@3x.png diff --git a/Projects/Chat/Sources/View/ChatAnalysisController.swift b/Projects/Chat/Sources/View/ChatAnalysisController.swift index b8b2e4e..fa3e860 100644 --- a/Projects/Chat/Sources/View/ChatAnalysisController.swift +++ b/Projects/Chat/Sources/View/ChatAnalysisController.swift @@ -10,18 +10,16 @@ import CommonUI import Domain public class ChatAnalysisController: BaseViewController { - - private let chatAnalysisView = ChatAnalysisView() + private let chatAnalysisView = ChatAnalysisView() let navigationBar = DefaultNavigationBar(leftImage: nil, - rightImage: nil, - title: "대화 분석", - isRightButtonHidden: true) - - private var messages: [ChatListVO] = [] - - public init(messages: [ChatListVO]) { - self.messages = messages + rightImage: CommonUIAssets.IconClose ?? nil, + title: "") + + private var chatDetail: ChatDetailVO + + public init(chatDetail: ChatDetailVO) { + self.chatDetail = chatDetail super.init() } @@ -31,17 +29,19 @@ public class ChatAnalysisController: BaseViewController { public override func viewDidLoad() { super.viewDidLoad() - chatAnalysisView.setupAnalysisData(messages) + navigationBar.setupViewProperty(title: chatDetail.chatRoom.title) + chatAnalysisView.setupAnalysisData(chatDetail.chatList) } - + public override func setupViewProperty() { view.backgroundColor = CommonUIAssets.LMOrange4 } - + public override func setupHierarchy() { - view.addSubview(chatAnalysisView) + [navigationBar, chatAnalysisView] + .forEach { view.addSubview($0) } } - + public override func setupLayout() { navigationBar.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index d1fd295..1dd0d89 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -16,8 +16,7 @@ public class ChatViewController: BaseViewController { let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, - title: nil, - isRightButtonHidden: true) + title: nil) private var messages: [ChatMessageVO] = [] @@ -46,6 +45,9 @@ public class ChatViewController: BaseViewController { bindActions() setupTextFieldActions() viewModel.startTextChat() + + // 초기에 endButton 숨기기 + chatView.hideEndButton() } public override func setupViewProperty() { @@ -125,9 +127,11 @@ public class ChatViewController: BaseViewController { } private func sendMessage(_ text: String) { - // 첫 번째 메시지 전송 시 추천 섹션 숨기기 + // 첫 번째 메시지 전송 시 추천 섹션 숨기기 및 endButton 보이기 if messages.isEmpty { chatView.hideRecommendSection() + chatView.showEndButton() + chatView.updateSubtitleText("대화를 종료하면 분석 결과를 제공해요") } // 사용자 메시지 UI에 추가 @@ -175,8 +179,12 @@ public class ChatViewController: BaseViewController { private func navigateToAnalysisResult() { print("📊 분석 결과 화면으로 이동") - let emptyChatList: [ChatListVO] = [] - let analysisController = ChatAnalysisController(messages: emptyChatList) + // 임시 ChatDetailVO 생성 (빈 데이터로) + let emptyChatDetail = ChatDetailVO( + chatRoom: ChatRoomVO(chatRoomId: 0, title: "대화 분석", createdAt: ""), + chatList: [] + ) + let analysisController = ChatAnalysisController(chatDetail: emptyChatDetail) analysisController.modalPresentationStyle = UIModalPresentationStyle.fullScreen present(analysisController, animated: true) @@ -196,7 +204,7 @@ public class ChatViewController: BaseViewController { } private func presentAnalysisController(with analysisResult: ChatDetailVO) { - let analysisController = ChatAnalysisController(messages: analysisResult.chatList) + let analysisController = ChatAnalysisController(chatDetail: analysisResult) analysisController.hidesBottomBarWhenPushed = true navigationController?.pushViewController(analysisController, animated: true) } diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index 475eafe..25900da 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -207,8 +207,8 @@ children = ( 9516ED502E708CF200F548A1 /* ChatMainView.swift */, 9516ED522E709BA300F548A1 /* ChatListCell.swift */, - 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */, 3EBDD10D8389EBB23B42C54F /* ChatView.swift */, + 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */, 9516ED492E7076FB00F548A1 /* ChatAnalysisView.swift */, ); path = Chat; diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/Contents.json b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/Contents.json new file mode 100644 index 0000000..a52e16e --- /dev/null +++ b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "close@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/close@2x.png b/Projects/CommonUI/Sources/Assets/Images.xcassets/Icon/close.imageset/close@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8dd74c60e39e5f64fcc9eb80dbdebaf04731d7e GIT binary patch literal 556 zcmV+{0@MA8P)4l!hAC}qmwg}d6FG5q zSHH3iYW(tgEymywgg7d#?;JozS;cb+0jG3s&tuor#@4K@yyvn`v?BvnY2MbVpb^3t zh)T4LPl!S=qqXXJ&rk z90V)lUo+Cznx0*Px0k0I32P`u6Xp-h0+qr3fjX!xHUO%iX|Px@4Vo4U_QR0SGwS05 z8~4cg!FM|B_t+6hLI+MHfk}Zfs4ON8hN$NnEi4IU-!2DB)`|>9F+eh|YwX2f6w?p( z$%V^wikppnk*-K1!KA5=9|sgF5?8^3VsF5SN*UxT7)=n>GRRf1uuTQ!s+cOububl_ z>tbn9o&}Z$Wkp<*=G@+gE*4Ev?WI-QKxxy(D8Yg^W?|NaNr%7N?e5gv;v>G}ma(mk uSFx*ug$ajO_^e2;2)x_sL*t?j2!s$B zFdGsK5eUR+HUyE6JNpj8i|u>!MM(UI*goGA|76*|vjJ}7HYisn-?!={fNwFSH9-8` zTUwZd5oGdXtBsU?qKHut#NSuDojyqDyT(*)Fd*v)w0c&7ILENl;4uvO3n*<5HAN6( zv$wJk6227=$d3z19#)VPjt3 z=gae(=|gLssp?q;Wn8mGNQMj7mjAuw>qIu5RPHv}t}v^}^_f*cKB(Ml4sD;O-uPU- zGdTWV_L)^hcDsM0p|*=D8*73T?N?J4))XoBT_4w28>HBu9amUer0A0$TdWJD=vN$5 ztShAGs}^0XOQh(p9#w3zbchr+dQrmWA5a@9>hxoZHARYAv*==NkfPo^s#sej4*`pW z{J;odQ9m~M4Q-G-0a&Ey5j2$g^YPcU{hh6OFpM!&11V#LHG&!-WsI@fP?Zsf{T{H@ zCmCIguv#~h>abYGYmM^bNq6jaJ)c#AnoclWVhf?RvmjSkEEDMxi)ABSVbKTD1r~iF zZL#PRX@f=INKFalLjqdMk_n(eWY+!P&V8l-M z?8D0auRf46{F-G`zu9LmULgXhvZhVunC>liI&hP63Qy-GDm$;4() + let disposeBag = DisposeBag() + private let scrollView = UIScrollView().then { $0.showsVerticalScrollIndicator = true $0.alwaysBounceVertical = true @@ -24,37 +29,73 @@ open class ChatAnalysisView: UIView { $0.spacing = 16 $0.alignment = .fill } - + + private let nonScrollView = UIView().then { + $0.backgroundColor = CommonUIAssets.LMOrange4 + } + + var chatSaveButton = LMButton(textColor: CommonUIAssets.LMBlack, + bgColor: CommonUIAssets.LMOrange1) public override init(frame: CGRect) { super.init(frame: frame) + initAttribute() setupUI() + setupConstraints() + bindEvents() } - + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + func initAttribute() { + chatSaveButton = chatSaveButton.then { + $0.setTitle("대화 저장하기", for: .normal) + } + } + private func setupUI() { backgroundColor = CommonUIAssets.LMOrange4 - self.addSubview(scrollView) + [scrollView, nonScrollView].forEach { addSubview($0) } scrollView.addSubview(contentView) contentView.addSubview(analysisStackView) + nonScrollView.addSubview(chatSaveButton) + } + public func setupConstraints() { scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() + $0.top.horizontalEdges.equalToSuperview() + $0.bottom.equalTo(nonScrollView.snp.top) } - + contentView.snp.makeConstraints { $0.edges.equalToSuperview() $0.width.equalToSuperview() } - + analysisStackView.snp.makeConstraints { $0.top.equalToSuperview().offset(20) $0.leading.trailing.equalToSuperview().inset(20) $0.bottom.equalToSuperview().offset(-20) } + + nonScrollView.snp.makeConstraints { + $0.width.equalToSuperview() + $0.bottom.equalTo(self.safeAreaLayoutGuide).offset(-20) + $0.height.equalTo(75) + } + + chatSaveButton.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.equalToSuperview().inset(20) + } + } + + func bindEvents() { + chatSaveButton.rx.tap + .bind(to: onSaveButtonTapped) + .disposed(by: disposeBag) } public func setupAnalysisData(_ messages: [ChatListVO]) { diff --git a/Projects/CommonUI/Sources/View/Chat/ChatView.swift b/Projects/CommonUI/Sources/View/Chat/ChatView.swift index 96b00ec..abdceec 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatView.swift @@ -87,6 +87,7 @@ open class ChatView: UIView { for (index, text) in newValue.enumerated() { if index < recommendLabels.count { recommendLabels[index].text = text + updateRecommendViewHeight(at: index) } } } @@ -143,11 +144,11 @@ open class ChatView: UIView { chatScrollView.addSubview(chatStackView) // 추천 뷰들을 StackView에 추가하고 각각에 라벨 추가 - for (view, label) in zip(recommendViews, recommendLabels) { + for (index, (view, label)) in zip(recommendViews, recommendLabels).enumerated() { recommendStackView.addArrangedSubview(view) view.addSubview(label) - // 뷰 높이 설정 + // 초기 뷰 높이 설정 (최소 높이) view.snp.makeConstraints { $0.height.equalTo(40) } // 라벨 레이아웃 설정 @@ -206,7 +207,7 @@ open class ChatView: UIView { } sendButton.snp.makeConstraints { - $0.bottom.equalTo(self.safeAreaLayoutGuide).offset(-30) + $0.bottom.equalTo(self.safeAreaLayoutGuide).offset(-20) $0.height.width.equalTo(44) $0.trailing.equalToSuperview().inset(20) } @@ -234,6 +235,21 @@ open class ChatView: UIView { } } + // endButton 숨기기 메서드 + public func hideEndButton() { + endButton.isHidden = true + } + + // endButton 보이기 메서드 + public func showEndButton() { + endButton.isHidden = false + } + + // subtitleLabel 텍스트 변경 메서드 + public func updateSubtitleText(_ text: String) { + subtitleLabel.text = text + } + // 메시지 추가 메서드 public func addMessageToUI(_ message: ChatMessageVO) { let messageView = createMessageView(message) @@ -292,6 +308,31 @@ open class ChatView: UIView { return containerView } + // 추천 뷰 높이를 동적으로 업데이트하는 메서드 + private func updateRecommendViewHeight(at index: Int) { + guard index < recommendLabels.count && index < recommendViews.count else { return } + + let label = recommendLabels[index] + let view = recommendViews[index] + + // 라벨의 intrinsic content size를 계산 + let maxWidth: CGFloat = 280 - 32 // recommendStackView width - label insets (16 * 2) + let size = label.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.infinity)) + + // 최소 높이 40, 최대 높이 100으로 제한 + let calculatedHeight = max(40, min(100, size.height + 16)) // 16은 상하 패딩 + + // 기존 높이 constraint 업데이트 + view.snp.updateConstraints { make in + make.height.equalTo(calculatedHeight) + } + + // 애니메이션과 함께 레이아웃 업데이트 + UIView.animate(withDuration: 0.3) { + self.layoutIfNeeded() + } + } + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift b/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift index e3bc20f..e845b6c 100644 --- a/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift +++ b/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift @@ -16,8 +16,7 @@ public final class DefaultNavigationBar: UIView { public init(leftImage: UIImage?, rightImage: UIImage?, - title: String?, - isRightButtonHidden: Bool) { + title: String?) { super.init(frame: .zero) setupUI() setupLayout() @@ -25,9 +24,9 @@ public final class DefaultNavigationBar: UIView { leftButton.setImage(leftImage, for: .normal) rightButton.setImage(rightImage, for: .normal) titleLabel.text = title - rightButton.isHidden = isRightButtonHidden leftButton.addTarget(self, action: #selector(leftButtonTapped), for: .touchUpInside) + rightButton.addTarget(self, action: #selector(rightButtonTapped), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -80,7 +79,13 @@ public final class DefaultNavigationBar: UIView { viewController.navigationController?.popViewController(animated: true) } } - + + @objc private func rightButtonTapped() { + if let viewController = findViewController() { + viewController.navigationController?.popToRootViewController(animated: true) + } + } + private func findViewController() -> UIViewController? { var responder: UIResponder? = self while let nextResponder = responder?.next { diff --git a/Projects/Home/Sources/View/QuizViewController.swift b/Projects/Home/Sources/View/QuizViewController.swift index 9b483d7..f1f6f3d 100644 --- a/Projects/Home/Sources/View/QuizViewController.swift +++ b/Projects/Home/Sources/View/QuizViewController.swift @@ -14,8 +14,7 @@ import RxSwift public class QuizViewController: UIViewController { let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, - title: nil, - isRightButtonHidden: true) + title: nil) let progressView = UIView().then { $0.backgroundColor = CommonUIAssets.LMOrange1 $0.layer.cornerRadius = 3 diff --git a/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift b/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift index a0b0142..7c7d334 100644 --- a/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift +++ b/Projects/LearnMate/Sources/Coordinator/TabBarCoordinator.swift @@ -77,10 +77,10 @@ final class DefaultTabBarController: TabBarCoordinator { return UITabBarItem(title: page.tabIconName(), image: CommonUIAssets.tabIconDiary?.original, selectedImage: CommonUIAssets.tabIconDiarySelected?.original) - case .stats: - return UITabBarItem(title: page.tabIconName(), - image: CommonUIAssets.tabIconStats?.original, - selectedImage: CommonUIAssets.tabIconStatsSelected?.original) +// case .stats: +// return UITabBarItem(title: page.tabIconName(), +// image: CommonUIAssets.tabIconStats?.original, +// selectedImage: CommonUIAssets.tabIconStatsSelected?.original) case .myPage: return UITabBarItem(title: page.tabIconName(), image: CommonUIAssets.tabIconMypage?.original, @@ -106,15 +106,15 @@ final class DefaultTabBarController: TabBarCoordinator { } enum TabBarPage: String, CaseIterable { - case home, chat, diary, stats, myPage + case home, chat, diary, myPage init?(index: Int) { switch index { case 0: self = .home case 1: self = .chat case 2: self = .diary - case 3: self = .stats - case 4: self = .myPage +// case 3: self = .stats + case 3: self = .myPage default: return nil } } @@ -124,8 +124,8 @@ enum TabBarPage: String, CaseIterable { case .home: return 0 case .chat: return 1 case .diary: return 2 - case .stats: return 3 - case .myPage: return 4 +// case .stats: return 3 + case .myPage: return 3 } } @@ -134,7 +134,7 @@ enum TabBarPage: String, CaseIterable { case .home: return "홈" case .chat: return "대화" case .diary: return "일기" - case .stats: return "통계" +// case .stats: return "통계" case .myPage: return "마이페이지" } } diff --git a/Projects/Login/Sources/View/SignInViewController.swift b/Projects/Login/Sources/View/SignInViewController.swift index 871bf97..7ac1a2c 100644 --- a/Projects/Login/Sources/View/SignInViewController.swift +++ b/Projects/Login/Sources/View/SignInViewController.swift @@ -15,8 +15,7 @@ public class SignInViewController: BaseViewController { let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, - title: nil, - isRightButtonHidden: true) + title: nil) let signInView = SignInView() public var onPresentSignUp: (() -> Void)? diff --git a/Projects/Login/Sources/View/SignUpViewController.swift b/Projects/Login/Sources/View/SignUpViewController.swift index 7d1abfe..125234a 100644 --- a/Projects/Login/Sources/View/SignUpViewController.swift +++ b/Projects/Login/Sources/View/SignUpViewController.swift @@ -16,8 +16,7 @@ public class SignUpViewController: BaseViewController { let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, - title: "회원가입", - isRightButtonHidden: true) + title: "회원가입") let scrollView = UIScrollView() let signUpView = SignUpView() From de313dcd36102a76c15929f3b283a37143597cb7 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sun, 14 Sep 2025 14:34:46 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[feat/chat]=20feat:=20Chat=20=EB=8C=80?= =?UTF-8?q?=ED=99=94=20=EB=B6=84=EC=84=9D=20=EA=B5=AC=ED=98=84=20-=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/ChatAnalysisController.swift | 49 ++++- .../Sources/View/ChatViewController.swift | 43 ++++- .../Sources/ViewModel/ChatViewModel.swift | 9 + .../CommonUI.xcodeproj/project.pbxproj | 4 + .../CommonUI/Sources/Component/LMAlert.swift | 177 ++++++++++++++++++ .../Navigation/DefaultNavigationBar.swift | 4 +- 6 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 Projects/CommonUI/Sources/Component/LMAlert.swift diff --git a/Projects/Chat/Sources/View/ChatAnalysisController.swift b/Projects/Chat/Sources/View/ChatAnalysisController.swift index fa3e860..fac13bc 100644 --- a/Projects/Chat/Sources/View/ChatAnalysisController.swift +++ b/Projects/Chat/Sources/View/ChatAnalysisController.swift @@ -8,9 +8,10 @@ import UIKit import CommonUI import Domain +import RxSwift public class ChatAnalysisController: BaseViewController { - + let viewModel: ChatViewModel private let chatAnalysisView = ChatAnalysisView() let navigationBar = DefaultNavigationBar(leftImage: nil, rightImage: CommonUIAssets.IconClose ?? nil, @@ -18,7 +19,8 @@ public class ChatAnalysisController: BaseViewController { private var chatDetail: ChatDetailVO - public init(chatDetail: ChatDetailVO) { + public init(chatViewModel: ChatViewModel, chatDetail: ChatDetailVO) { + self.viewModel = chatViewModel self.chatDetail = chatDetail super.init() } @@ -31,6 +33,7 @@ public class ChatAnalysisController: BaseViewController { super.viewDidLoad() navigationBar.setupViewProperty(title: chatDetail.chatRoom.title) chatAnalysisView.setupAnalysisData(chatDetail.chatList) + setupActions() } public override func setupViewProperty() { @@ -53,4 +56,46 @@ public class ChatAnalysisController: BaseViewController { $0.horizontalEdges.bottom.equalToSuperview() } } + + private func setupActions() { + // DefaultNavigationBar의 기본 타겟 제거 후 우리의 커스텀 액션 추가 + navigationBar.rightButton.removeTarget(navigationBar, action: #selector(DefaultNavigationBar.rightButtonTapped), for: .touchUpInside) + navigationBar.rightButton.addTarget(self, action: #selector(handleRightButtonTapped), for: .touchUpInside) + + // SaveButton 액션 설정 + chatAnalysisView.onSaveButtonTapped.subscribe(onNext: { [weak self] in + self?.handleSaveButtonTapped() + }).disposed(by: disposeBag) + } + + @objc private func handleRightButtonTapped() { + showExitConfirmationAlert() + } + + private func handleSaveButtonTapped() { + // 저장 버튼 클릭 시 루트 뷰컨트롤러로 이동 + navigationController?.popToRootViewController(animated: true) + } + + private func showExitConfirmationAlert() { + let lmAlert = LMAlert(title: "저장하지 않은 대화는 사라집니다.\n그래도 나가시겠습니까?") + + lmAlert.setCancelAction { + // 아니요 버튼 - 아무것도 하지 않음 + } + + lmAlert.setConfirmAction { [weak self] in + self?.deleteChatAndExit() + } + + lmAlert.show(in: view) + } + + private func deleteChatAndExit() { + // 대화방 삭제 API 호출 + viewModel.deleteChat() + + // 루트 뷰컨트롤러로 이동 + navigationController?.popToRootViewController(animated: true) + } } diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 1dd0d89..4ecec73 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -19,6 +19,7 @@ public class ChatViewController: BaseViewController { title: nil) private var messages: [ChatMessageVO] = [] + private var hasStartedConversation = false private let loadingView = ChatAnalysisLoadingView() @@ -115,6 +116,10 @@ public class ChatViewController: BaseViewController { self?.showAnalysisLoading() self?.viewModel.postChatAnalysis() } + + // DefaultNavigationBar의 기본 타겟 제거 후 우리의 커스텀 액션 추가 + navigationBar.leftButton.removeTarget(navigationBar, action: #selector(DefaultNavigationBar.leftButtonTapped), for: .touchUpInside) + navigationBar.leftButton.addTarget(self, action: #selector(handleBackButtonTapped), for: .touchUpInside) } private func setupTextFieldActions() { @@ -129,6 +134,7 @@ public class ChatViewController: BaseViewController { private func sendMessage(_ text: String) { // 첫 번째 메시지 전송 시 추천 섹션 숨기기 및 endButton 보이기 if messages.isEmpty { + hasStartedConversation = true chatView.hideRecommendSection() chatView.showEndButton() chatView.updateSubtitleText("대화를 종료하면 분석 결과를 제공해요") @@ -184,7 +190,8 @@ public class ChatViewController: BaseViewController { chatRoom: ChatRoomVO(chatRoomId: 0, title: "대화 분석", createdAt: ""), chatList: [] ) - let analysisController = ChatAnalysisController(chatDetail: emptyChatDetail) + let analysisController = ChatAnalysisController(chatViewModel: viewModel, + chatDetail: emptyChatDetail) analysisController.modalPresentationStyle = UIModalPresentationStyle.fullScreen present(analysisController, animated: true) @@ -204,8 +211,40 @@ public class ChatViewController: BaseViewController { } private func presentAnalysisController(with analysisResult: ChatDetailVO) { - let analysisController = ChatAnalysisController(chatDetail: analysisResult) + let analysisController = ChatAnalysisController(chatViewModel: viewModel, + chatDetail: analysisResult) analysisController.hidesBottomBarWhenPushed = true navigationController?.pushViewController(analysisController, animated: true) } + + @objc private func handleBackButtonTapped() { + if hasStartedConversation { + showEndConversationAlert() + } else { + // 대화가 시작되지 않았다면 바로 뒤로 가기 + navigationController?.popViewController(animated: true) + } + } + + private func showEndConversationAlert() { + let lmAlert = LMAlert(title: "대화를 종료하시겠습니까?") + + lmAlert.setCancelAction { + // 아니요 버튼 - 아무것도 하지 않음 + } + + lmAlert.setConfirmAction { [weak self] in + self?.endConversationAndGoBack() + } + + lmAlert.show(in: view) + } + + private func endConversationAndGoBack() { + // 대화방 삭제 API 호출 + viewModel.deleteChat() + + // 이전 화면으로 이동 + navigationController?.popViewController(animated: true) + } } diff --git a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift index 863e19c..5b709fc 100644 --- a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift +++ b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift @@ -75,4 +75,13 @@ public class ChatViewModel: ChatViewModelProtocol { print("❌ 대화 분석 실패: \(error)") }).disposed(by: disposeBag) } + + func deleteChat() { + chatUseCase.deleteChat(chatRoomId: currentChatRoomId) + .subscribe(onSuccess: { result in + print("✅ 대화방 삭제 성공: \(result)") + }, onFailure: { error in + print("❌ 대화방 삭제 실패: \(error)") + }).disposed(by: disposeBag) + } } diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index 25900da..b143966 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 9516ED4D2E707ACF00F548A1 /* ChatAnalysisLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */; }; 9516ED512E708CF700F548A1 /* ChatMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED502E708CF200F548A1 /* ChatMainView.swift */; }; 9516ED532E709BA600F548A1 /* ChatListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED522E709BA300F548A1 /* ChatListCell.swift */; }; + 956C4D702E767F8800E32F93 /* LMAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D6F2E767F8300E32F93 /* LMAlert.swift */; }; BAD8B768F782046D4AA1C073 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3DE048BEDCB92B48F401061 /* RxCocoa.framework */; }; BE81B1F3E60D37D75A058D2B /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CCDC15081A22BAED6318E3E /* SnapKit.framework */; }; C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012F45F908769FA7C3C0792F /* LoginView.swift */; }; @@ -84,6 +85,7 @@ 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisLoadingView.swift; sourceTree = ""; }; 9516ED502E708CF200F548A1 /* ChatMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMainView.swift; sourceTree = ""; }; 9516ED522E709BA300F548A1 /* ChatListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListCell.swift; sourceTree = ""; }; + 956C4D6F2E767F8300E32F93 /* LMAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMAlert.swift; sourceTree = ""; }; 9CCDC15081A22BAED6318E3E /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BACC7259FC0C14CB352A4E6B /* OptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionView.swift; sourceTree = ""; }; C28FE6392E1612667826E5C5 /* DiaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryView.swift; sourceTree = ""; }; @@ -187,6 +189,7 @@ 950A0D522E5C296400C07CF2 /* Component */ = { isa = PBXGroup; children = ( + 956C4D6F2E767F8300E32F93 /* LMAlert.swift */, 950A0D552E5C29CC00C07CF2 /* LMTextField.swift */, 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */, 950A0D612E5C561400C07CF2 /* LMInputField.swift */, @@ -386,6 +389,7 @@ CEADBDD98AC9921C05AAC1DA /* QuizCollectionViewCell.swift in Sources */, 950A0D512E5AADC400C07CF2 /* SignInView.swift in Sources */, 592FAEA836FD6B4B3F466D72 /* QuizCompleteAlertView.swift in Sources */, + 956C4D702E767F8800E32F93 /* LMAlert.swift in Sources */, 65B3402D15A9F06B3E88CE19 /* QuizView.swift in Sources */, C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */, 473C96577A92DD6F71241A76 /* MyPageView.swift in Sources */, diff --git a/Projects/CommonUI/Sources/Component/LMAlert.swift b/Projects/CommonUI/Sources/Component/LMAlert.swift new file mode 100644 index 0000000..44a1e8c --- /dev/null +++ b/Projects/CommonUI/Sources/Component/LMAlert.swift @@ -0,0 +1,177 @@ +// +// LMAlert.swift +// CommonUI +// +// Created by 박지윤 on 9/14/25. +// + +import UIKit +import SnapKit +import Then + +public class LMAlert: UIView { + + // MARK: - UI Components + private let backgroundView = UIView().then { + $0.backgroundColor = UIColor.black.withAlphaComponent(0.5) + $0.alpha = 0 + } + + private let containerView = UIView().then { + $0.backgroundColor = UIColor.white + $0.layer.cornerRadius = 16 + $0.layer.masksToBounds = true + } + + private let titleLabel = UILabel().then { + $0.font = .systemFont(ofSize: 18, weight: .semibold) + $0.textColor = .black + $0.textAlignment = .center + $0.numberOfLines = 0 + } + + private let buttonStackView = UIStackView().then { + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.spacing = 12 + } + + private let cancelButton = UIButton().then { + $0.backgroundColor = .white + $0.layer.borderColor = UIColor.lightGray.cgColor + $0.layer.borderWidth = 1 + $0.layer.cornerRadius = 8 + $0.setTitleColor(.black, for: .normal) + $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + $0.setTitle("아니요", for: .normal) + } + + private let confirmButton = UIButton().then { + $0.backgroundColor = UIColor(red: 1.0, green: 0.8, blue: 0.6, alpha: 1.0) // 연한 주황색 + $0.layer.cornerRadius = 8 + $0.setTitleColor(.black, for: .normal) + $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + $0.setTitle("네", for: .normal) + } + + // MARK: - Properties + private var cancelAction: (() -> Void)? + private var confirmAction: (() -> Void)? + + // MARK: - Initialization + public init(title: String, cancelTitle: String = "아니요", confirmTitle: String = "네") { + super.init(frame: .zero) + setupUI() + setupLayout() + configure(title: title, cancelTitle: cancelTitle, confirmTitle: confirmTitle) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + private func setupUI() { + addSubview(backgroundView) + addSubview(containerView) + + containerView.addSubview(titleLabel) + containerView.addSubview(buttonStackView) + + buttonStackView.addArrangedSubview(cancelButton) + buttonStackView.addArrangedSubview(confirmButton) + + // 버튼 액션 설정 + cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) + confirmButton.addTarget(self, action: #selector(confirmButtonTapped), for: .touchUpInside) + + // 배경 탭 제스처 + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundTapped)) + backgroundView.addGestureRecognizer(tapGesture) + } + + private func setupLayout() { + backgroundView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + containerView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.equalTo(280) + $0.height.equalTo(140) + } + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(24) + $0.horizontalEdges.equalToSuperview().inset(20) + } + + buttonStackView.snp.makeConstraints { + $0.bottom.equalToSuperview().inset(20) + $0.horizontalEdges.equalToSuperview().inset(20) + $0.height.equalTo(44) + } + } + + private func configure(title: String, cancelTitle: String, confirmTitle: String) { + titleLabel.text = title + cancelButton.setTitle(cancelTitle, for: .normal) + confirmButton.setTitle(confirmTitle, for: .normal) + } + + // MARK: - Actions + @objc private func cancelButtonTapped() { + hide { + self.cancelAction?() + } + } + + @objc private func confirmButtonTapped() { + hide { + self.confirmAction?() + } + } + + @objc private func backgroundTapped() { + hide { + self.cancelAction?() + } + } + + // MARK: - Public Methods + public func setCancelAction(_ action: @escaping () -> Void) { + self.cancelAction = action + } + + public func setConfirmAction(_ action: @escaping () -> Void) { + self.confirmAction = action + } + + public func show(in view: UIView) { + view.addSubview(self) + self.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + // 애니메이션으로 나타나기 + containerView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + containerView.alpha = 0 + + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut) { + self.backgroundView.alpha = 1 + self.containerView.alpha = 1 + self.containerView.transform = .identity + } + } + + private func hide(completion: @escaping () -> Void) { + UIView.animate(withDuration: 0.2, animations: { + self.backgroundView.alpha = 0 + self.containerView.alpha = 0 + self.containerView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + }) { _ in + self.removeFromSuperview() + completion() + } + } +} diff --git a/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift b/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift index e845b6c..0b0ca19 100644 --- a/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift +++ b/Projects/CommonUI/Sources/View/Navigation/DefaultNavigationBar.swift @@ -74,13 +74,13 @@ public final class DefaultNavigationBar: UIView { titleLabel.text = title } - @objc private func leftButtonTapped() { + @objc public func leftButtonTapped() { if let viewController = findViewController() { viewController.navigationController?.popViewController(animated: true) } } - @objc private func rightButtonTapped() { + @objc public func rightButtonTapped() { if let viewController = findViewController() { viewController.navigationController?.popToRootViewController(animated: true) } From da102cd26e1180c809bc91cbdb99359b18315b01 Mon Sep 17 00:00:00 2001 From: Bibi-urssu Date: Sun, 14 Sep 2025 16:33:33 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[feat/chat]=20feat:=20Chat=20=EB=8C=80?= =?UTF-8?q?=ED=99=94=20=EC=83=81=EC=84=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Chat/Chat.xcodeproj/project.pbxproj | 12 +- ...swift => ChatAnalysisViewController.swift} | 4 +- .../View/ChatDetailViewController.swift | 90 +++++++++ .../Sources/View/ChatMainViewController.swift | 12 ++ .../Sources/View/ChatViewController.swift | 4 +- .../Sources/ViewModel/ChatViewModel.swift | 11 + .../CommonUI.xcodeproj/project.pbxproj | 4 + .../Sources/View/Chat/ChatDetailView.swift | 190 ++++++++++++++++++ .../Sources/View/Chat/ChatMainView.swift | 23 ++- 9 files changed, 340 insertions(+), 10 deletions(-) rename Projects/Chat/Sources/View/{ChatAnalysisController.swift => ChatAnalysisViewController.swift} (96%) create mode 100644 Projects/Chat/Sources/View/ChatDetailViewController.swift create mode 100644 Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift diff --git a/Projects/Chat/Chat.xcodeproj/project.pbxproj b/Projects/Chat/Chat.xcodeproj/project.pbxproj index d5d1742..3848aaa 100644 --- a/Projects/Chat/Chat.xcodeproj/project.pbxproj +++ b/Projects/Chat/Chat.xcodeproj/project.pbxproj @@ -12,8 +12,9 @@ 7F3F4DE30EE902DEABC6040E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAFFC509053CC99EAA3B277 /* ChatViewModel.swift */; }; 9179FB9B1C2AEDCB7A3CFC7E /* ChatCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588CFD307FD55B64676DA657 /* ChatCoordinator.swift */; }; 91C973D65FBDD1ABF65A1A2A /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F910F30DEBC80ABF5CC5A6F9 /* Domain.framework */; }; - 9516ED482E7076F800F548A1 /* ChatAnalysisController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED472E7076E900F548A1 /* ChatAnalysisController.swift */; }; + 9516ED482E7076F800F548A1 /* ChatAnalysisViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED472E7076E900F548A1 /* ChatAnalysisViewController.swift */; }; 9516ED4F2E708CE100F548A1 /* ChatMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED4E2E708CD900F548A1 /* ChatMainViewController.swift */; }; + 956C4D722E7690C600E32F93 /* ChatDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D712E7690BF00E32F93 /* ChatDetailViewController.swift */; }; B6F14AC32696F30284073B2F /* Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEE28C25EAD3F5DC9E76FFAC /* Chat.framework */; }; B89B886F288E5BDECC82BA50 /* Common.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64BD6CA2A2FDA5A5D9C4A87D /* Common.framework */; }; E096380D552BAA6326ED8397 /* CommonUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB61D7206C15B58E35E2DEB /* CommonUI.framework */; }; @@ -62,8 +63,9 @@ 5AAFFC509053CC99EAA3B277 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = ""; }; 5E359E9093579B130E0EDD53 /* Then.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Then.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 64BD6CA2A2FDA5A5D9C4A87D /* Common.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Common.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9516ED472E7076E900F548A1 /* ChatAnalysisController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisController.swift; sourceTree = ""; }; + 9516ED472E7076E900F548A1 /* ChatAnalysisViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnalysisViewController.swift; sourceTree = ""; }; 9516ED4E2E708CD900F548A1 /* ChatMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMainViewController.swift; sourceTree = ""; }; + 956C4D712E7690BF00E32F93 /* ChatDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDetailViewController.swift; sourceTree = ""; }; BC90A71D97E9F538AACFD586 /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C8A6CF81DAC793585959F31D /* Chat-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Chat-Info.plist"; sourceTree = ""; }; E05B48A08FE1A700DE3FEE63 /* RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -160,7 +162,8 @@ children = ( 9516ED4E2E708CD900F548A1 /* ChatMainViewController.swift */, E083003E4769EA2B259F4BE8 /* ChatViewController.swift */, - 9516ED472E7076E900F548A1 /* ChatAnalysisController.swift */, + 9516ED472E7076E900F548A1 /* ChatAnalysisViewController.swift */, + 956C4D712E7690BF00E32F93 /* ChatDetailViewController.swift */, ); path = View; sourceTree = ""; @@ -286,7 +289,8 @@ 9179FB9B1C2AEDCB7A3CFC7E /* ChatCoordinator.swift in Sources */, 11E1631C9C29E4197C2782CB /* ChatViewController.swift in Sources */, 7F3F4DE30EE902DEABC6040E /* ChatViewModel.swift in Sources */, - 9516ED482E7076F800F548A1 /* ChatAnalysisController.swift in Sources */, + 9516ED482E7076F800F548A1 /* ChatAnalysisViewController.swift in Sources */, + 956C4D722E7690C600E32F93 /* ChatDetailViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Projects/Chat/Sources/View/ChatAnalysisController.swift b/Projects/Chat/Sources/View/ChatAnalysisViewController.swift similarity index 96% rename from Projects/Chat/Sources/View/ChatAnalysisController.swift rename to Projects/Chat/Sources/View/ChatAnalysisViewController.swift index fac13bc..f9b8cbb 100644 --- a/Projects/Chat/Sources/View/ChatAnalysisController.swift +++ b/Projects/Chat/Sources/View/ChatAnalysisViewController.swift @@ -1,5 +1,5 @@ // -// ChatAnalysisController.swift +// ChatAnalysisViewController.swift // Chat // // Created by 박지윤 on 9/9/25. @@ -10,7 +10,7 @@ import CommonUI import Domain import RxSwift -public class ChatAnalysisController: BaseViewController { +public class ChatAnalysisViewController: BaseViewController { let viewModel: ChatViewModel private let chatAnalysisView = ChatAnalysisView() let navigationBar = DefaultNavigationBar(leftImage: nil, diff --git a/Projects/Chat/Sources/View/ChatDetailViewController.swift b/Projects/Chat/Sources/View/ChatDetailViewController.swift new file mode 100644 index 0000000..ca8c226 --- /dev/null +++ b/Projects/Chat/Sources/View/ChatDetailViewController.swift @@ -0,0 +1,90 @@ +// +// ChatDetailViewController.swift +// Chat +// +// Created by 박지윤 on 1/1/25. +// + +import UIKit +import CommonUI +import Domain +import RxSwift + +public class ChatDetailViewController: BaseViewController { + + let viewModel: ChatViewModel + private let chatDetailView = ChatDetailView() + let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, + rightImage: nil, + title: "") + + private var chatRoom: ChatRoomVO + private var chatDetail: ChatDetailVO? + + public init(chatViewModel: ChatViewModel, chatRoom: ChatRoomVO) { + self.viewModel = chatViewModel + self.chatRoom = chatRoom + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + navigationBar.setupViewProperty(title: chatRoom.title) + setupActions() + bindData() + loadChatDetail() + } + + public override func setupViewProperty() { + view.backgroundColor = CommonUIAssets.LMOrange4 + } + + public override func setupHierarchy() { + [navigationBar, chatDetailView] + .forEach { view.addSubview($0) } + } + + public override func setupLayout() { + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.width.centerX.equalToSuperview() + } + + chatDetailView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.horizontalEdges.bottom.equalToSuperview() + } + } + + private func setupActions() { + // LeftButton 액션 (뒤로 가기) +// navigationBar.leftButton.removeTarget(navigationBar, action: #selector(DefaultNavigationBar.leftButtonTapped), for: .touchUpInside) +// navigationBar.leftButton.addTarget(self, action: #selector(handleBackButtonTapped), for: .touchUpInside) + } + + private func bindData() { + viewModel.chatDetailSubject + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] (chatDetail: ChatDetailVO) in + self?.updateChatDetail(chatDetail) + }) + .disposed(by: disposeBag) + } + + private func loadChatDetail() { + viewModel.getChatDetail(chatRoomId: chatRoom.chatRoomId) + } + + @objc private func handleBackButtonTapped() { + navigationController?.popViewController(animated: true) + } + + public func updateChatDetail(_ chatDetail: ChatDetailVO) { + self.chatDetail = chatDetail + chatDetailView.setupDetailData(chatDetail.chatList) + } +} diff --git a/Projects/Chat/Sources/View/ChatMainViewController.swift b/Projects/Chat/Sources/View/ChatMainViewController.swift index 564652a..7f12495 100644 --- a/Projects/Chat/Sources/View/ChatMainViewController.swift +++ b/Projects/Chat/Sources/View/ChatMainViewController.swift @@ -85,6 +85,12 @@ public class ChatMainViewController: BaseViewController { self?.presentNewChatView() } .disposed(by: disposeBag) + + chatMainView.chatCellTapped + .bind { [weak self] chatRoom in + self?.presentChatDetailView(chatRoom: chatRoom) + } + .disposed(by: disposeBag) } private func presentNewChatView() { @@ -92,4 +98,10 @@ public class ChatMainViewController: BaseViewController { chatViewController.hidesBottomBarWhenPushed = true navigationController?.pushViewController(chatViewController, animated: true) } + + private func presentChatDetailView(chatRoom: ChatRoomVO) { + let chatDetailViewController = ChatDetailViewController(chatViewModel: viewModel, chatRoom: chatRoom) + chatDetailViewController.hidesBottomBarWhenPushed = true + navigationController?.pushViewController(chatDetailViewController, animated: true) + } } diff --git a/Projects/Chat/Sources/View/ChatViewController.swift b/Projects/Chat/Sources/View/ChatViewController.swift index 4ecec73..0ff6c64 100644 --- a/Projects/Chat/Sources/View/ChatViewController.swift +++ b/Projects/Chat/Sources/View/ChatViewController.swift @@ -190,7 +190,7 @@ public class ChatViewController: BaseViewController { chatRoom: ChatRoomVO(chatRoomId: 0, title: "대화 분석", createdAt: ""), chatList: [] ) - let analysisController = ChatAnalysisController(chatViewModel: viewModel, + let analysisController = ChatAnalysisViewController(chatViewModel: viewModel, chatDetail: emptyChatDetail) analysisController.modalPresentationStyle = UIModalPresentationStyle.fullScreen @@ -211,7 +211,7 @@ public class ChatViewController: BaseViewController { } private func presentAnalysisController(with analysisResult: ChatDetailVO) { - let analysisController = ChatAnalysisController(chatViewModel: viewModel, + let analysisController = ChatAnalysisViewController(chatViewModel: viewModel, chatDetail: analysisResult) analysisController.hidesBottomBarWhenPushed = true navigationController?.pushViewController(analysisController, animated: true) diff --git a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift index 5b709fc..b476ab4 100644 --- a/Projects/Chat/Sources/ViewModel/ChatViewModel.swift +++ b/Projects/Chat/Sources/ViewModel/ChatViewModel.swift @@ -22,6 +22,7 @@ public class ChatViewModel: ChatViewModelProtocol { let chatSubject = PublishSubject() let messageSubject = PublishSubject() let analysisResultSubject = PublishSubject() + let chatDetailSubject = PublishSubject() private var currentChatRoomId: Int = 0 public init(chatUseCase: ChatUseCase, @@ -84,4 +85,14 @@ public class ChatViewModel: ChatViewModelProtocol { print("❌ 대화방 삭제 실패: \(error)") }).disposed(by: disposeBag) } + + func getChatDetail(chatRoomId: Int) { + chatUseCase.getChatDetail(chatRoomId: chatRoomId) + .subscribe(onSuccess: { [weak self] chatDetail in + print("✅ 대화 상세 조회 성공: \(chatDetail)") + self?.chatDetailSubject.onNext(chatDetail) + }, onFailure: { error in + print("❌ 대화 상세 조회 실패: \(error)") + }).disposed(by: disposeBag) + } } diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index b143966..5e9dac9 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 9516ED512E708CF700F548A1 /* ChatMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED502E708CF200F548A1 /* ChatMainView.swift */; }; 9516ED532E709BA600F548A1 /* ChatListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9516ED522E709BA300F548A1 /* ChatListCell.swift */; }; 956C4D702E767F8800E32F93 /* LMAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D6F2E767F8300E32F93 /* LMAlert.swift */; }; + 956C4D742E7690EA00E32F93 /* ChatDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956C4D732E7690E600E32F93 /* ChatDetailView.swift */; }; BAD8B768F782046D4AA1C073 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3DE048BEDCB92B48F401061 /* RxCocoa.framework */; }; BE81B1F3E60D37D75A058D2B /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CCDC15081A22BAED6318E3E /* SnapKit.framework */; }; C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012F45F908769FA7C3C0792F /* LoginView.swift */; }; @@ -86,6 +87,7 @@ 9516ED502E708CF200F548A1 /* ChatMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMainView.swift; sourceTree = ""; }; 9516ED522E709BA300F548A1 /* ChatListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListCell.swift; sourceTree = ""; }; 956C4D6F2E767F8300E32F93 /* LMAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMAlert.swift; sourceTree = ""; }; + 956C4D732E7690E600E32F93 /* ChatDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDetailView.swift; sourceTree = ""; }; 9CCDC15081A22BAED6318E3E /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BACC7259FC0C14CB352A4E6B /* OptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionView.swift; sourceTree = ""; }; C28FE6392E1612667826E5C5 /* DiaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryView.swift; sourceTree = ""; }; @@ -211,6 +213,7 @@ 9516ED502E708CF200F548A1 /* ChatMainView.swift */, 9516ED522E709BA300F548A1 /* ChatListCell.swift */, 3EBDD10D8389EBB23B42C54F /* ChatView.swift */, + 956C4D732E7690E600E32F93 /* ChatDetailView.swift */, 9516ED4C2E707AC900F548A1 /* ChatAnalysisLoadingView.swift */, 9516ED492E7076FB00F548A1 /* ChatAnalysisView.swift */, ); @@ -374,6 +377,7 @@ 950A0D962E605CEA00C07CF2 /* UIStackView+Extension.swift in Sources */, E055AA66777B1D4CC8C884E4 /* CommonUIAssets.swift in Sources */, 950A0D4F2E5AADB500C07CF2 /* SignUpView.swift in Sources */, + 956C4D742E7690EA00E32F93 /* ChatDetailView.swift in Sources */, F7673E4248628D67F3542848 /* ChatView.swift in Sources */, 950A0D562E5C29D000C07CF2 /* LMTextField.swift in Sources */, 9516ED4D2E707ACF00F548A1 /* ChatAnalysisLoadingView.swift in Sources */, diff --git a/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift b/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift new file mode 100644 index 0000000..feebeae --- /dev/null +++ b/Projects/CommonUI/Sources/View/Chat/ChatDetailView.swift @@ -0,0 +1,190 @@ +// +// ChatDetailView.swift +// CommonUI +// +// Created by 박지윤 on 1/1/25. +// + +import UIKit +import SnapKit +import Then +import Domain +import RxSwift +import RxRelay + +open class ChatDetailView: UIView { + + let disposeBag = DisposeBag() + + private let scrollView = UIScrollView().then { + $0.showsVerticalScrollIndicator = true + $0.alwaysBounceVertical = true + } + + private let contentView = UIView() + + private let analysisStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 16 + $0.alignment = .fill + } + + public override init(frame: CGRect) { + super.init(frame: frame) + initAttribute() + setupUI() + setupConstraints() + bindEvents() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func initAttribute() { + + } + + private func setupUI() { + backgroundColor = CommonUIAssets.LMOrange4 + + [scrollView].forEach { addSubview($0) } + [contentView].forEach { scrollView.addSubview($0) } + [analysisStackView].forEach { contentView.addSubview($0) } + } + + private func setupConstraints() { + scrollView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.width.equalToSuperview() + } + + analysisStackView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(20) + } + } + + private func bindEvents() { + // 저장 버튼 제거로 인한 이벤트 바인딩 제거 + } + + public func setupDetailData(_ chatList: [ChatListVO]) { + // 기존 뷰들 제거 + analysisStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + // 각 메시지를 UI로 변환 + for (index, chat) in chatList.enumerated() { + let messageView = createMessageView(chat: chat, index: index) + analysisStackView.addArrangedSubview(messageView) + } + + // 마지막에 여백 추가 + let spacerView = UIView() + spacerView.snp.makeConstraints { $0.height.equalTo(20) } + analysisStackView.addArrangedSubview(spacerView) + } + + private func createMessageView(chat: ChatListVO, index: Int) -> UIView { + let containerView = UIView() + + // 말풍선 배경 + let bubbleView = UIView().then { + $0.backgroundColor = chat.author == 0 ? UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0) : CommonUIAssets.LMBlue2 + $0.layer.cornerRadius = 16 + } + + // 메시지 내용 + let contentLabel = UILabel().then { + $0.font = .systemFont(ofSize: 16, weight: .regular) + $0.textColor = chat.author == 0 ? .black : .white + $0.text = chat.content + $0.numberOfLines = 0 + } + + containerView.addSubview(bubbleView) + bubbleView.addSubview(contentLabel) + + // 메시지 정렬 (사용자는 오른쪽, AI는 왼쪽) + if chat.author == 0 { + // AI 메시지 (왼쪽 정렬) + bubbleView.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.leading.equalToSuperview() + $0.width.lessThanOrEqualTo(250) + } + } else { + // 사용자 메시지 (오른쪽 정렬) + bubbleView.snp.makeConstraints { + $0.top.bottom.equalToSuperview() + $0.trailing.equalToSuperview() + $0.width.lessThanOrEqualTo(250) + } + } + + contentLabel.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) + } + + // 사용자 메시지에 comment 추가 + if chat.author != 0, let comment = chat.comment, !comment.isEmpty { + let commentView = createCommentView(comment: comment) + containerView.addSubview(commentView) + + commentView.snp.makeConstraints { + $0.top.equalTo(bubbleView.snp.bottom).offset(8) + $0.trailing.equalToSuperview() + $0.width.lessThanOrEqualTo(250) + $0.bottom.equalToSuperview() + } + + // bubbleView의 bottom constraint 수정 + bubbleView.snp.remakeConstraints { + $0.top.equalToSuperview() + $0.trailing.equalToSuperview() + $0.width.lessThanOrEqualTo(250) + $0.bottom.equalTo(commentView.snp.top).offset(-8) + } + } + + return containerView + } + + private func createCommentView(comment: String) -> UIView { + let commentContainer = UIView().then { + $0.backgroundColor = UIColor.systemGray6 + $0.layer.cornerRadius = 12 + } + + let commentLabel = UILabel().then { + $0.font = .systemFont(ofSize: 14, weight: .regular) + $0.textColor = .darkGray + $0.text = comment + $0.numberOfLines = 0 + } + + commentContainer.addSubview(commentLabel) + + commentLabel.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) + } + + return commentContainer + } + + private func formatDate(_ dateString: String) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + + if let date = formatter.date(from: dateString) { + let displayFormatter = DateFormatter() + displayFormatter.dateFormat = "yyyy.MM.dd HH:mm" + return displayFormatter.string(from: date) + } + + return dateString + } +} diff --git a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift index 783f7b3..d82c1dd 100644 --- a/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift +++ b/Projects/CommonUI/Sources/View/Chat/ChatMainView.swift @@ -15,6 +15,7 @@ import RxSwift public class ChatMainView: UIView { public let newChatButtonTapped = PublishRelay() + public let chatCellTapped = PublishRelay() let disposeBag = DisposeBag() private let emptyStateContainer = UIView() @@ -153,14 +154,18 @@ public class ChatMainView: UIView { } extension ChatMainView: UITableViewDataSource, UITableViewDelegate { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public func numberOfSections(in tableView: UITableView) -> Int { return chatRoomList.count } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ChatListCell", for: indexPath) as! ChatListCell - let chatRoom = chatRoomList[indexPath.row] + let chatRoom = chatRoomList[indexPath.section] cell.configure(title: chatRoom.title, date: chatRoom.createdAt) return cell @@ -169,4 +174,18 @@ extension ChatMainView: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 80 } + + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return section == 0 ? 0 : 5 + } + + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return UIView() + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let chatRoom = chatRoomList[indexPath.section] + chatCellTapped.accept(chatRoom) + } }