diff --git a/GAMSS.xcodeproj/project.pbxproj b/GAMSS.xcodeproj/project.pbxproj index 894cce8..c59227f 100644 --- a/GAMSS.xcodeproj/project.pbxproj +++ b/GAMSS.xcodeproj/project.pbxproj @@ -24,7 +24,9 @@ 8F4E518B79C7B76223B098BA /* Tokenizers in Frameworks */ = {isa = PBXBuildFile; productRef = 8E8487F478AF703D394C5EB6 /* Tokenizers */; }; 94A526BCCCD9DF730A39BF97 /* sample_sentences.json in Resources */ = {isa = PBXBuildFile; fileRef = D4BE9D664889F13369325DC9 /* sample_sentences.json */; }; 9EA6F6B8DF51021E18342083 /* HomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */; }; + FA90ADA39E7A145713881061 /* MainTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C15D652D0472A1EBD024674 /* MainTabTests.swift */; }; A8948FBCA4DF4E43F2C582EA /* Pods_GAMSS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB14F9D00BB8B80EFE9A40A4 /* Pods_GAMSS.framework */; }; + A8FEE0AD9A4A60908DC5F5F1 /* CreateMessageRequestDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAA522A6DCECC0B3666523C /* CreateMessageRequestDTOTests.swift */; }; ABF5C91182F3B409C1AA9E8E /* GetMessagesUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A67D7BECCB85A88C969AD7 /* GetMessagesUseCaseTests.swift */; }; B32ACCF04698A89364EDDECE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 316EA002FAC5D3328796EF90 /* Foundation.framework */; }; BA7748118B0D2759E0F6A207 /* Pods_GAMSSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1CC8FA289575E4B98C41539 /* Pods_GAMSSTests.framework */; }; @@ -34,7 +36,6 @@ CC604C7607382102024E0FA1 /* ConversationSummaryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CCBD8A019CAA37319E7984 /* ConversationSummaryPolicyTests.swift */; }; DC1369FEAE93D794107D5373 /* Color_GAMSSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596EE0F239D37460A7BA389D /* Color_GAMSSTests.swift */; }; E3A3BE7330A1CCAB9E144B41 /* DefaultEmotionAnalysisRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B816EE4A02D0B23C50F35FF9 /* DefaultEmotionAnalysisRepositoryTests.swift */; }; - A8FEE0AD9A4A60908DC5F5F1 /* CreateMessageRequestDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAA522A6DCECC0B3666523C /* CreateMessageRequestDTOTests.swift */; }; EE5555555555555555555555 /* TypographyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4444444444444444444444 /* TypographyTests.swift */; }; F1D4E5A20F2B3C4D5E6F7A8B /* SpacingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D4E5A30F2B3C4D5E6F7A8C /* SpacingTests.swift */; }; F29C98F332A3EC127B809DD3 /* ChatViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE5C85E76690E41AE394895D /* ChatViewModelTests.swift */; }; @@ -57,15 +58,16 @@ 1A8F630A15A80F5FAB52F2C1 /* Pods-GAMSSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GAMSSTests.debug.xcconfig"; path = "Target Support Files/Pods-GAMSSTests/Pods-GAMSSTests.debug.xcconfig"; sourceTree = ""; }; 316EA002FAC5D3328796EF90 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeViewModelTests.swift; sourceTree = ""; }; + 3C15D652D0472A1EBD024674 /* MainTabTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabTests.swift; sourceTree = ""; }; 41CCBD8A019CAA37319E7984 /* ConversationSummaryPolicyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConversationSummaryPolicyTests.swift; sourceTree = ""; }; 4DDF78D0118869F3D9E1E269 /* SendMessageUseCaseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SendMessageUseCaseTests.swift; sourceTree = ""; }; 596EE0F239D37460A7BA389D /* Color_GAMSSTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Color_GAMSSTests.swift; sourceTree = ""; }; 5B4F594F8D1059D3DF30DE92 /* GAMSSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GAMSSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 708F57F33003C651008DF433 /* GAMSS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GAMSS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7674D757C326ABEDB6302AA6 /* GAMSSTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GAMSSTests.swift; sourceTree = ""; }; - 7DAA522A6DCECC0B3666523C /* CreateMessageRequestDTOTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateMessageRequestDTOTests.swift; sourceTree = ""; }; 79F056320618BF2744EAC909 /* EmotionTestViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EmotionTestViewModelTests.swift; sourceTree = ""; }; 7BB1A4112363D93ED3AD1598 /* EmotionCharacterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EmotionCharacterTests.swift; sourceTree = ""; }; + 7DAA522A6DCECC0B3666523C /* CreateMessageRequestDTOTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateMessageRequestDTOTests.swift; sourceTree = ""; }; 83A67D7BECCB85A88C969AD7 /* GetMessagesUseCaseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GetMessagesUseCaseTests.swift; sourceTree = ""; }; 86563A77A8A674E7F050119E /* CommentRevealPolicyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentRevealPolicyTests.swift; sourceTree = ""; }; A17C099A15F7D9B1481998D0 /* DefaultSummaryRepositoryTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DefaultSummaryRepositoryTests.swift; sourceTree = ""; }; @@ -277,6 +279,7 @@ 79F056320618BF2744EAC909 /* EmotionTestViewModelTests.swift */, BE5C85E76690E41AE394895D /* ChatViewModelTests.swift */, 3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */, + 3C15D652D0472A1EBD024674 /* MainTabTests.swift */, ); path = Presentation; sourceTree = ""; @@ -516,14 +519,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-GAMSS/Pods-GAMSS-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-GAMSS/Pods-GAMSS-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GAMSS/Pods-GAMSS-resources.sh\"\n"; @@ -571,6 +570,7 @@ E3A3BE7330A1CCAB9E144B41 /* DefaultEmotionAnalysisRepositoryTests.swift in Sources */, 06BCF4ADC30405F292D7B64C /* EmotionTestViewModelTests.swift in Sources */, F29C98F332A3EC127B809DD3 /* ChatViewModelTests.swift in Sources */, + FA90ADA39E7A145713881061 /* MainTabTests.swift in Sources */, 9EA6F6B8DF51021E18342083 /* HomeViewModelTests.swift in Sources */, C63AB33F1424E4237FF76F6B /* SummaryErrorTests.swift in Sources */, 1D8DF6DFC7E7383B9B8C5850 /* DefaultSummaryRepositoryTests.swift in Sources */, diff --git a/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/Contents.json b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/Contents.json new file mode 100644 index 0000000..301254d --- /dev/null +++ b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "logo_gamss.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "logo_gamss@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "logo_gamss@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss.png b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss.png new file mode 100644 index 0000000..5f78c29 Binary files /dev/null and b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss.png differ diff --git a/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss@2x.png b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss@2x.png new file mode 100644 index 0000000..9337ba6 Binary files /dev/null and b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss@2x.png differ diff --git a/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss@3x.png b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss@3x.png new file mode 100644 index 0000000..6e24761 Binary files /dev/null and b/GAMSS/Resources/Assets.xcassets/logoGamss.imageset/logo_gamss@3x.png differ diff --git a/GAMSS/Sources/Presentation/Archive/ArchiveView.swift b/GAMSS/Sources/Presentation/Archive/ArchiveView.swift new file mode 100644 index 0000000..75f0a6a --- /dev/null +++ b/GAMSS/Sources/Presentation/Archive/ArchiveView.swift @@ -0,0 +1,24 @@ +// +// ArchiveView.swift +// GAMSS +// +// Created by cchanmi on 8/10/26. +// + +import SwiftUI + +struct ArchiveView: View { + var body: some View { + VStack { + Spacer() + Text("준비 중입니다") + .typography(.body3Regular) + .foregroundStyle(Color.colorGray500) + Spacer() + } + } +} + +#Preview { + ArchiveView() +} diff --git a/GAMSS/Sources/Presentation/ConversationList/ConversationListView.swift b/GAMSS/Sources/Presentation/ConversationList/ConversationListView.swift index 02da55c..aad5327 100644 --- a/GAMSS/Sources/Presentation/ConversationList/ConversationListView.swift +++ b/GAMSS/Sources/Presentation/ConversationList/ConversationListView.swift @@ -9,9 +9,11 @@ import SwiftUI struct ConversationListView: View { @StateObject private var viewModel: ConversationListViewModel + @Binding private var isTabBarHidden: Bool - init(viewModel: ConversationListViewModel) { + init(viewModel: ConversationListViewModel, isTabBarHidden: Binding) { _viewModel = StateObject(wrappedValue: viewModel) + _isTabBarHidden = isTabBarHidden } var body: some View { @@ -36,7 +38,8 @@ struct ConversationListView: View { conversationId: conversation.id ) ) - .toolbar(.hidden, for: .tabBar) + .onAppear { isTabBarHidden = true } + .onDisappear { isTabBarHidden = false } } } .task { @@ -57,6 +60,7 @@ struct ConversationListView: View { getConversationsUseCase: GetConversationsUseCase( conversationRepository: DefaultConversationRepository(networkManager: NetworkManager.shared) ) - ) + ), + isTabBarHidden: .constant(false) ) } diff --git a/GAMSS/Sources/Presentation/Home/HomeView.swift b/GAMSS/Sources/Presentation/Home/HomeView.swift index 082c002..2b85036 100644 --- a/GAMSS/Sources/Presentation/Home/HomeView.swift +++ b/GAMSS/Sources/Presentation/Home/HomeView.swift @@ -10,9 +10,11 @@ import SwiftUI struct HomeView: View { @StateObject private var viewModel: HomeViewModel @FocusState private var isInputFocused: Bool + @Binding private var isTabBarHidden: Bool - init(viewModel: HomeViewModel) { + init(viewModel: HomeViewModel, isTabBarHidden: Binding) { _viewModel = StateObject(wrappedValue: viewModel) + _isTabBarHidden = isTabBarHidden } var body: some View { @@ -55,7 +57,8 @@ struct HomeView: View { initialSentMessage: viewModel.createdSentMessage ) ) - .toolbar(.hidden, for: .tabBar) + .onAppear { isTabBarHidden = true } + .onDisappear { isTabBarHidden = false } } } .alert(viewModel.alertMessage ?? "", isPresented: Binding( @@ -68,13 +71,10 @@ struct HomeView: View { private var header: some View { HStack { - Text("GAMSS") - .typography(.subtitle3) - .foregroundStyle(Color.colorGray950) - .padding(.horizontal, Spacing.spacing300) - .padding(.vertical, Spacing.spacing100) - .background(Color.colorGray050) - .clipShape(Capsule()) + Image("logoGamss") + .resizable() + .scaledToFit() + .frame(height: 24) Spacer() @@ -139,6 +139,7 @@ struct HomeView: View { sendMessageUseCase: SendMessageUseCase( conversationRepository: DefaultConversationRepository(networkManager: NetworkManager.shared) ) - ) + ), + isTabBarHidden: .constant(false) ) } diff --git a/GAMSS/Sources/Presentation/Root/CustomTabBar.swift b/GAMSS/Sources/Presentation/Root/CustomTabBar.swift new file mode 100644 index 0000000..3933755 --- /dev/null +++ b/GAMSS/Sources/Presentation/Root/CustomTabBar.swift @@ -0,0 +1,59 @@ +// +// CustomTabBar.swift +// GAMSS +// +// Created by cchanmi on 8/10/26. +// + +import SwiftUI + +struct CustomTabBar: View { + @Binding var selectedTab: MainTab + + var body: some View { + HStack(spacing: 0) { + ForEach(MainTab.allCases, id: \.self) { tab in + tabButton(for: tab) + } + } + .padding(.top, Spacing.spacing200) + .padding(.bottom, Spacing.spacing100) + .background(Color.colorWhite) + .overlay( + Rectangle() + .stroke(Color.colorBlack, lineWidth: 1.5) + ) + } + + @ViewBuilder + private func tabButton(for tab: MainTab) -> some View { + let isSelected = selectedTab == tab + + Button { + selectedTab = tab + } label: { + VStack(spacing: Spacing.spacing050) { + Image(tab.iconName) + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(tab.title) + .typography(.caption3) + } + .foregroundStyle(isSelected ? Color.colorGray950 : Color.colorGray300) + .frame(maxWidth: .infinity) + } + .buttonStyle(.plain) + } +} + +#Preview { + VStack { + Spacer() + CustomTabBar(selectedTab: .constant(.home)) + .padding(.horizontal, Spacing.spacing400) + .padding(.bottom, Spacing.spacing200) + } +} diff --git a/GAMSS/Sources/Presentation/Root/MainTab.swift b/GAMSS/Sources/Presentation/Root/MainTab.swift new file mode 100644 index 0000000..c612673 --- /dev/null +++ b/GAMSS/Sources/Presentation/Root/MainTab.swift @@ -0,0 +1,30 @@ +// +// MainTab.swift +// GAMSS +// +// Created by cchanmi on 8/10/26. +// + +import Foundation + +enum MainTab: CaseIterable, Hashable { + case archive + case home + case chat + + var title: String { + switch self { + case .archive: return "보관함" + case .home: return "홈" + case .chat: return "대화" + } + } + + var iconName: String { + switch self { + case .archive: return "tabIconArchive" + case .home: return "tabIconHome" + case .chat: return "tabIconChat" + } + } +} diff --git a/GAMSS/Sources/Presentation/Root/MainTabView.swift b/GAMSS/Sources/Presentation/Root/MainTabView.swift index 2dcbb14..e9b09ce 100644 --- a/GAMSS/Sources/Presentation/Root/MainTabView.swift +++ b/GAMSS/Sources/Presentation/Root/MainTabView.swift @@ -8,25 +8,47 @@ import SwiftUI struct MainTabView: View { + @State private var selectedTab: MainTab = .home + @State private var isTabBarHidden = false + var body: some View { - TabView { + content + .safeAreaInset(edge: .bottom) { + if !isTabBarHidden { + CustomTabBar(selectedTab: $selectedTab) + .padding(.horizontal, Spacing.spacing400) + .padding(.bottom, Spacing.spacing200) + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 0.15), value: isTabBarHidden) + } + + @ViewBuilder + private var content: some View { + switch selectedTab { + case .archive: + ArchiveView() + + case .home: HomeView( viewModel: HomeViewModel( sendMessageUseCase: SendMessageUseCase( conversationRepository: DefaultConversationRepository(networkManager: NetworkManager.shared) ) - ) + ), + isTabBarHidden: $isTabBarHidden ) - .tabItem { Label("홈", systemImage: "house") } + case .chat: ConversationListView( viewModel: ConversationListViewModel( getConversationsUseCase: GetConversationsUseCase( conversationRepository: DefaultConversationRepository(networkManager: NetworkManager.shared) ) - ) + ), + isTabBarHidden: $isTabBarHidden ) - .tabItem { Label("대화", systemImage: "bubble.left.and.bubble.right") } } } } diff --git a/GAMSSTests/Presentation/MainTabTests.swift b/GAMSSTests/Presentation/MainTabTests.swift new file mode 100644 index 0000000..88751da --- /dev/null +++ b/GAMSSTests/Presentation/MainTabTests.swift @@ -0,0 +1,27 @@ +// +// MainTabTests.swift +// GAMSS +// +// Created by cchanmi on 8/10/26. +// + +import XCTest +@testable import GAMSS + +final class MainTabTests: XCTestCase { + func test_title_returnsKoreanLabelForEachTab() { + XCTAssertEqual(MainTab.archive.title, "보관함") + XCTAssertEqual(MainTab.home.title, "홈") + XCTAssertEqual(MainTab.chat.title, "대화") + } + + func test_iconName_returnsAssetNameForEachTab() { + XCTAssertEqual(MainTab.archive.iconName, "tabIconArchive") + XCTAssertEqual(MainTab.home.iconName, "tabIconHome") + XCTAssertEqual(MainTab.chat.iconName, "tabIconChat") + } + + func test_allCases_containsThreeTabsInOrder() { + XCTAssertEqual(MainTab.allCases, [.archive, .home, .chat]) + } +}